From 54691ca6b39402e53dd973f46b6fe5a4352c5cdf Mon Sep 17 00:00:00 2001 From: Roy-043 Date: Sat, 23 Aug 2025 12:34:06 +0200 Subject: [PATCH] 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):