Find touching face in a multi-body part

Programming and macros
laukejas
Posts: 195
Joined: Sun Sep 05, 2021 8:27 am
Answers: 0
x 42
x 108

Find touching face in a multi-body part

Unread post by laukejas »

Hi, I am writing a macro to automate creation of glued self-aligning joints for 3D printed parts. User splits the part, selects a face from the split, and runs the macro, which then creates a sketch on that face, does entity offset, cuts one part, then creates another sketch, offsets from the first sketch (to provide a small clearance), and does a extrude and merge with the second part. Both features done with a small draft angle:

Image

I had initially hoped to achieve this with a Library Feature, but couldn't find a reliable way to make it work with any kind of faces that may result after a cut, since Offset Entities is not dynamic.

So anyway, the problem in my macro is that since user selects face on the first body, macro later has to get the reference to the second body to perform the second operation. Since there might be many bodies in the model, I think the best way to find the other body is to find the touching face, which should be identical to the one user selected. Currently I implemented this by iterating through all the bodies in the model and all their faces, and finding a face that matches the area of the face user selected:

Code: Select all

    
    Dim swBodies As Variant
    swBodies = swPart.GetBodies2(0, False)
    Dim flag As Boolean
    Dim i As Integer
    For i = 0 To UBound(swBodies)
        If Not swBodies(i).Name = firstBody.Name Then
            Dim bodyFaces As Variant
            bodyFaces = swBodies(i).GetFaces
            
            Dim j As Integer
            For j = 0 To UBound(bodyFaces)
                Dim candidateFace As SldWorks.Face2
                Set candidateFace = bodyFaces(j)
                
                Dim candidateFaceArea As Double
                candidateFaceArea = candidateFace.GetArea
                
                If Round(firstFaceArea, 5) = Round(candidateFaceArea, 5) Then
                    Set secondFace = candidateFace
                    Set secondBody = candidateFace.GetBody
                    flag = True
                End If
                
                If flag = True Then Exit For
            Next
        End If
        If flag = True Then Exit For
    Next
There are two problems with this approach:
1. It is woefully inefficient if there are many bodies and faces;
2. If part is simple (say a rectangle extrusion or something) and has multiple splits, macro might latch on to another (non-touching) face that has the same area.

I tried comparing face center coordinates using https://www.codestack.net/solidworks-ap ... arameters/, but apparently however SW outputs these coordinates, makes it impossible to make such comparisons. And it still requires iteration through all bodies and faces.

Is there a simpler and better solution to find a touching face? Before you suggest it - I can't have the macro do the split itself (since then it would be easy to identify both bodies), because splits might be very complicated in actual parts.

Below is the whole macro code. Also attaching a sample part. To run the macro, select one of the faces from the split (use Select Other, or hide one of the bodies), and then run.

Code: Select all

Option Explicit

Const PI = 3.14159265358979
Const offset As Double = 0.0005
Const height As Double = 0.005
Const clearance As Double = 0.0001
Const draftDeg As Double = 10

Dim swApp As SldWorks.SldWorks
Dim swModel As SldWorks.ModelDoc2
Dim swPart As SldWorks.PartDoc

Dim firstFace As SldWorks.Face2
Dim firstBody As SldWorks.Body2
Dim secondFace As SldWorks.Face2
Dim secondBody As SldWorks.Body2

Dim sketchName As String

Sub main()
    Set swApp = Application.SldWorks
    Set swModel = swApp.ActiveDoc
    Set swPart = swModel
    
    SetObjectReferences
    DrawFirstSketchAndFeature
    DrawSecondSketchAndFeature
End Sub

Sub SetObjectReferences()
    Set firstFace = swModel.SelectionManager.GetSelectedObject6(1, -1)
    Set firstBody = firstFace.GetBody
   
    Dim firstFaceArea As Double
    firstFaceArea = firstFace.GetArea
    
    Dim swBodies As Variant
    swBodies = swPart.GetBodies2(0, False)
    Dim flag As Boolean
    Dim i As Integer
    For i = 0 To UBound(swBodies)
        If Not swBodies(i).Name = firstBody.Name Then
            Dim bodyFaces As Variant
            bodyFaces = swBodies(i).GetFaces
            
            Dim j As Integer
            For j = 0 To UBound(bodyFaces)
                Dim candidateFace As SldWorks.Face2
                Set candidateFace = bodyFaces(j)
                
                Dim candidateFaceArea As Double
                candidateFaceArea = candidateFace.GetArea
                
                If Round(firstFaceArea, 5) = Round(candidateFaceArea, 5) Then
                    Set secondFace = candidateFace
                    Set secondBody = candidateFace.GetBody
                    flag = True
                End If
                
                If flag = True Then Exit For
            Next
        End If
        If flag = True Then Exit For
    Next
    
    swPart.ClearSelection2 True
