How would i get attachments that are in a line?

i have attachments placed like this


each circle represents the attachments i want grouped together, although im not sure how i would get attachments like this

1 Like

Do you only care about cardinal directions? Or does it have to be any direction?

1 Like

well, id guess any direction because some may be diagonal.

If three points A, B and C are exactly on the same line, the length (magnitude) of the cross product of vector from A to B and vector from A to C is zero because the vectors are parallel. So by using two points that are known to be on a line, you can calculate whether some other point is on the same line. I haven’t tested this code.

local maxCrossProductMagnitude = 0.1 -- this can be changed

local attachments = -- table containing all attachments

local lines = {}

local function removeLinesContainingSubsetOfAttachmentsOfThisLine(line)
	local numberOfRemovedLines = 0
	for indexOfAnotherLine = #lines, 1, -1 do
		local anotherLine = lines[indexOfAnotherLine]
		if anotherLine == line then
			continue
		end
		local isSubsetOfGivenLine = true
		for _, attachment in anotherLine do
			if table.find(line, attachment) == nil then
				isSubsetOfGivenLine = false
				break
			end
		end
		if isSubsetOfGivenLine then
			table.remove(lines, indexOfAnotherLine)
			numberOfRemovedLines += 1
		end
	end
	return numberOfRemovedLines
end

-- Two points always have a line in which they both are so this first loop creates a "line" (attachment list) for each pair of attachments.
for i, attachment in attachments do
	for j = i + 1, #attachments do
		local anotherattachment = attachments[j]
		table.insert(lines, {attachment, anotherAttachment})
	end
end

local i = #lines
while i > 0 do
	local line = lines[i]
	local oneDirectionVectorOfTheLine = line[2].WorldPosition - line[1].WorldPosition
	for _, attachment in attachments do
		if table.find(line, attachment) ~= nil then
			continue
		end
		local vectorToThisAttachment = attachment.WorldPosition - line[1].WorldPosition
		local isInLine = oneDirectionVectorOfTheLine:Cross(vectorToThisAttachment).Magnitude <= maxCrossProductMagnitude
		if isInLine then
			table.insert(line, attachment)
		end
	end
	local howManyRemoved = removeLinesContainingSubsetOfAttachmentsOfThisLine(line)
	i = i - 1 - howManyRemoved
end

In your case, you should get your desired groups of attachments by removing the groups that only contain two attachments. This can be done by running the code below after the code above.

for i = #lines, 1, -1 do
	local line = lines[i]
	if #line == 2 then
		table.remove(lines, i)
	end
end

This seems like it would work but for the situation im trying to use this in, i do not have 2 points, only 1 random attachment that is somewhere along the line.

I do have code from my attempt at getting the direction of the line although i came to make this thread as i didnt think it was well made

local Attachment_PathSection = Attachment.Parent
local AttachmentsTable = {Attachment}

local ChosenOrientation1 = nil
local ChosenOrientation2 = nil
local psPos = Attachment_PathSection.Size


for i = 1, 4 do
	if not ChosenOrientation1 then
		local RightPosition = Attachment.WorldCFrame + ((Attachment.WorldCFrame * CFrame.fromOrientation(0,rad(90 * i), 0)).LookVector * Vector3.new(psPos.X, psPos.Y, psPos.Z))
		for _AttachmentNumber, _Attachment in ipairs(WallAttachTable) do
			if Wall_Delay_TimeNum >= Wall_Delay_Number*100 then task.wait(); Wall_Delay_TimeNum = 0 else Wall_Delay_TimeNum += 1 end
			if (_Attachment.WorldPosition - RightPosition.Position).Magnitude < 0.01 then
				if _Attachment ~= Attachment and not table.find(AttachmentsTable, _Attachment) then
					if _Attachment.Parent ~= Attachment.Parent then
						ChosenOrientation1 = CFrame.fromOrientation(0,rad(90 * i),0)
						ChosenOrientation2 = CFrame.fromOrientation(0,rad(-(90 * i)),0)
					end
				end
			end
		end
	end
