REVISIT: Help with computing shared face.

Need help, or want to share a macro? Post here!
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
keithsloan52
Veteran
Posts: 2756
Joined: Mon Feb 27, 2012 5:31 pm

Re: Help with computing shared face.

Post by keithsloan52 »

edwilliams16 wrote: Tue Jun 28, 2022 8:06 pm So, given a method powerful enough to match curved surfaces, surely it could handle flat ones?
Likely to be expensive in terms of processing so probably better to check for flat.
edwilliams16
Veteran
Posts: 3108
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Help with computing shared face.

Post by edwilliams16 »

keithsloan52 wrote: Tue Jun 28, 2022 9:06 pm Likely to be expensive in terms of processing so probably better to check for flat.
Possibly. To quote Donald Knuth:
"We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%"

Optimizing a routine that doesn't yet exist is premature IMO.
edwilliams16
Veteran
Posts: 3108
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Help with computing shared face.

Post by edwilliams16 »

This finds the common faces between two objects with general B-Spline surfaces. It's very slow - ~30 sec on my 12yo laptop, but it works.
You can optimize special cases to your heart's content, or try to find another faster, general face-matching algorithm.

It was frustrating to find that Part 3DOffset only implements one direction of offset. However, the makeOffsetShape()
method does not have this limitation.
There's probably at least a factor of two in speed immediately available by offsetting the two surfaces in opposite directions rather than both ways and fusing them. I didn't check if the direction was always determinate.

Code: Select all

#find the any common face areas between Cut and Extrude001
doc = App.ActiveDocument
obj1 = doc.getObject("Cut")
obj2 = doc.getObject("Extrude001")
faces1 = obj1.Shape.Faces
faces2 = obj2.Shape.Faces
#shared are faces1[2] and faces1[5] with faces2[0]

def makeThickFace(obj, thick = 0.1):
    offsets =[]
    for face in obj.Shape.Faces:
    	fplus = face.makeOffsetShape(thick/2, thick/5,fill = True)
    	fminus = face.makeOffsetShape(-thick/2, thick/5,fill = True)
    	f = fplus.fuse(fminus)
    	offsets.append((f, face))
    return offsets


obj1offsets = makeThickFace(obj1)
obj2offsets = makeThickFace(obj2)
#show the ones that will be in common
Part.show(obj1offsets[2][0], 'ThickFace')
Part.show(obj1offsets[5][0], 'ThickFace')
Part.show(obj2offsets[0][0], 'ThickFace')

com =[]
for o1, f1 in obj1offsets:
	for o2, f2 in obj2offsets:
		c = o1.common(o2)
		if c.Area > 0:
		    com.append((c,f1,f2))

print(f' Volume and Area of Intersections {[(c.Volume, c.Area) for c, f1, f2 in com]}')

# [max([face.Area for face in c.Faces]) for c, f1, f2 in common]

#This indicates that common[1] and common[4] are surface intersections, the rest are 
#edge/surface intersection with much smaller volume
#some tweaking will be required to distinguish these
#one possibility is the volumes of the latter will scale quadratically with thick

#To return the faces intersect the thickened intersections back with their parent  face
Part.show(com[1][0].common(com[1][1]), 'SharedFace')
Part.show(com[4][0].common(com[4][1]), 'SharedFace')

Screen Shot 2022-06-28 at 4.48.31 PM.png
Screen Shot 2022-06-28 at 4.48.31 PM.png (44.17 KiB) Viewed 994 times
Screen Shot 2022-06-28 at 4.53.56 PM.png
Screen Shot 2022-06-28 at 4.53.56 PM.png (12.8 KiB) Viewed 994 times
https://www.dropbox.com/s/bzzzi6bz6989v ... FCStd?dl=0
keithsloan52
Veteran
Posts: 2756
Joined: Mon Feb 27, 2012 5:31 pm

Re: Help with computing shared face.

Post by keithsloan52 »

edwilliams16 wrote: Wed Jun 29, 2022 3:08 am This finds the common faces between two objects with general B-Spline surfaces. It's very slow - ~30 sec on my 12yo laptop, but it works.
You can optimize special cases to your heart's content, or try to find another faster, general face-matching algorithm.
If I understand your proposed algorithm correctly in the attached sketch it would find the edge between the Box and the Cylinder and that is not a Face, or do I not understand your algorithm.
CubeCyl.jpg
CubeCyl.jpg (151.12 KiB) Viewed 964 times

Also what thickness to use, some physicists model GDML objects at the micro and nano meter level
edwilliams16
Veteran
Posts: 3108
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Help with computing shared face.

Post by edwilliams16 »

