| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import FreeCAD |
| | import Part |
| | import Path |
| | import Path.Op.Base as PathOp |
| | import Path.Op.EngraveBase as PathEngraveBase |
| | import PathScripts.PathUtils as PathUtils |
| | import math |
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| |
|
| | __doc__ = "Class and implementation of CAM Vcarve operation" |
| |
|
| | PRIMARY = 0 |
| | SECONDARY = 1 |
| | EXTERIOR1 = 2 |
| | EXTERIOR2 = 3 |
| | COLINEAR = 4 |
| | TWIN = 5 |
| | BORDERLINE = 6 |
| |
|
| | |
| |
|
| | if False: |
| | Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) |
| | Path.Log.trackModule(Path.Log.thisModule()) |
| | else: |
| | Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) |
| |
|
| |
|
| | translate = FreeCAD.Qt.translate |
| |
|
| |
|
| | def _collectVoronoiWires(vd): |
| | edges = [e for e in vd.Edges if e.Color == PRIMARY] |
| | vertex = {} |
| | for e in edges: |
| | for v in e.Vertices: |
| | i = v.Index |
| | j = vertex.get(i, []) |
| | j.append(e) |
| | vertex[i] = j |
| |
|
| | |
| | knots = [i for i in vertex if len(vertex[i]) == 1] |
| | knots.extend([i for i in vertex if len(vertex[i]) > 2]) |
| | if len(knots) == 0: |
| | for i in vertex: |
| | if len(vertex[i]) > 0: |
| | knots.append(i) |
| | break |
| |
|
| | def consume(v, edge): |
| | vertex[v] = [e for e in vertex[v] if e.Index != edge.Index] |
| | return len(vertex[v]) == 0 |
| |
|
| | def traverse(vStart, edge, edges): |
| | if vStart == edge.Vertices[0].Index: |
| | vEnd = edge.Vertices[1].Index |
| | edges.append(edge) |
| | else: |
| | vEnd = edge.Vertices[0].Index |
| | edges.append(edge.Twin) |
| |
|
| | consume(vStart, edge) |
| | if consume(vEnd, edge): |
| | return None |
| | return vEnd |
| |
|
| | wires = [] |
| | while knots: |
| | we = [] |
| | vFirst = knots[0] |
| | vStart = vFirst |
| | vLast = vFirst |
| | if len(vertex[vStart]): |
| | while vStart is not None: |
| | vLast = vStart |
| | edges = vertex[vStart] |
| | if len(edges) > 0: |
| | edge = edges[0] |
| | vStart = traverse(vStart, edge, we) |
| | else: |
| | vStart = None |
| | wires.append(we) |
| | if len(vertex[vFirst]) == 0: |
| | knots = [v for v in knots if v != vFirst] |
| | if len(vertex[vLast]) == 0: |
| | knots = [v for v in knots if v != vLast] |
| | return wires |
| |
|
| |
|
| | def _sortVoronoiWires(wires, start=FreeCAD.Vector(0, 0, 0)): |
| | def closestTo(start, point): |
| | p = None |
| | length = None |
| | for i in point: |
| | if length is None or length > start.distanceToPoint(point[i]): |
| | length = start.distanceToPoint(point[i]) |
| | p = i |
| | return (p, length) |
| |
|
| | begin = {} |
| | end = {} |
| |
|
| | for i, w in enumerate(wires): |
| | begin[i] = w[0].Vertices[0].toPoint() |
| | end[i] = w[-1].Vertices[1].toPoint() |
| |
|
| | result = [] |
| | while begin: |
| | (bIdx, bLen) = closestTo(start, begin) |
| | (eIdx, eLen) = closestTo(start, end) |
| | if bLen < eLen: |
| | result.append(wires[bIdx]) |
| | start = end[bIdx] |
| | del begin[bIdx] |
| | del end[bIdx] |
| | else: |
| | result.append([e.Twin for e in reversed(wires[eIdx])]) |
| | start = begin[eIdx] |
| | del begin[eIdx] |
| | del end[eIdx] |
| |
|
| | return result |
| |
|
| |
|
| | def getReversedEdge(edge): |
| | |
| | curve = edge.Curve |
| | first = edge.FirstParameter |
| | last = edge.LastParameter |
| | curve_c = curve.copy() |
| | curve_c.reverse() |
| | return Part.Edge(curve_c, curve_c.reversedParameter(last), curve_c.reversedParameter(first)) |
| |
|
| |
|
| | def generateVirtualBackTrackEdges(positionHistory, nextEdge, tolerance) -> list: |
| | """ |
| | Generate a list of "virtual edges" to backtrack using normal G1 moves instead lifting |
| | toolbit and repositioning using G0 to get to beginning of nextEdge. |
| | Those virtual edges are either already carved or are part of nextEdge anyway so it's safe |
| | to follow them without lifting toolbit. This approach makes carving a lot of faster. |
| | """ |
| |
|
| | if not positionHistory or len(positionHistory) < 2: |
| | return [] |
| |
|
| | backTrackEdges = [] |
| |
|
| | currentPosition = positionHistory[-1] |
| | previousPosition = positionHistory[-2] |
| |
|
| | nextEdgeStart = nextEdge.valueAt(nextEdge.FirstParameter) |
| | nextEdgeEnd = nextEdge.valueAt(nextEdge.LastParameter) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | if nextEdgeEnd.isEqual(currentPosition, tolerance): |
| | |
| | virtualEdge = Part.Edge(Part.LineSegment(nextEdgeEnd, nextEdgeStart)) |
| | backTrackEdges.append(virtualEdge) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | elif nextEdgeEnd.isEqual(previousPosition, tolerance): |
| | |
| | virtualEdge = Part.Edge(Part.LineSegment(currentPosition, previousPosition)) |
| | backTrackEdges.append(virtualEdge) |
| | |
| | backTrackEdges.append(getReversedEdge(nextEdge)) |
| |
|
| | return backTrackEdges |
| |
|
| |
|
| | def canSkipRepositioning(positionHistory, newPosition, tolerance): |
| | """ |
| | Calculate if it makes sense to raise head to safe height and reposition before |
| | starting to cut another edge |
| | """ |
| |
|
| | if not positionHistory: |
| | return False |
| |
|
| | currentPosition = positionHistory[-1] |
| | previousPosition = positionHistory[-2] |
| |
|
| | |
| | v0 = FreeCAD.Base.Vector(currentPosition.x, currentPosition.y) |
| | v1 = FreeCAD.Base.Vector(newPosition.x, newPosition.y) |
| |
|
| | |
| | if v0.distanceToPoint(v1) <= 0.5: |
| | return True |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | if newPosition.isEqual(previousPosition, tolerance): |
| | return True |
| |
|
| | return False |
| |
|
| |
|
| | class _Geometry(object): |
| | """POD class so the limits only have to be calculated once.""" |
| |
|
| | def __init__(self, zStart, zStop, zScale, zStepDown): |
| | self.start = zStart |
| | self.stop = zStop |
| | self.scale = zScale |
| | self.stepDown = zStepDown |
| | self.stepDownPass = 1 |
| |
|
| | |
| | |
| | |
| | self.offset = 0 |
| |
|
| | def incrementStepDownDepth(self, maximumUsableDepth): |
| | """ |
| | Increase stepDown depth before starting new carving pass. |
| | :returns: True if successful, False if maximum depth achieved |
| | """ |
| |
|
| | |
| | if self.maximumDepth == self.stop: |
| | return False |
| |
|
| | |
| | |
| |
|
| | if self.maximumDepth <= maximumUsableDepth: |
| | return False |
| |
|
| | self.stepDownPass += 1 |
| | return True |
| |
|
| | @property |
| | def maximumDepth(self): |
| | """ |
| | Return maximum vcarving depth computed from step down setting and pass number |
| | """ |
| |
|
| | if self.stepDown == 0: |
| | return self.stop |
| |
|
| | return max(self.stop, self.start - (self.stepDownPass * self.stepDown)) |
| |
|
| | @classmethod |
| | def FromTool(cls, tool, zStart, zFinal, zStepDown=0): |
| | rMax = float(tool.Diameter) / 2.0 |
| | rMin = float(tool.TipDiameter) / 2.0 |
| | toolangle = math.tan(math.radians(tool.CuttingEdgeAngle.Value / 2.0)) |
| | zScale = 1.0 / toolangle |
| | zStop = zStart - rMax * zScale |
| | zOff = rMin * zScale |
| |
|
| | return _Geometry(zStart + zOff, max(zStop + zOff, zFinal), zScale, zStepDown) |
| |
|
| | @classmethod |
| | def FromObj(cls, obj, model): |
| | if obj.BaseShapes and hasattr(obj.BaseShapes[0], "Shape"): |
| | zStart = obj.BaseShapes[0].Shape.BoundBox.ZMax |
| | elif obj.Base and obj.Base[0][0] and hasattr(obj.Base[0][0], "Shape"): |
| | if len(obj.Base[0]) > 1 and "Face" in obj.Base[0][1][0]: |
| | faceName = obj.Base[0][1][0] |
| | faceIndex = int(faceName.replace("Face", "")) - 1 |
| | face = obj.Base[0][0].Shape.Faces[faceIndex] |
| | zStart = face.BoundBox.ZMax |
| | else: |
| | zStart = obj.Base[0][0].Shape.BoundBox.ZMax |
| | else: |
| | zStart = model.Shape.BoundBox.ZMax |
| | Path.Log.error("Base object not set") |
| | finalDepth = obj.FinalDepth.Value |
| | stepDown = abs(obj.StepDown.Value) |
| |
|
| | return cls.FromTool(obj.ToolController.Tool, zStart, finalDepth, stepDown) |
| |
|
| |
|
| | def _calculate_depth(MIC, geom): |
| | |
| | |
| | depth = geom.start - round(MIC * geom.scale, 4) |
| |
|
| | return max(depth, geom.maximumDepth) + geom.offset |
| |
|
| |
|
| | def _get_maximumUsableDepth(wires, geom): |
| | """ |
| | Calculate maximum engraving depth for a list of wires |
| | belonging to one face. |
| | """ |
| |
|
| | def _get_depth(MIC, geom): |
| | """Similar logic to _calculate_depth but without stepdown and offset calculations""" |
| | depth = geom.start - round(MIC * geom.scale, 4) |
| | return max(depth, geom.stop) |
| |
|
| | min_depth = None |
| |
|
| | for wire in wires: |
| | for edge in wire: |
| | dist = edge.getDistances() |
| | depth = min(_get_depth(dist[0], geom), _get_depth(dist[1], geom)) |
| |
|
| | if min_depth is None: |
| | min_depth = depth |
| | else: |
| | min_depth = min(min_depth, depth) |
| |
|
| | return min_depth |
| |
|
| |
|
| | def _getPartEdge(edge, geom): |
| | dist = edge.getDistances() |
| | zBegin = _calculate_depth(dist[0], geom) |
| | zEnd = _calculate_depth(dist[1], geom) |
| | return edge.toShape(zBegin, zEnd) |
| |
|
| |
|
| | def _getPartEdges(obj, vWire, geom): |
| | edges = [] |
| | for e in vWire: |
| | edges.append(_getPartEdge(e, geom)) |
| | return edges |
| |
|
| |
|
| | class ObjectVcarve(PathEngraveBase.ObjectOp): |
| | """Proxy class for Vcarve operation.""" |
| |
|
| | def opFeatures(self, obj): |
| | """opFeatures(obj) ... return all standard features and edges based geometries""" |
| | return ( |
| | PathOp.FeatureTool |
| | | PathOp.FeatureHeights |
| | | PathOp.FeatureDepths |
| | | PathOp.FeatureStepDown |
| | | PathOp.FeatureBaseFaces |
| | | PathOp.FeatureCoolant |
| | ) |
| |
|
| | def setupAdditionalProperties(self, obj): |
| | if not hasattr(obj, "BaseShapes"): |
| | obj.addProperty( |
| | "App::PropertyLinkList", |
| | "BaseShapes", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Additional base objects to be engraved"), |
| | ) |
| | obj.setEditorMode("BaseShapes", 2) |
| |
|
| | if not hasattr(obj, "OptimizeMovements"): |
| |
|
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "OptimizeMovements", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Optimize movements"), |
| | ) |
| | obj.OptimizeMovements = False |
| |
|
| | if not hasattr(obj, "FinishingPass"): |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "FinishingPass", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Add finishing pass"), |
| | ) |
| | obj.FinishingPass = False |
| |
|
| | if not hasattr(obj, "FinishingPassZOffset"): |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "FinishingPassZOffset", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Finishing pass Z offset"), |
| | ) |
| |
|
| | obj.FinishingPassZOffset = "0.00" |
| |
|
| | def initOperation(self, obj): |
| | """initOperation(obj) ... create vcarve specific properties.""" |
| | obj.addProperty( |
| | "App::PropertyFloat", |
| | "Discretize", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "The deflection value for discretizing arcs"), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyFloat", |
| | "Colinear", |
| | "Path", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Cutoff for removing colinear segments (degrees). \ |
| | default=10.0.", |
| | ), |
| | ) |
| | obj.addProperty( |
| | "App::PropertyFloat", |
| | "Tolerance", |
| | "Path", |
| | QT_TRANSLATE_NOOP("App::Property", "Vcarve Tolerance"), |
| | ) |
| |
|
| | obj.Colinear = 10.0 |
| | obj.Discretize = 0.25 |
| | obj.Tolerance = Path.Preferences.defaultGeometryTolerance() |
| | |
| | |
| | |
| | self.Tolerance = getattr(obj, "Tolerance", Path.Preferences.defaultGeometryTolerance()) |
| | self.setupAdditionalProperties(obj) |
| |
|
| | def opOnDocumentRestored(self, obj): |
| | |
| | self.setupAdditionalProperties(obj) |
| |
|
| | def buildMedialWires(self, obj, faces): |
| | """ |
| | constructs a medial axis path using openvoronoi |
| | :returns: dictionary - each face object is a key containing list of wires""" |
| |
|
| | medial_wires_by_face = dict() |
| | edges_by_face = dict() |
| |
|
| | self.voronoiDebugMedialCache = dict() |
| | self.voronoiDebugEdgeCache = dict() |
| |
|
| | def is_exterior(vertex, face): |
| | vector = FreeCAD.Vector(vertex.toPoint(face.BoundBox.ZMin)) |
| | (u, v) = face.Surface.parameter(vector) |
| | |
| | return not face.isPartOfDomain(u, v) |
| |
|
| | def insert_many_wires(vd, wires): |
| | for wire in wires: |
| | Path.Log.debug("discretize value: {}".format(obj.Discretize)) |
| | pts = wire.discretize(QuasiDeflection=obj.Discretize) |
| | ptv = [FreeCAD.Vector(p.x, p.y) for p in pts] |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if len(ptv) > 0: |
| | dist = ptv[-1].distanceToPoint(ptv[0]) |
| | if dist < FreeCAD.Base.Precision.confusion(): |
| | Path.Log.debug( |
| | "Removing bad carve point: {} from polygon origin".format(dist) |
| | ) |
| | del ptv[-1] |
| | ptv.append(ptv[0]) |
| |
|
| | for i in range(len(ptv) - 1): |
| | vd.addSegment(ptv[i], ptv[i + 1]) |
| |
|
| | for f in faces: |
| | voronoiWires = [] |
| | vd = Path.Voronoi.Diagram() |
| | insert_many_wires(vd, f.Wires) |
| |
|
| | vd.construct() |
| | edges_by_face[f] = vd.Edges |
| |
|
| | for e in vd.Edges: |
| | if e.isPrimary(): |
| | if e.isBorderline(): |
| | e.Color = BORDERLINE |
| | else: |
| | e.Color = PRIMARY |
| | else: |
| | e.Color = SECONDARY |
| |
|
| | |
| | |
| | vd.colorColinear(COLINEAR, obj.Colinear) |
| |
|
| | vd.colorExterior(EXTERIOR1) |
| | vd.colorExterior(EXTERIOR2, lambda v: is_exterior(v, f)) |
| |
|
| | |
| | |
| | |
| | vd.colorTwins(TWIN) |
| |
|
| | wires = _collectVoronoiWires(vd) |
| | wires = _sortVoronoiWires(wires) |
| | voronoiWires.extend(wires) |
| |
|
| | medial_wires_by_face[f] = voronoiWires |
| |
|
| | self.voronoiDebugMedialCache = medial_wires_by_face |
| | self.voronoiDebugEdgeCache = edges_by_face |
| |
|
| | return medial_wires_by_face |
| |
|
| | def buildCommandList(self, obj, faces): |
| | """ |
| | Build command list to cut wires - based on voronoi |
| | wire list from buildMedialWires |
| | """ |
| |
|
| | def getPositionHistory(wire): |
| | """ |
| | Get CNC current and previous head position assuming it reached the end of the wire |
| | returns: previousPosition, currentPostion tuple |
| | """ |
| |
|
| | if not wire: |
| | return None |
| |
|
| | lastEdge = wire[-1] |
| | return ( |
| | lastEdge.valueAt(lastEdge.FirstParameter), |
| | lastEdge.valueAt(lastEdge.LastParameter), |
| | ) |
| |
|
| | def cutWires(wires, pathlist, optimizeMovements=False): |
| |
|
| | positionHistory = None |
| |
|
| | for w in wires: |
| | pWire = _getPartEdges(obj, w, geom) |
| | if pWire: |
| | pathlist.extend(_cutWire(pWire, positionHistory)) |
| |
|
| | |
| | if optimizeMovements: |
| | positionHistory = getPositionHistory(pWire) |
| |
|
| | def _cutWire(wire, positionHistory=None): |
| | path = [] |
| |
|
| | backtrack_edges = [] |
| |
|
| | |
| | |
| | |
| |
|
| | backtrack_edges = generateVirtualBackTrackEdges(positionHistory, wire[0], obj.Tolerance) |
| |
|
| | edge_list = backtrack_edges + wire |
| |
|
| | e = edge_list[0] |
| | newPosition = e.valueAt(e.FirstParameter) |
| |
|
| | hSpeed = obj.ToolController.HorizFeed.Value |
| | vSpeed = obj.ToolController.VertFeed.Value |
| |
|
| | |
| | if not canSkipRepositioning(positionHistory, newPosition, obj.Tolerance): |
| | path.append(Path.Command("G0", {"Z": obj.SafeHeight.Value})) |
| | path.append( |
| | Path.Command( |
| | "G0", |
| | { |
| | "X": newPosition.x, |
| | "Y": newPosition.y, |
| | "Z": obj.SafeHeight.Value, |
| | }, |
| | ) |
| | ) |
| |
|
| | path.append( |
| | Path.Command( |
| | "G1", |
| | { |
| | "X": newPosition.x, |
| | "Y": newPosition.y, |
| | "Z": newPosition.z, |
| | "F": vSpeed, |
| | }, |
| | ) |
| | ) |
| | else: |
| | |
| | |
| | |
| | path.append( |
| | Path.Command( |
| | "G1 X{} Y{} Z{} F{}".format( |
| | newPosition.x, newPosition.y, newPosition.z, hSpeed |
| | ) |
| | ) |
| | ) |
| |
|
| | for e in edge_list: |
| | path.extend(Path.Geom.cmdsForEdge(e, hSpeed=hSpeed, vSpeed=vSpeed)) |
| |
|
| | return path |
| |
|
| | pathlist = [] |
| | pathlist.append(Path.Command("(starting)")) |
| |
|
| | geom = _Geometry.FromObj(obj, self.model[0]) |
| |
|
| | |
| | for face, wires in self.buildMedialWires(obj, faces).items(): |
| |
|
| | |
| | |
| | |
| |
|
| | maximumUsableDepth = geom.stop |
| |
|
| | if geom.stepDown > 0: |
| | _maximumUsableDepth = _get_maximumUsableDepth(wires, geom) |
| | if _maximumUsableDepth is not None: |
| | maximumUsableDepth = _maximumUsableDepth |
| | Path.Log.debug(f"Maximum usable depth for current face: {maximumUsableDepth}") |
| |
|
| | |
| | cutWires(wires, pathlist, obj.OptimizeMovements) |
| |
|
| | |
| | while geom.incrementStepDownDepth(maximumUsableDepth): |
| | cutWires(wires, pathlist, obj.OptimizeMovements) |
| |
|
| | |
| |
|
| | if obj.FinishingPass: |
| | geom.offset = obj.FinishingPassZOffset.Value |
| |
|
| | cutWires(wires, pathlist, obj.OptimizeMovements) |
| |
|
| | self.commandlist = pathlist |
| |
|
| | def opExecute(self, obj): |
| | """opExecute(obj) ... process engraving operation""" |
| | Path.Log.track() |
| |
|
| | self.voronoiDebugMedialCache = None |
| | self.voronoiDebugEdgesCache = None |
| |
|
| | if obj.ToolController is None: |
| | return |
| |
|
| | if not hasattr(obj.ToolController.Tool, "CuttingEdgeAngle"): |
| | Path.Log.info( |
| | translate( |
| | "CAM_Vcarve", |
| | "VCarve requires an engraving cutter with a cutting edge angle", |
| | ) |
| | ) |
| | return |
| |
|
| | if obj.ToolController.Tool.CuttingEdgeAngle >= 180.0: |
| | Path.Log.info( |
| | translate("CAM_Vcarve", "Engraver cutting edge angle must be < 180 degrees.") |
| | ) |
| | return |
| |
|
| | try: |
| | faces = [] |
| |
|
| | for base in obj.BaseShapes: |
| | faces.extend(base.Shape.Faces) |
| |
|
| | for base in obj.Base: |
| | for sub in base[1]: |
| | shape = getattr(base[0].Shape, sub) |
| | if isinstance(shape, Part.Face): |
| | faces.append(shape) |
| |
|
| | if not faces: |
| | for model in self.model: |
| | if model.isDerivedFrom("Sketcher::SketchObject") or model.isDerivedFrom( |
| | "Part::Part2DObject" |
| | ): |
| | faces.extend(model.Shape.Faces) |
| |
|
| | if faces: |
| | self.buildCommandList(obj, faces) |
| | else: |
| | Path.Log.error( |
| | translate( |
| | "PathVcarve", |
| | "The Job Base Object has no engraveable element. Engraving operation will produce no output.", |
| | ) |
| | ) |
| |
|
| | except Exception: |
| | Path.Log.warning( |
| | "Error processing Base object. Engraving operation will produce no output." |
| | ) |
| | import traceback |
| |
|
| | Path.Log.error(f"Engraving operation exception: {traceback.format_exc()}") |
| |
|
| | def opUpdateDepths(self, obj, ignoreErrors=False): |
| | """updateDepths(obj) ... engraving is always done at the top most z-value""" |
| | job = PathUtils.findParentJob(obj) |
| | self.opSetDefaultValues(obj, job) |
| |
|
| | def opSetDefaultValues(self, obj, job): |
| | """opSetDefaultValues(obj) ... set depths for vcarving""" |
| | if PathOp.FeatureDepths & self.opFeatures(obj): |
| | if job and len(job.Model.Group) > 0: |
| | bb = job.Proxy.modelBoundBox(job) |
| | obj.OpStartDepth = bb.ZMax |
| | obj.OpFinalDepth = job.Stock.Shape.BoundBox.ZMin |
| | else: |
| | obj.OpFinalDepth = -0.1 |
| |
|
| | def isToolSupported(self, obj, tool): |
| | """isToolSupported(obj, tool) ... returns True if v-carve op can work with tool.""" |
| | return ( |
| | hasattr(tool, "Diameter") |
| | and hasattr(tool, "CuttingEdgeAngle") |
| | and hasattr(tool, "TipDiameter") |
| | ) |
| |
|
| | def debugVoronoiMedial(self, obj): |
| | """Debug function to display calculated voronoi medial wires""" |
| |
|
| | if not getattr(self, "voronoiDebugMedialCache", None): |
| | Path.Log.error("debugVoronoi: empty debug cache. Recompute VCarve operation first") |
| | return |
| |
|
| | vPart = FreeCAD.activeDocument().addObject("App::Part", f"{obj.Name}-VoronoiDebugMedial") |
| |
|
| | wiresToShow = [] |
| |
|
| | for face, wires in self.voronoiDebugMedialCache.items(): |
| | for wire in wires: |
| | currentPartWire = Part.Wire() |
| | currentPartWire.fixTolerance(0.01) |
| | for edge in wire: |
| | currentEdge = edge.toShape() |
| |
|
| | for v in currentEdge.Vertexes: |
| | v.fixTolerance(0.1) |
| |
|
| | currentPartWire.add(currentEdge) |
| | wiresToShow.append(currentPartWire) |
| |
|
| | for w in wiresToShow: |
| | vPart.addObject(Part.show(w)) |
| |
|
| | def debugVoronoiEdges(self, obj): |
| | """Debug function to display calculated voronoi edges""" |
| |
|
| | if not getattr(self, "voronoiDebugEdgeCache", None): |
| | Path.Log.error("debugVoronoi: empty debug cache. Recompute VCarve operation first") |
| | return |
| |
|
| | vPart = FreeCAD.activeDocument().addObject("App::Part", f"{obj.Name}-VoronoiDebugEdge") |
| |
|
| | edgesToShow = [] |
| |
|
| | for face, edges in self.voronoiDebugEdgeCache.items(): |
| | for edge in edges: |
| | currentEdge = edge.toShape() |
| |
|
| | edgesToShow.append(currentEdge) |
| |
|
| | for e in edgesToShow: |
| | vPart.addObject(Part.show(e)) |
| |
|
| |
|
| | def SetupProperties(): |
| | return ["Discretize"] |
| |
|
| |
|
| | def Create(name, obj=None, parentJob=None): |
| | """Create(name) ... Creates and returns a Vcarve operation.""" |
| | if obj is None: |
| | obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) |
| | obj.Proxy = ObjectVcarve(obj, name, parentJob) |
| | return obj |
| |
|