Skip to content

Commit 6c66564

Browse files
authored
Merge pull request FreeCAD#21220 from dbtayl/fix_adaptive_bspline
CAM: Adaptive: Fix bspline processing
2 parents fef12ad + 4f9ef13 commit 6c66564

File tree

1 file changed

+70
-9
lines changed

1 file changed

+70
-9
lines changed

src/Mod/CAM/Path/Op/Adaptive.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
9821003
def 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

Comments
 (0)