From 800fab5acb1928d5fb391dad8359b5ab74c9fa8d Mon Sep 17 00:00:00 2001 From: shai Date: Sat, 11 Jan 2025 21:14:05 +0200 Subject: [PATCH] Integrate new sheetmetal unfolder and update readme to PRs #430, #432, #433 and #435 --- README.md | 4 + Resources/panels/SMprefs.ui | 28 ++++++ SheetMetalNewUnfolder.py | 178 +++++++++++++++++++++--------------- SheetMetalTools.py | 3 + SheetMetalUnfoldCmd.py | 82 ++++++++++++----- SheetMetalUnfolder.py | 2 +- package.xml | 4 +- 7 files changed, 202 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 946a000..98bd74e 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,9 @@ Starting from FreeCAD 0.17 it can be installed via the [Addon Manager](https://g * FreeCAD Forum announcement/discussion [thread](https://forum.freecadweb.org/viewtopic.php?f=3&t=60818) #### Release notes: +* V0.7.00 11 Jan 2025: New SheetMetal Unfolder! by [@alexneufeld][alexneufeld]. + - Unfolder backward compatibility fixes by [@GS90][GS90]. + - Typo fixes by [@hasecilu][hasecilu]. * V0.6.13 25 Dec 2024: AddBase: Fix wrong shading on LinkStage. * V0.6.12 23 Dec 2024: AddBase: Sketch remain visible while task UI is open. * V0.6.11 20 Dec 2024: Reinstate unattended unfold command. @@ -283,6 +286,7 @@ Starting from FreeCAD 0.17 it can be installed via the [Addon Manager](https://g [adrianinsaval]: https://github.com/adrianinsaval [robbeban]: https://github.com/robbeban [sheetmetalman]: https://github.com/sheetmetalman +[GS90]: https://github.com/GS90 [topic82482]: https://forum.freecad.org/viewtopic.php?t=82482 [30]: https://github.com/shaise/FreeCAD_SheetMetal/issues/30 [33]: https://github.com/shaise/FreeCAD_SheetMetal/issues/33 diff --git a/Resources/panels/SMprefs.ui b/Resources/panels/SMprefs.ui index 46b1348..6d9f433 100644 --- a/Resources/panels/SMprefs.ui +++ b/Resources/panels/SMprefs.ui @@ -132,6 +132,29 @@ + + + + 0 + + + + + Qt::LeftToRight + + + Revert To Old Unfolder + + + UseOldUnfolder + + + Mod/SheetMetal + + + + + @@ -182,6 +205,11 @@ QComboBox
Gui/PrefWidgets.h
+ + Gui::PrefCheckBox + QCheckBox +
Gui/PrefWidgets.h
+
diff --git a/SheetMetalNewUnfolder.py b/SheetMetalNewUnfolder.py index 5bff7be..988d09c 100644 --- a/SheetMetalNewUnfolder.py +++ b/SheetMetalNewUnfolder.py @@ -32,7 +32,7 @@ import FreeCAD import Part -from Draft import makeSketch +import Draft from FreeCAD import Matrix, Placement, Rotation, Vector from TechDraw import projectEx as project_shape_to_plane @@ -448,23 +448,47 @@ class SketchExtraction: @staticmethod def edges_to_sketch_object( - edges: list[Part.Edge], object_name: str + edges: list[Part.Edge], + object_name: str, + existing_sketches: list[str] = None, + color: str = "#00FF00" ) -> FreeCAD.DocumentObject: """Uses functionality from the Draft API to convert a list of edges into a Sketch document object. This allows the user to more easily make small changes to the sheet metal cutting pattern when prepping it for fabrication.""" cleaned_up_edges = Edge2DCleanup.cleanup_sketch(edges, 0.1) - sk = makeSketch( + #cleaned_up_edges = edges + + # See if there is an existing sketch with the same name, use it insted of creating + if existing_sketches is None: + existing_sketch_name = "" + else: + existing_sketch_name = next((item for item in existing_sketches if item.startswith(object_name)), "") + existing_sketch = FreeCAD.ActiveDocument.getObject(existing_sketch_name) + if existing_sketch is not None: + existing_sketch.deleteAllGeometry() + + sk = Draft.makeSketch( # NOTE: in testing, using the autoconstraint feature # caused errors with some shapes cleaned_up_edges, autoconstraints=False, - addTo=None, - delete=False, - name=object_name, + addTo = existing_sketch, + delete = False, + name = object_name, ) sk.Label = object_name + sk.recompute() + + if FreeCAD.GuiUp: + rgb_color = tuple(int(color[i : i + 2], 16) for i in (1, 3, 5)) + v = FreeCAD.Version() + if v[0] == '0' and int(v[1]) < 21: + rgb_color = tuple(i / 255 for i in rgb_color) + sk.ViewObject.LineColor = rgb_color + sk.ViewObject.PointColor = rgb_color + return sk @staticmethod @@ -655,8 +679,12 @@ class Edge2DCleanup: replace bezier curves and other geometry types with lines and arcs""" @staticmethod - def bspline_to_single_arc(curve: Part.Edge) -> tuple[Part.Edge, float]: - line = Part.makeLine(curve.firstVertex().Point, curve.lastVertex().Point) + def bspline_to_line(curve: Part.Edge) -> tuple[Part.Edge, float]: + p1 = curve.firstVertex().Point + p2 = curve.lastVertex().Point + if p1.distanceToPoint(p2) < eps: + return Part.Edge(), float("inf") + line = Part.makeLine(p1, p2) max_err = Edge2DCleanup.check_err(curve, line) return line, max_err @@ -679,13 +707,25 @@ def check_err(curve1: Part.Edge, curve2: Part.Edge) -> float: return max_err @staticmethod - def bspline_to_line(curve: Part.Edge) -> tuple[Part.Edge, float]: + def bspline_to_arc(curve: Part.Edge) -> tuple[Part.Edge, float]: point1 = curve.firstVertex().Point - point3 = curve.lastVertex().Point point2 = curve.valueAt( curve.FirstParameter + 0.5 * (curve.LastParameter - curve.FirstParameter) ) - arc = Part.Arc(point1, point2, point3).toShape().Edges[0] + point3 = curve.lastVertex().Point + if point1.distanceToPoint(point3) < eps: + # full circle + point4 = curve.valueAt( + curve.FirstParameter + + 0.25 * (curve.LastParameter - curve.FirstParameter) + ) + radius = point1.distanceToPoint(point2) / 2 + center = point1 + 0.5 * (point2 - point1) + axis = (point1 - center).cross(point4 - center) + arc = Part.makeCircle(radius, center, axis) + else: + # partial circle + arc = Part.Arc(point1, point2, point3).toShape().Edges[0] max_err = Edge2DCleanup.check_err(curve, arc) return arc, max_err @@ -693,24 +733,25 @@ def bspline_to_line(curve: Part.Edge) -> tuple[Part.Edge, float]: def cleanup_sketch(sketch: list[Part.Edge], tolerance: float) -> list[Part.Edge]: new_edge_list = [] for edge in sketch: - if isinstance(edge.Curve, (Part.Line, Part.Arc)): - new_edge_list.append(edge) - else: - if isinstance(edge.Curve, Part.BSplineCurve): - bspline = edge - else: - bspline = edge.toNurbs().Edges[0] - line, max_err = Edge2DCleanup.bspline_to_line(bspline) - if max_err < tolerance: - new_edge_list.append(line) - continue - arc, max_err = Edge2DCleanup.bspline_to_single_arc(bspline) - if max_err < tolerance: - new_edge_list.append(line) - continue - new_edge_list.extend( - a.toShape().Edges[0] for a in bspline.Curve.toBiArcs(tolerance) - ) + match edge.Curve.TypeId: + case "Part::GeomLine" | "Part::GeomCircle": + new_edge_list.append(edge) + case _: + if isinstance(edge.Curve, Part.BSplineCurve): + bspline = edge + else: + bspline = edge.toNurbs().Edges[0] + new_edge, max_err = Edge2DCleanup.bspline_to_line(bspline) + if max_err < tolerance: + new_edge_list.append(new_edge) + continue + new_edge, max_err = Edge2DCleanup.bspline_to_arc(bspline) + if max_err < tolerance: + new_edge_list.append(new_edge) + continue + new_edge_list.extend( + a.toShape().Edges[0] for a in bspline.Curve.toBiArcs(tolerance) + ) return new_edge_list @@ -1056,76 +1097,65 @@ def unfold( return solid, bend_lines -def gui_unfold(bac: BendAllowanceCalculator) -> None: - """This is the main entry-point for the unfolder. - It grabs a selected sheet metal part and reference face from the active - FreeCAD document, and creates new objects showing the unfold results.""" - # the user must select a single flat face of a sheet metal part in the - # active document - selection = FreeCAD.Gui.Selection.getCompleteSelection()[0] - selected_object = selection.Object - object_placement = selected_object.getGlobalPlacement().toMatrix() - shp = selected_object.Shape.transformed(object_placement.inverse()) - root_face_index = int(selection.SubElementNames[0][4:]) - 1 +def getUnfold( + bac: BendAllowanceCalculator, solid: Part.Feature, facename : str + ) -> tuple[Part.Face, Part.Shape, Part.Compound, FreeCAD.Vector]: + object_placement = solid.Placement.toMatrix() + shp = solid.Shape.transformed(object_placement.inverse()) + root_face_index = int(facename[4:]) - 1 unfolded_shape, bend_lines = unfold(shp, root_face_index, bac) - # show the unfolded solid in the active document - unfold_doc_obj = Part.show(unfolded_shape, selected_object.Label + "_Unfold") - unfold_vobj = unfold_doc_obj.ViewObject - unfold_doc_obj.Placement = Placement(object_placement) - # set appearance - unfold_vobj.ShapeAppearance = selected_object.ViewObject.ShapeAppearance - unfold_vobj.Transparency = 70 # FIXME: hardcoded value root_normal = shp.Faces[root_face_index].normalAt(0, 0) + return shp.Faces[root_face_index], unfolded_shape, bend_lines, root_normal + +def getUnfoldSketches( + selected_face: Part.Face, + unfolded_shape: Part.Shape, + bend_lines: Part.Compound, + root_normal: FreeCAD.Vector, + existing_sketches: list[str], + split_sketches: bool = False, + sketch_color: str = "#000080", + bend_sketch_color: str = "#c00000", + internal_sketch_solor: str ="#ff5733" +) -> list[Part.Feature]: sketch_profile, inner_wires, hole_wires = SketchExtraction.extract_manually( unfolded_shape, root_normal ) - SEPERATE_SKETCHES = False # FIXME: hardcoded value - if not SEPERATE_SKETCHES: - sketch_profile = Part.makeCompound([sketch_profile, *inner_wires, *hole_wires]) - inner_wires = None - hole_wires = None - # move the sketch profiles nicely to the origin + # create transform to move the sketch profiles nicely to the origin sketch_align_transform = SketchExtraction.move_to_origin( - sketch_profile, shp.Faces[root_face_index] + sketch_profile, selected_face ) + + if not split_sketches: + sketch_profile = Part.makeCompound([sketch_profile, *inner_wires, *hole_wires, bend_lines]) + inner_wires = None + hole_wires = None + bend_lines = None sketch_profile = sketch_profile.transformed(sketch_align_transform) # organize the unfold sketch layers in a group sketch_doc_obj = SketchExtraction.edges_to_sketch_object( - sketch_profile.Edges, selected_object.Label + "_UnfoldProfile" + sketch_profile.Edges, "Unfold_Sketch", existing_sketches, sketch_color ) - sketch_objects_list = [ - sketch_doc_obj, - ] - sketch_color = (255, 0, 0, 0) # FIXME: hardcoded value - sketch_doc_obj.ViewObject.LineColor = sketch_color - sketch_doc_obj.ViewObject.PointColor = sketch_color + sketch_objects_list = [sketch_doc_obj] # bend lines are sometimes not present - if bend_lines.Edges: + if bend_lines and bend_lines.Edges: bend_lines = bend_lines.transformed(sketch_align_transform) bend_lines_doc_obj = SketchExtraction.edges_to_sketch_object( - bend_lines.Edges, selected_object.Label + "_UnfoldBendLines" + bend_lines.Edges, "Unfold_Sketch_Bends", existing_sketches, bend_sketch_color ) - bend_color = (0, 255, 0, 0) # FIXME: hardcoded value - bend_lines_doc_obj.ViewObject.LineColor = bend_color - bend_lines_doc_obj.ViewObject.PointColor = bend_color bend_lines_doc_obj.ViewObject.DrawStyle = "Dashdot" sketch_objects_list.append(bend_lines_doc_obj) # inner lines are sometimes not present if inner_wires: inner_lines = Part.makeCompound(inner_wires).transformed(sketch_align_transform) inner_lines_doc_obj = SketchExtraction.edges_to_sketch_object( - inner_lines.Edges, selected_object.Label + "_UnfoldInnerLines" + inner_lines.Edges, "Unfold_Sketch_Internal", existing_sketches, internal_sketch_solor ) - inner_color = (0, 0, 255, 0) # FIXME: hardcoded value - inner_lines_doc_obj.ViewObject.LineColor = inner_color - inner_lines_doc_obj.ViewObject.PointColor = inner_color sketch_objects_list.append(inner_lines_doc_obj) if hole_wires: hole_lines = Part.makeCompound(hole_wires).transformed(sketch_align_transform) hole_lines_doc_obj = SketchExtraction.edges_to_sketch_object( - hole_lines.Edges, selected_object.Label + "_UnfoldHoles" + hole_lines.Edges, "Unfold_Sketch_Holes", existing_sketches, internal_sketch_solor ) - hole_color = (255, 255, 0, 0) # FIXME: hardcoded value - hole_lines_doc_obj.ViewObject.LineColor = hole_color - hole_lines_doc_obj.ViewObject.PointColor = hole_color sketch_objects_list.append(hole_lines_doc_obj) + return sketch_objects_list diff --git a/SheetMetalTools.py b/SheetMetalTools.py index 3257978..a15eaef 100644 --- a/SheetMetalTools.py +++ b/SheetMetalTools.py @@ -551,6 +551,9 @@ def smIsOperationLegal(body, selobj): def is_autolink_enabled(): return params.GetInt("AutoLinkBendRadius", 0) +def use_old_unfolder(): + return params.GetBool("UseOldUnfolder", False) + def GetViewConfig(obj): if smIsSketchObject(obj): return None diff --git a/SheetMetalUnfoldCmd.py b/SheetMetalUnfoldCmd.py index 289b18f..897dc4c 100644 --- a/SheetMetalUnfoldCmd.py +++ b/SheetMetalUnfoldCmd.py @@ -33,6 +33,15 @@ from SheetMetalTools import SMLogger, UnfoldException from engineering_mode import engineering_mode_enabled +try: + import SheetMetalNewUnfolder + from SheetMetalNewUnfolder import BendAllowanceCalculator + NewUnfolderAvailable = True +except ImportError: + NewUnfolderAvailable = False + FreeCAD.Console.PrintWarning( + "New unfolder not available on versions pre FreeCAD 1.0. Using old Unfolder\n" + ) translate = FreeCAD.Qt.translate @@ -108,7 +117,6 @@ def __init__(self, obj, selobj, sel_elements): self.ExportType = "dxf" self.visibleSketches = [] SheetMetalTools.taskRestoreDefaults(self, smUnfoldNonSavedDefaultVars) - obj.Proxy = self self.UnfoldSketches = [] @@ -132,7 +140,7 @@ def _addProperties(self, obj): "App::PropertyString", "MaterialSheet", translate( "SheetMetal", "Material definition sheet" ), - "_none", + "_manual", readOnly = True ) SheetMetalTools.smAddBoolProperty( @@ -187,33 +195,65 @@ def onChanged(self, obj, prop): if not isVisible: obj.Proxy.visibleSketches = visibleSketches - def execute(self, fp): - '''"Print a short message when doing a recomputation, this method is mandatory"''' - self._addProperties(fp) - if fp.ManualRecompute and not SheetMetalTools.smForceRecompute: - SheetMetalTools.smAddToRecompute(fp) - return False - kFactorTable = {1: fp.KFactor} - if fp.MaterialSheet != "_manual" and fp.MaterialSheet != "_none": - lookupTable = SheetMetalKfactor.KFactorLookupTable(fp.MaterialSheet) + def newUnfolder(self, obj): + ''' Use new unfolder system ''' + if obj.MaterialSheet in ["_manual", "_none"]: + bac = BendAllowanceCalculator.from_single_value(obj.KFactor) + else: + sheet = FreeCAD.ActiveDocument.getObject(obj.MaterialSheet) + bac = BendAllowanceCalculator.from_spreadsheet(sheet) + sel_face, unfolded_shape, bend_lines, root_normal = SheetMetalNewUnfolder.getUnfold( + bac, obj.baseObject[0], obj.baseObject[1][0] + ) + + sketches = [] + if obj.GenerateSketch and unfolded_shape is not None: + sketches = SheetMetalNewUnfolder.getUnfoldSketches( + sel_face, + unfolded_shape, + bend_lines, + root_normal, + obj.UnfoldSketches, + obj.SeparateSketchLayers, + obj.Proxy.SketchColor, + obj.Proxy.InternalColor, + obj.Proxy.BendLineColor, + ) + return unfolded_shape, sketches + + def oldUnfolder(self, obj): + ''' Use old unfolder system ''' + kFactorTable = {1: obj.KFactor} + if obj.MaterialSheet != "_manual" and obj.MaterialSheet != "_none": + lookupTable = SheetMetalKfactor.KFactorLookupTable(obj.MaterialSheet) kFactorTable = lookupTable.k_factor_lookup shape, foldComp, norm, _thename, _err_cd, _fSel, _obN = SheetMetalUnfolder.getUnfold( - kFactorTable, fp.baseObject[0], fp.baseObject[1][0], fp.KFactorStandard + kFactorTable, obj.baseObject[0], obj.baseObject[1][0], obj.KFactorStandard ) sketches = [] - if fp.GenerateSketch and shape is not None: + if obj.GenerateSketch and shape is not None: sketches = SheetMetalUnfolder.getUnfoldSketches( shape, foldComp.Edges, norm, - fp.UnfoldSketches, - fp.SeparateSketchLayers, - fp.Proxy.SketchColor, - bendSketchColor=fp.Proxy.InternalColor, - internalSketchColor=fp.Proxy.BendLineColor, + obj.UnfoldSketches, + obj.SeparateSketchLayers, + obj.Proxy.SketchColor, + bendSketchColor=obj.Proxy.InternalColor, + internalSketchColor=obj.Proxy.BendLineColor, ) + return shape, sketches + + def execute(self, fp): + '''"Print a short message when doing a recomputation, this method is mandatory"''' + self._addProperties(fp) + + if not NewUnfolderAvailable or SheetMetalTools.use_old_unfolder(): + shape, sketches = self.oldUnfolder(fp) + else: + shape, sketches = self.newUnfolder(fp) fp.Shape = shape parent = SheetMetalTools.smGetParentBody(fp) @@ -262,7 +302,6 @@ def claimChildren(self): for itemName in self.Object.UnfoldSketches: item = self.Object.Document.getObject(itemName) if item is not None: - item.recompute(True) objs.append(item) return objs @@ -371,8 +410,8 @@ def recomputeObject(self, closeTask = False): else: FreeCAD.ActiveDocument.recompute() SheetMetalTools.smForceRecompute = False - if len(self.obj.UnfoldSketches) > 0: - FreeCAD.ActiveDocument.recompute() + # if len(self.obj.UnfoldSketches) > 0: + # FreeCAD.ActiveDocument.recompute() def accept(self): if not self.checkKFactorValid(): @@ -479,7 +518,6 @@ def Activated(self): SMUnfoldViewProvider(newObj.ViewObject) SheetMetalTools.smAddNewObject( selobj, newObj, activeBody, SMUnfoldTaskPanel) - return def IsActive(self): if ( diff --git a/SheetMetalUnfolder.py b/SheetMetalUnfolder.py index 77cf421..bb7ffbd 100644 --- a/SheetMetalUnfolder.py +++ b/SheetMetalUnfolder.py @@ -3094,7 +3094,7 @@ def getUnfoldSketches( if len(foldLines) > 0 and splitSketches: unfold_sketch_bend = generateSketch( - foldEdges, "Unfold_Sketch_bends", bendSketchColor, existingSketches + foldEdges, "Unfold_Sketch_Bends", bendSketchColor, existingSketches ) sketches.append(unfold_sketch_bend) diff --git a/package.xml b/package.xml index 4183487..1752d40 100644 --- a/package.xml +++ b/package.xml @@ -2,8 +2,8 @@ SheetMetal Workbench A simple sheet metal tools workbench for FreeCAD. - 0.6.13 - 2024-12-25 + 0.7.00 + 2025-01-11 Shai Seger LGPL-2.1-or-later https://github.com/shaise/FreeCAD_SheetMetal