CAM: Adaptive: Make machined regions respect stock and model in 3D, not just the 2D projections of the stock and selected machining bounding box
This commit is contained in:
@@ -396,10 +396,109 @@ class TestPathAdaptive(PathTestBase):
|
||||
break
|
||||
self.assertTrue(isInBox, "No paths originating within the inner hole.")
|
||||
|
||||
def test08(self):
|
||||
"""test08() Tests stock awareness- avoids cutting into the model regardless
|
||||
of bounding box selected."""
|
||||
# Instantiate a Adaptive operation and set Base Geometry
|
||||
adaptive = PathAdaptive.Create("Adaptive")
|
||||
adaptive.Base = [(self.doc.Fusion, ["Face3", "Face10"])] # (base, subs_list)
|
||||
adaptive.Label = "test08+"
|
||||
adaptive.Comment = "test08() Verify path generated on adjacent, combined Face3 and Face10. The Z heights are different. Result should be the combination at Z=10 (faces from (0,0) to (40,25), minus tool radius), and only the lower face at Z=5: (15,0) to (40,25)."
|
||||
|
||||
# Set additional operation properties
|
||||
setDepthsAndHeights(adaptive, 15, 0)
|
||||
adaptive.FinishingProfile = False
|
||||
adaptive.HelixAngle = 75.0
|
||||
adaptive.HelixDiameterLimit.Value = 1.0
|
||||
adaptive.LiftDistance.Value = 1.0
|
||||
adaptive.StepOver = 75
|
||||
adaptive.UseOutline = False
|
||||
adaptive.setExpression("StepDown", None)
|
||||
adaptive.StepDown.Value = (
|
||||
5.0 # Have to set expression to None before numerical value assignment
|
||||
)
|
||||
|
||||
_addViewProvider(adaptive)
|
||||
self.doc.recompute()
|
||||
|
||||
# Check:
|
||||
# - Bounding box at Z=10 stays within Face3 and Face10- so -X for Face3,
|
||||
# +X and +/-Y for Face10
|
||||
# - bounding box at Z=5 stays within Face10
|
||||
# - No toolpaths at Z=0
|
||||
|
||||
paths = [c for c in adaptive.Path.Commands if c.Name in ["G0", "G00", "G1", "G01"]]
|
||||
toolr = adaptive.OpToolDiameter.Value / 2
|
||||
tol = adaptive.Tolerance
|
||||
|
||||
# Make clean up math below- combine tool radius and tolerance into a
|
||||
# single field that can be added/subtracted to/from bounding boxes
|
||||
moffset = toolr - tol
|
||||
|
||||
zDict = {10: None, 5: None, 0: None}
|
||||
|
||||
getPathBoundaries(paths, zDict)
|
||||
|
||||
# NOTE: Face3 is at Z=10, Face10 is at Z=5
|
||||
bbf3 = self.doc.Fusion.Shape.getElement("Face3").BoundBox
|
||||
bbf10 = self.doc.Fusion.Shape.getElement("Face10").BoundBox
|
||||
|
||||
okAt10 = (
|
||||
zDict[10] is not None
|
||||
and zDict[10]["min"][0] >= bbf3.XMin + moffset
|
||||
and zDict[10]["min"][1] >= bbf10.YMin + moffset
|
||||
and zDict[10]["max"][0] <= bbf10.XMax - moffset
|
||||
and zDict[10]["max"][1] <= bbf10.YMax - moffset
|
||||
)
|
||||
|
||||
okAt5 = (
|
||||
zDict[5] is not None
|
||||
and zDict[5]["min"][0] >= bbf10.XMin + moffset
|
||||
and zDict[5]["min"][1] >= bbf10.YMin + moffset
|
||||
and zDict[5]["max"][0] < bbf10.XMax - moffset
|
||||
and zDict[5]["max"][1] < bbf10.YMax - moffset
|
||||
)
|
||||
|
||||
okAt0 = not zDict[0]
|
||||
|
||||
self.assertTrue(okAt10 and okAt5 and okAt0, "Path boundaries outside of expected regions")
|
||||
|
||||
|
||||
# Eclass
|
||||
|
||||
|
||||
def getPathBoundaries(paths, zDict):
|
||||
"""getPathBoundaries(paths, zDict): Takes the list of paths and dictionary
|
||||
of Z depths of interest, and finds the bounding box of the paths at each
|
||||
depth.
|
||||
|
||||
NOTE: You'd think that using Path.BoundBox would give us what we want,
|
||||
but... no, for whatever reason it appears to always extend to (0,0,0)
|
||||
"""
|
||||
last = FreeCAD.Vector(0.0, 0.0, 0.0)
|
||||
# First make sure each element has X, Y, and Z coordinates
|
||||
for p in paths:
|
||||
params = p.Parameters
|
||||
last.x = p.X if "X" in params else last.x
|
||||
last.y = p.Y if "Y" in params else last.y
|
||||
last.z = p.Z if "Z" in params else last.z
|
||||
|
||||
p.X = last.x
|
||||
p.Y = last.y
|
||||
p.Z = last.z
|
||||
|
||||
for z in zDict:
|
||||
zpaths = [k for k in paths if k.Z == z]
|
||||
if not zpaths:
|
||||
zDict[z] = None
|
||||
continue
|
||||
xmin = min([k.X for k in zpaths])
|
||||
xmax = max([k.X for k in zpaths])
|
||||
ymin = min([k.Y for k in zpaths])
|
||||
ymax = max([k.Y for k in zpaths])
|
||||
zDict[z] = {"min": (xmin, ymin), "max": (xmax, ymax)}
|
||||
|
||||
|
||||
def setDepthsAndHeights(op, strDep=20.0, finDep=0.0):
|
||||
"""setDepthsAndHeights(op, strDep=20.0, finDep=0.0)... Sets default depths and heights for `op` passed to it"""
|
||||
|
||||
@@ -421,43 +520,29 @@ def getGcodeMoves(cmdList, includeRapids=True, includeLines=True, includeArcs=Tr
|
||||
"""getGcodeMoves(cmdList, includeRapids=True, includeLines=True, includeArcs=True)...
|
||||
Accepts command dict and returns point string coordinate.
|
||||
"""
|
||||
|
||||
# NOTE: Can NOT just check "if p.get("X")" or similar- that chokes when X is
|
||||
# zero. That becomes especially obvious when Z=0, and moves end up on the
|
||||
# wrong depth
|
||||
gcode_list = list()
|
||||
last = FreeCAD.Vector(0.0, 0.0, 0.0)
|
||||
for c in cmdList:
|
||||
p = c.Parameters
|
||||
name = c.Name
|
||||
if includeRapids and name in ["G0", "G00"]:
|
||||
if (includeRapids and name in ["G0", "G00"]) or (includeLines and name in ["G1", "G01"]):
|
||||
gcode = name
|
||||
x = last.x
|
||||
y = last.y
|
||||
z = last.z
|
||||
if p.get("X"):
|
||||
if "X" in p:
|
||||
x = round(p["X"], 2)
|
||||
gcode += " X" + str(x)
|
||||
if p.get("Y"):
|
||||
gcode += " X" + str(x)
|
||||
if "Y" in p:
|
||||
y = round(p["Y"], 2)
|
||||
gcode += " Y" + str(y)
|
||||
if p.get("Z"):
|
||||
gcode += " Y" + str(y)
|
||||
if "Z" in p:
|
||||
z = round(p["Z"], 2)
|
||||
gcode += " Z" + str(z)
|
||||
last.x = x
|
||||
last.y = y
|
||||
last.z = z
|
||||
gcode_list.append(gcode)
|
||||
elif includeLines and name in ["G1", "G01"]:
|
||||
gcode = name
|
||||
x = last.x
|
||||
y = last.y
|
||||
z = last.z
|
||||
if p.get("X"):
|
||||
x = round(p["X"], 2)
|
||||
gcode += " X" + str(x)
|
||||
if p.get("Y"):
|
||||
y = round(p["Y"], 2)
|
||||
gcode += " Y" + str(y)
|
||||
if p.get("Z"):
|
||||
z = round(p["Z"], 2)
|
||||
gcode += " Z" + str(z)
|
||||
gcode += " Z" + str(z)
|
||||
last.x = x
|
||||
last.y = y
|
||||
last.z = z
|
||||
@@ -470,23 +555,23 @@ def getGcodeMoves(cmdList, includeRapids=True, includeLines=True, includeArcs=Tr
|
||||
i = 0.0
|
||||
j = 0.0
|
||||
k = 0.0
|
||||
if p.get("I"):
|
||||
if "I" in p:
|
||||
i = round(p["I"], 2)
|
||||
gcode += " I" + str(i)
|
||||
if p.get("J"):
|
||||
if "J" in p:
|
||||
j = round(p["J"], 2)
|
||||
gcode += " J" + str(j)
|
||||
if p.get("K"):
|
||||
if "K" in p:
|
||||
k = round(p["K"], 2)
|
||||
gcode += " K" + str(k)
|
||||
|
||||
if p.get("X"):
|
||||
if "X" in p:
|
||||
x = round(p["X"], 2)
|
||||
gcode += " X" + str(x)
|
||||
if p.get("Y"):
|
||||
if "Y" in p:
|
||||
y = round(p["Y"], 2)
|
||||
gcode += " Y" + str(y)
|
||||
if p.get("Z"):
|
||||
if "Z" in p:
|
||||
z = round(p["Z"], 2)
|
||||
gcode += " Z" + str(z)
|
||||
|
||||
@@ -501,7 +586,7 @@ def pathOriginatesInBox(cmd, minPoint, maxPoint):
|
||||
p = cmd.Parameters
|
||||
name = cmd.Name
|
||||
if name in ["G0", "G00", "G1", "G01"]:
|
||||
if p.get("X") and p.get("Y"):
|
||||
if "X" in p and "Y" in p:
|
||||
x = p.get("X")
|
||||
y = p.get("Y")
|
||||
if x > minPoint.x and y > minPoint.y and x < maxPoint.x and y < maxPoint.y:
|
||||
|
||||
@@ -72,6 +72,9 @@ sceneGraph = None
|
||||
scenePathNodes = [] # for scene cleanup afterwards
|
||||
topZ = 10
|
||||
|
||||
# Constants to avoid magic numbers in the code
|
||||
_ADAPTIVE_MIN_STEPDOWN = 0.1
|
||||
|
||||
|
||||
def sceneDrawPath(path, color=(0, 0, 1)):
|
||||
coPoint = coin.SoCoordinate3()
|
||||
@@ -117,7 +120,7 @@ def CalcHelixConePoint(height, cur_z, radius, angle):
|
||||
|
||||
|
||||
def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
|
||||
if len(adaptiveResults) == 0 or len(adaptiveResults[0]["AdaptivePaths"]) == 0:
|
||||
if not adaptiveResults or not adaptiveResults[0]["AdaptivePaths"]:
|
||||
return
|
||||
|
||||
# minLiftDistance = op.tool.Diameter
|
||||
@@ -125,74 +128,55 @@ def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
|
||||
for region in adaptiveResults:
|
||||
p1 = region["HelixCenterPoint"]
|
||||
p2 = region["StartPoint"]
|
||||
r = math.sqrt((p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1]))
|
||||
if r > helixRadius:
|
||||
helixRadius = r
|
||||
helixRadius = max(math.dist(p1[:2], p2[:2]), helixRadius)
|
||||
|
||||
stepDown = obj.StepDown.Value
|
||||
passStartDepth = obj.StartDepth.Value
|
||||
|
||||
if stepDown < 0.1:
|
||||
stepDown = 0.1
|
||||
stepDown = max(obj.StepDown.Value, _ADAPTIVE_MIN_STEPDOWN)
|
||||
|
||||
length = 2 * math.pi * helixRadius
|
||||
|
||||
if float(obj.HelixAngle) < 1:
|
||||
obj.HelixAngle = 1
|
||||
if float(obj.HelixAngle) > 89:
|
||||
obj.HelixAngle = 89
|
||||
obj.HelixAngle = min(89, max(obj.HelixAngle.Value, 1))
|
||||
obj.HelixConeAngle = max(obj.HelixConeAngle, 0)
|
||||
|
||||
if float(obj.HelixConeAngle) < 0:
|
||||
obj.HelixConeAngle = 0
|
||||
|
||||
helixAngleRad = math.pi * float(obj.HelixAngle) / 180.0
|
||||
helixAngleRad = math.radians(obj.HelixAngle)
|
||||
depthPerOneCircle = length * math.tan(helixAngleRad)
|
||||
# print("Helix circle depth: {}".format(depthPerOneCircle))
|
||||
|
||||
stepUp = obj.LiftDistance.Value
|
||||
if stepUp < 0:
|
||||
stepUp = 0
|
||||
stepUp = max(obj.LiftDistance.Value, 0)
|
||||
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
if finish_step > stepDown:
|
||||
finish_step = stepDown
|
||||
# TODO: finishStep is of limited utility with how regions are now broken
|
||||
# up based on the model geometry- the "finish" step gets applied to each
|
||||
# region separately, which results in excessive "finish" steps being taken
|
||||
# where they really need not be. Leaving stock in Z generally makes more
|
||||
# sense, but both technically have their uses, so leaving this here as
|
||||
# option. Implementing flat area detection would make better use of both.
|
||||
finishStep = min(obj.FinishDepth.Value, stepDown) if hasattr(obj, "FinishDepth") else 0.0
|
||||
|
||||
depth_params = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=obj.StartDepth.Value,
|
||||
step_down=stepDown,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=obj.FinalDepth.Value,
|
||||
user_depths=None,
|
||||
)
|
||||
# Track Z position to determine when changing height is necessary prior to a move
|
||||
lz = obj.StartDepth.Value
|
||||
|
||||
# ml: this is dangerous because it'll hide all unused variables hence forward
|
||||
# however, I don't know what lx and ly signify so I'll leave them for now
|
||||
# lx = adaptiveResults[0]["HelixCenterPoint"][0]
|
||||
# ly = adaptiveResults[0]["HelixCenterPoint"][1]
|
||||
lz = passStartDepth
|
||||
step = 0
|
||||
for region in adaptiveResults:
|
||||
passStartDepth = region["TopDepth"]
|
||||
|
||||
for passEndDepth in depth_params.data:
|
||||
step = step + 1
|
||||
depthParams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=region["TopDepth"],
|
||||
step_down=stepDown,
|
||||
z_finish_step=finishStep,
|
||||
final_depth=region["BottomDepth"],
|
||||
user_depths=None,
|
||||
)
|
||||
|
||||
for region in adaptiveResults:
|
||||
for passEndDepth in depthParams.data:
|
||||
startAngle = math.atan2(
|
||||
region["StartPoint"][1] - region["HelixCenterPoint"][1],
|
||||
region["StartPoint"][0] - region["HelixCenterPoint"][0],
|
||||
)
|
||||
|
||||
# lx = region["HelixCenterPoint"][0]
|
||||
# ly = region["HelixCenterPoint"][1]
|
||||
|
||||
passDepth = passStartDepth - passEndDepth
|
||||
|
||||
p1 = region["HelixCenterPoint"]
|
||||
p2 = region["StartPoint"]
|
||||
helixRadius = math.sqrt(
|
||||
(p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1])
|
||||
)
|
||||
helixRadius = math.dist(p1[:2], p2[:2])
|
||||
|
||||
# Helix ramp
|
||||
if helixRadius > 0.01:
|
||||
@@ -256,8 +240,6 @@ def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
|
||||
op.commandlist.append(
|
||||
Path.Command("G1", {"X": x, "Y": y, "Z": z, "F": op.vertFeed})
|
||||
)
|
||||
# lx = x
|
||||
# ly = y
|
||||
fi = fi + math.pi / 16
|
||||
|
||||
# one more circle at target depth to make sure center is cleared
|
||||
@@ -269,13 +251,11 @@ def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
|
||||
op.commandlist.append(
|
||||
Path.Command("G1", {"X": x, "Y": y, "Z": z, "F": op.horizFeed})
|
||||
)
|
||||
# lx = x
|
||||
# ly = y
|
||||
fi = fi + math.pi / 16
|
||||
|
||||
else:
|
||||
# Cone
|
||||
_HelixAngle = 360 - (float(obj.HelixAngle) * 4)
|
||||
_HelixAngle = 360 - (obj.HelixAngle.Value * 4)
|
||||
|
||||
if obj.HelixConeAngle > 6:
|
||||
obj.HelixConeAngle = 6
|
||||
@@ -510,8 +490,6 @@ def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
|
||||
},
|
||||
)
|
||||
)
|
||||
# lx = x
|
||||
# ly = y
|
||||
|
||||
else: # no helix entry
|
||||
# rapid move to clearance height
|
||||
@@ -551,8 +529,6 @@ def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
|
||||
x = pt[0]
|
||||
y = pt[1]
|
||||
|
||||
# dist = math.sqrt((x-lx)*(x-lx) + (y-ly)*(y-ly))
|
||||
|
||||
if motionType == area.AdaptiveMotionType.Cutting:
|
||||
z = passEndDepth
|
||||
if z != lz:
|
||||
@@ -576,13 +552,6 @@ def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
|
||||
|
||||
op.commandlist.append(Path.Command("G0", {"X": x, "Y": y}))
|
||||
|
||||
# elif motionType == area.AdaptiveMotionType.LinkClearAtPrevPass:
|
||||
# if lx!=x or ly!=y:
|
||||
# op.commandlist.append(Path.Command("G0", { "X": lx, "Y":ly, "Z":passStartDepth+stepUp}))
|
||||
# op.commandlist.append(Path.Command("G0", { "X": x, "Y":y, "Z":passStartDepth+stepUp}))
|
||||
|
||||
# lx = x
|
||||
# ly = y
|
||||
lz = z
|
||||
|
||||
# return to safe height in this Z pass
|
||||
@@ -592,14 +561,14 @@ def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
|
||||
|
||||
lz = z
|
||||
|
||||
passStartDepth = passEndDepth
|
||||
passStartDepth = passEndDepth
|
||||
|
||||
# return to safe height in this Z pass
|
||||
z = obj.ClearanceHeight.Value
|
||||
if z != lz:
|
||||
op.commandlist.append(Path.Command("G0", {"Z": z}))
|
||||
# return to safe height in this Z pass
|
||||
z = obj.ClearanceHeight.Value
|
||||
if z != lz:
|
||||
op.commandlist.append(Path.Command("G0", {"Z": z}))
|
||||
|
||||
lz = z
|
||||
lz = z
|
||||
|
||||
z = obj.ClearanceHeight.Value
|
||||
if z != lz:
|
||||
@@ -634,70 +603,100 @@ def Execute(op, obj):
|
||||
topZ = op.stock.Shape.BoundBox.ZMax
|
||||
obj.Stopped = False
|
||||
obj.StopProcessing = False
|
||||
if obj.Tolerance < 0.001:
|
||||
obj.Tolerance = 0.001
|
||||
obj.Tolerance = max(0.001, obj.Tolerance)
|
||||
|
||||
# Get list of working edges for adaptive algorithm
|
||||
pathArray = op.pathArray
|
||||
if not pathArray:
|
||||
msg = translate(
|
||||
"CAM",
|
||||
"Adaptive operation couldn't determine the boundary wire. Did you select base geometry?",
|
||||
# NOTE: Reminder that stock is formatted differently than inside/outside!
|
||||
stockPaths = {d: convertTo2d(op.stockPathArray[d]) for d in op.stockPathArray}
|
||||
|
||||
outsideOpType = area.AdaptiveOperationType.ClearingOutside
|
||||
insideOpType = area.AdaptiveOperationType.ClearingInside
|
||||
|
||||
# List every REGION separately- we can then calculate a toolpath based
|
||||
# on the region. One or more stepdowns may use that same toolpath by
|
||||
# keeping a reference to the region without requiring we calculate the
|
||||
# toolpath once per step down OR forcing all stepdowns of a region into
|
||||
# a single list.
|
||||
regionOps = list()
|
||||
outsidePathArray2dDepthTuples = list()
|
||||
insidePathArray2dDepthTuples = list()
|
||||
# NOTE: Make sure the depth lists are sorted for use in order-by-depth
|
||||
# and order-by-region algorithms below
|
||||
# NOTE: Pretty sure sorting is already guaranteed by how these are
|
||||
# created, but best to not assume that
|
||||
for rdict in op.outsidePathArray:
|
||||
regionOps.append(
|
||||
{
|
||||
"opType": outsideOpType,
|
||||
"path2d": convertTo2d(rdict["edges"]),
|
||||
# FIXME: Kinda gross- just use this to match up with the
|
||||
# appropriate stockpaths entry...
|
||||
"startdepth": rdict["depths"][0],
|
||||
}
|
||||
)
|
||||
outsidePathArray2dDepthTuples.append(
|
||||
(sorted(rdict["depths"], reverse=True), regionOps[-1])
|
||||
)
|
||||
for rdict in op.insidePathArray:
|
||||
regionOps.append(
|
||||
{
|
||||
"opType": insideOpType,
|
||||
"path2d": convertTo2d(rdict["edges"]),
|
||||
# FIXME: Kinda gross- just use this to match up with the
|
||||
# appropriate stockpaths entry...
|
||||
"startdepth": rdict["depths"][0],
|
||||
}
|
||||
)
|
||||
insidePathArray2dDepthTuples.append(
|
||||
(sorted(rdict["depths"], reverse=True), regionOps[-1])
|
||||
)
|
||||
FreeCAD.Console.PrintUserWarning(msg)
|
||||
return
|
||||
|
||||
path2d = convertTo2d(pathArray)
|
||||
|
||||
# Use the 2D outline of the stock as the stock
|
||||
# FIXME: This does not account for holes in the middle of stock!
|
||||
outer_wire = TechDraw.findShapeOutline(op.stock.Shape, 1, FreeCAD.Vector(0, 0, 1))
|
||||
stockPaths = [[discretize(outer_wire)]]
|
||||
|
||||
stockPath2d = convertTo2d(stockPaths)
|
||||
|
||||
# opType = area.AdaptiveOperationType.ClearingInside # Commented out per LGTM suggestion
|
||||
if obj.OperationType == "Clearing":
|
||||
if obj.Side == "Outside":
|
||||
opType = area.AdaptiveOperationType.ClearingOutside
|
||||
|
||||
else:
|
||||
opType = area.AdaptiveOperationType.ClearingInside
|
||||
|
||||
else: # profiling
|
||||
if obj.Side == "Outside":
|
||||
opType = area.AdaptiveOperationType.ProfilingOutside
|
||||
|
||||
else:
|
||||
opType = area.AdaptiveOperationType.ProfilingInside
|
||||
|
||||
keepToolDownRatio = 3.0
|
||||
if hasattr(obj, "KeepToolDownRatio"):
|
||||
keepToolDownRatio = float(obj.KeepToolDownRatio)
|
||||
keepToolDownRatio = obj.KeepToolDownRatio.Value
|
||||
|
||||
# put here all properties that influence calculation of adaptive base paths,
|
||||
|
||||
inputStateObject = {
|
||||
"tool": float(op.tool.Diameter),
|
||||
"tolerance": float(obj.Tolerance),
|
||||
"geometry": path2d,
|
||||
"stockGeometry": stockPath2d,
|
||||
"stepover": float(obj.StepOver),
|
||||
"effectiveHelixDiameter": float(helixDiameter),
|
||||
"operationType": obj.OperationType,
|
||||
"side": obj.Side,
|
||||
# These fields are used to determine if toolpaths should be recalculated
|
||||
outsideInputStateObject = {
|
||||
"tool": op.tool.Diameter.Value,
|
||||
"tolerance": obj.Tolerance,
|
||||
"geometry": [k["path2d"] for k in regionOps if k["opType"] == outsideOpType],
|
||||
"stockGeometry": stockPaths,
|
||||
"stepover": obj.StepOver,
|
||||
"effectiveHelixDiameter": helixDiameter,
|
||||
"operationType": "Clearing",
|
||||
"side": "Outside",
|
||||
"forceInsideOut": obj.ForceInsideOut,
|
||||
"finishingProfile": obj.FinishingProfile,
|
||||
"keepToolDownRatio": keepToolDownRatio,
|
||||
"stockToLeave": float(obj.StockToLeave),
|
||||
"stockToLeave": obj.StockToLeave.Value,
|
||||
}
|
||||
|
||||
insideInputStateObject = {
|
||||
"tool": op.tool.Diameter.Value,
|
||||
"tolerance": obj.Tolerance,
|
||||
"geometry": [k["path2d"] for k in regionOps if k["opType"] == insideOpType],
|
||||
"stockGeometry": stockPaths,
|
||||
"stepover": obj.StepOver,
|
||||
"effectiveHelixDiameter": helixDiameter,
|
||||
"operationType": "Clearing",
|
||||
"side": "Inside",
|
||||
"forceInsideOut": obj.ForceInsideOut,
|
||||
"finishingProfile": obj.FinishingProfile,
|
||||
"keepToolDownRatio": keepToolDownRatio,
|
||||
"stockToLeave": obj.StockToLeave.Value,
|
||||
}
|
||||
|
||||
inputStateObject = [outsideInputStateObject, insideInputStateObject]
|
||||
|
||||
inputStateChanged = False
|
||||
adaptiveResults = None
|
||||
|
||||
if obj.AdaptiveOutputState is not None and obj.AdaptiveOutputState != "":
|
||||
# If we have a valid... path? Something. Generated, make that
|
||||
# tentatively the output
|
||||
if obj.AdaptiveOutputState:
|
||||
adaptiveResults = obj.AdaptiveOutputState
|
||||
|
||||
# If ANYTHING in our input-cutting parameters, cutting regions,
|
||||
# etc.- changes, force recalculating
|
||||
if json.dumps(obj.AdaptiveInputState) != json.dumps(inputStateObject):
|
||||
inputStateChanged = True
|
||||
adaptiveResults = None
|
||||
@@ -721,31 +720,67 @@ def Execute(op, obj):
|
||||
start = time.time()
|
||||
|
||||
if inputStateChanged or adaptiveResults is None:
|
||||
a2d = area.Adaptive2d()
|
||||
a2d.stepOverFactor = 0.01 * obj.StepOver
|
||||
a2d.toolDiameter = float(op.tool.Diameter)
|
||||
a2d.helixRampDiameter = helixDiameter
|
||||
a2d.keepToolDownDistRatio = keepToolDownRatio
|
||||
a2d.stockToLeave = float(obj.StockToLeave)
|
||||
a2d.tolerance = float(obj.Tolerance)
|
||||
a2d.forceInsideOut = obj.ForceInsideOut
|
||||
a2d.finishingProfile = obj.FinishingProfile
|
||||
a2d.opType = opType
|
||||
# NOTE: Seem to need to create a new a2d for each area when we're
|
||||
# stepping down depths like this. If we don't, it will keep history
|
||||
# from the last region we did.
|
||||
|
||||
# EXECUTE
|
||||
results = a2d.Execute(stockPath2d, path2d, progressFn)
|
||||
# TODO: QThread/QRunnable trigger Python's global interpretor lock
|
||||
# (GIL). To calculate toolpaths in parallel, making a C++ shim that
|
||||
# takes in the array of regions/stock paths and parallelizes in
|
||||
# C++-land is probably the way to do it.
|
||||
|
||||
# Create a toolpath for each region to avoid re-calculating for
|
||||
# identical stepdowns
|
||||
for rdict in regionOps:
|
||||
path2d = rdict["path2d"]
|
||||
opType = rdict["opType"]
|
||||
|
||||
a2d = area.Adaptive2d()
|
||||
a2d.stepOverFactor = 0.01 * obj.StepOver
|
||||
a2d.toolDiameter = op.tool.Diameter.Value
|
||||
a2d.helixRampDiameter = helixDiameter
|
||||
a2d.keepToolDownDistRatio = keepToolDownRatio
|
||||
a2d.stockToLeave = obj.StockToLeave.Value
|
||||
a2d.tolerance = obj.Tolerance
|
||||
a2d.forceInsideOut = obj.ForceInsideOut
|
||||
a2d.finishingProfile = obj.FinishingProfile
|
||||
a2d.opType = opType
|
||||
|
||||
rdict["toolpaths"] = a2d.Execute(
|
||||
stockPaths[rdict["startdepth"]], path2d, progressFn
|
||||
)
|
||||
|
||||
# Create list of regions to cut, in an order they can be cut in
|
||||
cutlist = list()
|
||||
# Region IDs that have been cut already
|
||||
cutids = list()
|
||||
# Create sorted list of unique depths
|
||||
# NOTE: reverse because we cut top-down!
|
||||
depths = list()
|
||||
# NOTE: alltuples is sorted by depth already
|
||||
alltuples = outsidePathArray2dDepthTuples + insidePathArray2dDepthTuples
|
||||
for t in alltuples:
|
||||
depths += [d for d in t[0]]
|
||||
depths = sorted(list(set(depths)), reverse=True)
|
||||
for d in depths:
|
||||
cutlist += [([d], o[1]) for o in outsidePathArray2dDepthTuples if d in o[0]]
|
||||
cutlist += [([d], i[1]) for i in insidePathArray2dDepthTuples if d in i[0]]
|
||||
|
||||
# need to convert results to python object to be JSON serializable
|
||||
adaptiveResults = []
|
||||
for result in results:
|
||||
adaptiveResults.append(
|
||||
{
|
||||
"HelixCenterPoint": result.HelixCenterPoint,
|
||||
"StartPoint": result.StartPoint,
|
||||
"AdaptivePaths": result.AdaptivePaths,
|
||||
"ReturnMotionType": result.ReturnMotionType,
|
||||
}
|
||||
)
|
||||
stepdown = max(obj.StepDown.Value, _ADAPTIVE_MIN_STEPDOWN)
|
||||
adaptiveResults = list()
|
||||
for depths, region in cutlist:
|
||||
for result in region["toolpaths"]:
|
||||
adaptiveResults.append(
|
||||
{
|
||||
"HelixCenterPoint": result.HelixCenterPoint,
|
||||
"StartPoint": result.StartPoint,
|
||||
"AdaptivePaths": result.AdaptivePaths,
|
||||
"ReturnMotionType": result.ReturnMotionType,
|
||||
"TopDepth": depths[0] + stepdown,
|
||||
"BottomDepth": depths[-1],
|
||||
}
|
||||
)
|
||||
|
||||
# GENERATE
|
||||
GenerateGCode(op, obj, adaptiveResults, helixDiameter)
|
||||
@@ -765,71 +800,328 @@ def Execute(op, obj):
|
||||
sceneClean()
|
||||
|
||||
|
||||
def _get_working_edges(op, obj):
|
||||
"""_get_working_edges(op, obj)...
|
||||
Compile all working edges from the Base Geometry selection (obj.Base)
|
||||
for the current operation.
|
||||
Additional modifications to selected region(face), such as extensions,
|
||||
should be placed within this function.
|
||||
def projectFacesToXY(faces, minEdgeLength=1e-10):
|
||||
"""projectFacesToXY(faces, minEdgeLength)
|
||||
Calculates the projection of the provided list of faces onto the XY plane.
|
||||
The returned value is a single shape that may contain multiple faces if
|
||||
there were disjoint projections. Each individual face will be clean, without
|
||||
triangulated geometry, etc., and will be at Z=0 on the XY plane
|
||||
|
||||
minEdgeLength is provided to (eg) filter out the tips of cones that are
|
||||
internally represented as arbitrarily-small circular faces- using those for
|
||||
additional operations causes problems.
|
||||
"""
|
||||
all_regions = list()
|
||||
edge_list = list()
|
||||
avoidFeatures = list()
|
||||
rawEdges = list()
|
||||
projdir = FreeCAD.Vector(0, 0, 1)
|
||||
outfaces = []
|
||||
for f in faces:
|
||||
# Vertical cones and spheres will still have a projection on the XY
|
||||
# plane. Cylinders and flat faces will not.
|
||||
if Path.Geom.isVertical(f) and type(f.Surface) not in [Part.Cone, Part.Sphere]:
|
||||
continue
|
||||
|
||||
# NOTE: Wires/edges get clipped if we have an "exact fit" bounding box
|
||||
projface = Path.Geom.makeBoundBoxFace(f.BoundBox, offset=1, zHeight=0)
|
||||
|
||||
# NOTE: Cylinders, cones, and spheres are messy:
|
||||
# - Internal representation of non-truncted cones and spheres includes
|
||||
# the "tip" with a ~0-area closed edge. This is different than the
|
||||
# "isNull() note" at the top in magnitude
|
||||
# - Projecting edges doesn't naively work due to the way seams are handled
|
||||
# - There may be holes at either end that may or may not line up- any
|
||||
# overlap is a hole in the projection
|
||||
if type(f.Surface) in [Part.Cone, Part.Cylinder, Part.Sphere]:
|
||||
# This gets most of the face outline, but since cylinder/cone faces
|
||||
# are hollow, if the ends overlap in the projection there may be a
|
||||
# hole we need to remove from the solid projection
|
||||
oface = Part.makeFace(TechDraw.findShapeOutline(f, 1, projdir))
|
||||
|
||||
# "endfacewires" is JUST the end faces of a cylinder/cone, used to
|
||||
# determine if there's a hole we can see through the shape that
|
||||
# should NOT be solid in the projection
|
||||
endfacewires = DraftGeomUtils.findWires(
|
||||
[e for e in f.Edges if not e.isSeam(f) and e.Length > minEdgeLength]
|
||||
)
|
||||
|
||||
# Need to verify that there actually is a projection before taking
|
||||
# a wire from the list, else this could nicely be one line.
|
||||
projwires = []
|
||||
for w in endfacewires:
|
||||
pp = projface.makeParallelProjection(w, projdir).Wires
|
||||
if pp:
|
||||
projwires.append(pp[0])
|
||||
|
||||
if len(projwires) > 1:
|
||||
faces = [Part.makeFace(x) for x in projwires]
|
||||
overlap = faces[0].common(faces[1:])
|
||||
outfaces.append(oface.cut(overlap))
|
||||
else:
|
||||
outfaces.append(oface)
|
||||
# For other cases, projecting the wires to a plane should suffice
|
||||
else:
|
||||
facewires = list()
|
||||
for w in f.Wires:
|
||||
if w.isClosed():
|
||||
projwire = projface.makeParallelProjection(w, projdir).Wires[0]
|
||||
if projwire.isClosed():
|
||||
facewires.append(projwire)
|
||||
if facewires:
|
||||
outfaces.append(Part.makeFace(facewires))
|
||||
if outfaces:
|
||||
fusion = outfaces[0].fuse(outfaces[1:])
|
||||
# removeSplitter fixes occasional concatenate issues for some face orders
|
||||
return DraftGeomUtils.concatenate(fusion.removeSplitter())
|
||||
else:
|
||||
return Part.Shape()
|
||||
|
||||
|
||||
def _getSolidProjection(shp, z):
|
||||
"""_getSolidProjection(shp, z)
|
||||
Calculates a shape obtained by slicing shp at the height z, then projecting
|
||||
the solids above that height onto a region of proj_face, and creating a
|
||||
simplified face
|
||||
"""
|
||||
bb = shp.BoundBox
|
||||
|
||||
# Find all faces above the machining depth. This is used to mask future
|
||||
# interior cuts, and the outer wire is used as the external wire
|
||||
bbCutTop = Part.makeBox(
|
||||
bb.XLength,
|
||||
bb.YLength,
|
||||
max(bb.ZLength, bb.ZLength - z),
|
||||
FreeCAD.Vector(bb.XMin, bb.YMin, z),
|
||||
)
|
||||
aboveSolids = shp.common(bbCutTop).Solids
|
||||
|
||||
faces = list()
|
||||
for s in aboveSolids:
|
||||
faces += s.Faces
|
||||
|
||||
return projectFacesToXY(faces)
|
||||
|
||||
|
||||
def _workingEdgeHelperManual(op, obj, depths):
|
||||
# Final calculated regions- list of dicts with entries:
|
||||
# "region" - actual shape
|
||||
# "depths" - list of depths this region applies to
|
||||
insideRegions = list()
|
||||
outsideRegions = list()
|
||||
|
||||
# User selections, with extensions
|
||||
selectedRegions = list()
|
||||
selectedEdges = list()
|
||||
|
||||
# Get extensions and identify faces to avoid
|
||||
extensions = FeatureExtensions.getExtensions(obj)
|
||||
for e in extensions:
|
||||
if e.avoid:
|
||||
avoidFeatures.append(e.feature)
|
||||
avoidFeatures = [e for e in extensions if e.avoid]
|
||||
|
||||
# Get faces selected by user
|
||||
for base, subs in obj.Base:
|
||||
for sub in subs:
|
||||
if sub.startswith("Face"):
|
||||
if sub not in avoidFeatures:
|
||||
if obj.UseOutline:
|
||||
face = base.Shape.getElement(sub)
|
||||
# get outline with wire_A method used in PocketShape, but it does not play nicely later
|
||||
# wire_A = TechDraw.findShapeOutline(face, 1, FreeCAD.Vector(0.0, 0.0, 1.0))
|
||||
wire_B = face.OuterWire
|
||||
shape = Part.Face(wire_B)
|
||||
else:
|
||||
shape = base.Shape.getElement(sub)
|
||||
all_regions.append(shape)
|
||||
elif sub.startswith("Edge"):
|
||||
# Save edges for later processing
|
||||
rawEdges.append(base.Shape.getElement(sub))
|
||||
# Efor
|
||||
|
||||
# Process selected edges
|
||||
if rawEdges:
|
||||
edgeWires = DraftGeomUtils.findWires(rawEdges)
|
||||
if edgeWires:
|
||||
for w in edgeWires:
|
||||
for e in w.Edges:
|
||||
edge_list.append([discretize(e)])
|
||||
|
||||
# Apply regular Extensions
|
||||
op.exts = []
|
||||
# Similarly, expand selected regions with extensions
|
||||
for ext in extensions:
|
||||
if not ext.avoid:
|
||||
wire = ext.getWire()
|
||||
if wire:
|
||||
for f in ext.getExtensionFaces(wire):
|
||||
op.exts.append(f)
|
||||
all_regions.append(f)
|
||||
if wire := ext.getWire():
|
||||
selectedRegions += [f for f in ext.getExtensionFaces(wire)]
|
||||
|
||||
# Second face-combining method attempted
|
||||
horizontal = Path.Geom.combineHorizontalFaces(all_regions)
|
||||
if horizontal:
|
||||
obj.removalshape = Part.makeCompound(horizontal)
|
||||
for f in horizontal:
|
||||
for w in f.Wires:
|
||||
for e in w.Edges:
|
||||
edge_list.append([discretize(e)])
|
||||
for base, subs in obj.Base:
|
||||
for sub in subs:
|
||||
element = base.Shape.getElement(sub)
|
||||
if sub.startswith("Face") and sub not in avoidFeatures:
|
||||
shape = Part.Face(element.OuterWire) if obj.UseOutline else element
|
||||
selectedRegions.append(shape)
|
||||
# Omit vertical edges, since they project to nothing in the XY plane
|
||||
# and cause processing failures later if included
|
||||
elif sub.startswith("Edge") and not Path.Geom.isVertical(element):
|
||||
selectedEdges.append(element)
|
||||
|
||||
return edge_list
|
||||
# Multiple input solids can be selected- make a single part out of them,
|
||||
# will process each solid separately as appropriate
|
||||
shps = op.model[0].Shape.fuse([k.Shape for k in op.model[1:]])
|
||||
|
||||
# Make a face to project onto
|
||||
# NOTE: Use 0 as the height, since that's what TechDraw.findShapeOutline
|
||||
# uses, which we use to find the machining boundary, and the actual depth
|
||||
# is tracked separately.
|
||||
# NOTE: Project to the PART bounding box- with some padding- not the stock,
|
||||
# since the stock may be smaller than the part
|
||||
projface = Path.Geom.makeBoundBoxFace(shps.BoundBox, offset=1, zHeight=0)
|
||||
projdir = FreeCAD.Vector(0, 0, 1)
|
||||
|
||||
# When looking for selected edges, project to a single plane first, THEN try
|
||||
# to find wires. Take all the resulting wires and make faces in one shot to
|
||||
# make bullseye-style cutouts where selected wires nest.
|
||||
edgefaces = list()
|
||||
if selectedEdges:
|
||||
pp = [projface.makeParallelProjection(e, projdir).Wires[0] for e in selectedEdges]
|
||||
ppe = list()
|
||||
for w in pp:
|
||||
ppe += w.Edges
|
||||
edgeWires = DraftGeomUtils.findWires(ppe)
|
||||
edgefaces = Part.makeFace(edgeWires).Faces
|
||||
|
||||
selectedRefined = projectFacesToXY(selectedRegions + edgefaces)
|
||||
|
||||
# If the user selected only faces that don't have an XY projection AND no
|
||||
# edges, give a useful error
|
||||
if not selectedRefined.Wires:
|
||||
Path.Log.warning("Selected faces/wires have no projection on the XY plane")
|
||||
return insideRegions, outsideRegions
|
||||
|
||||
lastdepth = obj.StartDepth.Value
|
||||
|
||||
for depth in depths:
|
||||
# If our depth is above the top of the stock, there's nothing to machine
|
||||
if depth >= op.stock.Shape.BoundBox.ZMax:
|
||||
lastdepth = depth
|
||||
continue
|
||||
|
||||
aboveRefined = _getSolidProjection(shps, depth)
|
||||
|
||||
# Create appropriate tuples and add to list, processing inside/outside
|
||||
# as requested by operation
|
||||
if obj.Side == "Outside":
|
||||
# Outside is based on the outer wire of the faces of aboveRefined
|
||||
# Insides are based on the remaining "below" regions, masked by the
|
||||
# "above"- if something is above an area, we can't machine it in 2.5D
|
||||
|
||||
# Outside: Take the outer wire of the above faces, added to selected
|
||||
# edges and regions
|
||||
# NOTE: Exactly one entry per depth (not necessarily one depth entry per
|
||||
# stepdown, however), which is a LIST of the wires we're staying outside
|
||||
# NOTE: Do this FIRST- if any inside regions share enough of an edge
|
||||
# with an outside region for a tool to get through, we want to skip them
|
||||
# for the current stepdown
|
||||
# NOTE: This check naively seems unnecessary, but it's possible the
|
||||
# user selected a vertical face as the only face to stay outside of,
|
||||
# and we're above the model, causing keepOutFaces to be empty
|
||||
if keepOutFaces := [
|
||||
Part.makeFace(TechDraw.findShapeOutline(f, 1, projdir))
|
||||
for f in aboveRefined.Faces + selectedRefined.Faces
|
||||
]:
|
||||
finalMerge = keepOutFaces[0].fuse(keepOutFaces[1:])
|
||||
else:
|
||||
finalMerge = selectedRefined
|
||||
# Without removeSplitter(), concatenate will sometimes fail when
|
||||
# trying to merge faces that are (eg) connected A-B and B-C,
|
||||
# seemingly when trying to merge A-C
|
||||
regions = DraftGeomUtils.concatenate(finalMerge.removeSplitter())
|
||||
|
||||
# If this region exists in our list, it has to be the last entry, due to
|
||||
# proceeding in order and having only one per depth. If it's already
|
||||
# there, replace with the new, deeper depth, else add new
|
||||
# NOTE: Do NOT need a check for whether outsideRegions[-1]["region"]
|
||||
# is valid since we have a user-specified region regardless of depth
|
||||
# NOTE: See "isNull() note" at top of file
|
||||
if (
|
||||
outsideRegions
|
||||
and regions.Wires
|
||||
and not regions.cut(outsideRegions[-1]["region"]).Wires
|
||||
):
|
||||
outsideRegions[-1]["depths"].append(depth)
|
||||
else:
|
||||
outsideRegions.append({"region": regions, "depths": [depth]})
|
||||
|
||||
# Inside
|
||||
# For every area selected by the user, project to a plane
|
||||
# NOTE: See "isNull() note" at top of file
|
||||
else:
|
||||
if aboveRefined.Wires:
|
||||
finalCut = selectedRefined.cut(aboveRefined)
|
||||
else:
|
||||
finalCut = selectedRefined
|
||||
|
||||
# Split up into individual faces if any are disjoint, then update
|
||||
# insideRegions- either by adding a new entry OR by updating the depth
|
||||
# of an existing entry
|
||||
for f in finalCut.Faces:
|
||||
addNew = True
|
||||
# Brute-force search all existing regions to see if any are the same
|
||||
newtop = lastdepth
|
||||
for rdict in insideRegions:
|
||||
# FIXME: Smarter way to do this than a full cut operation?
|
||||
if not rdict["region"].cut(f).Wires:
|
||||
rdict["depths"].append(depth)
|
||||
addNew = False
|
||||
break
|
||||
if addNew:
|
||||
insideRegions.append({"region": f, "depths": [depth]})
|
||||
|
||||
# Update the last depth step
|
||||
lastdepth = depth
|
||||
# end for depth
|
||||
|
||||
return insideRegions, outsideRegions
|
||||
|
||||
|
||||
def _getWorkingEdges(op, obj):
|
||||
"""_getWorkingEdges(op, obj)...
|
||||
Compile all working edges from the Base Geometry selection (obj.Base)
|
||||
for the current operation (or the entire model if no selections).
|
||||
Additional modifications to selected region(face), such as extensions,
|
||||
should be placed within this function.
|
||||
This version will return two lists- one for outside (keepout) edges and one
|
||||
for inside ("machine inside") edges. Each list will be a dict with "region"
|
||||
and "depths" entries- the former being discretized geometry of the region,
|
||||
the latter being a list of every depth the geometry is machined on
|
||||
"""
|
||||
|
||||
# Find depth steps, throwing out all depths above anywhere we might cut
|
||||
# NOTE: Finish stepdown = 0 here- it's actually applied when gcode is
|
||||
# generated; doing so here would cause it to be applied twice.
|
||||
depthParams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=obj.StartDepth.Value,
|
||||
step_down=max(obj.StepDown.Value, _ADAPTIVE_MIN_STEPDOWN),
|
||||
z_finish_step=0.0,
|
||||
final_depth=obj.FinalDepth.Value,
|
||||
user_depths=None,
|
||||
)
|
||||
|
||||
depths = [d for d in depthParams.data if d < op.stock.Shape.BoundBox.ZMax]
|
||||
|
||||
# Get the stock outline at each stepdown. Used to calculate toolpaths and
|
||||
# for calcuating cut regions in some instances
|
||||
# NOTE: Stock is handled DIFFERENTLY than inside and outside regions!
|
||||
# Combining different depths just adds code to look up the correct outline
|
||||
# when computing inside/outside regions, for no real benefit.
|
||||
stockProjectionDict = {d: _getSolidProjection(op.stock.Shape, d) for d in depths}
|
||||
|
||||
# If user specified edges, calculate the machining regions based on that
|
||||
# input. Otherwise, process entire model
|
||||
# Output are lists of dicts with "region" and "depths" entries. Depths are
|
||||
# a list of Z depths that the region applies to
|
||||
# Inside regions are a single face; outside regions consist of ALL geometry
|
||||
# to be avoided at those depths.
|
||||
insideRegions, outsideRegions = _workingEdgeHelperManual(op, obj, depths)
|
||||
|
||||
# Create discretized regions
|
||||
def _createDiscretizedRegions(regionDicts):
|
||||
discretizedRegions = list()
|
||||
for rdict in regionDicts:
|
||||
discretizedRegions.append(
|
||||
{
|
||||
"edges": [[discretize(w)] for w in rdict["region"].Wires],
|
||||
"depths": rdict["depths"],
|
||||
}
|
||||
)
|
||||
return discretizedRegions
|
||||
|
||||
insideDiscretized = _createDiscretizedRegions(insideRegions)
|
||||
outsideDiscretized = _createDiscretizedRegions(outsideRegions)
|
||||
|
||||
# NOTE: REMINDER: This is notably different from machining regions- just
|
||||
# a dict with depth: region entries, single depth for easy lookup
|
||||
stockDiscretized = {}
|
||||
for d in stockProjectionDict:
|
||||
discretizedEdges = list()
|
||||
for a in stockProjectionDict[d].Faces:
|
||||
for w in a.Wires:
|
||||
discretizedEdges.append([discretize(w)])
|
||||
stockDiscretized[d] = discretizedEdges
|
||||
|
||||
# Return found inside and outside regions/depths. Up to the caller to decide
|
||||
# which ones it cares about.
|
||||
# NOTE: REMINDER: Stock is notably different from machining regions- just
|
||||
# a dict with depth: region entries, single depth for easy lookup
|
||||
return insideDiscretized, outsideDiscretized, stockDiscretized
|
||||
|
||||
|
||||
class PathAdaptive(PathOp.ObjectOp):
|
||||
@@ -1108,7 +1400,14 @@ class PathAdaptive(PathOp.ObjectOp):
|
||||
See documentation of execute() for a list of base functionality provided.
|
||||
Should be overwritten by subclasses."""
|
||||
|
||||
self.pathArray = _get_working_edges(self, obj)
|
||||
# Contains both geometry to machine and the applicable depths
|
||||
# NOTE: Reminder that stock is formatted differently than inside/outside!
|
||||
inside, outside, stock = _getWorkingEdges(self, obj)
|
||||
|
||||
self.insidePathArray = inside
|
||||
self.outsidePathArray = outside
|
||||
self.stockPathArray = stock
|
||||
|
||||
Execute(self, obj)
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
|
||||
Reference in New Issue
Block a user