End Sub

Sub DrawFirstSketchAndFeature()
    Dim swEntity As SldWorks.Entity
    Set swEntity = firstFace
    swEntity.Select4 False, swModel.SelectionManager.CreateSelectData
    
    swPart.SketchManager.InsertSketch True
    swPart.SketchOffsetEntities2 -offset, False, False
    sketchName = swModel.FeatureByPositionReverse(0).Name
    swPart.ClearSelection2 True
    swPart.Extension.SelectByID2 firstBody.Name, "SOLIDBODY", 0, 0, 0, True, 8, Nothing, 0
    swPart.FeatureManager.FeatureCut4 True, False, False, 0, 0, height, 0, True, False, False, False, draftDeg * PI / 180, 0, False, False, False, False, False, True, False, True, True, False, 0, 0, False, False
    swPart.ClearSelection2 True
End Sub

Sub DrawSecondSketchAndFeature()
    Dim swEntity As SldWorks.Entity
    Set swEntity = secondFace
    swEntity.Select4 False, swModel.SelectionManager.CreateSelectData
    
    swPart.SketchManager.InsertSketch True
    swPart.Extension.SelectByID2 sketchName, "SKETCH", 0, 0, 0, False, 0, Nothing, 0
    swPart.SketchOffsetEntities2 clearance, False, False
    swPart.ClearSelection2 True
    swPart.Extension.SelectByID2 secondBody.Name, "SOLIDBODY", 0, 0, 0, True, 8, Nothing, 0
    swPart.FeatureManager.FeatureExtrusion2 True, False, False, 0, 0, height - clearance, 0, True, False, False, False, draftDeg * PI / 180, 0, False, False, False, False, True, True, False, 0, 0, False
    swPart.ClearSelection2 True
End Sub

P.S. I forgot that I can use Face2.IsCoincident to check if faces are on the same plane, but that still leaves possibility of messing up if there are multiple faces of same size and on the same plane (parallel) in the model, and still requires to do these iterations through every single face in the model.
Attachments
test joint macro.SLDPRT
(65.77 KiB) Downloaded 47 times
mario malic
Posts: 9
Joined: Thu Apr 13, 2023 4:19 am
Answers: 1
Location: Croatia
x 8
x 2

Re: Find touching face in a multi-body part

Unread post by mario malic »

Is it feasible for user to select any feature (solid/surface/edge/point) on the second body so you can get its reference, then your macro can only search for faces on the second body? Otherwise, you can try finding bodies whose bounding box (GetBodyBox method) intersects, however, it's not robust.


You can use a dot product of face normals (face of the first body with all faces of the second body) which should be close or equal to -1 meaning, face vectors are facing the opposite direction. If there are multiple, then compare the surface of found faces, still, this is not a guarantee that matched face is exactly the one you are looking for.
Looking for opportunities related to SolidWorks API usage.
laukejas
Posts: 195
Joined: Sun Sep 05, 2021 8:27 am
Answers: 0
x 42
x 108

Re: Find touching face in a multi-body part

Unread post by laukejas »

mario malic wrote: Sat Apr 13, 2024 11:27 am Is it feasible for user to select any feature (solid/surface/edge/point) on the second body so you can get its reference, then your macro can only search for faces on the second body? Otherwise, you can try finding bodies whose bounding box (GetBodyBox method) intersects, however, it's not robust.


You can use a dot product of face normals (face of the first body with all faces of the second body) which should be close or equal to -1 meaning, face vectors are facing the opposite direction. If there are multiple, then compare the surface of found faces, still, this is not a guarantee that matched face is exactly the one you are looking for.
Some good ideas there, thank you. Unfortunately GetBodyBox is not very performant, it would probably take longer than just iterating through faces and bodies. Face normal comparison might work, I will try to implement that check. I suppose the most robust tool would be InterferenceDetectionManager with TreatCoincidenceAsInterference option enabled, but it is slow as heck...
User avatar
josh
Posts: 293
Joined: Thu Mar 11, 2021 1:05 pm
Answers: 16
x 22
x 500

