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

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 2*d*, 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.

(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.

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.

simplyjustbased:

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

`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!

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