Draft: new make_linear_dimension functions for more precision

A single `make_dimension` handles three types of dimensions,
(1) simple linear, (2) linear linked to an object, and (3) linked
to a circular edge.

So, we provide two new functions, `make_linear_dimension`
and `make_linear_dimension_obj`, to handle the first two cases.
In this way we can check the input parameters much better.

We adjust the `Draft_Dimension` Gui Command accordingly.
This commit is contained in:
vocx-fc
2020-06-09 18:03:10 -05:00
committed by Yorik van Havre
parent 93f1e87bc0
commit 2cb2b10d8f
5 changed files with 418 additions and 134 deletions

View File

@@ -390,6 +390,8 @@ from draftobjects.dimension import (LinearDimension,
from draftmake.make_dimension import (make_dimension,
makeDimension,
make_linear_dimension,
make_linear_dimension_obj,
make_angular_dimension,
makeAngularDimension)

View File

@@ -46,6 +46,7 @@ import DraftVecUtils
import draftguitools.gui_base_original as gui_base_original
import draftguitools.gui_tool_utils as gui_tool_utils
import draftguitools.gui_trackers as trackers
from draftutils.translate import translate
from draftutils.messages import _msg
@@ -102,11 +103,11 @@ class Dimension(gui_base_original.Creator):
name = translate("draft", "Dimension")
if self.cont:
self.finish()
elif self.hasMeasures():
elif self.selected_app_measure():
super(Dimension, self).Activated(name)
self.dimtrack = trackers.dimTracker()
self.arctrack = trackers.arcTracker()
self.createOnMeasures()
self.create_with_app_measure()
self.finish()
else:
super(Dimension, self).Activated(name)
@@ -129,40 +130,52 @@ class Dimension(gui_base_original.Creator):
self.force = None
self.info = None
self.selectmode = False
self.setFromSelection()
self.set_selection()
_msg(translate("draft", "Pick first point"))
Gui.draftToolBar.show()
def setFromSelection(self):
def set_selection(self):
"""Fill the nodes according to the selected geometry."""
sel = Gui.Selection.getSelectionEx()
if len(sel) == 1:
if len(sel[0].SubElementNames) == 1:
if "Edge" in sel[0].SubElementNames[0]:
edge = sel[0].SubObjects[0]
n = int(sel[0].SubElementNames[0].lstrip("Edge")) - 1
self.indices.append(n)
if DraftGeomUtils.geomType(edge) == "Line":
self.node.extend([edge.Vertexes[0].Point,
edge.Vertexes[1].Point])
v1 = None
v2 = None
for i, v in enumerate(sel[0].Object.Shape.Vertexes):
if v.Point == edge.Vertexes[0].Point:
v1 = i
if v.Point == edge.Vertexes[1].Point:
v2 = i
if (v1 is not None) and (v2 is not None):
self.link = [sel[0].Object, v1, v2]
elif DraftGeomUtils.geomType(edge) == "Circle":
self.node.extend([edge.Curve.Center,
edge.Vertexes[0].Point])
self.edges = [edge]
self.arcmode = "diameter"
self.link = [sel[0].Object, n]
if (len(sel) == 1
and len(sel[0].SubElementNames) == 1
and "Edge" in sel[0].SubElementNames[0]):
# The selection is just a single `Edge`
sel_object = sel[0]
edge = sel_object.SubObjects[0]
def hasMeasures(self):
"""Check if measurement objects are selected."""
# `n` is the edge number starting from 0 not from 1.
# The reason is lists in Python start from 0, although
# in the object's `Shape`, they start from 1
n = int(sel_object.SubElementNames[0].lstrip("Edge")) - 1
self.indices.append(n)
if DraftGeomUtils.geomType(edge) == "Line":
self.node.extend([edge.Vertexes[0].Point,
edge.Vertexes[1].Point])
# Iterate over the vertices of the parent `Object`;
# when the vertices match those of the selected `edge`
# save the index of vertex in the parent object
v1 = None
v2 = None
for i, v in enumerate(sel_object.Object.Shape.Vertexes):
if v.Point == edge.Vertexes[0].Point:
v1 = i
if v.Point == edge.Vertexes[1].Point:
v2 = i
if v1 and v2:
self.link = [sel_object.Object, v1, v2]
elif DraftGeomUtils.geomType(edge) == "Circle":
self.node.extend([edge.Curve.Center,
edge.Vertexes[0].Point])
self.edges = [edge]
self.arcmode = "diameter"
self.link = [sel_object.Object, n]
def selected_app_measure(self):
"""Check if App::MeasureDistance objects are selected."""
sel = Gui.Selection.getSelection()
if not sel:
return False
@@ -180,8 +193,14 @@ class Dimension(gui_base_original.Creator):
self.dimtrack.finalize()
self.arctrack.finalize()
def createOnMeasures(self):
"""Create on measurement objects."""
def create_with_app_measure(self):
"""Create on measurement objects.
This is used when the selection is an `'App::MeasureDistance'`,
which is created with the basic tool `Std_MeasureDistance`.
This object is removed and in its place a `Draft Dimension`
is created.
"""
for o in Gui.Selection.getSelection():
p1 = o.P1
p2 = o.P2
@@ -189,12 +208,13 @@ class Dimension(gui_base_original.Creator):
_ch = _root.getChildren()[1].getChildren()[0].getChildren()[0]
pt = _ch.getChildren()[3]
p3 = App.Vector(pt.point.getValues()[2].getValue())
Gui.addModule("Draft")
_cmd = 'Draft.make_dimension'
_cmd = 'Draft.make_linear_dimension'
_cmd += '('
_cmd += DraftVecUtils.toString(p1) + ', '
_cmd += DraftVecUtils.toString(p2) + ', '
_cmd += DraftVecUtils.toString(p3)
_cmd += 'dim_line=' + DraftVecUtils.toString(p3)
_cmd += ')'
_rem = 'FreeCAD.ActiveDocument.removeObject("' + o.Name + '")'
_cmd_list = ['_dim_ = ' + _cmd,
@@ -204,106 +224,121 @@ class Dimension(gui_base_original.Creator):
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
def create_angle_dimension(self):
"""Create an angular dimension from a center and two angles."""
normal_str = "None"
if len(self.edges) == 2:
v1 = DraftGeomUtils.vec(self.edges[0])
v2 = DraftGeomUtils.vec(self.edges[1])
norm = v1.cross(v2)
norm.normalize()
normal_str = DraftVecUtils.toString(norm)
ang1 = math.degrees(self.angledata[1])
ang2 = math.degrees(self.angledata[0])
if ang1 > 360:
ang1 = ang1 - 360
if ang2 > 360:
ang2 = ang2 - 360
_cmd = 'Draft.make_angular_dimension'
_cmd += '('
_cmd += 'center=' + DraftVecUtils.toString(self.center) + ', '
_cmd += 'angles='
_cmd += '['
_cmd += str(ang1) + ', '
_cmd += str(ang2)
_cmd += '], '
_cmd += 'dim_line=' + DraftVecUtils.toString(self.node[-1]) + ', '
_cmd += 'normal=' + normal_str
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd,
'Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
def create_linear_dimension(self):
"""Create a simple linear dimension, not linked to an edge."""
_cmd = 'Draft.make_linear_dimension'
_cmd += '('
_cmd += DraftVecUtils.toString(self.node[0]) + ', '
_cmd += DraftVecUtils.toString(self.node[1]) + ', '
_cmd += 'dim_line=' + DraftVecUtils.toString(self.node[2])
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd,
'Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
def create_linear_dimension_obj(self, direction=None):
"""Create a linear dimension linked to an edge.
The `link` attribute has indices of vertices as they appear
in the list `Shape.Vertexes`, so they start as zero 0.
The `LinearDimension` class, created by `make_linear_dimension_obj`,
considers the vertices of a `Shape` which are numbered to start
with 1, that is, `Vertex1`.
Therefore the value in `link` has to be incremented by 1.
"""
_cmd = 'Draft.make_linear_dimension_obj'
_cmd += '('
_cmd += 'FreeCAD.ActiveDocument.' + self.link[0].Name + ', '
_cmd += 'i1=' + str(self.link[1] + 1) + ', '
_cmd += 'i2=' + str(self.link[2] + 1) + ', '
_cmd += 'dim_line=' + DraftVecUtils.toString(self.node[2])
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd]
if direction == "X":
_cmd_list += ['_dim_.Direction = FreeCAD.Vector(1, 0, 0)']
elif direction == "Y":
_cmd_list += ['_dim_.Direction = FreeCAD.Vector(0, 1, 0)']
_cmd_list += ['Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
def create_radial_dimension_obj(self):
"""Create a radial dimension linked to a circular edge."""
_cmd = 'Draft.make_dimension'
_cmd += '('
_cmd += 'FreeCAD.ActiveDocument.' + self.link[0].Name + ', '
_cmd += str(self.link[1]) + ', '
_cmd += '"' + str(self.arcmode) + '", '
_cmd += DraftVecUtils.toString(self.node[2])
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd,
'Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension (radial)"),
_cmd_list)
def createObject(self):
"""Create the actual object in the current document."""
Gui.addModule("Draft")
if self.angledata:
normal = "None"
if len(self.edges) == 2:
v1 = DraftGeomUtils.vec(self.edges[0])
v2 = DraftGeomUtils.vec(self.edges[1])
normal = DraftVecUtils.toString((v1.cross(v2)).normalize())
_cmd = 'Draft.make_angular_dimension'
_cmd += '('
_cmd += 'center=' + DraftVecUtils.toString(self.center) + ', '
_cmd += 'angles='
_cmd += '['
_cmd += str(math.degrees(self.angledata[1])) + ', '
_cmd += str(math.degrees(self.angledata[0]))
_cmd += '], '
_cmd += 'dim_line=' + DraftVecUtils.toString(self.node[-1]) + ', '
_cmd += 'normal=' + normal
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd,
'Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
# Angle dimension, with two angles provided
self.create_angle_dimension()
elif self.link and not self.arcmode:
# Linear dimension, linked
# Linear dimension, linked to a straight edge
if self.force == 1:
_cmd = 'Draft.make_dimension'
_cmd += '('
_cmd += 'FreeCAD.ActiveDocument.' + self.link[0].Name + ', '
_cmd += str(self.link[1]) + ', '
_cmd += str(self.link[2]) + ', '
_cmd += DraftVecUtils.toString(self.node[2])
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd,
'_dim_.Direction = FreeCAD.Vector(0, 1, 0)',
'Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
self.create_linear_dimension_obj("Y")
elif self.force == 2:
_cmd = 'Draft.make_dimension'
_cmd += '('
_cmd += 'FreeCAD.ActiveDocument.' + self.link[0].Name + ', '
_cmd += str(self.link[1]) + ', '
_cmd += str(self.link[2]) + ', '
_cmd += DraftVecUtils.toString(self.node[2])
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd,
'_dim_.Direction = FreeCAD.Vector(1, 0, 0)',
'Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
self.create_linear_dimension_obj("X")
else:
_cmd = 'Draft.make_dimension'
_cmd += '('
_cmd += 'FreeCAD.ActiveDocument.' + self.link[0].Name + ', '
_cmd += str(self.link[1]) + ', '
_cmd += str(self.link[2]) + ', '
_cmd += DraftVecUtils.toString(self.node[2])
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd,
'Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
self.create_linear_dimension_obj()
elif self.arcmode:
# Radius or dimeter dimension, linked
_cmd = 'Draft.make_dimension'
_cmd += '('
_cmd += 'FreeCAD.ActiveDocument.' + self.link[0].Name + ', '
_cmd += str(self.link[1]) + ', '
_cmd += '"' + str(self.arcmode) + '", '
_cmd += DraftVecUtils.toString(self.node[2])
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd,
'Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
# Radius or dimeter dimension, linked to a circular edge
self.create_radial_dimension_obj()
else:
# Linear dimension, non-linked
_cmd = 'Draft.make_dimension'
_cmd += '('
_cmd += DraftVecUtils.toString(self.node[0]) + ', '
_cmd += DraftVecUtils.toString(self.node[1]) + ', '
_cmd += DraftVecUtils.toString(self.node[2])
_cmd += ')'
_cmd_list = ['_dim_ = ' + _cmd,
'Draft.autogroup(_dim_)',
'FreeCAD.ActiveDocument.recompute()']
self.commit(translate("draft", "Create Dimension"),
_cmd_list)
# Linear dimension, not linked to any edge
self.create_linear_dimension()
if self.ui.continueMode:
self.cont = self.node[2]
@@ -314,12 +349,14 @@ class Dimension(gui_base_original.Creator):
self.dir = v2.sub(v1)
else:
self.dir = self.node[1].sub(self.node[0])
self.node = [self.node[1]]
self.link = None
def selectEdge(self):
"""Toggle the select mode to the opposite state."""
self.selectmode = not(self.selectmode)
self.selectmode = not self.selectmode
def action(self, arg):
"""Handle the 3D scene events.

View File

@@ -172,9 +172,221 @@ def makeDimension(p1, p2, p3=None, p4=None):
return make_dimension(p1, p2, p3, p4)
def make_linear_dimension(p1, p2, dim_line=None):
"""Create a free linear dimension from two main points.
Parameters
----------
p1: Base::Vector3
First point of the measurement.
p2: Base::Vector3
Second point of the measurement.
dim_line: Base::Vector3, optional
It defaults to `None`.
This is a point through which the extension of the dimension line
will pass.
This point controls how close or how far the dimension line is
positioned from the measured segment that goes from `p1` to `p2`.
If it is `None`, this point will be calculated from the intermediate
distance betwwen `p1` and `p2`.
Returns
-------
App::FeaturePython
A scripted object of type `'LinearDimension'`.
This object does not have a `Shape` attribute, as the text and lines
are created on screen by Coin (pivy).
None
If there is a problem it will return `None`.
"""
_name = "make_linear_dimension"
utils.print_header(_name, "Linear dimension")
found, doc = utils.find_doc(App.activeDocument())
if not found:
_err(_tr("No active document. Aborting."))
return None
_msg("p1: {}".format(p1))
try:
utils.type_check([(p1, App.Vector)], name=_name)
except TypeError:
_err(_tr("Wrong input: must be a vector."))
return None
_msg("p2: {}".format(p2))
try:
utils.type_check([(p2, App.Vector)], name=_name)
except TypeError:
_err(_tr("Wrong input: must be a vector."))
return None
_msg("dim_line: {}".format(dim_line))
if dim_line:
try:
utils.type_check([(dim_line, App.Vector)], name=_name)
except TypeError:
_err(_tr("Wrong input: must be a vector."))
return None
else:
diff = p2.sub(p1)
diff.multiply(0.5)
dim_line = p1.add(diff)
new_obj = make_dimension(p1, p2, dim_line)
return new_obj
def make_linear_dimension_obj(edge_object, i1=1, i2=2, dim_line=None):
"""Create a linear dimension from an object.
Parameters
----------
edge_object: Part::Feature
The object which has an edge which will be measured.
It must have a `Part::TopoShape`, and at least one element
in `Shape.Vertexes`, to be able to measure a distance.
i1: int, optional
It defaults to `1`.
It is the index of the first vertex in `edge_object` from which
the measurement will be taken.
The minimum value should be `1`, which will be interpreted
as `'Vertex1'`.
If the value is below `1`, it will be set to `1`.
i2: int, optional
It defaults to `2`, which will be converted to `'Vertex2'`.
It is the index of the second vertex in `edge_object` that determines
the endpoint of the measurement.
If it is the same value as `i1`, the resulting measurement will be
made from the origin `(0, 0, 0)` to the vertex indicated by `i1`.
If the value is below `1`, it will be set to the last vertex
in `edge_object`.
Then to measure the first and last, this could be used
::
make_linear_dimension_obj(edge_object, i1=1, i2=-1)
dim_line: Base::Vector3
It defaults to `None`.
This is a point through which the extension of the dimension line
will pass.
This point controls how close or how far the dimension line is
positioned from the measured segment in `edge_object`.
If it is `None`, this point will be calculated from the intermediate
distance betwwen the vertices defined by `i1` and `i2`.
Returns
-------
App::FeaturePython
A scripted object of type `'LinearDimension'`.
This object does not have a `Shape` attribute, as the text and lines
are created on screen by Coin (pivy).
None
If there is a problem it will return `None`.
"""
_name = "make_linear_dimension_obj"
utils.print_header(_name, "Linear dimension")
found, doc = utils.find_doc(App.activeDocument())
if not found:
_err(_tr("No active document. Aborting."))
return None
if isinstance(edge_object, str):
edge_object_str = edge_object
if isinstance(edge_object, (list, tuple)):
_msg("edge_object: {}".format(edge_object))
_err(_tr("Wrong input: object must not be a list."))
return None
found, edge_object = utils.find_object(edge_object, doc)
if not found:
_msg("edge_object: {}".format(edge_object_str))
_err(_tr("Wrong input: object not in document."))
return None
_msg("edge_object: {}".format(edge_object.Label))
if not hasattr(edge_object, "Shape"):
_err(_tr("Wrong input: object doesn't have a 'Shape' to measure."))
return None
if (not hasattr(edge_object.Shape, "Vertexes")
or len(edge_object.Shape.Vertexes) < 1):
_err(_tr("Wrong input: object doesn't have at least one element "
"in 'Vertexes' to use for measuring."))
return None
_msg("i1: {}".format(i1))
try:
utils.type_check([(i1, int)], name=_name)
except TypeError:
_err(_tr("Wrong input: must be an integer."))
return None
if i1 < 1:
i1 = 1
_wrn(_tr("i1: values below 1 are not allowed; will be set to 1."))
vx1 = edge_object.getSubObject("Vertex" + str(i1))
if not vx1:
_err(_tr("Wrong input: vertex not in object."))
return None
_msg("i2: {}".format(i2))
try:
utils.type_check([(i2, int)], name=_name)
except TypeError:
_err(_tr("Wrong input: must be a vector."))
return None
if i2 < 1:
i2 = len(edge_object.Shape.Vertexes)
_wrn(_tr("i2: values below 1 are not allowed; "
"will be set to the last vertex in the object."))
vx2 = edge_object.getSubObject("Vertex" + str(i2))
if not vx2:
_err(_tr("Wrong input: vertex not in object."))
return None
_msg("dim_line: {}".format(dim_line))
if dim_line:
try:
utils.type_check([(dim_line, App.Vector)], name=_name)
except TypeError:
_err(_tr("Wrong input: must be a vector."))
return None
else:
diff = vx2.Point.sub(vx1.Point)
diff.multiply(0.5)
dim_line = vx1.Point.add(diff)
# TODO: the internal function expects an index starting with 0
# so we need to decrease the value here.
# This should be changed in the future in the internal function.
i1 -= 1
i2 -= 1
new_obj = make_dimension(edge_object, i1, i2, dim_line)
return new_obj
def make_angular_dimension(center=App.Vector(0, 0, 0),
angles=[0, 90],
dim_line=App.Vector(3, 3, 0), normal=None):
dim_line=App.Vector(10, 10, 0), normal=None):
"""Create an angular dimension from the given center and angles.
Parameters
@@ -194,9 +406,10 @@ def make_angular_dimension(center=App.Vector(0, 0, 0),
angles = [-30 60] # same angle
dim_line: Base::Vector3, optional
It defaults to `Vector(3, 3, 0)`.
Point through which the dimension line will pass.
This defines the radius of the dimension line, the circular arc.
It defaults to `Vector(10, 10, 0)`.
This is a point through which the extension of the dimension line
will pass. This defines the radius of the dimension line,
the circular arc.
normal: Base::Vector3, optional
It defaults to `None`, in which case the `normal` is taken

View File

@@ -211,9 +211,12 @@ def _create_objects(doc=None,
# Linear dimension
_msg(16 * "-")
_msg("Linear dimension")
dimension = Draft.make_dimension(Vector(8500, 500, 0),
Vector(8500, 1000, 0),
Vector(9000, 750, 0))
line = Draft.make_wire([Vector(8700, 200, 0),
Vector(8700, 1200, 0)])
dimension = Draft.make_linear_dimension(Vector(8600, 200, 0),
Vector(8600, 1000, 0),
Vector(8400, 750, 0))
if App.GuiUp:
dimension.ViewObject.ArrowSize = 15
dimension.ViewObject.ExtLines = 1000
@@ -221,6 +224,19 @@ def _create_objects(doc=None,
dimension.ViewObject.DimOvershoot = 50
dimension.ViewObject.FontSize = 100
dimension.ViewObject.ShowUnit = False
doc.recompute()
dim_obj = Draft.make_linear_dimension_obj(line, 1, 2,
Vector(9000, 750, 0))
if App.GuiUp:
dim_obj.ViewObject.ArrowSize = 15
dim_obj.ViewObject.ArrowType = "Arrow"
dim_obj.ViewObject.ExtLines = 100
dim_obj.ViewObject.ExtOvershoot = 100
dim_obj.ViewObject.DimOvershoot = 50
dim_obj.ViewObject.FontSize = 100
dim_obj.ViewObject.ShowUnit = False
t_xpos += 680
_set_text(["Dimension"], Vector(t_xpos, t_ypos, 0))

View File

@@ -170,8 +170,8 @@ class DraftCreation(unittest.TestCase):
obj = Draft.make_text(text)
self.assertTrue(obj, "'{}' failed".format(operation))
def test_dimension_linear(self):
"""Create a linear dimension."""
def test_dimension_linear_simple(self):
"""Create a simple linear dimension not linked to an object."""
operation = "Draft Dimension"
_msg(" Test '{}'".format(operation))
_msg(" Occasionally crashes")
@@ -180,7 +180,23 @@ class DraftCreation(unittest.TestCase):
c = Vector(4, -1, 0)
_msg(" a={0}, b={1}".format(a, b))
_msg(" c={}".format(c))
obj = Draft.make_dimension(a, b, c)
obj = Draft.make_linear_dimension(a, b, c)
self.assertTrue(obj, "'{}' failed".format(operation))
def test_dimension_linear_obj(self):
"""Create a linear dimension linked to an object."""
operation = "Draft Dimension"
_msg(" Test '{}'".format(operation))
_msg(" Occasionally crashes")
a = Vector(0, 0, 0)
b = Vector(9, 0, 0)
_msg(" a={0}, b={1}".format(a, b))
line = Draft.make_line(a, b)
self.doc.recompute()
obj = Draft.make_linear_dimension_obj(line,
i1=1, i2=2,
dim_line=Vector(5, 3, 0))
self.assertTrue(obj, "'{}' failed".format(operation))
def test_dimension_radial(self):