end

(section from a bigger script)

For any two attachments, there is a line in which both these attachments are although that may not be one of the lines you are looking for because the line can be between walls and you want attachments that are on a line that goes along a wall.

So for any attachment pair, it’s possible to calculate which other attachments are on the same line.

If it’s possible that there are three attachments that are on the same line but should not be grouped (the line does not go along a wall), then my code will not give you a desired result. Otherwise, if the code works as expected, I think it should give the correct groups.

What is the problem with your current code? Does it not work or do you just want another way to do this?

(sorry for the late response, i had school)

well it works but im looking for another way to do it with less mess and without so many bugs

because currently with the code im having it the same attachments in multiple tables, not sure why. (this is on me)

but aswell as problems with overlapping attachments due to how my generation works
which is a issue because of what i will be doing with these line segements later.
^
As for that the only solution i can think of with how this is scripted currently is going all the way through each attachment again then checking for overlapping attachments and then determining which one matches the orientation of the starting attachment and just so on

all this feels like it can just be simplified alot.
Also theres a ton of if statements which doesnt look good

Try this. This time I’ve actually tested my code. In the main loop, it goes through the given attachments table which should contain all the attachments that should be divided into groups. Whenever the main loop finds an attachment that isn’t in a group yet, it creates a new group. The main loop will not continue to its next iteration until all the attachments that should be in this group are added to this group using the other loops. So on every main loop iteration, either 0 or 1 new group is added.

On every iteration of the repeat until loop, either 0 or 1 attachment is added to the new group created in the main loop. If no attachment is added, then the group is complete, the repeat until loop breaks and the main loop continues to its next iteration.

The for loops inside the repeat until loop are used to check if any attachment that is not in a group yet is in the correct direction and at the correct distance from any attachment that is in the new group. The correct direction and distance are calculated based on part orientation and part size. The wall direction in the code is a world direction vector of the part axis on which the size of the part is the greatest. If such an attachment is found, it is added to the new group and both for loops are broken resulting in a new iteration of the repeat until loop. If such an attachment is not found, then wasANewAttachmentAddedInIteration will be false at the end of the repeat until loop iteration which will result in breaking the repeat until loop (which, as mentioned, means that the group is complete and the main loop continues).

The reason why the code repeatedly does the direction and distance check for every attachment that is not in a group until no attachment satisfies the conditions is the distance constraint. For example, let’s say we have three attachments a1, a2 and a3 on the same wall, a2 is located (WorldPosition) between a1 and a3, d is the distance between attachments of adjacent parts of the wall, and a1 is the first attachment in the group. If the code looped through the attachment table only once after initializing the group with a1 and a3 came before a2 in the loop, a3 would not be added to the group because its distance from a1 is 2d, although its distance from a2 is d which means that it should be added to the group.

local equalityMaxDifference = 0.1 -- this can be changed

local function areApproximatelyEqual(num1, num2)
	return math.abs(num1 - num2) <= equalityMaxDifference
end

