| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| | import FreeCAD |
| | import Path |
| | import Path.Op.Base as PathOp |
| | import Path.Op.PocketBase as PathPocketBase |
| |
|
| |
|
| | |
| | from lazy_loader.lazy_loader import LazyLoader |
| |
|
| | Part = LazyLoader("Part", globals(), "Part") |
| | TechDraw = LazyLoader("TechDraw", globals(), "TechDraw") |
| | math = LazyLoader("math", globals(), "math") |
| | PathUtils = LazyLoader("PathScripts.PathUtils", globals(), "PathScripts.PathUtils") |
| | FeatureExtensions = LazyLoader("Path.Op.FeatureExtension", globals(), "Path.Op.FeatureExtension") |
| |
|
| |
|
| | __title__ = "CAM Pocket Shape Operation" |
| | __author__ = "sliptonic (Brad Collette)" |
| | __url__ = "https://www.freecad.org" |
| | __doc__ = "Class and implementation of shape based Pocket operation." |
| |
|
| |
|
| | 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()) |
| |
|
| |
|
| | class ObjectPocket(PathPocketBase.ObjectPocket): |
| | """Proxy object for Pocket operation.""" |
| |
|
| | def areaOpFeatures(self, obj): |
| | return super(self.__class__, self).areaOpFeatures(obj) | PathOp.FeatureLocations |
| |
|
| | def removeHoles(self, solid, face): |
| | """removeHoles(solid, face) ... Remove hole wires from a face, keeping outer wire and boss wires. |
| | |
| | Uses a cross-section algorithm: sections the solid slightly above the face level. |
| | Wires that appear in the section are bosses (material above). |
| | Wires that don't appear are holes (voids). |
| | |
| | Args: |
| | solid: The parent solid object |
| | face: The face to process |
| | |
| | Returns: |
| | New face with outer wire and boss wires only |
| | """ |
| | outer_wire = face.OuterWire |
| | candidate_wires = [w for w in face.Wires if not w.isSame(outer_wire)] |
| |
|
| | |
| | adaptive_tolerance = max(1e-6, min(1e-2, face.BoundBox.DiagonalLength * 1e-5)) |
| | Path.Log.debug( |
| | f"removeHoles: Using adaptive tolerance {adaptive_tolerance} (face diagonal: {face.BoundBox.DiagonalLength})" |
| | ) |
| |
|
| | for i, w in enumerate(candidate_wires): |
| | Path.Log.debug(f" Candidate {i}: Length={w.Length}") |
| |
|
| | if not candidate_wires: |
| | return face |
| |
|
| | boss_wires = [] |
| |
|
| | try: |
| | |
| | cutting_plane = Part.Face(outer_wire) |
| | cutting_plane.translate(FreeCAD.Vector(0, 0, adaptive_tolerance)) |
| |
|
| | |
| | section = solid.Shape.section(cutting_plane) |
| |
|
| | if hasattr(section, "Edges") and section.Edges: |
| | |
| | translated_edges = [] |
| | for edge in section.Edges: |
| | translated_edge = edge.copy() |
| | translated_edge.translate(FreeCAD.Vector(0, 0, -adaptive_tolerance)) |
| | translated_edges.append(translated_edge) |
| |
|
| | |
| | edge_groups = Part.sortEdges(translated_edges) |
| | all_section_wires = [] |
| |
|
| | for edge_list in edge_groups: |
| | try: |
| | wire = Part.Wire(edge_list) |
| | if wire.isClosed(): |
| | all_section_wires.append(wire) |
| | except Exception: |
| | |
| | pass |
| |
|
| | Path.Log.debug(f"removeHoles: Section found {len(all_section_wires)} wires") |
| | for i, w in enumerate(all_section_wires): |
| | Path.Log.debug(f" Section wire {i}: Length={w.Length}") |
| |
|
| | |
| | for wire in all_section_wires: |
| | if not wire.isSame(outer_wire): |
| | length_diff = abs(wire.Length - outer_wire.Length) |
| | if length_diff > adaptive_tolerance: |
| | boss_wires.append(wire) |
| | Path.Log.debug( |
| | f" Preserving boss wire: Length={wire.Length}, diff={length_diff}" |
| | ) |
| | else: |
| | Path.Log.debug( |
| | f" Discarding wire (too similar to outer): Length={wire.Length}, diff={length_diff}" |
| | ) |
| |
|
| | except Exception as e: |
| | Path.Log.error("removeHoles: Section algorithm failed: {}".format(e)) |
| | boss_wires = candidate_wires |
| | Path.Log.debug("removeHoles: Section failed, preserving all candidate wires as bosses") |
| |
|
| | Path.Log.debug(f"removeHoles: Preserving {len(boss_wires)} boss wires") |
| | for i, w in enumerate(boss_wires): |
| | Path.Log.debug(f" Preserved boss {i}: Length={w.Length}") |
| |
|
| | removed_wires = [w for w in candidate_wires if not any(w.isSame(bw) for bw in boss_wires)] |
| | Path.Log.debug(f"removeHoles: Removing {len(removed_wires)} hole wires") |
| | for i, w in enumerate(removed_wires): |
| | Path.Log.debug(f" Removed hole {i}: Length={w.Length}") |
| |
|
| | |
| | wire_compound = Part.makeCompound([outer_wire] + boss_wires) |
| | new_face = Part.makeFace(wire_compound, "Part::FaceMakerBullseye") |
| |
|
| | return new_face |
| |
|
| | def initPocketOp(self, obj): |
| | """initPocketOp(obj) ... setup receiver""" |
| | if not hasattr(obj, "UseOutline"): |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "UseOutline", |
| | "Pocket", |
| | QT_TRANSLATE_NOOP("App::Property", "Uses the outline of the base geometry."), |
| | ) |
| |
|
| | FeatureExtensions.initialize_properties(obj) |
| |
|
| | def areaOpOnDocumentRestored(self, obj): |
| | """opOnDocumentRestored(obj) ... adds the UseOutline property if it doesn't exist.""" |
| | self.initPocketOp(obj) |
| |
|
| | def pocketInvertExtraOffset(self): |
| | return False |
| |
|
| | def areaOpSetDefaultValues(self, obj, job): |
| | """areaOpSetDefaultValues(obj, job) ... set default values""" |
| | obj.ClearingPattern = "Offset" |
| | obj.StepOver = 50 |
| | obj.ZigZagAngle = 45 |
| | obj.UseOutline = False |
| | FeatureExtensions.set_default_property_values(obj, job) |
| |
|
| | def areaOpShapes(self, obj): |
| | """areaOpShapes(obj) ... return shapes representing the solids to be removed.""" |
| | Path.Log.track() |
| | self.removalshapes = [] |
| |
|
| | |
| | self.removalshapes = [] |
| | avoidFeatures = list() |
| |
|
| | |
| | extensions = FeatureExtensions.getExtensions(obj) |
| | for e in extensions: |
| | if e.avoid: |
| | avoidFeatures.append(e.feature) |
| |
|
| | if obj.Base: |
| | Path.Log.debug("base items exist. Processing...") |
| | self.horiz = [] |
| | self.vert = [] |
| | for base, subList in obj.Base: |
| | for sub in subList: |
| | if "Face" in sub: |
| | if sub not in avoidFeatures and not self.classifySub(base, sub): |
| | Path.Log.error( |
| | "Pocket does not support shape {}.{}".format(base.Label, sub) |
| | ) |
| |
|
| | |
| | Path.Log.debug("UseOutline: {}".format(obj.UseOutline)) |
| | Path.Log.debug("self.horiz: {}".format(self.horiz)) |
| | if obj.UseOutline and self.horiz: |
| | horiz = [self.removeHoles(base, face) for (face, base) in self.horiz] |
| | self.horiz = horiz |
| | else: |
| | |
| | self.horiz = [face for (face, base) in self.horiz] |
| |
|
| | |
| | if len(self.vert) > 0: |
| | self.vertical = Path.Geom.combineConnectedShapes(self.vert) |
| | self.vWires = [ |
| | TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) |
| | for shape in self.vertical |
| | ] |
| | for wire in self.vWires: |
| | w = Path.Geom.removeDuplicateEdges(wire) |
| | face = Part.Face(w) |
| | |
| | if Path.Geom.isRoughly(face.Area, 0): |
| | Path.Log.error("Vertical faces do not form a loop - ignoring") |
| | else: |
| | self.horiz.append(face) |
| |
|
| | |
| | |
| | self.exts = [] |
| | for ext in extensions: |
| | if not ext.avoid: |
| | wire = ext.getWire() |
| | if wire: |
| | faces = ext.getExtensionFaces(wire) |
| | for f in faces: |
| | self.horiz.append(f) |
| | self.exts.append(f) |
| |
|
| | |
| | self.horizontal = Path.Geom.combineHorizontalFaces(self.horiz) |
| |
|
| | |
| | |
| | if self.job.GeometryTolerance.Value == 0.0: |
| | buffer = 0.000001 |
| | else: |
| | buffer = self.job.GeometryTolerance.Value / 10.0 |
| | for h in self.horizontal: |
| | h.translate( |
| | FreeCAD.Vector(0.0, 0.0, obj.FinalDepth.Value - h.BoundBox.ZMin - buffer) |
| | ) |
| |
|
| | |
| | extent = FreeCAD.Vector(0, 0, obj.StartDepth.Value - obj.FinalDepth.Value + buffer) |
| | self.removalshapes = [ |
| | (face.removeSplitter().extrude(extent), False) for face in self.horizontal |
| | ] |
| |
|
| | else: |
| | Path.Log.debug("processing the whole job base object") |
| | self.outlines = [ |
| | Part.Face(TechDraw.findShapeOutline(base.Shape, 1, FreeCAD.Vector(0, 0, 1))) |
| | for base in self.model |
| | ] |
| | stockBB = self.stock.Shape.BoundBox |
| |
|
| | self.bodies = [] |
| | for outline in self.outlines: |
| | outline.translate(FreeCAD.Vector(0, 0, stockBB.ZMin - 1)) |
| | body = outline.extrude(FreeCAD.Vector(0, 0, stockBB.ZLength + 2)) |
| | self.bodies.append(body) |
| | self.removalshapes.append((self.stock.Shape.cut(body), False)) |
| |
|
| | |
| | |
| | |
| |
|
| | if self.removalshapes: |
| | obj.removalshape = Part.makeCompound([tup[0] for tup in self.removalshapes]) |
| |
|
| | return self.removalshapes |
| |
|
| | |
| | def isVerticalExtrusionFace(self, face): |
| | fBB = face.BoundBox |
| | if Path.Geom.isRoughly(fBB.ZLength, 0.0): |
| | return False |
| | extr = face.extrude(FreeCAD.Vector(0.0, 0.0, fBB.ZLength)) |
| | if hasattr(extr, "Volume"): |
| | if Path.Geom.isRoughly(extr.Volume, 0.0): |
| | return True |
| | return False |
| |
|
| | def classifySub(self, bs, sub): |
| | """classifySub(bs, sub)... |
| | Given a base and a sub-feature name, returns True |
| | if the sub-feature is a horizontally or vertically oriented flat face. |
| | """ |
| | face = bs.Shape.getElement(sub) |
| |
|
| | if isinstance(face.Surface, Part.Plane): |
| | Path.Log.debug("type() == Part.Plane") |
| | if Path.Geom.isVertical(face.Surface.Axis): |
| | Path.Log.debug(" -isVertical()") |
| | |
| | self.horiz.append((face, bs)) |
| | return True |
| |
|
| | elif Path.Geom.isHorizontal(face.Surface.Axis): |
| | Path.Log.debug(" -isHorizontal()") |
| | self.vert.append(face) |
| | return True |
| | else: |
| | return False |
| |
|
| | elif isinstance(face.Surface, Part.BSplineSurface): |
| | Path.Log.debug("face Part.BSplineSurface") |
| | if Path.Geom.isRoughly(face.BoundBox.ZLength, 0): |
| | Path.Log.debug(" flat horizontal or almost flat horizontal") |
| | self.horiz.append((face, bs)) |
| | return True |
| |
|
| | elif isinstance(face.Surface, Part.Cylinder) and Path.Geom.isVertical(face.Surface.Axis): |
| | Path.Log.debug("type() == Part.Cylinder") |
| | |
| | if any(e.isClosed() for e in face.Edges): |
| | Path.Log.debug(" -e.isClosed()") |
| | |
| | circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center) |
| | disk = Part.Face(Part.Wire(circle)) |
| | disk.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - disk.BoundBox.ZMin)) |
| | self.horiz.append((disk, bs)) |
| | return True |
| |
|
| | else: |
| | Path.Log.debug(" -none isClosed()") |
| | |
| | self.vert.append(face) |
| | return True |
| |
|
| | elif isinstance(face.Surface, Part.SurfaceOfExtrusion): |
| | |
| | Path.Log.debug("type() == Part.SurfaceOfExtrusion") |
| | |
| | if self.isVerticalExtrusionFace(face): |
| | self.vert.append(face) |
| | return True |
| | else: |
| | Path.Log.error("Failed to identify vertical face from {}".format(sub)) |
| | return False |
| |
|
| | else: |
| | Path.Log.debug(" -type(face.Surface): {}".format(type(face.Surface))) |
| | return False |
| |
|
| |
|
| | |
| |
|
| |
|
| | def SetupProperties(): |
| | setup = PathPocketBase.SetupProperties() |
| | setup.extend(FeatureExtensions.SetupProperties()) |
| |
|
| | |
| | setup.append("UseOutline") |
| | return setup |
| |
|
| |
|
| | def Create(name, obj=None, parentJob=None): |
| | """Create(name) ... Creates and returns a Pocket operation.""" |
| | if obj is None: |
| | obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) |
| | obj.Proxy = ObjectPocket(obj, name, parentJob) |
| | return obj |
| |
|