If you look at the example in my file, with two objects A and B, it finds the common face between AcutB and B. It was a convenient way of generating an arbitary matching surface.

The thickness would have to scale with the model dimensions.
edwilliams16
Veteran
Posts: 3108
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Help with computing shared face.

Post by edwilliams16 »

It is more efficient to only thicken one surface and the intersect it with an unthickened one. It works well and quickly with simple surfaces like flat and cylindrical ones. Here we have a cube corner penetrating another cube:
Screen Shot 2022-06-29 at 5.26.16 PM.png
Screen Shot 2022-06-29 at 5.26.16 PM.png (28.15 KiB) Viewed 904 times
The algorithm quickly generates the three common faces:
Screen Shot 2022-06-29 at 5.33.01 PM.png
Screen Shot 2022-06-29 at 5.33.01 PM.png (9.39 KiB) Viewed 904 times
with the following script

Code: Select all

makecom1 = False  #if True thicken obj1 faces as well
visibleNumber = 3  #toggle remaining invisible to reduce clutter
thickness = 1.0 #how much to thicken faces

def sortbyarea(comlist):
    ''' sort list of commons by descending order of area '''
    paired = [(c, c[0].Area) for c in comlist]
    spaired = sorted(paired, key = lambda paired: -paired[1])
    return [c for c, area in spaired]

#find the any common face areas beteen Cut and Extrude001
doc = App.ActiveDocument
obj1 = doc.getObject("Cut")
#choose the tool object here
#obj2 = doc.getObject("Extrude001")
obj2 = doc.getObject("Box")
#obj2 = doc.getObject('Cylinder')
faces1 = obj1.Shape.Faces
faces2 = obj2.Shape.Faces
#shared is faces1[3] with faces2[0]

def makeThickFace(obj, thick = 1, pm = 0):
    #pm =0 both, =1 plus , = -1 minus
    offsets =[]
    for face in obj.Shape.Faces:
        if pm >= 0:
            fplus = face.makeOffsetShape(thick/2, thick/50, fill = True)
            f = fplus
        if pm <= 0:
            fminus = face.makeOffsetShape(-thick/2, thick/50, fill = True)
            f = fminus
        if pm == 0:
            f = fplus.fuse(fminus)
        offsets.append((f, face))
    return offsets


if makecom1:
    obj1offsets = makeThickFace(obj1, thick = thickness, pm = 0)

obj2offsets = makeThickFace(obj2, thick = thickness, pm = 0)
#show the ones that will be in common
#Part.show(obj1offsets[3][0], 'ThickFace')
#Part.show(obj2offsets[0][0], 'ThickFace')


com =[]
for f1 in faces1:
    for o2, f2 in obj2offsets:
        #c = f1.common(o2)
        c = o2.common(f1)  #does order matter?
        #if c.Area != 0:
        if len(c.Faces) != 0:
            com.append((c,f1))

App.Console.PrintMessage(f' Area of Intersections com {[c.Area for c, f in sortbyarea(com)]}\n')

if makecom1:
    com1 =[]
    for f2 in faces2:
        for o1, f1 in obj1offsets:
            #c = f2.common(o1)
            c = o1.common(f2)
            #if c.Area != 0:
            if len(c.Faces)  != 0:
                com1.append((c,f2))
    App.Console.PrintMessage(f' Area of Intersections com1  {[c.Area for c, f in sortbyarea(com1)]}\n')

for i, cc in enumerate(sortbyarea(com)):
    p = Part.show(cc[0],'Com')
    if i >= visibleNumber:
        p.Visibility = False

if makecom1:
    for i, cc in enumerate(sortbyarea(com1)):
        p = Part.show(cc[0],'Com1')
        if i >= visibleNumber:
            p.Visibility = False

However the thickened face intersection with the surfaces also creates ribbon-like intersections associated the edges. I sorted the intersections by area. The first three are the desired ones, the rest are spurious. This needs to be automated.

Setting

Code: Select all

obj2 = doc.getObject('Cylinder')
in the script and using the next file which cuts a chunk of cylinder from a cube. The script generates the required single face plus the edge-related detritus. (see next post - file limit...)
sharedfacetester3.FCStd
(43.46 KiB) Downloaded 20 times
Attachments
Screen Shot 2022-06-29 at 5.41.55 PM.png
Screen Shot 2022-06-29 at 5.41.55 PM.png (37.87 KiB) Viewed 904 times
sharedfacetester4.FCStd
(23.77 KiB) Downloaded 14 times
edwilliams16
Veteran
Posts: 3108
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Help with computing shared face.

Post by edwilliams16 »