local function getWallDirectionAndAttachmentDistance(attachment: Attachment)
	local part = attachment:FindFirstAncestorWhichIsA("BasePart")
	local verticalAxisInLocalCoordinateSystem
	if areApproximatelyEqual(part.CFrame.RightVector:Cross(Vector3.yAxis).Magnitude, 0) then
		verticalAxisInLocalCoordinateSystem = "X"
	elseif areApproximatelyEqual(part.CFrame.UpVector:Cross(Vector3.yAxis).Magnitude, 0) then
		verticalAxisInLocalCoordinateSystem = "Y"
	elseif areApproximatelyEqual(part.CFrame.LookVector:Cross(Vector3.yAxis).Magnitude, 0) then
		verticalAxisInLocalCoordinateSystem = "Z"
	else
		error("Part orientation is incompatible for this.")
	end
	
	local sizesToCompare = {"X", "Y", "Z"}
	table.remove(sizesToCompare, table.find(sizesToCompare, verticalAxisInLocalCoordinateSystem))
	
	-- horAxis means horizontal axis in local coordinate system (local coordinate system axis that
	-- is parallel to the world coordinate system xy-plane)
	local horAxis1, horAxis2 = sizesToCompare[1], sizesToCompare[2]
	local size1, size2 = part.Size[horAxis1], part.Size[horAxis2]
	local biggestSizeAxisVector: Vector3, biggestSize: number
	if size1 > size2 then
		biggestSizeAxisVector = Vector3.FromAxis(Enum.Axis[horAxis1])
		biggestSize = size1
	else
		biggestSizeAxisVector = Vector3.FromAxis(Enum.Axis[horAxis2])
		biggestSize = size2
	end
	
	local biggestSizeAxisWorldSpaceVector = part.CFrame.Rotation * biggestSizeAxisVector
	return biggestSizeAxisWorldSpaceVector, biggestSize
end

local function getAttachmentGroups(attachments: {Attachment}): {{Attachment}}
	local groups = {}
	local hasBeenAddedToAGroup = {}
	for _, attachment in attachments do
		if hasBeenAddedToAGroup[attachment] then
			continue
		end
		
		local newGroup = {attachment}
		table.insert(groups, newGroup)
		hasBeenAddedToAGroup[attachment] = true
		
		local wallDirection, wallAttachmentDistance = getWallDirectionAndAttachmentDistance(attachment)
		
		repeat
			local wasANewAttachmentAddedInIteration = false
			for _, attachmentInGroup in newGroup do
				for _, otherAttachment in attachments do
					if hasBeenAddedToAGroup[otherAttachment] then
						continue
					end

					local otherAttachmentWallDirection, otherAttachmentWallAttachmentDistance = getWallDirectionAndAttachmentDistance(otherAttachment)
					local areWallDirectionsSame = areApproximatelyEqual(otherAttachmentWallDirection:Cross(wallDirection).Magnitude, 0)
					if not areWallDirectionsSame then
						continue
					end

					local isInLine = areApproximatelyEqual(wallDirection:Cross(otherAttachment.WorldPosition - attachmentInGroup.WorldPosition).Magnitude, 0)
					local isAtCorrectDistance = areApproximatelyEqual((otherAttachment.WorldPosition - attachmentInGroup.WorldPosition).Magnitude, wallAttachmentDistance / 2 + otherAttachmentWallAttachmentDistance / 2)

					if isInLine and isAtCorrectDistance then
						table.insert(newGroup, otherAttachment)
						hasBeenAddedToAGroup[otherAttachment] = true
						wasANewAttachmentAddedInIteration = true
						break
					end
				end
				if wasANewAttachmentAddedInIteration then
					-- This break and the break above together start a new iteration of the repeat until loop.
					break
				end
			end
		until wasANewAttachmentAddedInIteration == false
	end
	return groups
end

Here’s the code that I used for testing this. workspace.AttachmentGroupingTest is a folder that contains the parts I used for testing, and each part has an attachment as its child.

local attachments: {Attachment} = {}

for _, descendant in workspace.AttachmentGroupingTest:GetDescendants() do
	if descendant:IsA("Attachment") then
		table.insert(attachments, descendant)
	end
end

local groups = getAttachmentGroups(attachments)
for groupIndex, group in groups do
	for _, attachment in group do
		attachment.Name = tostring(groupIndex)
		local part = attachment:FindFirstAncestorWhichIsA("BasePart")
		part.Name = tostring(groupIndex)
		local colorBrightness = groupIndex / #groups
		part.Color = Color3.new(colorBrightness, colorBrightness, colorBrightness)
	end
end
1 Like

Hey!, Sorry for getting back so late, i just kinda was lazy and didnt want to code for a bit.

I wont be going through the entire information providing responses to each bit as what you said pretty much all makes sense and lines up with the code, although i do have a question;

