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.
This commit is contained in:
Roy-043
2025-08-23 12:34:06 +02:00
parent 1ffd333edd
commit 54691ca6b3
3 changed files with 77 additions and 117 deletions

View File

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

View File

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

View File

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