FreeCAD
609 строк · 28.2 Кб
1# ***************************************************************************
2# * (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net> *
3# * (c) 2009, 2010 Ken Cline <cline@frii.com> *
4# * (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de> *
5# * *
6# * This file is part of the FreeCAD CAx development system. *
7# * *
8# * This program is free software; you can redistribute it and/or modify *
9# * it under the terms of the GNU Lesser General Public License (LGPL) *
10# * as published by the Free Software Foundation; either version 2 of *
11# * the License, or (at your option) any later version. *
12# * for detail see the LICENCE text file. *
13# * *
14# * FreeCAD is distributed in the hope that it will be useful, *
15# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
16# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
17# * GNU Library General Public License for more details. *
18# * *
19# * You should have received a copy of the GNU Library General Public *
20# * License along with FreeCAD; if not, write to the Free Software *
21# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
22# * USA *
23# * *
24# ***************************************************************************
25"""Provides GUI tools to create dimension objects.
26
27The objects can be simple linear dimensions that measure between two arbitrary
28points, or linear dimensions linked to an edge.
29It can also be radius or diameter dimensions that measure circles
30and circular arcs.
31And it can also be an angular dimension measuring the angle between
32two straight lines.
33"""
34## @package gui_dimensions
35# \ingroup draftguitools
36# \brief Provides GUI tools to create dimension objects.
37
38import math39import lazy_loader.lazy_loader as lz40from PySide.QtCore import QT_TRANSLATE_NOOP41
42import FreeCAD as App43import FreeCADGui as Gui44import Draft_rc45import DraftVecUtils46import draftguitools.gui_base_original as gui_base_original47import draftguitools.gui_tool_utils as gui_tool_utils48import draftguitools.gui_trackers as trackers49import draftutils.gui_utils as gui_utils50
51from draftutils.translate import translate52from draftutils.messages import _toolmsg, _msg53
54DraftGeomUtils = lz.LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils")55
56# The module is used to prevent complaints from code checkers (flake8)
57True if Draft_rc.__name__ else False58
59## \addtogroup draftguitools
60# @{
61
62
63class Dimension(gui_base_original.Creator):64"""Gui command for the Dimension tool.65
66This includes at the moment linear, radial, diametrical,
67and angular dimensions depending on the selected object
68and the modifier key (ALT) used.
69
70Maybe in the future each type can be in its own class,
71and they can inherit their basic properties from a parent class.
72"""
73
74def __init__(self):75super().__init__()76self.max = 277self.cont = None78self.dir = None79
80def GetResources(self):81"""Set icon, menu and tooltip."""82
83return {'Pixmap': 'Draft_Dimension',84'Accel': "D, I",85'MenuText': QT_TRANSLATE_NOOP("Draft_Dimension", "Dimension"),86'ToolTip': QT_TRANSLATE_NOOP("Draft_Dimension", "Creates a dimension.\n\n- Pick three points to create a simple linear dimension.\n- Select a straight line to create a linear dimension linked to that line.\n- Select an arc or circle to create a radius or diameter dimension linked to that arc.\n- Select two straight lines to create an angular dimension between them.\nCTRL to snap, SHIFT to constrain, ALT to select an edge or arc.\n\nYou may select a single line or single circular arc before launching this command\nto create the corresponding linked dimension.\nYou may also select an 'App::MeasureDistance' object before launching this command\nto turn it into a 'Draft Dimension' object.")}87
88def Activated(self):89"""Execute when the command is called."""90if self.cont:91self.finish()92elif self.selected_app_measure():93super().Activated(name="Dimension")94self.dimtrack = trackers.dimTracker()95self.arctrack = trackers.arcTracker()96self.create_with_app_measure()97self.finish()98else:99super().Activated(name="Dimension")100if self.ui:101self.ui.pointUi(title=translate("draft", "Dimension"), icon="Draft_Dimension")102self.ui.continueCmd.show()103self.ui.selectButton.show()104self.altdown = False105self.call = self.view.addEventCallback("SoEvent", self.action)106self.dimtrack = trackers.dimTracker()107self.arctrack = trackers.arcTracker()108self.link = None109self.edges = []110self.angles = []111self.angledata = None112self.indices = []113self.center = None114self.arcmode = False115self.point1 = None116self.point2 = None117self.proj_point1 = None118self.proj_point2 = None119self.force = None120self.info = None121self.selectmode = False122self.set_selection()123_toolmsg(translate("draft", "Pick first point"))124
125def set_selection(self):126"""Fill the nodes according to the selected geometry."""127sel = Gui.Selection.getSelectionEx()128if (len(sel) == 1129and len(sel[0].SubElementNames) == 1130and "Edge" in sel[0].SubElementNames[0]):131# The selection is just a single `Edge`132sel_object = sel[0]133edge = sel_object.SubObjects[0]134
135# `n` is the edge number starting from 0 not from 1.136# The reason is lists in Python start from 0, although137# in the object's `Shape`, they start from 1138n = int(sel_object.SubElementNames[0].lstrip("Edge")) - 1139self.indices.append(n)140
141if DraftGeomUtils.geomType(edge) == "Line":142self.node.extend([edge.Vertexes[0].Point,143edge.Vertexes[1].Point])144
145# Iterate over the vertices of the parent `Object`;146# when the vertices match those of the selected `edge`147# save the index of vertex in the parent object148v1 = None149v2 = None150for i, v in enumerate(sel_object.Object.Shape.Vertexes):151if v.Point == edge.Vertexes[0].Point:152v1 = i153if v.Point == edge.Vertexes[1].Point:154v2 = i155
156if v1 is not None and v2 is not None: # note that v1 or v2 can be zero157self.link = [sel_object.Object, v1, v2]158elif DraftGeomUtils.geomType(edge) == "Circle":159self.node.extend([edge.Curve.Center,160edge.Vertexes[0].Point])161self.edges = [edge]162self.arcmode = "diameter"163self.link = [sel_object.Object, n]164
165def selected_app_measure(self):166"""Check if App::MeasureDistance objects are selected."""167sel = Gui.Selection.getSelection()168if not sel:169return False170for o in sel:171if not o.isDerivedFrom("App::MeasureDistance"):172return False173return True174
175def finish(self, cont=False):176"""Terminate the operation."""177self.end_callbacks(self.call)178self.cont = None179self.dir = None180if self.ui:181self.dimtrack.finalize()182self.arctrack.finalize()183super().finish()184
185def angle_dimension_normal(self, edge1, edge2):186rot = App.Rotation(DraftGeomUtils.vec(edge1),187DraftGeomUtils.vec(edge2),188self.wp.axis,189"XYZ")190norm = rot.multVec(App.Vector(0, 0, 1))191vnorm = gui_utils.get_3d_view().getViewDirection()192if vnorm.getAngle(norm) < math.pi / 2:193norm = norm.negative()194return norm195
196def create_with_app_measure(self):197"""Create on measurement objects.198
199This is used when the selection is an `'App::MeasureDistance'`,
200which is created with the basic tool `Std_MeasureDistance`.
201This object is removed and in its place a `Draft Dimension`
202is created.
203"""
204for o in Gui.Selection.getSelection():205p1 = o.P1206p2 = o.P2207_root = o.ViewObject.RootNode208_ch = _root.getChildren()[1].getChildren()[0].getChildren()[0]209pt = _ch.getChildren()[3]210p3 = App.Vector(pt.point.getValues()[2].getValue())211
212Gui.addModule("Draft")213_cmd = 'Draft.make_linear_dimension'214_cmd += '('215_cmd += DraftVecUtils.toString(p1) + ', '216_cmd += DraftVecUtils.toString(p2) + ', '217_cmd += 'dim_line=' + DraftVecUtils.toString(p3)218_cmd += ')'219_rem = 'FreeCAD.ActiveDocument.removeObject("' + o.Name + '")'220_cmd_list = ['_dim_ = ' + _cmd,221_rem,222'Draft.autogroup(_dim_)',223'FreeCAD.ActiveDocument.recompute()']224self.commit(translate("draft", "Create Dimension"),225_cmd_list)226
227def create_angle_dimension(self):228"""Create an angular dimension from a center and two angles."""229ang1 = math.degrees(self.angledata[1])230ang2 = math.degrees(self.angledata[0])231norm = self.angle_dimension_normal(self.edges[0], self.edges[1])232
233_cmd = 'Draft.make_angular_dimension'234_cmd += '('235_cmd += 'center=' + DraftVecUtils.toString(self.center) + ', '236_cmd += 'angles='237_cmd += '['238_cmd += str(ang1) + ', '239_cmd += str(ang2)240_cmd += '], '241_cmd += 'dim_line=' + DraftVecUtils.toString(self.node[-1]) + ', '242_cmd += 'normal=' + DraftVecUtils.toString(norm)243_cmd += ')'244_cmd_list = ['_dim_ = ' + _cmd,245'Draft.autogroup(_dim_)',246'FreeCAD.ActiveDocument.recompute()']247self.commit(translate("draft", "Create Dimension"),248_cmd_list)249
250def create_linear_dimension(self):251"""Create a simple linear dimension, not linked to an edge."""252_cmd = 'Draft.make_linear_dimension'253_cmd += '('254_cmd += DraftVecUtils.toString(self.node[0]) + ', '255_cmd += DraftVecUtils.toString(self.node[1]) + ', '256_cmd += 'dim_line=' + DraftVecUtils.toString(self.node[2])257_cmd += ')'258_cmd_list = ['_dim_ = ' + _cmd,259'Draft.autogroup(_dim_)',260'FreeCAD.ActiveDocument.recompute()']261self.commit(translate("draft", "Create Dimension"),262_cmd_list)263
264def create_linear_dimension_obj(self, direction=None):265"""Create a linear dimension linked to an edge.266
267The `link` attribute has indices of vertices as they appear
268in the list `Shape.Vertexes`, so they start as zero 0.
269
270The `LinearDimension` class, created by `make_linear_dimension_obj`,
271considers the vertices of a `Shape` which are numbered to start
272with 1, that is, `Vertex1`.
273Therefore the value in `link` has to be incremented by 1.
274"""
275_cmd = 'Draft.make_linear_dimension_obj'276_cmd += '('277_cmd += 'FreeCAD.ActiveDocument.' + self.link[0].Name + ', '278_cmd += 'i1=' + str(self.link[1] + 1) + ', '279_cmd += 'i2=' + str(self.link[2] + 1) + ', '280_cmd += 'dim_line=' + DraftVecUtils.toString(self.node[2])281_cmd += ')'282_cmd_list = ['_dim_ = ' + _cmd]283
284dir_u = DraftVecUtils.toString(self.wp.u)285dir_v = DraftVecUtils.toString(self.wp.v)286if direction == "X":287_cmd_list += ['_dim_.Direction = ' + dir_u]288elif direction == "Y":289_cmd_list += ['_dim_.Direction = ' + dir_v]290
291_cmd_list += ['Draft.autogroup(_dim_)',292'FreeCAD.ActiveDocument.recompute()']293self.commit(translate("draft", "Create Dimension"),294_cmd_list)295
296def create_radial_dimension_obj(self):297"""Create a radial dimension linked to a circular edge."""298_cmd = 'Draft.make_radial_dimension_obj'299_cmd += '('300_cmd += 'FreeCAD.ActiveDocument.' + self.link[0].Name + ', '301_cmd += 'index=' + str(self.link[1] + 1) + ', '302_cmd += 'mode="' + str(self.arcmode) + '", '303_cmd += 'dim_line=' + DraftVecUtils.toString(self.node[2])304_cmd += ')'305_cmd_list = ['_dim_ = ' + _cmd,306'Draft.autogroup(_dim_)',307'FreeCAD.ActiveDocument.recompute()']308self.commit(translate("draft", "Create Dimension (radial)"),309_cmd_list)310
311def createObject(self):312"""Create the actual object in the current document."""313Gui.addModule("Draft")314
315if self.angledata:316# Angle dimension, with two angles provided317self.create_angle_dimension()318elif self.link and not self.arcmode:319# Linear dimension, linked to a straight edge320if self.force == 1:321self.create_linear_dimension_obj("Y")322elif self.force == 2:323self.create_linear_dimension_obj("X")324else:325self.create_linear_dimension_obj()326elif self.arcmode:327# Radius or dimeter dimension, linked to a circular edge328self.create_radial_dimension_obj()329else:330# Linear dimension, not linked to any edge331self.create_linear_dimension()332
333if self.ui.continueMode:334self.cont = self.node[2]335if not self.dir:336if self.link:337v1 = self.link[0].Shape.Vertexes[self.link[1]].Point338v2 = self.link[0].Shape.Vertexes[self.link[2]].Point339self.dir = v2.sub(v1)340else:341self.dir = self.node[1].sub(self.node[0])342
343self.node = [self.node[1]]344
345self.link = None346
347def selectEdge(self):348"""Toggle the select mode to the opposite state."""349self.selectmode = not self.selectmode350
351def action(self, arg):352"""Handle the 3D scene events.353
354This is installed as an EventCallback in the Inventor view.
355
356Parameters
357----------
358arg: dict
359Dictionary with strings that indicates the type of event received
360from the 3D view.
361"""
362if arg["Type"] == "SoKeyboardEvent":363if arg["Key"] == "ESCAPE":364self.finish()365elif arg["Type"] == "SoLocation2Event": # mouse movement detection366shift = gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_constrain_key())367if self.arcmode or self.point2:368gui_tool_utils.setMod(arg, gui_tool_utils.get_mod_constrain_key(), False)369(self.point,370ctrlPoint, self.info) = gui_tool_utils.getPoint(self, arg,371noTracker=(len(self.node)>0))372if (gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key())373or self.selectmode) and (len(self.node) < 3):374self.dimtrack.off()375if not self.altdown:376self.altdown = True377self.ui.switchUi(True)378if hasattr(Gui, "Snapper"):379Gui.Snapper.setSelectMode(True)380snapped = self.view.getObjectInfo((arg["Position"][0],381arg["Position"][1]))382if snapped:383ob = self.doc.getObject(snapped['Object'])384if "Edge" in snapped['Component']:385num = int(snapped['Component'].lstrip('Edge')) - 1386ed = ob.Shape.Edges[num]387v1 = ed.Vertexes[0].Point388v2 = ed.Vertexes[-1].Point389self.dimtrack.update([v1, v2, self.cont])390else:391if self.node and (len(self.edges) < 2):392self.dimtrack.on()393if len(self.edges) == 2:394# angular dimension395self.dimtrack.off()396
397vnorm = gui_utils.get_3d_view().getViewDirection()398anorm = self.arctrack.normal399
400# Code below taken from WorkingPlane.projectPoint:401cos = vnorm.dot(anorm)402delta_ax_proj = (self.point - self.center).dot(anorm)403proj = self.point - delta_ax_proj / cos * vnorm404self.point = proj405
406r = self.point.sub(self.center)407self.arctrack.setRadius(r.Length)408a = self.arctrack.getAngle(self.point)409pair = DraftGeomUtils.getBoundaryAngles(a, self.angles)410if not (pair[0] < a < pair[1]):411self.angledata = [4 * math.pi - pair[0],4122 * math.pi - pair[1]]413else:414self.angledata = [2 * math.pi - pair[0],4152 * math.pi - pair[1]]416self.arctrack.setStartAngle(self.angledata[0])417self.arctrack.setEndAngle(self.angledata[1])418if self.altdown:419self.altdown = False420self.ui.switchUi(False)421if hasattr(Gui, "Snapper"):422Gui.Snapper.setSelectMode(False)423if self.node and self.dir and len(self.node) < 2:424_p = DraftVecUtils.project(self.point.sub(self.node[0]),425self.dir)426self.point = self.node[0].add(_p)427if len(self.node) == 2:428if self.arcmode and self.edges:429cen = self.edges[0].Curve.Center430rad = self.edges[0].Curve.Radius431baseray = self.point.sub(cen)432v2 = DraftVecUtils.scaleTo(baseray, rad)433v1 = v2.negative()434if shift:435self.node = [cen, cen.add(v2)]436self.arcmode = "radius"437else:438self.node = [cen.add(v1), cen.add(v2)]439self.arcmode = "diameter"440self.dimtrack.update(self.node)441# Draw constraint tracker line.442if shift and (not self.arcmode):443if len(self.node) == 2:444if not self.point1:445self.point1 = self.node[0]446if not self.point2:447self.point2 = self.node[1]448# else:449# self.node[1] = self.point2450self.set_constraint_node()451else:452self.force = None453self.proj_point1 = None454self.proj_point2 = None455if self.point1:456self.node[0] = self.point1457if self.point2 and (len(self.node) > 1):458self.node[1] = self.point2459# self.point2 = None460# update the dimline461if self.node and not self.arcmode:462self.dimtrack.update(self.node463+ [self.point] + [self.cont])464gui_tool_utils.redraw3DView()465elif arg["Type"] == "SoMouseButtonEvent":466if (arg["State"] == "DOWN") and (arg["Button"] == "BUTTON1"):467if self.point:468self.ui.redraw()469if (not self.node) and (not self.support):470gui_tool_utils.getSupport(arg)471if (gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key())472or self.selectmode) and (len(self.node) < 3):473# print("snapped: ",self.info)474if self.info:475ob = self.doc.getObject(self.info['Object'])476if 'Edge' in self.info['Component']:477num = int(self.info['Component'].lstrip('Edge')) - 1478ed = ob.Shape.Edges[num]479v1 = ed.Vertexes[0].Point480v2 = ed.Vertexes[-1].Point481i1 = i2 = None482for i in range(len(ob.Shape.Vertexes)):483if v1 == ob.Shape.Vertexes[i].Point:484i1 = i485if v2 == ob.Shape.Vertexes[i].Point:486i2 = i487if (i1 is not None) and (i2 is not None):488self.indices.append(num)489if not self.edges:490# nothing snapped yet, we treat it491# as a normal edge-snapped dimension492self.node = [v1, v2]493self.link = [ob, i1, i2]494self.edges.append(ed)495if DraftGeomUtils.geomType(ed) == "Circle":496# snapped edge is an arc497self.arcmode = "diameter"498self.link = [ob, num]499else:500# there is already a snapped edge,501# so we start angular dimension502self.edges.append(ed)503# self.node now has the 4 endpoints504self.node.extend([v1, v2])505c = DraftGeomUtils.findIntersection(self.node[0],506self.node[1],507self.node[2],508self.node[3],509True, True)510if c:511# print("centers:",c)512self.center = c[0]513self.arctrack.setCenter(self.center)514self.arctrack.normal = self.angle_dimension_normal(self.edges[0], self.edges[1])515self.arctrack.on()516for e in self.edges:517if e.Length < 0.00003: # Edge must be long enough for the tolerance of 0.00001mm to make sense.518_msg(translate("draft", "Edge too short!"))519self.finish()520return521for i in [0, 1]:522pt = e.Vertexes[i].Point523if pt.isEqual(self.center, 0.00001): # A relatively high tolerance is required.524pt = e.Vertexes[i - 1].Point # Use the other point instead.525self.angles.append(self.arctrack.getAngle(pt))526self.link = [self.link[0], ob]527else:528_msg(translate("draft", "Edges don't intersect!"))529self.finish()530return531self.dimtrack.on()532else:533self.node.append(self.point)534self.selectmode = False535# print("node", self.node)536self.dimtrack.update(self.node)537if len(self.node) == 2:538self.point2 = self.node[1]539if len(self.node) == 1:540self.dimtrack.on()541if self.planetrack:542self.planetrack.set(self.node[0])543elif len(self.node) == 2 and self.cont:544self.node.append(self.cont)545self.createObject()546if not self.cont:547self.finish()548elif len(self.node) == 3:549# for unlinked arc mode:550# if self.arcmode:551# v = self.node[1].sub(self.node[0])552# v.multiply(0.5)553# cen = self.node[0].add(v)554# self.node = [self.node[0], self.node[1], cen]555self.createObject()556if not self.cont:557self.finish()558elif self.angledata:559self.node.append(self.point)560self.createObject()561self.finish()562
563def numericInput(self, numx, numy, numz):564"""Validate the entry fields in the user interface.565
566This function is called by the toolbar or taskpanel interface
567when valid x, y, and z have been entered in the input fields.
568"""
569self.point = App.Vector(numx, numy, numz)570self.node.append(self.point)571self.dimtrack.update(self.node)572if len(self.node) == 1:573self.dimtrack.on()574elif len(self.node) == 3:575self.createObject()576if not self.cont:577self.finish()578
579def set_constraint_node(self):580"""Set constrained nodes for vertical or horizontal dimension581by projecting on the working plane.
582"""
583if not self.proj_point1 or not self.proj_point2:584self.proj_point1 = self.wp.project_point(self.node[0])585self.proj_point2 = self.wp.project_point(self.node[1])586proj_u= self.wp.u.dot(self.proj_point2 - self.proj_point1)587proj_v= self.wp.v.dot(self.proj_point2 - self.proj_point1)588active_view = Gui.ActiveDocument.ActiveView589cursor = active_view.getCursorPos()590cursor_point = active_view.getPoint(cursor)591self.point = self.wp.project_point(cursor_point)592if not self.force:593ref_point = self.point - (self.proj_point2 + self.proj_point1)*1/2594ref_angle = abs(ref_point.getAngle(self.wp.u))595if (ref_angle > math.pi/4) and (ref_angle <= 0.75*math.pi):596self.force = 2597else:598self.force = 1599if self.force == 1:600self.node[0] = self.proj_point1601self.node[1] = self.proj_point1 + self.wp.v*proj_v602elif self.force == 2:603self.node[0] = self.proj_point1604self.node[1] = self.proj_point1 + self.wp.u*proj_u605
606
607Gui.addCommand('Draft_Dimension', Dimension())608
609## @}
610