Re: Find touching face in a multi-body part

Unread post by josh »

I would use the user-selected point on the first face (make sure you use GetClosestPointOn with the values from GetSelectionPoint), along with its face normal, as inputs to RayIntersections. If the two faces are still coincident, you might have to offset your start point coordinates backward along the face normal by just a small amount.

Of course, nothing is going to be more "performant" than just having the user select both matching faces. Unless you're having them do a whole lot more selections, I think selecting two surfaces is not too onerous a task to require.
User avatar
JSculley
Posts: 638
Joined: Tue May 04, 2021 7:28 am
Answers: 55
x 9
x 868

Re: Find touching face in a multi-body part

Unread post by JSculley »

laukejas wrote: Sat Apr 13, 2024 7:10 am Since there might be many bodies in the model, I think the best way to find the other body is to find the touching face, which should be identical to the one user selected.
Is there a guarantee the faces are 'identical'? What about a part like this:
image.png
User avatar
josh
Posts: 293
Joined: Thu Mar 11, 2021 1:05 pm
Answers: 16
x 22
x 500

Re: Find touching face in a multi-body part

Unread post by josh »

I think splitting the body in such a way as to create identical faces is a requirement of this function as he's going to be offsetting sketch entities from the separated faces to create this mating glue joint. If you split this body at the joint between "blocks" then the offset thing wouldn't work.
laukejas
Posts: 195
Joined: Sun Sep 05, 2021 8:27 am
Answers: 0
x 42
x 108

Re: Find touching face in a multi-body part

Unread post by laukejas »

josh wrote: Sun Apr 14, 2024 9:06 am I would use the user-selected point on the first face (make sure you use GetClosestPointOn with the values from GetSelectionPoint), along with its face normal, as inputs to RayIntersections. If the two faces are still coincident, you might have to offset your start point coordinates backward along the face normal by just a small amount.

Of course, nothing is going to be more "performant" than just having the user select both matching faces. Unless you're having them do a whole lot more selections, I think selecting two surfaces is not too onerous a task to require.
Thank you, that's a good suggestion. I will try to implement that RayIntersections. True, selecting both faces would make it easier, but it's very awkward to do, since none of them are visible, and you have to do Select Other twice. It is much easier to simply hide one body to reveal one of the touching faces and select it normally.
JSculley wrote: Mon Apr 15, 2024 3:55 pm Is there a guarantee the faces are 'identical'? What about a part like this:
image.png
Yeah, like Josh said, I don't plan on using this macro with such parts. Then again, my macro draws the second sketch on the same face as the original sketch, and offsets from it, rather than from the second face, so theoretically it should still work, assuming user selected the smaller face. But for such case I would have to omit checking if the second face is the same size and shape and hope for the best.
len_1962
Posts: 85
Joined: Fri Apr 09, 2021 9:55 am
Answers: 1
Location: Mesa, Arizona
x 74
x 42
Contact:

Re: Find touching face in a multi-body part

Unread post by len_1962 »

how will it work on parts that are all ready shelled aka wall thickness?

I get alot of parts created by students that need to be broken apart and then have this type of feature.
as you know selecting a face that has inside outside edges will on offset the outer edge..... "No Bueno"

Been doing this for 25 years in SW as a model maker to machine and 3D print plastic parts for prototyping so this really interest me.
User avatar
JSculley
Posts: 638
Joined: Tue May 04, 2021 7:28 am
Answers: 55
x 9
x 868

Re: Find touching face in a multi-body part

Unread post by JSculley »

