From 5a854ed3d32c0717908315d717a4fa0b946c82a3 Mon Sep 17 00:00:00 2001 From: Roy-043 Date: Sat, 23 Aug 2025 12:34:06 +0200 Subject: [PATCH 1/4] Draft: more logical placement for 3 point arcs The placement of 3 point arcs very often was unexpected and typically did not match the placement of circles created on the same working plane. This PR updates the code to use the placement of the working plane to adjust the placement of the created arc. Additonally: * Avoided code duplication by just passing the edge from make_arc_3points to make_circle. * Removed inconsistent map_mode argument from make_arc_3points and the cryptic code related to it. The handling of the support property is something that has to be reviewed for all Draft commands at some point though. --- src/Mod/Draft/draftguitools/gui_arcs.py | 9 +- src/Mod/Draft/draftmake/make_arc_3points.py | 90 +++---------------- src/Mod/Draft/draftmake/make_circle.py | 95 +++++++++++++-------- 3 files changed, 77 insertions(+), 117 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_arcs.py b/src/Mod/Draft/draftguitools/gui_arcs.py index 01599b376d..cc22899f4c 100644 --- a/src/Mod/Draft/draftguitools/gui_arcs.py +++ b/src/Mod/Draft/draftguitools/gui_arcs.py @@ -584,12 +584,17 @@ class Arc_3Points(gui_base.GuiCommandBase): # proceed with creating the final object. # Draw a simple `Part::Feature` if the parameter is `True`. Gui.addModule("Draft") + Gui.addModule("WorkingPlane") _cmd = "Draft.make_arc_3points([" _cmd += "FreeCAD." + str(self.points[0]) _cmd += ", FreeCAD." + str(self.points[1]) _cmd += ", FreeCAD." + str(self.points[2]) - _cmd += "], primitive=" + str(params.get_param("UsePartPrimitives")) + ")" - _cmd_list = ["circle = " + _cmd, + _cmd += "]" + _cmd += ", placement=pl" + _cmd += ", primitive=" + str(params.get_param("UsePartPrimitives")) + _cmd += ")" + _cmd_list = ["pl = WorkingPlane.get_working_plane().get_placement()", + "circle = " + _cmd, "Draft.autogroup(circle)"] if params.get_param("UsePartPrimitives"): _cmd_list.append("Draft.select(circle)") diff --git a/src/Mod/Draft/draftmake/make_arc_3points.py b/src/Mod/Draft/draftmake/make_arc_3points.py index 26167deca7..02d37c6bf8 100644 --- a/src/Mod/Draft/draftmake/make_arc_3points.py +++ b/src/Mod/Draft/draftmake/make_arc_3points.py @@ -39,10 +39,8 @@ from draftutils.translate import translate import draftutils.gui_utils as gui_utils -def make_arc_3points(points, placement=None, face=False, - support=None, map_mode="Deactivated", - primitive=False): - """Draw a circular arc defined by three points in the circumference. +def make_arc_3points(points, placement=None, face=False, support=None, primitive=False): + """Draw a circular arc defined by three points on the circumference. Parameters ---------- @@ -50,17 +48,9 @@ def make_arc_3points(points, placement=None, face=False, A list that must be three points. placement: Base::Placement, optional - It defaults to `None`. - It is a placement, comprised of a `Base` (`Base::Vector3`), - and a `Rotation` (`Base::Rotation`). - If it exists it moves the center of the new object to the point - indicated by `placement.Base`, while `placement.Rotation` - is ignored so that the arc keeps the same orientation - with which it was created. - - If both `support` and `placement` are given, - `placement.Base` is used for the `AttachmentOffset.Base`, - and again `placement.Rotation` is ignored. + It is adjusted to match the geometry of the created edge. + The Z axis of the adjusted placement will be parallel to the + (negative) edge axis. Its Base will match the edge center. face: bool, optional It defaults to `False`. @@ -72,39 +62,14 @@ def make_arc_3points(points, placement=None, face=False, It is a list containing tuples to define the attachment of the new object. - A tuple in the list needs two elements; - the first is an external object, and the second is another tuple - with the names of sub-elements on that external object - likes vertices or faces. - :: - support = [(obj, ("Face1"))] - support = [(obj, ("Vertex1", "Vertex5", "Vertex8"))] - - This parameter sets the `Support` property but it only really affects - the position of the new object when the `map_mode` - is set to other than `'Deactivated'`. - - map_mode: str, optional - It defaults to `'Deactivated'`. - It defines the type of `'MapMode'` of the new object. - This parameter only works when a `support` is also provided. - - Example: place the new object on a face or another object. - :: - support = [(obj, ("Face1"))] - map_mode = 'FlatFace' - - Example: place the new object on a plane created by three vertices - of an object. - :: - support = [(obj, ("Vertex1", "Vertex5", "Vertex8"))] - map_mode = 'ThreePointsPlane' + This parameter sets the `Support` property but it only really + affects the position of the new object if its `MapMode` is + set to other than `'Deactivated'`. primitive: bool, optional It defaults to `False`. If it is `True`, it will create a Part primitive instead of a Draft object. - In this case, `placement`, `face`, `support`, and `map_mode` - are ignored. + In this case, `placement`, `face` and `support` are ignored. Returns ------- @@ -130,14 +95,6 @@ def make_arc_3points(points, placement=None, face=False, _err(translate("draft","Wrong input: must be list or tuple of 3 points exactly.")) return None - if placement is not None: - try: - utils.type_check([(placement, App.Placement)], name=_name) - except TypeError: - _err(translate("draft","Placement:") + " {}".format(placement)) - _err(translate("draft","Wrong input: incorrect type of placement.")) - return None - p1, p2, p3 = points try: @@ -149,41 +106,16 @@ def make_arc_3points(points, placement=None, face=False, return None try: - _edge = Part.Arc(p1, p2, p3) + edge = Part.Arc(p1, p2, p3).toShape() except Part.OCCError as error: _err(translate("draft","Cannot generate shape:") + " " + "{}".format(error)) return None - edge = _edge.toShape() - radius = edge.Curve.Radius - center = edge.Curve.Center - if primitive: obj = App.ActiveDocument.addObject("Part::Feature", "Arc") obj.Shape = edge return obj - rot = App.Rotation(edge.Curve.XAxis, - edge.Curve.YAxis, - edge.Curve.Axis, "ZXY") - _placement = App.Placement(center, rot) - start = edge.FirstParameter - end = math.degrees(edge.LastParameter) - obj = Draft.make_circle(radius, - placement=_placement, face=face, - startangle=start, endangle=end, - support=support) - - original_placement = obj.Placement - - if placement and not support: - obj.Placement.Base = placement.Base - if support: - obj.MapMode = map_mode - if placement: - obj.AttachmentOffset.Base = placement.Base - obj.AttachmentOffset.Rotation = original_placement.Rotation - - return obj + return Draft.make_circle(edge, placement=placement, face=face, support=support) ## @} diff --git a/src/Mod/Draft/draftmake/make_circle.py b/src/Mod/Draft/draftmake/make_circle.py index de8bacb0af..71f02e6e1c 100644 --- a/src/Mod/Draft/draftmake/make_circle.py +++ b/src/Mod/Draft/draftmake/make_circle.py @@ -32,6 +32,7 @@ import math import FreeCAD as App import Part import DraftGeomUtils +import DraftVecUtils import draftutils.utils as utils import draftutils.gui_utils as gui_utils @@ -41,37 +42,61 @@ if App.GuiUp: from draftviewproviders.view_base import ViewProviderDraft +def _get_normal(axis, ref_rot): + local_axis = ref_rot.inverted().multVec(axis) + x, y, z = [abs(coord) for coord in list(local_axis)] + # Use local X, Y or Z axis for comparison: + if z >= x and z >= y: + local_comp_vec = App.Vector(0, 0, 1) + elif y >= x and y >= z: + local_comp_vec = App.Vector(0, -1, 0) # -Y to match the Front view + else: + local_comp_vec = App.Vector(1, 0, 0) + comp_vec = ref_rot.multVec(local_comp_vec) + axis = App.Vector(axis) # create independent copy + if axis.getAngle(comp_vec) > math.pi/2: + axis = axis.negative() + return axis + + def make_circle(radius, placement=None, face=None, startangle=None, endangle=None, support=None): - """make_circle(radius, [placement, face, startangle, endangle]) - or make_circle(edge,[face]): + """make_circle(radius, [placement], [face], [startangle], [endangle]) + or make_circle(edge, [placement], [face]): Creates a circle object with given parameters. - If startangle and endangle are provided and not equal, the object will show + If startangle and endangle are provided and not equal, the object will be an arc instead of a full circle. Parameters ---------- - radius : the radius of the circle. + radius: the radius of the circle or the shape of a circular edge + If it is an edge, startangle and endangle are ignored. + edge.Curve must be a Part.Circle. - placement : - If placement is given, it is used. + placement: optional + If radius is an edge, placement is adjusted to match the geometry + of the edge. The Z axis of the adjusted placement will be parallel + to the (negative) edge axis. Its Base will match the edge center. - face : Bool + face: Bool If face is False, the circle is shown as a wireframe, otherwise as a face. - startangle : start angle of the circle (in degrees) + startangle: start angle of the circle (in degrees) Recalculated if not in the -360 to 360 range. - endangle : end angle of the circle (in degrees) + endangle: end angle of the circle (in degrees) Recalculated if not in the -360 to 360 range. - edge : edge.Curve must be a 'Part.Circle' - The circle is created from the given edge. + support: App::PropertyLinkSubList, optional + It defaults to `None`. + It is a list containing tuples to define the attachment + of the new object. - support : - TODO: Describe + This parameter sets the `Support` property but it only really + affects the position of the new object if its `MapMode` is + set to other than `'Deactivated'`. """ if not App.ActiveDocument: @@ -81,38 +106,36 @@ def make_circle(radius, placement=None, face=None, startangle=None, endangle=Non if placement: utils.type_check([(placement,App.Placement)], "make_circle") - if startangle != endangle: - _name = "Arc" + if (isinstance(radius, Part.Edge) and len(radius.Vertexes) > 1) \ + or startangle != endangle: + name = "Arc" else: - _name = "Circle" + name = "Circle" - obj = App.ActiveDocument.addObject("Part::Part2DObjectPython", _name) + obj = App.ActiveDocument.addObject("Part::Part2DObjectPython", name) Circle(obj) if face is not None: obj.MakeFace = face - if isinstance(radius,Part.Edge): + if isinstance(radius, Part.Edge) and DraftGeomUtils.geomType(radius) == "Circle": edge = radius - if DraftGeomUtils.geomType(edge) == "Circle": - obj.Radius = edge.Curve.Radius - placement = App.Placement(edge.Placement) - delta = edge.Curve.Center.sub(placement.Base) - placement.move(delta) - # Rotation of the edge - rotOk = App.Rotation(edge.Curve.XAxis, edge.Curve.YAxis, edge.Curve.Axis, "ZXY") - placement.Rotation = rotOk - if len(edge.Vertexes) > 1: - v0 = edge.Curve.XAxis - v1 = (edge.Vertexes[0].Point).sub(edge.Curve.Center) - v2 = (edge.Vertexes[-1].Point).sub(edge.Curve.Center) - # Angle between edge.Curve.XAxis and the vector from center to start of arc - a0 = math.degrees(App.Vector.getAngle(v0, v1)) - # Angle between edge.Curve.XAxis and the vector from center to end of arc - a1 = math.degrees(App.Vector.getAngle(v0, v2)) - obj.FirstAngle = a0 - obj.LastAngle = a1 + obj.Radius = edge.Curve.Radius + axis = edge.Curve.Axis + ref_rot = App.Rotation() if placement is None else placement.Rotation + normal = _get_normal(axis, ref_rot) + x_axis = ref_rot.multVec(App.Vector(1, 0, 0)) + y_axis = ref_rot.multVec(App.Vector(0, 1, 0)) + rot = App.Rotation(x_axis, y_axis, normal, "ZXY") + placement = App.Placement(edge.Curve.Center, rot) + if len(edge.Vertexes) > 1: + v1 = (edge.Vertexes[0].Point).sub(edge.Curve.Center) + v2 = (edge.Vertexes[-1].Point).sub(edge.Curve.Center) + if not axis.isEqual(normal, 1e-4): + v1, v2 = v2, v1 + obj.FirstAngle = math.degrees(DraftVecUtils.angle(x_axis, v1, normal)) + obj.LastAngle = math.degrees(DraftVecUtils.angle(x_axis, v2, normal)) else: obj.Radius = radius if (startangle is not None) and (endangle is not None): From 9807bd6e02ad01cbaffc68d089ba75e3905bab37 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:04:39 +0200 Subject: [PATCH 2/4] Do not import the Draft module --- src/Mod/Draft/draftmake/make_arc_3points.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Mod/Draft/draftmake/make_arc_3points.py b/src/Mod/Draft/draftmake/make_arc_3points.py index 02d37c6bf8..0a2c376abc 100644 --- a/src/Mod/Draft/draftmake/make_arc_3points.py +++ b/src/Mod/Draft/draftmake/make_arc_3points.py @@ -31,13 +31,12 @@ import math import FreeCAD as App import Part -import Draft -import draftutils.utils as utils +from draftmake import make_circle +from draftutils import gui_utils +from draftutils import utils from draftutils.messages import _err from draftutils.translate import translate -import draftutils.gui_utils as gui_utils - def make_arc_3points(points, placement=None, face=False, support=None, primitive=False): """Draw a circular arc defined by three points on the circumference. @@ -116,6 +115,6 @@ def make_arc_3points(points, placement=None, face=False, support=None, primitive obj.Shape = edge return obj - return Draft.make_circle(edge, placement=placement, face=face, support=support) + return make_circle.make_circle(edge, placement=placement, face=face, support=support) ## @} From 28cae33d0cf87256e7132b4f510ad53234bb3d51 Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:06:32 +0200 Subject: [PATCH 3/4] Bring back placement argument type check --- src/Mod/Draft/draftmake/make_arc_3points.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Mod/Draft/draftmake/make_arc_3points.py b/src/Mod/Draft/draftmake/make_arc_3points.py index 0a2c376abc..191e511eb1 100644 --- a/src/Mod/Draft/draftmake/make_arc_3points.py +++ b/src/Mod/Draft/draftmake/make_arc_3points.py @@ -104,6 +104,14 @@ def make_arc_3points(points, placement=None, face=False, support=None, primitive _err(translate("draft","Wrong input: incorrect type of points.")) return None + if placement is not None: + try: + utils.type_check([(placement, App.Placement)], name=_name) + except TypeError: + _err(translate("draft","Placement:") + " {}".format(placement)) + _err(translate("draft","Wrong input: incorrect type of placement.")) + return None + try: edge = Part.Arc(p1, p2, p3).toShape() except Part.OCCError as error: From 6e5a1cd31c5d165af06e489adcd67210cc7bc83c Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:34:42 +0200 Subject: [PATCH 4/4] Remove gui_utils import from make_arc_3points.py --- src/Mod/Draft/draftmake/make_arc_3points.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Mod/Draft/draftmake/make_arc_3points.py b/src/Mod/Draft/draftmake/make_arc_3points.py index 191e511eb1..30947900fe 100644 --- a/src/Mod/Draft/draftmake/make_arc_3points.py +++ b/src/Mod/Draft/draftmake/make_arc_3points.py @@ -32,7 +32,6 @@ import math import FreeCAD as App import Part from draftmake import make_circle -from draftutils import gui_utils from draftutils import utils from draftutils.messages import _err from draftutils.translate import translate