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).
This commit is contained in:
Yorik van Havre
2025-05-27 19:17:43 +02:00
committed by GitHub
parent 6b7e4185e1
commit 1f6ecf83b2
6 changed files with 345 additions and 27 deletions

View File

@@ -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

View File

@@ -94,6 +94,7 @@
<file>icons/Draft_Snap_Ortho.svg</file>
<file>icons/Draft_Snap_Parallel.svg</file>
<file>icons/Draft_Snap_Perpendicular.svg</file>
<file>icons/Draft_Snap_Recenter.svg</file>
<file>icons/Draft_Snap_Special.svg</file>
<file>icons/Draft_Snap_WorkingPlane.svg</file>
<file>icons/Draft_Split.svg</file>

View File

@@ -0,0 +1,224 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="svg2726"
height="64px"
width="64px"
sodipodi:docname="Draft_Snap_Recenter.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview27"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="true"
inkscape:zoom="8.5278495"
inkscape:cx="48.136403"
inkscape:cy="33.888966"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg2726">
<inkscape:grid
type="xygrid"
id="grid904" />
</sodipodi:namedview>
<defs
id="defs2728">
<linearGradient
id="linearGradient3773">
<stop
id="stop3775"
offset="0"
style="stop-color:#06989a;stop-opacity:1" />
<stop
id="stop3777"
offset="1"
style="stop-color:#34e0e2;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3765">
<stop
id="stop3767"
offset="0"
style="stop-color:#06989a;stop-opacity:1" />
<stop
id="stop3769"
offset="1"
style="stop-color:#34e0e2;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3144">
<stop
id="stop3146"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop3148"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<radialGradient
r="34.345188"
fy="672.79736"
fx="225.26402"
cy="672.79736"
cx="225.26402"
gradientTransform="matrix(1,0,0,0.6985294,0,202.82863)"
gradientUnits="userSpaceOnUse"
id="radialGradient3850"
xlink:href="#linearGradient3144" />
<radialGradient
r="34.345188"
fy="672.79736"
fx="225.26402"
cy="672.79736"
cx="225.26402"
gradientTransform="matrix(1,0,0,0.6985294,0,202.82863)"
gradientUnits="userSpaceOnUse"
id="radialGradient3850-4"
xlink:href="#linearGradient3144-1" />
<linearGradient
id="linearGradient3144-1">
<stop
id="stop3146-9"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop3148-8"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="457.10004"
x2="133.47093"
y1="802.50574"
x1="207.48643"
id="linearGradient3771"
xlink:href="#linearGradient3765"
gradientTransform="matrix(0.16670802,0,0,0.16316094,5.836926,-67.568816)" />
<linearGradient
gradientUnits="userSpaceOnUse"
y2="35.090908"
x2="22.545454"
y1="49.272728"
x1="25.81818"
id="linearGradient3779"
xlink:href="#linearGradient3773"
gradientTransform="matrix(0.91666668,0,0,0.91666668,13.333334,-12.166667)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3765"
id="linearGradient970"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.16670802,0,0,0.16316094,5.836926,-67.568816)"
x1="207.48643"
y1="802.50574"
x2="133.47093"
y2="457.10004" />
</defs>
<g
id="g344-7"
transform="matrix(0.78805516,0,0,0.78805516,-0.42818596,14.42218)"
style="opacity:0.57483108">
<rect
ry="4.3329797"
y="1.9802104"
x="2.3813963"
height="58.36945"
width="59.638393"
id="rect3857-5"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#linearGradient970);fill-opacity:1;fill-rule:nonzero;stroke:#042a2a;stroke-width:2.58167;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
<path
id="rect3857-3-3"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#34e0e2;stroke-width:2.53356;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
d="m 6.8880787,4.5357235 c -1.0783427,0 -1.9481926,0.9160312 -1.9481926,2.0516245 v 49.196853 c 0,1.135593 0.8698499,2.051624 1.9481926,2.051624 H 57.518586 c 1.078342,0 1.945691,-0.916031 1.945691,-2.051624 V 6.587348 c 0,-1.1355933 -0.867349,-2.0516245 -1.945691,-2.0516245 z" />
</g>
<g
id="g344"
transform="matrix(0.78805516,0,0,0.78805516,13.140579,2.42389)">
<rect
ry="4.3329797"
y="1.9802104"
x="2.3813963"
height="58.36945"
width="59.638393"
id="rect3857"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#linearGradient3771);fill-opacity:1;fill-rule:nonzero;stroke:#042a2a;stroke-width:2.58167;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
<path
id="rect3857-3"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#34e0e2;stroke-width:2.53356;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
d="m 6.8880787,4.5357235 c -1.0783427,0 -1.9481926,0.9160312 -1.9481926,2.0516245 v 49.196853 c 0,1.135593 0.8698499,2.051624 1.9481926,2.051624 H 57.518586 c 1.078342,0 1.945691,-0.916031 1.945691,-2.051624 V 6.587348 c 0,-1.1355933 -0.867349,-2.0516245 -1.945691,-2.0516245 z" />
</g>
<g
id="g348"
transform="translate(2)">
<path
d="m 44.999999,26.000001 a 8.9999996,9.0000004 0 0 1 -9,9 A 8.9999996,9.0000004 0 0 1 27,26.000001 8.9999996,9.0000004 0 0 1 35.999999,17 a 8.9999996,9.0000004 0 0 1 9,9.000001 z"
id="path4644"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#34e0e2;fill-opacity:1;fill-rule:nonzero;stroke:#042a2a;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
<path
d="M 43,26.000001 A 7,7 0 0 1 36,33 7,7 0 0 1 29,26.000001 7,7 0 0 1 36,19 a 7,7 0 0 1 7,7.000001 z"
id="path4644-6"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#linearGradient3779);fill-opacity:1;stroke:#34e0e2;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
</g>
<metadata
id="metadata6669">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="https://www.gnu.org/copyleft/lesser.html" />
<dc:date>Fri Mar 7 15:58:51 2014 -0300</dc:date>
<dc:creator>
<cc:Agent>
<dc:title>[Yorik van Havre]</dc:title>
</cc:Agent>
</dc:creator>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/Draft/Resources/icons/Draft_Snap_WorkingPlane.svg</dc:identifier>
<dc:relation>https://www.freecad.org/wiki/index.php?title=Artwork</dc:relation>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
<dc:description>Square with small circe in lower left corner</dc:description>
<dc:subject>
<rdf:Bag>
<rdf:li>plane</rdf:li>
<rdf:li>square</rdf:li>
<rdf:li>circle</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>456</width>
<height>338</height>
<width>513</width>
<height>516</height>
</rect>
</property>
<property name="windowTitle">
@@ -41,7 +41,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -89,7 +89,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -137,7 +137,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -185,7 +185,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -217,7 +217,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -249,7 +249,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -281,7 +281,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -313,7 +313,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -345,7 +345,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -377,7 +377,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -409,7 +409,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -441,7 +441,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -473,7 +473,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -505,7 +505,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -537,7 +537,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -569,7 +569,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -601,7 +601,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -633,7 +633,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -665,7 +665,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -697,7 +697,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -729,7 +729,7 @@
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled" stdset="0">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -740,6 +740,38 @@
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_inCommandShortcutRecenter">
<property name="text">
<string>Recenter</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="Gui::PrefLineEdit" name="lineEdit_inCommandShortcutRecenter">
<property name="maximumSize">
<size>
<width>25</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>D</string>
</property>
<property name="maxLength">
<number>1</number>
</property>
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>inCommandShortcutRecenter</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -814,6 +846,12 @@
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>

View File

@@ -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)

View File

@@ -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))
## @}