@@ -979,6 +979,27 @@ def addToCutList(tuples):
979979 sceneClean ()
980980
981981
982+ # As part of projecting faces to the XY plane, handling BSplines requires
983+ # removing projections that double-back on themselves. This function is used
984+ # in that check.
985+ def vectorsOnStraightLine (v ):
986+ if len (v ) <= 1 :
987+ return False
988+ if len (v ) == 2 :
989+ return True
990+
991+ v0 = v [0 ] - v [1 ]
992+ for k in v [2 :]:
993+ dxc = k .x - v [0 ].x
994+ dyc = k .y - v [0 ].y
995+
996+ cross = dxc * v0 .y - dyc * v0 .x
997+ if abs (cross ) > 1e-5 :
998+ return False
999+
1000+ return True
1001+
1002+
9821003def projectFacesToXY (faces , minEdgeLength = 1e-10 ):
9831004 """projectFacesToXY(faces, minEdgeLength)
9841005 Calculates the projection of the provided list of faces onto the XY plane.
@@ -1001,18 +1022,40 @@ def projectFacesToXY(faces, minEdgeLength=1e-10):
10011022 # NOTE: Wires/edges get clipped if we have an "exact fit" bounding box
10021023 projface = Path .Geom .makeBoundBoxFace (f .BoundBox , offset = 1 , zHeight = 0 )
10031024
1004- # NOTE: Cylinders, cones, and spheres are messy:
1025+ # NOTE: Cylinders, cones, B-splines, and spheres are messy:
10051026 # - Internal representation of non-truncted cones and spheres includes
10061027 # the "tip" with a ~0-area closed edge. This is different than the
10071028 # "isNull() note" at the top in magnitude
10081029 # - Projecting edges doesn't naively work due to the way seams are handled
10091030 # - There may be holes at either end that may or may not line up- any
10101031 # overlap is a hole in the projection
1011- if type (f .Surface ) in [Part .Cone , Part .Cylinder , Part .Sphere ]:
1032+ # - BSplines may not project nicely- they may double-back on themselves
1033+ # if they're (eg) an arc in the XZ plane
1034+ if type (f .Surface ) in [Part .Cone , Part .Cylinder , Part .Sphere ] or (
1035+ type (f .Surface ) is Part .SurfaceOfExtrusion
1036+ and sum ([e .isSeam (f ) for e in f .OuterWire .Edges ])
1037+ ):
10121038 # This gets most of the face outline, but since cylinder/cone faces
10131039 # are hollow, if the ends overlap in the projection there may be a
10141040 # hole we need to remove from the solid projection
1015- oface = Part .makeFace (TechDraw .findShapeOutline (f , 1 , projdir ))
1041+ if type (f .Surface ) is Part .SurfaceOfExtrusion :
1042+ el = []
1043+ for e in TechDraw .findShapeOutline (f , 1 , projdir ).Edges :
1044+ # Problematic splines are only those that are lines that
1045+ # double back on themselves
1046+ if type (e .Curve ) is Part .BSplineCurve and vectorsOnStraightLine (
1047+ e .Curve .getPoles ()
1048+ ):
1049+ el .append (Part .makeLine (e .Vertexes [0 ].Point , e .Vertexes [- 1 ].Point ))
1050+ else :
1051+ el .append (e )
1052+
1053+ # findShapeOutline doesn't always put edges in order -> open wire
1054+ ew = TechDraw .edgeWalker (el , True )
1055+
1056+ oface = Part .makeFace (ew )
1057+ else :
1058+ oface = Part .makeFace (TechDraw .findShapeOutline (f , 1 , projdir ))
10161059
10171060 # "endfacewires" is JUST the end faces of a cylinder/cone, used to
10181061 # determine if there's a hole we can see through the shape that
@@ -1025,12 +1068,16 @@ def projectFacesToXY(faces, minEdgeLength=1e-10):
10251068 # a wire from the list, else this could nicely be one line.
10261069 projwires = []
10271070 for w in endfacewires :
1028- pp = projface .makeParallelProjection (w , projdir ).Wires
1029- if pp :
1071+ if pp := projface .makeParallelProjection (w , projdir ).Wires :
10301072 projwires .append (pp [0 ])
10311073
10321074 if len (projwires ) > 1 :
1033- faces = [Part .makeFace (x ) for x in projwires ]
1075+ # FIXME: Occasionally an open projected wire is present that
1076+ # doesn't appear to be related to the model geometry. This check
1077+ # prevents "wire not closed" errors in those cases, but the root
1078+ # cause has not been identified.
1079+ faces = [Part .makeFace (x ) for x in projwires if x .isClosed ()]
1080+
10341081 overlap = faces [0 ].common (faces [1 :])
10351082 outfaces .append (oface .cut (overlap ))
10361083 else :
@@ -1047,8 +1094,21 @@ def projectFacesToXY(faces, minEdgeLength=1e-10):
10471094 outfaces .append (Part .makeFace (facewires ))
10481095 if outfaces :
10491096 fusion = outfaces [0 ].fuse (outfaces [1 :])
1050- # removeSplitter fixes occasional concatenate issues for some face orders
1051- return DraftGeomUtils .concatenate (fusion .removeSplitter ())
1097+ # Best effort to merge faces into one nice clean one without internal
1098+ # edges or similar. Failure to do so can result in incorrect regions
1099+ # being machined for unknown reasons- presumably something to do with
1100+ # the resulting face having many subfaces.
1101+ #
1102+ # removeSplitter is sometimes required to make concatenate succeed.
1103+ try :
1104+ fusion = fusion .removeSplitter ()
1105+ except :
1106+ Path .Log .warning ("projectFacesToXY: removeSplitter failure" )
1107+ try :
1108+ fusion = DraftGeomUtils .concatenate (fusion )
1109+ except :
1110+ Path .Log .warning ("projectFacesToXY: concatenate failure" )
1111+ return fusion
10521112 else :
10531113 return Part .Shape ()
10541114
@@ -1547,7 +1607,8 @@ def _regionChildSplitterHelper(regions, areInsideRegions):
15471607 continue
15481608
15491609 # If the region cut with the stock at a new depth is different than
1550- # the original cut, we need to split this region
1610+ # the original cut, we need to split this region. Only applies if
1611+ # the region cut with the stock is non-empty
15511612 # The new region gets all of the children, and becomes a child of
15521613 # the existing region.
15531614 parentdepths = depths [0 :1 ]
0 commit comments