laukejas wrote: Tue Apr 16, 2024 4:51 am Thank you, that's a good suggestion. I will try to implement that RayIntersections. True, selecting both faces would make it easier, but it's very awkward to do, since none of them are visible, and you have to do Select Other twice. It is much easier to simply hide one body to reveal one of the touching faces and select it normally.
After looking at a few different ways to do this, I think I found a good, fast, efficient method. Here's my test part:
image.png
I can split it using any of the three sketch profiles and get a list of face pairs pretty much instantaneously (90 ms), having only selected the Split feature in the tree:
image.png
How was it done? Like this (C# code, nothing preventing this from being done in VBA):

First get the selected Feature (the Split) and then get the Faces:

Code: Select all

Feature f = selMgr.GetSelectedObject6(1, -1) as Feature;              
object[] faceObjArray = f.GetFaces() as object[];     
Then, I get a Dictionary of face pairs using my matchFaces method:

Code: Select all

Dictionary<Face2,Face2> matchingFaces = matchFaces(faceObjArray);
The magic happens in the matchFaces method. This method loops through the array of faces and compares them two at a time. The comparison is made between the UV parameters (retrieved by calling Face::GetUVBounds) of each face. These parameters will be identical for two matching faces along the split and unique with respect to any non-matching faces, so they are the perfect thing to compare. The method looks like this:

Code: Select all

private Dictionary<Face2, Face2> matchFaces(object[] faceObjArray)
{
	Dictionary<Face2, Face2> matchingFaces = new Dictionary<Face2, Face2>();

	/* Use a double loop to check each possible pairing.  For example, if there were 4
	 * faces (0,1,2,3), there are 6 possible combinations (0,1),(0,2),(0,3),(1,2),(1,3),(2,3)             
	 */
	for (int i = 0; i < faceObjArray.Length; i++)
	{
		for (int j = i + 1; j < faceObjArray.Length; j++)
		{
			Face2 firstFace = faceObjArray[i] as Face2;
			UVBounds firstFaceBounds = new UVBounds(firstFace.GetUVBounds());
			Face2 secondFace = faceObjArray[j] as Face2;
			UVBounds secondFaceBounds = new UVBounds(secondFace.GetUVBounds());
			if (!firstFaceBounds.Equals(secondFaceBounds)) //Faces don't match
			{
				if (matchingFaces.Count == 0)
				{
					matchingFaces.Add(firstFace, null);
					//Debug.Print("Added " + i + " , null");
				}
				else
				{
					if (matchingFaces.ContainsValue(secondFace))
					{
					   //Debug.Print("Value " + j + " already added -- skipping");
						continue;
					}
				}
			}
			else //Faces match
			{
				if (matchingFaces.ContainsKey(firstFace) && !matchingFaces.ContainsValue(secondFace))
				{
					//First face was found, add the second face as its partner
					//Debug.Print("Found first " + i + " Adding match " + j);
					matchingFaces[firstFace] = secondFace;
				}
				else if (matchingFaces.ContainsKey(secondFace) && !matchingFaces.ContainsValue(firstFace))
				{
					//Second face was found, add first face as its partner
				   // Debug.Print("Found second" + j + " Adding match " +i);
					matchingFaces[secondFace] = firstFace;
				}
				else
				{
					//Neither face was found, add them as a pair
					//Debug.Print("Adding full pair " + i + "," + j);
					matchingFaces.Add(firstFace, secondFace);
				}
			}
		}
	}
	return matchingFaces;
}           
The method uses a little helper class to hold the individual UV data elements and to provide an Equals method to easily compare them:

Code: Select all

 public class UVBounds
    {
        private double uMin, uMax, vMin, vMax;
        public UVBounds(object uvDataObj)
        {
            double[] uvData = uvDataObj as double[];
            this.uMin = uvData[0];
            this.uMax = uvData[1];
            this.vMin = uvData[2];
            this.vMax = uvData[3];
        }

        public override bool Equals(object o)
        {
            if (!(o is UVBounds))
            {
                return false;
            }
            UVBounds b = o as UVBounds;
            return this.uMin == b.uMin &&
                this.uMax == b.uMax &&
                this.vMin == b.vMin &&
                this.vMax == b.vMax;
        }
    }
Once you have your matched pairs of faces, you can do whatever you want.
laukejas
Posts: 195
Joined: Sun Sep 05, 2021 8:27 am
Answers: 0
x 42
x 108

Re: Find touching face in a multi-body part

Unread post by laukejas »

len_1962 wrote: Tue Apr 16, 2024 10:09 am how will it work on parts that are all ready shelled aka wall thickness?

I get alot of parts created by students that need to be broken apart and then have this type of feature.
as you know selecting a face that has inside outside edges will on offset the outer edge..... "No Bueno"

Been doing this for 25 years in SW as a model maker to machine and 3D print plastic parts for prototyping so this really interest me.
Sorry for a late reply. In current implementation, I don't think it would work with shelled parts, since the face would have multiple contours, while Offset Entities only uses outer contour. There should be a way to get the inner contour as well. For now, a quick workaround should be splitting the mating faces into two on both parts so that they shelled face does not contain a continuous loop around the part, and then run this macro on each of these two faces. Haven't tested it, but it should work.
JSculley wrote: Wed Apr 17, 2024 3:23 pm After looking at a few different ways to do this, I think I found a good, fast, efficient method. Here's my test part:
image.png
I can split it using any of the three sketch profiles and get a list of face pairs pretty much instantaneously (90 ms), having only selected the Split feature in the tree:
image.png
How was it done? Like this (C# code, nothing preventing this from being done in VBA):

First get the selected Feature (the Split) and then get the Faces:

Code: Select all

Feature f = selMgr.GetSelectedObject6(1, -1) as Feature;              
object[] faceObjArray = f.GetFaces() as object[];     
Then, I get a Dictionary of face pairs using my matchFaces method:

Code: Select all

Dictionary<Face2,Face2> matchingFaces = matchFaces(faceObjArray);
The magic happens in the matchFaces method. This method loops through the array of faces and compares them two at a time. The comparison is made between the UV parameters (retrieved by calling Face::GetUVBounds) of each face. These parameters will be identical for two matching faces along the split and unique with respect to any non-matching faces, so they are the perfect thing to compare. The method looks like this:

Code: Select all

private Dictionary<Face2, Face2> matchFaces(object[] faceObjArray)
{
	Dictionary<Face2, Face2> matchingFaces = new Dictionary<Face2, Face2>();

	/* Use a double loop to check each possible pairing.  For example, if there were 4
	 * faces (0,1,2,3), there are 6 possible combinations (0,1),(0,2),(0,3),(1,2),(1,3),(2,3)             
	 */
	for (int i = 0; i < faceObjArray.Length; i++)
	{
		for (int j = i + 1; j < faceObjArray.Length; j++)
		{
			Face2 firstFace = faceObjArray[i] as Face2;
			UVBounds firstFaceBounds = new UVBounds(firstFace.GetUVBounds());
			Face2 secondFace = faceObjArray[j] as Face2;
			UVBounds secondFaceBounds = new UVBounds(secondFace.GetUVBounds());
			if (!firstFaceBounds.Equals(secondFaceBounds)) //Faces don't match
			{
				if (matchingFaces.Count == 0)
				{
					matchingFaces.Add(firstFace, null);
					//Debug.Print("Added " + i + " , null");
				}
				else
				{
					if (matchingFaces.ContainsValue(secondFace))
					{
					   //Debug.Print("Value " + j + " already added -- skipping");
						continue;
					}
				}
			}
			else //Faces match
			{
				if (matchingFaces.ContainsKey(firstFace) && !matchingFaces.ContainsValue(secondFace))
				{
					//First face was found, add the second face as its partner
					//Debug.Print("Found first " + i + " Adding match " + j);
					matchingFaces[firstFace] = secondFace;
				}
				else if (matchingFaces.ContainsKey(secondFace) && !matchingFaces.ContainsValue(firstFace))
				{
					//Second face was found, add first face as its partner
				   // Debug.Print("Found second" + j + " Adding match " +i);
					matchingFaces[secondFace] = firstFace;
				}
				else
				{
					//Neither face was found, add them as a pair
					//Debug.Print("Adding full pair " + i + "," + j);
					matchingFaces.Add(firstFace, secondFace);
				}
			}
		}
	}
	return matchingFaces;
}           
The method uses a little helper class to hold the individual UV data elements and to provide an Equals method to easily compare them:

Code: Select all

 public class UVBounds
    {
        private double uMin, uMax, vMin, vMax;
        public UVBounds(object uvDataObj)
        {
            double[] uvData = uvDataObj as double[];
            this.uMin = uvData[0];
            this.uMax = uvData[1];
            this.vMin = uvData[2];
            this.vMax = uvData[3];
        }

        public override bool Equals(object o)
        {
            if (!(o is UVBounds))
            {
                return false;
            }
            UVBounds b = o as UVBounds;
            return this.uMin == b.uMin &&
                this.uMax == b.uMax &&
                this.vMin == b.vMin &&
                this.vMax == b.vMax;
        }
    }
Once you have your matched pairs of faces, you can do whatever you want.
This is really great. Thank you very much for taking the time to write this code. I will try to integrate it into my macro when I have the time. Most awesome!
Post Reply