Screen Shot 2022-06-29 at 5.45.39 PM.png
Screen Shot 2022-06-29 at 5.45.39 PM.png (11.97 KiB) Viewed 898 times
With more complex B-Spline - B-Spline intersections the Part common() method is a bit flakey. The thickness parameters have to be tweaked and it is sensitive to seams close to the edges. But it seems it can be made to work, but would be trickier to automate. One might have to provide user-adjustable parameters if it fails.


Screen Shot 2022-06-29 at 5.56.24 PM.png
Screen Shot 2022-06-29 at 5.56.24 PM.png (10.17 KiB) Viewed 898 times
Screen Shot 2022-06-29 at 5.55.59 PM.png
Screen Shot 2022-06-29 at 5.55.59 PM.png (41.67 KiB) Viewed 898 times
This is a 3.6MB file, so:
https://www.dropbox.com/s/b0l2zba163ndr ... FCStd?dl=0

Intersecting two thickened surfaces seems a bit more robust with these very complex faces, but it is even slower.
We seem to be running up against the limitations of Open Cascade's Boolean Common.
vm4dim
Posts: 129
Joined: Tue Nov 23, 2021 1:05 am

Re: Help with computing shared face.

Post by vm4dim »

Maybe this
face.png
face.png (28.04 KiB) Viewed 871 times

Code: Select all

import FreeCAD
import FreeCADGui
import Part

doc = FreeCAD.newDocument("Face")

b = doc.addObject("Part::Box", "Box")
c1 = doc.addObject("Part::Cylinder", "Cylinder_1")
c2 = doc.addObject("Part::Cylinder", "Cylinder_2")

c1.Placement = FreeCAD.Placement(FreeCAD.Vector( 0,0,1),FreeCAD.Rotation(FreeCAD.Vector(0,0,1),0))
c2.Placement = FreeCAD.Placement(FreeCAD.Vector(10,0,1),FreeCAD.Rotation(FreeCAD.Vector(0,0,1),0))

doc.recompute()

s1 = doc.addObject("Part::Feature", "Shell_1")
s2 = doc.addObject("Part::Feature", "Shell_2")

s1.Shape = Part.Shell(c1.Shape.Faces).removeSplitter()
s2.Shape = Part.Shell(c2.Shape.Faces).removeSplitter()

doc.recompute()

r1 = doc.addObject("Part::MultiCommon", "Result_1")
r2 = doc.addObject("Part::MultiCommon", "Result_2")

r1.Shapes = [s1, b]
r2.Shapes = [s2, b]

doc.recompute()

rr2 = doc.addObject("Part::Reverse","Result-2_Rev").Source = r2

b.Visibility  = False
c1.Visibility = False
c2.Visibility = False
s1.Visibility = False
s2.Visibility = False
r2.Visibility = False

FreeCADGui.activeDocument().activeView().viewIsometric()
FreeCADGui.runCommand('Std_OrthographicCamera',1)
FreeCADGui.runCommand('Std_PerspectiveCamera',0)

doc.recompute()

FreeCADGui.SendMsgToActiveView("ViewFit")
edwilliams16
Veteran
Posts: 3108
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Help with computing shared face.

Post by edwilliams16 »

vm4dim wrote: Thu Jun 30, 2022 8:20 am
@keithsloan52

The OP wants to find shared faces between pairs of objects - not necessarily created by a cut, so I had to modify quite a bit. I used the Part::MultiCommon (which may be based on the common() method for shapes, but it seems more robust) intersecting a shell with the faces of the second object.

So the following script works quite well. Select the two candidate objects and run it.

Code: Select all

import FreeCAD
import FreeCADGui
import Part

sel = Gui.Selection.getSelection()
if len(sel) == 2:
    obj1, obj2 = sel
else:
    App.Console.PrintMessage('Select two objects in the Tree View')


doc = App.ActiveDocument
#obj1 = doc.getObject("Extrude001")
sh1 = doc.addObject("Part::Feature", "Shell_1")
sh1.Shape = Part.Shell(obj1.Shape.Faces).removeSplitter()
sh1.Visibility = False
#obj2 = doc.getObject("Cut")
indexlist =[]
for i, face in enumerate(obj2.Shape.Faces):
    sh2 = doc.addObject("Part::Feature", "Shell_2")
    sh2.Shape = Part.Shell([face]).removeSplitter()
    sh2.Visibility = False
    doc.recompute(None,True,True)
    rcom = doc.addObject("Part::MultiCommon", "Result_Common")
    rcom.Shapes = [sh2, sh1]
    doc.recompute(None,True,True)
    if len(rcom.Shape.Faces) == 0: # checks for null intersections, but get warning message
        doc.removeObject(rcom.Name)
        doc.removeObject(sh2.Name) 
    else:
        indexlist.append(i) 
     