As for how cross works here (and in general), i read through this post but i still dont really understand how it works here

if areApproximatelyEqual(part.CFrame.RightVector:Cross(Vector3.yAxis).Magnitude, 0) then
		verticalAxisInLocalCoordinateSystem = "X"
	elseif areApproximatelyEqual(part.CFrame.UpVector:Cross(Vector3.yAxis).Magnitude, 0) then
		verticalAxisInLocalCoordinateSystem = "Y"
	elseif areApproximatelyEqual(part.CFrame.LookVector:Cross(Vector3.yAxis).Magnitude, 0) then
		verticalAxisInLocalCoordinateSystem = "Z"
	else
		error("Part orientation is incompatible for this.")
	end
(Lines 11-21)

and here

areApproximatelyEqual(otherAttachmentWallDirection:Cross(wallDirection).Magnitude, 0)
(Line 67)

On a unrelated note
How would it even be possible for it to error with incompatible part orientation?

Cross product
The tutorial you linked explains what cross product usually does. It usually gives a vector that is perpendicular to both of the given vectors. Usually, there’s exactly one line direction that is perpendicular to both vectors and the vector given by the cross product has one of the two opposite directions in which you can move in this line direction (which direction it is depends on the order of the vectors in the cross product).

What the tutorial didn’t mention is how it works when there’s an infinite number of directions that are perpendicular to both of the given vectors. When the vectors are parallel they are both perpendicular to a whole plane with a spesific orientation. There is an infinite number of directions you can move in on a plane. In this case, the cross product just gives a zero vector (which means the magnitude of the cross product is 0).

With a line direction I mean a pair of two opposite directions. The reason why I was talking about one line direction and one plane orientation instead of one line and one plane was because for any line direction or any plane orientation, there is an infinite number of parallel lines or planes with that direction or orientation.

In the case of coding, the attachment positions and the cross product calculations are not necessarily accurate so the cross product may not be exactly a zero vector. However, when the directions of the given vectors approach parallel directions, the length (magnitude) of the cross product approaches zero. So we can calculate whether the vectors are approximately parallel by calculating whether the length of their cross product is close to zero.

So, for example, if

areApproximatelyEqual(part.CFrame.RightVector:Cross(Vector3.yAxis).Magnitude, 0)

is true, then the RightVector of the part (which tells direction of the part’s x-axis in the world coordinate system) is parallel to a direction vector of the world y-axis in the world coordinate system. Because both Vector3.yAxis (== Vector3.new(0, 1, 0)) and CFrame.RightVector are unit vectors this means that the RightVector is approximately equal to either Vector3.new(0, 1, 0) or Vector3.new(0, -1, 0). This means that the x-axis of the part (which is perpendicular to the left and right faces of the part) is vertical in the world coordinate system and thus verticalAxisInLocalCoordinateSystem is set to "X".

Dot product as an alternative
As the tutorial mentioned, dot product can be used for finding how similar the directions of two vectors are. However, the tutorial didn’t mention that dot product will only work in measuring the similarity of vector directions when both vectors are unit vectors (or when dividing the dot product of vectors that are not necessarily unit vectors by the lengths of the vectors). The dot product equation:

dotp(a, b) = cos(a, b)|a||b|

In the equation, cos(a, b) is the cosine of the angle between vectors a and b and it is multiplied by the lengths of both vectors. If we divide the dot product by the lengths we get just the cosine which can be used to measure the similarity of the directions.

Instead of checking whether the length of the cross product of two vectors is close to zero, I could have checked whether the dot product of the unit vectors of the two vectors is close to either -1 or 1. Using cross product just felt like an easier option in this case (only one case to check).

Error
My code is designed for cases where one of the three axes of the part is parallel to the world y-axis (which means that one of its faces is parallel to the world xz-plane). If, for example, the part was rotated 45 degrees around the world x-axis, this condition wouldn’t be satisfied and my code would give that error.

