| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | """Provides the object code for the dimension objects. |
| | |
| | This includes the `LinearDimension` and `AgularDimension`. |
| | The first one measures a distance between two points or vertices |
| | in an object; it includes radial dimensions of circular arcs. |
| | The second one creates an arc between two straight lines to measure |
| | the angle between both. |
| | |
| | To Do |
| | ----- |
| | 1. Currently the angular dimension does not use linked geometrical |
| | elements, meaning that it cannot update its `Angle` by picking lines or edges |
| | from objects. If fact, `LinkedGeometry` is hidden to prevent the user |
| | from picking any object. |
| | |
| | At the moment the user must manually modify `FirstAngle` and `LastAngle` |
| | to obtain a new `Angle`, but since the values are manually entered |
| | the result is not parametrically tied to any actual object in the document. |
| | |
| | We introduced a function `measure_two_obj_angles` to calculate |
| | the corresponding parameters from a pair of objects and their edges. |
| | Currently this function is deactivated because we don't consider it |
| | to be ready; it is there for testing purposes only. |
| | This needs to be improved because at the moment it only gives |
| | one possible angle. We should be able to get the four angles |
| | of a two-line intersection. Maybe a new property is required |
| | to indicate the quadrant to choose and display. |
| | |
| | 2. In general, the `LinkedGeometry` property must be changed in type, |
| | as it does not need to be an `App::PropertyLinkSubList`. |
| | A `LinkSubList` is to select multiple subelements (vertices, edges) |
| | from multiple objects (two lines). However, since we typically measure |
| | a single object, for example, a single line or circle, the subelements |
| | that we can choose must belong to this object only. |
| | |
| | Therefore, just like in the case of the `PathArray` class the best property |
| | that could be used is `App::PropertyLinkSub`. |
| | Then in the property editor we will be unable to select more than one object |
| | thus preventing errors of the subelements not matching the measured object. |
| | |
| | 3. Currently the `LinearDimension` class is able to measure the distance |
| | between two arbitrary vertices in two distinct objects. |
| | For this case `App::PropertyLinkSubList` is in fact the right property |
| | to use, however, neither the `make_dimension` functions |
| | nor the Gui Command are set up to use this type of information. |
| | This has to be done manually by picking the two objects and the two vertices |
| | in the property editor. That is, this functionality is not entirely intuitive, |
| | so it is somewhat 'hidden' from the user. |
| | |
| | So, the make function and the Gui Command should be expanded to consider |
| | this case. |
| | |
| | Another possibility would be to use one property (LinkSub) for single-object |
| | measurements (linear, radial), and a second property (LinkSubList) |
| | for two-object measurements (linear, angular). This would require adjustments |
| | to the `execute` method to handle both cases properly. It may be necessary |
| | to have another property to control which type to use. |
| | |
| | 4. The `Support` property is not used at all, so it should be removed. |
| | It is just set at creation time by the `make_dimension` function |
| | but it actually isn't used in the `execute` code. |
| | |
| | 5. In fact the `DimensionBase` class is not the best base class |
| | than can be used as parent for all dimensions because it defines `Normal`, |
| | `Support`, and `LinkedGeometry`, which aren't used in all cases. |
| | In some of the derived classes, these properties are hidden. |
| | |
| | So, together with what is explained in point 3, we probably need to use |
| | a more generic base class, while at the same time improve the way |
| | the link properties are used. |
| | """ |
| | |
| | |
| | |
| |
|
| | |
| | |
| | import math |
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| |
|
| | import FreeCAD as App |
| | import DraftVecUtils |
| | import DraftGeomUtils |
| | import WorkingPlane |
| | from draftobjects.draft_annotation import DraftAnnotation |
| | from draftutils import gui_utils |
| | from draftutils import utils |
| | from draftutils.messages import _log |
| |
|
| |
|
| | class DimensionBase(DraftAnnotation): |
| | """The base objects for dimension objects. |
| | |
| | This class inherits `DraftAnnotation` to define the basic properties |
| | of all annotation type objects, like a scaling multiplier. |
| | |
| | This class is not used directly, but inherited by all dimension |
| | objects. |
| | """ |
| |
|
| | def set_properties(self, obj): |
| | """Set basic properties only if they don't exist.""" |
| | properties = obj.PropertiesList |
| |
|
| | if "Normal" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", "The normal direction of the text " "of the dimension" |
| | ) |
| | obj.addProperty("App::PropertyVector", "Normal", "Dimension", _tip, locked=True) |
| | obj.Normal = App.Vector(0, 0, 1) |
| |
|
| | |
| | |
| | |
| | if "Support" not in properties: |
| | _tip = QT_TRANSLATE_NOOP("App::Property", "The object measured by this dimension") |
| | obj.addProperty("App::PropertyLink", "Support", "Dimension", _tip, locked=True) |
| | obj.Support = None |
| |
|
| | if "LinkedGeometry" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The object, and specific subelements " |
| | "of it,\n" |
| | "that this dimension " |
| | "is measuring.\n" |
| | "\n" |
| | "There are various possibilities:\n" |
| | "- An object, and one of its edges.\n" |
| | "- An object, and two of its vertices.\n" |
| | "- An arc object, and its edge.", |
| | ) |
| | obj.addProperty( |
| | "App::PropertyLinkSubList", "LinkedGeometry", "Dimension", _tip, locked=True |
| | ) |
| | obj.LinkedGeometry = [] |
| |
|
| | if "Dimline" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "A point through which the dimension " |
| | "line, or an extrapolation of it, " |
| | "will pass.\n" |
| | "\n" |
| | "- For linear dimensions, this property " |
| | "controls how close the dimension line\n" |
| | "is to the measured object.\n" |
| | "- For radial dimensions, this controls " |
| | "the direction of the dimension line\n" |
| | "that displays the measured radius or " |
| | "diameter.\n" |
| | "- For angular dimensions, " |
| | "this controls the radius of the " |
| | "dimension arc\n" |
| | "that displays the measured angle.", |
| | ) |
| | obj.addProperty( |
| | "App::PropertyVectorDistance", "Dimline", "Dimension", _tip, locked=True |
| | ) |
| | obj.Dimline = App.Vector(0, 1, 0) |
| |
|
| | def update_properties_0v21(self, obj, vobj): |
| | """Update view properties.""" |
| | vobj.Proxy.set_text_properties(vobj, vobj.PropertiesList) |
| | vobj.TextColor = vobj.LineColor |
| | _log("v0.21, " + obj.Name + ", added view property 'TextColor'") |
| | _log("v0.21, " + obj.Name + ", renamed 'DisplayMode' options to 'World/Screen'") |
| |
|
| |
|
| | class LinearDimension(DimensionBase): |
| | """The linear dimension object. |
| | |
| | This inherits `DimensionBase` to provide the basic functionality of |
| | a dimension. |
| | |
| | This linear dimension includes measurements between two vertices, |
| | but also a radial dimension of a circular edge or arc. |
| | """ |
| |
|
| | def __init__(self, obj): |
| | obj.Proxy = self |
| | self.Type = "LinearDimension" |
| | self.set_properties(obj) |
| |
|
| | def set_properties(self, obj): |
| | """Set basic properties only if they don't exist.""" |
| | super().set_properties(obj) |
| |
|
| | properties = obj.PropertiesList |
| |
|
| | if "Start" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Starting point of the dimension line.\n" |
| | "\n" |
| | "If it is a radius dimension it will be " |
| | "the center of the arc.\n" |
| | "If it is a diameter dimension " |
| | "it will be a point that lies " |
| | "on the arc.", |
| | ) |
| | obj.addProperty( |
| | "App::PropertyVectorDistance", "Start", "Linear/radial dimension", _tip, locked=True |
| | ) |
| | obj.Start = App.Vector(0, 0, 0) |
| |
|
| | if "End" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Ending point of the dimension line.\n" |
| | "\n" |
| | "If it is a radius or diameter " |
| | "dimension\n" |
| | "it will be a point that lies " |
| | "on the arc.", |
| | ) |
| | obj.addProperty( |
| | "App::PropertyVectorDistance", "End", "Linear/radial dimension", _tip, locked=True |
| | ) |
| | obj.End = App.Vector(1, 0, 0) |
| |
|
| | if "Direction" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The direction of the dimension line.\n" |
| | "If this remains '(0,0,0)', " |
| | "the direction will be calculated " |
| | "automatically.", |
| | ) |
| | obj.addProperty( |
| | "App::PropertyVector", "Direction", "Linear/radial dimension", _tip, locked=True |
| | ) |
| |
|
| | if "Distance" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The value of the measurement.\n" |
| | "\n" |
| | "This property is read-only because " |
| | "the value is calculated\n" |
| | "from the 'Start' and 'End' properties.\n" |
| | "\n" |
| | "If the 'Linked Geometry' " |
| | "is an arc or circle, this 'Distance'\n" |
| | "is the radius or diameter, depending " |
| | "on the 'Diameter' property.", |
| | ) |
| | obj.addProperty( |
| | "App::PropertyLength", "Distance", "Linear/radial dimension", _tip, locked=True |
| | ) |
| | obj.Distance = 0 |
| |
|
| | if "Diameter" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "When measuring circular arcs, " |
| | "it determines whether to display\n" |
| | "the radius or the diameter value", |
| | ) |
| | obj.addProperty("App::PropertyBool", "Diameter", "Radial dimension", _tip, locked=True) |
| | obj.Diameter = False |
| |
|
| | def onDocumentRestored(self, obj): |
| | """Execute code when the document is restored.""" |
| | super().onDocumentRestored(obj) |
| | gui_utils.restore_view_object( |
| | obj, vp_module="view_dimension", vp_class="ViewProviderLinearDimension" |
| | ) |
| |
|
| | vobj = getattr(obj, "ViewObject", None) |
| | if vobj is None: |
| | return |
| |
|
| | if not hasattr(vobj, "TextColor"): |
| | self.update_properties_0v21(obj, vobj) |
| |
|
| | def loads(self, state): |
| | self.Type = "LinearDimension" |
| |
|
| | def onChanged(self, obj, prop): |
| | """Execute when a property is changed. |
| | |
| | It just sets some properties to be read-only or hidden, |
| | as they aren't used. |
| | """ |
| | if hasattr(obj, "Distance"): |
| | obj.setPropertyStatus("Distance", "ReadOnly") |
| |
|
| | |
| | |
| | if hasattr(obj, "Support"): |
| | obj.setPropertyStatus("Support", "Hidden") |
| |
|
| | def transform(self, obj, pla): |
| | """Transform the object by applying a placement.""" |
| | obj.Start = pla.multVec(obj.Start) |
| | obj.End = pla.multVec(obj.End) |
| | obj.Dimline = pla.multVec(obj.Dimline) |
| | obj.Normal = pla.Rotation.multVec(obj.Normal) |
| | obj.Direction = pla.Rotation.multVec(obj.Direction) |
| |
|
| | def execute(self, obj): |
| | """Execute when the object is created or recomputed. |
| | |
| | Set start point and end point according to the linked geometry |
| | and the number of subelements. |
| | |
| | If it has one subelement, we assume a straight edge or a circular edge. |
| | If it has two subelements, we assume a straight edge (two vertices). |
| | """ |
| | if obj.LinkedGeometry: |
| | if len(obj.LinkedGeometry) == 1: |
| | linked_obj = obj.LinkedGeometry[0][0] |
| | sub_list = obj.LinkedGeometry[0][1] |
| |
|
| | if len(sub_list) == 1: |
| | |
| | |
| | subelement = sub_list[0] |
| | (obj.Start, obj.End) = measure_one_obj_edge( |
| | linked_obj, subelement, obj.Dimline, obj.Diameter |
| | ) |
| | elif len(sub_list) == 2: |
| | |
| | |
| | (obj.Start, obj.End) = measure_one_obj_vertices(linked_obj, sub_list) |
| |
|
| | elif len(obj.LinkedGeometry) == 2: |
| | |
| | |
| | (obj.Start, obj.End) = measure_two_objects( |
| | obj.LinkedGeometry[0], obj.LinkedGeometry[1] |
| | ) |
| |
|
| | |
| | |
| | net_length = (obj.Start.sub(obj.End)).Length |
| | rounded_1 = round(obj.Distance.Value, utils.precision()) |
| | rounded_2 = round(net_length, utils.precision()) |
| |
|
| | if rounded_1 != rounded_2: |
| | obj.Distance = net_length |
| |
|
| | |
| | |
| | if App.GuiUp and obj.ViewObject: |
| | obj.ViewObject.update() |
| |
|
| |
|
| | def measure_one_obj_edge(obj, subelement, dim_point, diameter=False): |
| | """Measure one object with one subelement, a straight or circular edge. |
| | |
| | Parameters |
| | ---------- |
| | obj: Part::Feature |
| | The object that is measured. |
| | |
| | subelement: str |
| | The subelement that is measured, for example, `'Edge1'`. |
| | |
| | dim_line: Base::Vector3 |
| | A point through which the dimension goes through. |
| | """ |
| | start = App.Vector() |
| | end = App.Vector() |
| |
|
| | if "Edge" in subelement: |
| | n = int(subelement[4:]) - 1 |
| | edge = obj.Shape.Edges[n] |
| |
|
| | if DraftGeomUtils.geomType(edge) == "Line": |
| | start = edge.Vertexes[0].Point |
| | end = edge.Vertexes[-1].Point |
| | elif DraftGeomUtils.geomType(edge) == "Circle": |
| | center = edge.Curve.Center |
| | radius = edge.Curve.Radius |
| | axis = edge.Curve.Axis |
| | dim_line = dim_point.sub(center) |
| |
|
| | |
| | |
| | ray = dim_line.projectToPlane(App.Vector(0, 0, 0), axis) |
| |
|
| | if ray.Length == 0: |
| | ray = axis.cross(App.Vector(1, 0, 0)) |
| | if ray.Length == 0: |
| | ray = axis.cross(App.Vector(0, 1, 0)) |
| |
|
| | |
| | |
| | ray = DraftVecUtils.scaleTo(ray, radius) |
| |
|
| | if diameter: |
| | |
| | start = center.add(ray.negative()) |
| | end = center.add(ray) |
| | else: |
| | |
| | start = center |
| | end = center.add(ray) |
| |
|
| | return start, end |
| |
|
| |
|
| | def measure_one_obj_vertices(obj, subelements): |
| | """Measure two vertices in the same object.""" |
| | start = App.Vector() |
| | end = App.Vector() |
| |
|
| | subelement1 = subelements[0] |
| | subelement2 = subelements[1] |
| |
|
| | if "Vertex" in subelement1 and "Vertex" in subelement2: |
| | n1 = int(subelement1[6:]) - 1 |
| | n2 = int(subelement2[6:]) - 1 |
| | start = obj.Shape.Vertexes[n1].Point |
| | end = obj.Shape.Vertexes[n2].Point |
| |
|
| | return start, end |
| |
|
| |
|
| | def measure_two_objects(link_sub_1, link_sub_2): |
| | """Measure two vertices from two different objects. |
| | |
| | Parameters |
| | ---------- |
| | link_sub_1: tuple |
| | A tuple containing one object and a list of subelement strings, |
| | which may be empty. Only the first subelement is considered, which |
| | must be a vertex. |
| | :: |
| | link_sub_1 = (obj1, ['VertexN', ...]) |
| | |
| | link_sub_2: tuple |
| | Same. |
| | """ |
| | start = App.Vector() |
| | end = App.Vector() |
| |
|
| | obj1 = link_sub_1[0] |
| | lsub1 = link_sub_1[1] |
| |
|
| | obj2 = link_sub_2[0] |
| | lsub2 = link_sub_2[1] |
| |
|
| | |
| | |
| | if lsub1 and lsub2: |
| | subelement1 = lsub1[0] |
| | subelement2 = lsub2[0] |
| |
|
| | if "Vertex" in subelement1 and "Vertex" in subelement2: |
| | n1 = int(subelement1[6:]) - 1 |
| | n2 = int(subelement2[6:]) - 1 |
| | start = obj1.Shape.Vertexes[n1].Point |
| | end = obj2.Shape.Vertexes[n2].Point |
| |
|
| | return start, end |
| |
|
| |
|
| | |
| | _Dimension = LinearDimension |
| |
|
| |
|
| | class AngularDimension(DimensionBase): |
| | """The angular dimension object. |
| | |
| | This inherits `DimensionBase` to provide the basic functionality of |
| | a dimension. |
| | """ |
| |
|
| | def __init__(self, obj): |
| | obj.Proxy = self |
| | self.Type = "AngularDimension" |
| | self.set_properties(obj) |
| |
|
| | def set_properties(self, obj): |
| | """Set basic properties only if they don't exist.""" |
| | super().set_properties(obj) |
| |
|
| | properties = obj.PropertiesList |
| |
|
| | if "FirstAngle" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Starting angle of the dimension line " |
| | "(circular arc).\n" |
| | "The arc is drawn counter-clockwise.", |
| | ) |
| | obj.addProperty( |
| | "App::PropertyAngle", "FirstAngle", "Angular dimension", _tip, locked=True |
| | ) |
| | obj.FirstAngle = 0 |
| |
|
| | if "LastAngle" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Ending angle of the dimension line " |
| | "(circular arc).\n" |
| | "The arc is drawn counter-clockwise.", |
| | ) |
| | obj.addProperty( |
| | "App::PropertyAngle", "LastAngle", "Angular dimension", _tip, locked=True |
| | ) |
| | obj.LastAngle = 90 |
| |
|
| | if "Center" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The center point of the dimension " |
| | "line, which is a circular arc.\n" |
| | "\n" |
| | "This is normally the point where two " |
| | "line segments, or their extensions\n" |
| | "intersect, resulting in the " |
| | "measured 'Angle' between them.", |
| | ) |
| | obj.addProperty( |
| | "App::PropertyVectorDistance", "Center", "Angular dimension", _tip, locked=True |
| | ) |
| | obj.Center = App.Vector(0, 0, 0) |
| |
|
| | if "Angle" not in properties: |
| | _tip = QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "The value of the measurement.\n" |
| | "\n" |
| | "This property is read-only because " |
| | "the value is calculated from\n" |
| | "the 'First Angle' and " |
| | "'Last Angle' properties.", |
| | ) |
| | obj.addProperty("App::PropertyAngle", "Angle", "Angular dimension", _tip, locked=True) |
| | obj.Angle = 0 |
| |
|
| | def onDocumentRestored(self, obj): |
| | """Execute code when the document is restored.""" |
| | super().onDocumentRestored(obj) |
| | gui_utils.restore_view_object( |
| | obj, vp_module="view_dimension", vp_class="ViewProviderAngularDimension" |
| | ) |
| |
|
| | vobj = getattr(obj, "ViewObject", None) |
| | if vobj is None: |
| | return |
| |
|
| | if not hasattr(vobj, "TextColor"): |
| | self.update_properties_0v21(obj, vobj) |
| |
|
| | def loads(self, state): |
| | self.Type = "AngularDimension" |
| |
|
| | def transform(self, obj, pla): |
| | """Transform the object by applying a placement.""" |
| | import Part |
| |
|
| | new_normal = pla.Rotation.multVec(obj.Normal) |
| | c_old = Part.makeCircle(1, App.Vector(), obj.Normal) |
| | c_tra = c_old.transformShape((pla * c_old.Placement).Rotation.Matrix) |
| | c_new = Part.makeCircle(1, App.Vector(), new_normal) |
| | delta_angle = math.degrees(c_new.Curve.parameter(c_tra.firstVertex().Point)) |
| | first_angle = (obj.FirstAngle.Value + delta_angle) % 360 |
| | last_angle = (obj.LastAngle.Value + delta_angle) % 360 |
| |
|
| | obj.Center = pla.multVec(obj.Center) |
| | obj.Dimline = pla.multVec(obj.Dimline) |
| | obj.Normal = new_normal |
| | obj.FirstAngle = first_angle |
| | obj.LastAngle = last_angle |
| |
|
| | def execute(self, obj): |
| | """Execute when the object is created or recomputed. |
| | |
| | Nothing is actually done here, except update the viewprovider, |
| | as the lines and text are created in the viewprovider. |
| | """ |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if App.GuiUp and obj.ViewObject: |
| | obj.ViewObject.update() |
| |
|
| | def onChanged(self, obj, prop): |
| | """Execute when a property is changed. |
| | |
| | It just sets some properties to be read-only or hidden, |
| | as they aren't used. |
| | """ |
| | if hasattr(obj, "Angle"): |
| | obj.setPropertyStatus("Angle", "ReadOnly") |
| |
|
| | if hasattr(obj, "Normal"): |
| | obj.setPropertyStatus("Normal", "Hidden") |
| | if hasattr(obj, "Support"): |
| | obj.setPropertyStatus("Support", "Hidden") |
| | if hasattr(obj, "LinkedGeometry"): |
| | obj.setPropertyStatus("LinkedGeometry", "Hidden") |
| |
|
| |
|
| | def measure_two_obj_angles(link_sub_1, link_sub_2): |
| | """Measure two edges from two different objects to measure the angle. |
| | |
| | This function is a prototype because it does not determine all possible |
| | starting and ending angles that could be used to draw the dimension line, |
| | which is a circular arc. |
| | |
| | Parameters |
| | ---------- |
| | link_sub_1: tuple |
| | A tuple containing one object and a list of subelement strings, |
| | which may be empty. Only the first subelement is considered, which |
| | must be an edge. |
| | :: |
| | link_sub_1 = (obj1, ['EdgeN', ...]) |
| | |
| | link_sub_2: tuple |
| | Same. |
| | """ |
| | start = 0 |
| | end = 0 |
| |
|
| | obj1 = link_sub_1[0] |
| | lsub1 = link_sub_1[1] |
| |
|
| | obj2 = link_sub_2[0] |
| | lsub2 = link_sub_2[1] |
| |
|
| | |
| | |
| | if lsub1 and lsub2: |
| | subelement1 = lsub1[0] |
| | subelement2 = lsub2[0] |
| |
|
| | if "Edge" in subelement1 and "Edge" in subelement2: |
| | n1 = int(subelement1[4:]) - 1 |
| | n2 = int(subelement2[4:]) - 1 |
| | start = obj1.Shape.Edges[n1].Curve.Direction |
| | end = obj2.Shape.Edges[n2].Curve.Direction |
| |
|
| | |
| | wp = WorkingPlane.get_working_plane(update=False) |
| | start_r = DraftVecUtils.angle(start, wp.u) |
| | end_r = DraftVecUtils.angle(end, wp.u) |
| | start = math.degrees(start_r) |
| | end = math.degrees(end_r) |
| |
|
| | |
| | |
| | |
| | if start < 0: |
| | start = abs(start) |
| | if end < 0: |
| | end = abs(end) |
| |
|
| | return start, end |
| |
|
| |
|
| | |
| | _AngularDimension = AngularDimension |
| |
|
| | |
| |
|