From 1f6ecf83b2e31616471a9b933e587495f1eaac2f Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Tue, 27 May 2025 19:17:43 +0200 Subject: [PATCH] Draft: Added snap recenter functionality (#19728) * Allow to align the working plane on selected edge + face of a same object, which aligns the plane with the face, but positions it on the edge (the WP is positioned on the edge's first vertex, the WP's X axis is aligned with the edge, and the face's center point provides the third point to define the plane) * Added a "Recenter" in-command shortcut. This moves the WP to be centered on the current snap position (the WorkingPlane snap button is taken into account, so one can only move the WP in the same plane or not). --- src/Mod/Draft/DraftGui.py | 4 + src/Mod/Draft/Resources/Draft.qrc | 1 + .../Resources/icons/Draft_Snap_Recenter.svg | 224 ++++++++++++++++++ .../ui/preferences-draftinterface.ui | 84 +++++-- src/Mod/Draft/WorkingPlane.py | 50 +++- src/Mod/Draft/draftguitools/gui_snapper.py | 9 +- 6 files changed, 345 insertions(+), 27 deletions(-) create mode 100644 src/Mod/Draft/Resources/icons/Draft_Snap_Recenter.svg diff --git a/src/Mod/Draft/DraftGui.py b/src/Mod/Draft/DraftGui.py index 9afe2cd512..45ccea8769 100644 --- a/src/Mod/Draft/DraftGui.py +++ b/src/Mod/Draft/DraftGui.py @@ -1155,6 +1155,10 @@ class DraftToolBar: if hasattr(FreeCADGui,"Snapper"): FreeCADGui.Snapper.addHoldPoint() spec = True + elif txt == _get_incmd_shortcut("Recenter"): + if hasattr(FreeCADGui,"Snapper"): + FreeCADGui.Snapper.recenter_workingplane() + spec = True elif txt == _get_incmd_shortcut("Snap"): self.togglesnap() spec = True diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index 089dac09c9..10d86ded07 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -94,6 +94,7 @@ icons/Draft_Snap_Ortho.svg icons/Draft_Snap_Parallel.svg icons/Draft_Snap_Perpendicular.svg + icons/Draft_Snap_Recenter.svg icons/Draft_Snap_Special.svg icons/Draft_Snap_WorkingPlane.svg icons/Draft_Split.svg diff --git a/src/Mod/Draft/Resources/icons/Draft_Snap_Recenter.svg b/src/Mod/Draft/Resources/icons/Draft_Snap_Recenter.svg new file mode 100644 index 0000000000..3c2c1ff83d --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_Snap_Recenter.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Fri Mar 7 15:58:51 2014 -0300 + + + [Yorik van Havre] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_Snap_WorkingPlane.svg + https://www.freecad.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson + + + Square with small circe in lower left corner + + + plane + square + circle + + + + + + diff --git a/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui b/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui index bcb96e972f..ce2d03cc55 100644 --- a/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui +++ b/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui @@ -6,8 +6,8 @@ 0 0 - 456 - 338 + 513 + 516 @@ -41,7 +41,7 @@ 1 - + false @@ -89,7 +89,7 @@ 1 - + false @@ -137,7 +137,7 @@ 1 - + false @@ -185,7 +185,7 @@ 1 - + false @@ -217,7 +217,7 @@ 1 - + false @@ -249,7 +249,7 @@ 1 - + false @@ -281,7 +281,7 @@ 1 - + false @@ -313,7 +313,7 @@ 1 - + false @@ -345,7 +345,7 @@ 1 - + false @@ -377,7 +377,7 @@ 1 - + false @@ -409,7 +409,7 @@ 1 - + false @@ -441,7 +441,7 @@ 1 - + false @@ -473,7 +473,7 @@ 1 - + false @@ -505,7 +505,7 @@ 1 - + false @@ -537,7 +537,7 @@ 1 - + false @@ -569,7 +569,7 @@ 1 - + false @@ -601,7 +601,7 @@ 1 - + false @@ -633,7 +633,7 @@ 1 - + false @@ -665,7 +665,7 @@ 1 - + false @@ -697,7 +697,7 @@ 1 - + false @@ -729,7 +729,7 @@ 1 - + false @@ -740,6 +740,38 @@ + + + + Recenter + + + + + + + + 25 + 16777215 + + + + D + + + 1 + + + false + + + inCommandShortcutRecenter + + + Mod/Draft + + + @@ -814,6 +846,12 @@ Qt::Vertical + + + 0 + 0 + + diff --git a/src/Mod/Draft/WorkingPlane.py b/src/Mod/Draft/WorkingPlane.py index 657c1c9019..3f26173a4c 100644 --- a/src/Mod/Draft/WorkingPlane.py +++ b/src/Mod/Draft/WorkingPlane.py @@ -241,6 +241,40 @@ class PlaneBase: self.position = pos + (self.axis * offset) return True + def align_to_face_and_edge(self, face, edge, offset=0): + """Align the WP to a face and an edge. + + The face must be planar. + + The WP will lie on the face, but its `position` will be the + first vertex of the edge, and its `u` vector will be aligned + with the edge. + + Parameters + ---------- + + face: Part.Face + Face. + edge: Part.Edge + Edge, need not be an edge of the face. + offset: float, optional + Defaults to zero. + Offset along the WP `axis`. + + Returns + ------- + `True`/`False` + `True` if successful. + """ + if face.Surface.isPlanar() is False: + return False + axis = face.normalAt(0,0) + point = edge.Vertexes[0].Point + upvec = edge.Vertexes[-1].Point.sub(point) + return self.align_to_point_and_axis(point, axis, offset, upvec) + #vertex = Part.Vertex(face.CenterOfMass) + #return self.align_to_edges_vertexes([edge, vertex], offset) + def align_to_face(self, shape, offset=0): """Align the WP to a face with an optional offset. @@ -1256,11 +1290,14 @@ class PlaneGui(PlaneBase): objs.append(Part.getShape(sel.Object, sub, needSubElement=True, retType=1)) if len(objs) != 1: + ret = False if all([obj[0].isNull() is False and obj[0].ShapeType in ["Edge", "Vertex"] for obj in objs]): ret = self.align_to_edges_vertexes([obj[0] for obj in objs], offset, _hist_add) - else: - ret = False - + elif all([obj[0].isNull() is False and obj[0].ShapeType in ["Edge", "Face"] for obj in objs]): + edges = [obj[0] for obj in objs if obj[0].ShapeType == "Edge"] + faces = [obj[0] for obj in objs if obj[0].ShapeType == "Face"] + if faces and edges: + ret = self.align_to_face_and_edge(faces[0], edges[0], offset, _hist_add) if ret is False: _wrn(translate("draft", "Selected shapes do not define a plane")) return ret @@ -1329,6 +1366,13 @@ class PlaneGui(PlaneBase): self._handle_custom(_hist_add) return True + def align_to_face_and_edge(self, face, edge, offset=0, _hist_add=True): + """See PlaneBase.align_to_face.""" + if super().align_to_face_and_edge(face, edge, offset) is False: + return False + self._handle_custom(_hist_add) + return True + def align_to_placement(self, place, offset=0, _hist_add=True): """See PlaneBase.align_to_placement.""" super().align_to_placement(place, offset) diff --git a/src/Mod/Draft/draftguitools/gui_snapper.py b/src/Mod/Draft/draftguitools/gui_snapper.py index fe36aea2e7..28215cfa21 100644 --- a/src/Mod/Draft/draftguitools/gui_snapper.py +++ b/src/Mod/Draft/draftguitools/gui_snapper.py @@ -49,6 +49,7 @@ import Part import Draft import DraftVecUtils import DraftGeomUtils +import WorkingPlane from draftguitools import gui_trackers as trackers from draftutils import gui_utils from draftutils import params @@ -159,7 +160,7 @@ class Snapper: def _get_wp(self): - return App.DraftWorkingPlane + return WorkingPlane.get_working_plane() def init_active_snaps(self): @@ -1649,4 +1650,10 @@ class Snapper: self.holdTracker.on() self.holdPoints.append(self.spoint) + def recenter_workingplane(self): + """Recenters the working plane on the current snap position""" + if self.spoint: + self._get_wp().set_to_position(self.toWP(self.spoint)) + + ## @}