indexlist1 =[]
for i, face1 in enumerate(obj1.Shape.Faces):
    sh1 = doc.addObject("Part::Feature", "Shell_1")
    sh1.Shape = Part.Shell([face1]).removeSplitter()
    sh1.Visibility = False
    for face2 in [obj2.Shape.Faces[k] for k in indexlist]:
        sh2 = doc.addObject("Part::Feature", "Shell_2")
        sh2.Shape = Part.Shell([face2]).removeSplitter()
        sh2.Visibility = False
        doc.recompute(None,True,True)
        rcom = doc.addObject("Part::MultiCommon", "Result_Common")
        rcom.Shapes = [sh2, sh1]
        doc.recompute(None,True,True)
        if len(rcom.Shape.Faces) != 0:
            indexlist1.append(i) 
        doc.removeObject(rcom.Name)
        doc.removeObject(sh2.Name) 
    doc.removeObject(sh1.Name)


App.Console.PrintMessage(f' {obj2.Name} Face indices {indexlist}\n')
App.Console.PrintMessage(f' {obj1.Name} Face indices {indexlist1}\n')

             
I didn't mess with links or part containers. I was just working the geometry issue.

It creates the shared interface and lists the face indices that are members of it.
There's one annoyance, when the common between two objects is null, I get an error message in the report view, which I didn't yet figure how to suppress - some try/except block, I expect.
Screen Shot 2022-06-30 at 5.59.42 PM.png
Screen Shot 2022-06-30 at 5.59.42 PM.png (31.28 KiB) Viewed 823 times
Screen Shot 2022-06-30 at 6.04.44 PM.png
Screen Shot 2022-06-30 at 6.04.44 PM.png (12.33 KiB) Viewed 823 times
Attachments
sharedfacetester5.FCStd
(53.66 KiB) Downloaded 16 times
edwilliams16
Veteran
Posts: 3108
Joined: Thu Sep 24, 2020 10:31 pm
Location: Hawaii
Contact:

Re: Help with computing shared face.

Post by edwilliams16 »

Since I started working on this, it appears I've obtained an improved common() method, which was failing earlier, causing the diversion into thickening faces. This appears to be unnecessary now.
Help now says
Help on built-in function common:

common(...) method of Part.Solid instance
Intersection of this and a given (list of) topo shape.
common(tool) -> Shape
or
common((tool1,tool2,...),[tolerance=0.0]) -> Shape
--
Supports:
- Fuzzy Boolean operations (global tolerance for a Boolean operation)
- Support of multiple arguments for a single Boolean operation (s1 AND (s2 OR s3))
- Parallelization of Boolean Operations algorithm

OCC 6.9.0 or later is required.


and (non-solid!) objects on which it failed earlier now succeed. All my test files now succeed using default tolerance. Assuming that the Part::MultiCommon in my previous post was based on this new common, I rewrote the script to avoid making and deleting document objects. It is now quite fast in all tested cases, and is free of warning messages.

Code: Select all

'''
Script to create common face(s) of two objects. Select them and run.
A compound of the shell intersection is created and the corresponding participating faces
are enumerated in the report window.
Ed Williams  (edwilliams16) 7/1/22
'''


import FreeCAD
import FreeCADGui
import Part

tolerance = 0  #default to global tolerance - tweak if required
sel = Gui.Selection.getSelection()
if len(sel) == 2:
    obj1, obj2 = sel
else:
    App.Console.PrintMessage('Select two objects in the Tree View')

doc = App.ActiveDocument
shp1 = Part.Shell(obj1.Shape.Faces).removeSplitter()
comlist =[]
for i, face in enumerate(obj2.Shape.Faces):
    shp2 = Part.Shell([face]).removeSplitter()
    #doc.recompute(None,True,True)
    comshp = shp1.common(shp2, tolerance)
    if len(comshp.Faces) != 0:
        comlist.append((comshp, i))


indexlist1 =[]
for i, face1 in enumerate(obj1.Shape.Faces):
    for face2, j in comlist:
        comshp = face1.common(face2, tolerance)
        if len(comshp.Faces) != 0:
            indexlist1.append(i)


if len(comlist) > 0:
    comdoc = doc.addObject('Part::Feature', obj1.Name + '_and_' + obj2.Name)
    comdoc.Shape = Part.Compound([l[0] for l in comlist]).removeSplitter()
    doc.recompute()
    App.Console.PrintMessage(f' {obj2.Name} Face indices {[com[1] for com in comlist]}\n')
    App.Console.PrintMessage(f' {obj1.Name} Face indices {indexlist1}\n')
else:
    App.Console.PrintMessage('No common faces found\n')

Post Reply