Having one of its axes be parallel to the world y-axis means that the other two axes are parallel to the world xz-plane. However, they are not required to be parallel to the world x and z axes so the wall part can be rotated around the world y-axis.

1 Like

Ok reading through this all,
So what cross product is doing here
(part.CFrame.RightVector:Cross(Vector3.yAxis).Magnitude)
is going to return close to 0 if its parallel to the axis, and close 1/-1 if its perpendicular?
if so im guessing the getWallDirectionAndAttachmentDistance function is just getting the Direction and size of wall of the biggest axis that isnt up?

and then this section

local otherAttachmentWallDirection, otherAttachmentWallAttachmentDistance = getWallDirectionAndAttachmentDistance(otherAttachment)
local areWallDirectionsSame = areApproximatelyEqual(otherAttachmentWallDirection:Cross(wallDirection).Magnitude, 0)
					
if not areWallDirectionsSame then
	continue
end

Checks if they are on the same line as it will return 0 if both the directions are parallel to each other?

Also for this does it matter which way they are cross vector’d as you did say

(this i didnt really understand that great)

moving on from that
im now getting a little more lost as

local isInLine = areApproximatelyEqual(wallDirection:Cross(otherAttachment.WorldPosition - attachmentInGroup.WorldPosition).Magnitude, 0)

Is isInLine similar to areWallDirectionsSame because im not seeing that much of a difference right now

Ah, i see now. this shouldn’t be an issue for me, mainly because even working with stuff rotated oddly on all 3 axis’s is very annoying

Again, sorry taking a entire day to get back to you, its the same excuse before. :sweat_smile:

(edit)
also just wanted to point out you went absolutely wild on that post, 9 edits is the most ive seen ever im pretty sure

Yes, cross product length will be close to zero if they are parallel. However, the -1 and 1 aren’t related to cross product. They are what dot product would give for parallel unit vectors (1 in case of same direction, -1 in case of opposite directions). I didn’t use dot product in my code so it was kind of unrelated. I just noticed that the tutorial you linked told that dot product can be used for measuring similarity of vector directions so I decided to mention how it could have been used in my code as an alternative to cross product.

And yes, you understood correctly what getWallDirectionAndAttachmentDistance returns.

areWallDirectionsSame being true means that the attachments are either on the same wall or on parallel walls. So if attachmentInGroup is in the group marked with black in your original post, areWallDirectionsSame will be true if otherAttachment is either in the black group or the blue group.

The reason why I added the areWallDirectionsSame check is that otherwise, in the following picture, the attachment of the red part and the attachment of the blue part could be considered to be on the same wall if I only took in consideration whether the distance between attachmentInGroup and otherAttachment is correct and the vector between them is parallel to the wall direction calculated based on the part attachmentInGroup is attached to. In the picture, if the attachment of the red part is attachmentInGroup and the attachment of the blue part is otherAttachment, they’d be considered to be on the same wall if areWallDirectionsSame wasn’t checked. I don’t know if your generation code can result in a situation in which this matters, though, so this check may be unnecessary.
image

The order of the vectors only affects the direction of the cross product. In this case, we are only interested in the length of the cross product so in this case, the order doesn’t matter.

isInLine is true when the direction of the vector from attachmentInGroup to otherAttachment is parallel to the wall direction calculated based on the part attachmentInGroup is attached to. If isInLine is true, otherAttachment is on the line that goes through attachmentInGroup and is parallel to wallDirection. isInLine would be true if attachmentInGroup was the attachment of the red part and otherAttachment the attachment of the blue part in the picture above if isInLine was calculated in that situation. However, areWallDirectionsSame would be false in this case so the inner for loop would continue to the next iteration without even calculating isInLine.

1 Like

Ah, I think I finally get the entire code, thanks for providing the information on what they do! :slight_smile:

Before i go i do have to say that
Honestly what you’ve done is amazing. Not many people would go this far to help and provide answers for people on devforum, I truly appreciate what you’ve done and what you’ve helped me with.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.