FreeCAD
634 строки · 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 circular arc objects."""
26## @package gui_arcs
27# \ingroup draftguitools
28# \brief Provides GUI tools to create circular arc objects.
29
30## \addtogroup draftguitools
31# @{
32import math
33from PySide.QtCore import QT_TRANSLATE_NOOP
34
35import FreeCAD as App
36import FreeCADGui as Gui
37import Draft
38import Draft_rc
39import DraftVecUtils
40from FreeCAD import Units as U
41from draftguitools import gui_base
42from draftguitools import gui_base_original
43from draftguitools import gui_tool_utils
44from draftguitools import gui_trackers as trackers
45from draftutils import params
46from draftutils import utils
47from draftutils.messages import _err, _toolmsg
48from draftutils.translate import translate
49
50# The module is used to prevent complaints from code checkers (flake8)
51True if Draft_rc.__name__ else False
52
53
54class Arc(gui_base_original.Creator):
55"""Gui command for the Circular Arc tool."""
56
57def __init__(self):
58super().__init__()
59self.closedCircle = False
60self.featureName = "Arc"
61
62def GetResources(self):
63"""Set icon, menu and tooltip."""
64return {'Pixmap': 'Draft_Arc',
65'Accel': "A, R",
66'MenuText': QT_TRANSLATE_NOOP("Draft_Arc", "Arc"),
67'ToolTip': QT_TRANSLATE_NOOP("Draft_Arc", "Creates a circular arc by a center point and a radius.\nCTRL to snap, SHIFT to constrain.")}
68
69def Activated(self):
70"""Execute when the command is called."""
71super().Activated(name=self.featureName)
72if self.ui:
73self.step = 0
74self.center = None
75self.rad = None
76self.angle = 0 # angle inscribed by arc
77self.tangents = []
78self.tanpoints = []
79if self.featureName == "Arc":
80self.ui.arcUi()
81else:
82self.ui.circleUi()
83self.altdown = False
84self.ui.sourceCmd = self
85self.linetrack = trackers.lineTracker(dotted=True)
86self.arctrack = trackers.arcTracker()
87self.call = self.view.addEventCallback("SoEvent", self.action)
88_toolmsg(translate("draft", "Pick center point"))
89
90def finish(self, cont=False):
91"""Terminate the operation.
92
93Parameters
94----------
95cont: bool or None, optional
96Restart (continue) the command if `True`, or if `None` and
97`ui.continueMode` is `True`.
98"""
99self.end_callbacks(self.call)
100if self.ui:
101self.linetrack.finalize()
102self.arctrack.finalize()
103super().finish()
104if cont or (cont is None and self.ui and self.ui.continueMode):
105self.Activated()
106
107def updateAngle(self, angle):
108"""Update the angle with the new value."""
109# previous absolute angle
110lastangle = self.firstangle + self.angle
111if lastangle <= -2 * math.pi:
112lastangle += 2 * math.pi
113if lastangle >= 2 * math.pi:
114lastangle -= 2 * math.pi
115# compute delta = change in angle:
116d0 = angle - lastangle
117d1 = d0 + 2 * math.pi
118d2 = d0 - 2 * math.pi
119if abs(d0) < min(abs(d1), abs(d2)):
120delta = d0
121elif abs(d1) < abs(d2):
122delta = d1
123else:
124delta = d2
125newangle = self.angle + delta
126# normalize angle, preserving direction
127if newangle >= 2 * math.pi:
128newangle -= 2 * math.pi
129if newangle <= -2 * math.pi:
130newangle += 2 * math.pi
131self.angle = newangle
132
133def action(self, arg):
134"""Handle the 3D scene events.
135
136This is installed as an EventCallback in the Inventor view.
137
138Parameters
139----------
140arg: dict
141Dictionary with strings that indicates the type of event received
142from the 3D view.
143"""
144import DraftGeomUtils
145
146if arg["Type"] == "SoKeyboardEvent":
147if arg["Key"] == "ESCAPE":
148self.finish()
149elif arg["Type"] == "SoLocation2Event": # mouse movement detection
150self.point, ctrlPoint, info = gui_tool_utils.getPoint(self, arg)
151# this is to make sure radius is what you see on screen
152if self.center and DraftVecUtils.dist(self.point, self.center) > 0:
153viewdelta = DraftVecUtils.project(self.point.sub(self.center),
154self.wp.axis)
155if not DraftVecUtils.isNull(viewdelta):
156self.point = self.point.add(viewdelta.negative())
157if self.step == 0: # choose center
158if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key()):
159if not self.altdown:
160self.altdown = True
161self.ui.switchUi(True)
162else:
163if self.altdown:
164self.altdown = False
165self.ui.switchUi(False)
166elif self.step == 1: # choose radius
167if len(self.tangents) == 2:
168cir = DraftGeomUtils.circleFrom2tan1pt(self.tangents[0],
169self.tangents[1],
170self.point)
171_c = DraftGeomUtils.findClosestCircle(self.point, cir)
172self.center = _c.Center
173self.arctrack.setCenter(self.center)
174elif self.tangents and self.tanpoints:
175cir = DraftGeomUtils.circleFrom1tan2pt(self.tangents[0],
176self.tanpoints[0],
177self.point)
178_c = DraftGeomUtils.findClosestCircle(self.point, cir)
179self.center = _c.Center
180self.arctrack.setCenter(self.center)
181if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key()):
182if not self.altdown:
183self.altdown = True
184if info:
185ob = self.doc.getObject(info['Object'])
186num = int(info['Component'].lstrip('Edge')) - 1
187ed = ob.Shape.Edges[num]
188if len(self.tangents) == 2:
189cir = DraftGeomUtils.circleFrom3tan(self.tangents[0],
190self.tangents[1],
191ed)
192cl = DraftGeomUtils.findClosestCircle(self.point, cir)
193self.center = cl.Center
194self.rad = cl.Radius
195self.arctrack.setCenter(self.center)
196else:
197self.rad = self.center.add(DraftGeomUtils.findDistance(self.center, ed).sub(self.center)).Length
198else:
199self.rad = DraftVecUtils.dist(self.point, self.center)
200else:
201if self.altdown:
202self.altdown = False
203self.rad = DraftVecUtils.dist(self.point, self.center)
204self.ui.setRadiusValue(self.rad, "Length")
205self.arctrack.setRadius(self.rad)
206self.linetrack.p1(self.center)
207self.linetrack.p2(self.point)
208self.linetrack.on()
209elif (self.step == 2): # choose first angle
210currentrad = DraftVecUtils.dist(self.point, self.center)
211if currentrad != 0:
212angle = DraftVecUtils.angle(self.wp.u, self.point.sub(self.center), self.wp.axis)
213else:
214angle = 0
215self.linetrack.p2(DraftVecUtils.scaleTo(self.point.sub(self.center), self.rad).add(self.center))
216self.ui.setRadiusValue(math.degrees(angle), unit="Angle")
217self.firstangle = angle
218else:
219# choose second angle
220currentrad = DraftVecUtils.dist(self.point, self.center)
221if currentrad != 0:
222angle = DraftVecUtils.angle(self.wp.u, self.point.sub(self.center), self.wp.axis)
223else:
224angle = 0
225self.linetrack.p2(DraftVecUtils.scaleTo(self.point.sub(self.center), self.rad).add(self.center))
226self.updateAngle(angle)
227self.ui.setRadiusValue(math.degrees(self.angle), unit="Angle")
228self.arctrack.setApertureAngle(self.angle)
229
230gui_tool_utils.redraw3DView()
231
232elif arg["Type"] == "SoMouseButtonEvent": # mouse click
233if arg["State"] == "DOWN" and arg["Button"] == "BUTTON1":
234if self.point:
235if self.step == 0: # choose center
236if not self.support:
237gui_tool_utils.getSupport(arg)
238(self.point,
239ctrlPoint, info) = gui_tool_utils.getPoint(self, arg)
240if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key()):
241snapped = self.view.getObjectInfo((arg["Position"][0],
242arg["Position"][1]))
243if snapped:
244ob = self.doc.getObject(snapped['Object'])
245num = int(snapped['Component'].lstrip('Edge')) - 1
246ed = ob.Shape.Edges[num]
247self.tangents.append(ed)
248if len(self.tangents) == 2:
249self.arctrack.on()
250self.ui.radiusUi()
251self.step = 1
252self.ui.setNextFocus()
253self.linetrack.on()
254_toolmsg(translate("draft", "Pick radius"))
255else:
256if len(self.tangents) == 1:
257self.tanpoints.append(self.point)
258else:
259self.center = self.point
260self.node = [self.point]
261self.arctrack.setCenter(self.center)
262self.linetrack.p1(self.center)
263self.linetrack.p2(self.view.getPoint(arg["Position"][0],
264arg["Position"][1]))
265self.arctrack.on()
266self.ui.radiusUi()
267self.step = 1
268self.ui.setNextFocus()
269self.linetrack.on()
270_toolmsg(translate("draft", "Pick radius"))
271if self.planetrack:
272self.planetrack.set(self.point)
273elif self.step == 1: # choose radius
274if self.closedCircle:
275self.drawArc()
276else:
277self.ui.labelRadius.setText(translate("draft", "Start angle"))
278self.ui.radiusValue.setToolTip(translate("draft", "Start angle"))
279self.ui.radiusValue.setText(U.Quantity(0, U.Angle).UserString)
280self.linetrack.p1(self.center)
281self.linetrack.on()
282self.step = 2
283_toolmsg(translate("draft", "Pick start angle"))
284elif self.step == 2: # choose first angle
285self.ui.labelRadius.setText(translate("draft", "Aperture angle"))
286self.ui.radiusValue.setToolTip(translate("draft", "Aperture angle"))
287ang_offset = DraftVecUtils.angle(self.wp.u,
288self.arctrack.getDeviation(),
289self.wp.axis)
290self.arctrack.setStartAngle(self.firstangle - ang_offset)
291self.step = 3
292_toolmsg(translate("draft", "Pick aperture"))
293else: # choose second angle
294self.step = 4
295self.drawArc()
296
297def drawArc(self):
298"""Actually draw the arc object."""
299rot, sup, pts, fil = self.getStrings()
300if self.closedCircle:
301try:
302# The command to run is built as a series of text strings
303# to be committed through the `draftutils.todo.ToDo` class.
304Gui.addModule("Draft")
305if params.get_param("UsePartPrimitives"):
306# Insert a Part::Primitive object
307_base = DraftVecUtils.toString(self.center)
308_cmd = 'FreeCAD.ActiveDocument.'
309_cmd += 'addObject("Part::Circle", "Circle")'
310_cmd_list = ['circle = ' + _cmd,
311'circle.Radius = ' + str(self.rad),
312'pl = FreeCAD.Placement()',
313'pl.Rotation.Q = ' + rot,
314'pl.Base = ' + _base,
315'circle.Placement = pl',
316'Draft.autogroup(circle)',
317'Draft.select(circle)',
318'FreeCAD.ActiveDocument.recompute()']
319self.commit(translate("draft", "Create Circle (Part)"),
320_cmd_list)
321else:
322# Insert a Draft circle
323_base = DraftVecUtils.toString(self.center)
324_cmd = 'Draft.make_circle'
325_cmd += '('
326_cmd += 'radius=' + str(self.rad) + ', '
327_cmd += 'placement=pl, '
328_cmd += 'face=' + fil + ', '
329_cmd += 'support=' + sup
330_cmd += ')'
331_cmd_list = ['pl=FreeCAD.Placement()',
332'pl.Rotation.Q=' + rot,
333'pl.Base=' + _base,
334'circle = ' + _cmd,
335'Draft.autogroup(circle)',
336'FreeCAD.ActiveDocument.recompute()']
337self.commit(translate("draft", "Create Circle"),
338_cmd_list)
339except Exception:
340_err("Draft: error delaying commit")
341else:
342# Not a closed circle, therefore a circular arc
343sta = math.degrees(self.firstangle)
344end = math.degrees(self.firstangle + self.angle)
345if end < sta:
346sta, end = end, sta
347sta %= 360
348end %= 360
349
350try:
351Gui.addModule("Draft")
352if params.get_param("UsePartPrimitives"):
353# Insert a Part::Primitive object
354_base = DraftVecUtils.toString(self.center)
355_cmd = 'FreeCAD.ActiveDocument.'
356_cmd += 'addObject("Part::Circle", "Circle")'
357_cmd_list = ['circle = ' + _cmd,
358'circle.Radius = ' + str(self.rad),
359'circle.Angle1 = ' + str(sta),
360'circle.Angle2 = ' + str(end),
361'pl = FreeCAD.Placement()',
362'pl.Rotation.Q = ' + rot,
363'pl.Base = ' + _base,
364'circle.Placement = pl',
365'Draft.autogroup(circle)',
366'Draft.select(circle)',
367'FreeCAD.ActiveDocument.recompute()']
368self.commit(translate("draft", "Create Arc (Part)"),
369_cmd_list)
370else:
371# Insert a Draft circle
372_base = DraftVecUtils.toString(self.center)
373_cmd = 'Draft.make_circle'
374_cmd += '('
375_cmd += 'radius=' + str(self.rad) + ', '
376_cmd += 'placement=pl, '
377_cmd += 'face=' + fil + ', '
378_cmd += 'startangle=' + str(sta) + ', '
379_cmd += 'endangle=' + str(end) + ', '
380_cmd += 'support=' + sup
381_cmd += ')'
382_cmd_list = ['pl = FreeCAD.Placement()',
383'pl.Rotation.Q = ' + rot,
384'pl.Base = ' + _base,
385'circle = ' + _cmd,
386'Draft.autogroup(circle)',
387'FreeCAD.ActiveDocument.recompute()']
388self.commit(translate("draft", "Create Arc"),
389_cmd_list)
390except Exception:
391_err("Draft: error delaying commit")
392
393# Finalize full circle or circular arc
394self.finish(cont=None)
395
396def numericInput(self, numx, numy, numz):
397"""Validate the entry fields in the user interface.
398
399This function is called by the toolbar or taskpanel interface
400when valid x, y, and z have been entered in the input fields.
401"""
402self.center = App.Vector(numx, numy, numz)
403self.node = [self.center]
404self.arctrack.setCenter(self.center)
405self.arctrack.on()
406self.ui.radiusUi()
407self.step = 1
408self.ui.setNextFocus()
409_toolmsg(translate("draft", "Pick radius"))
410
411def numericRadius(self, rad):
412"""Validate the entry radius in the user interface.
413
414This function is called by the toolbar or taskpanel interface
415when a valid radius has been entered in the input field.
416"""
417import DraftGeomUtils
418
419if self.step == 1:
420self.rad = rad
421if len(self.tangents) == 2:
422cir = DraftGeomUtils.circleFrom2tan1rad(self.tangents[0],
423self.tangents[1],
424rad)
425if self.center:
426_c = DraftGeomUtils.findClosestCircle(self.center, cir)
427self.center = _c.Center
428else:
429self.center = cir[-1].Center
430elif self.tangents and self.tanpoints:
431cir = DraftGeomUtils.circleFrom1tan1pt1rad(self.tangents[0],
432self.tanpoints[0],
433rad)
434if self.center:
435_c = DraftGeomUtils.findClosestCircle(self.center, cir)
436self.center = _c.Center
437else:
438self.center = cir[-1].Center
439if self.closedCircle:
440self.drawArc()
441else:
442self.step = 2
443self.arctrack.setCenter(self.center)
444self.ui.labelRadius.setText(translate("draft", "Start angle"))
445self.ui.radiusValue.setToolTip(translate("draft", "Start angle"))
446self.linetrack.p1(self.center)
447self.linetrack.on()
448self.ui.radiusValue.setText("")
449self.ui.radiusValue.setFocus()
450_toolmsg(translate("draft", "Pick start angle"))
451elif self.step == 2:
452self.ui.labelRadius.setText(translate("draft", "Aperture angle"))
453self.ui.radiusValue.setToolTip(translate("draft", "Aperture angle"))
454self.firstangle = math.radians(rad)
455ang_offset = DraftVecUtils.angle(self.wp.u,
456self.arctrack.getDeviation(),
457self.wp.axis)
458self.arctrack.setStartAngle(self.firstangle - ang_offset)
459self.step = 3
460self.ui.radiusValue.setText("")
461self.ui.radiusValue.setFocus()
462_toolmsg(translate("draft", "Pick aperture angle"))
463else:
464self.updateAngle(rad)
465self.angle = math.radians(rad)
466self.step = 4
467self.drawArc()
468
469
470Gui.addCommand('Draft_Arc', Arc())
471
472
473class Arc_3Points(gui_base.GuiCommandBase):
474"""GuiCommand for the Draft_Arc_3Points tool."""
475
476def GetResources(self):
477"""Set icon, menu and tooltip."""
478return {'Pixmap': "Draft_Arc_3Points",
479'Accel': "A,T",
480'MenuText': QT_TRANSLATE_NOOP("Draft_Arc_3Points", "Arc by 3 points"),
481'ToolTip': QT_TRANSLATE_NOOP("Draft_Arc_3Points", "Creates a circular arc by picking 3 points.\nCTRL to snap, SHIFT to constrain.")}
482
483def Activated(self):
484"""Execute when the command is called."""
485if App.activeDraftCommand:
486App.activeDraftCommand.finish()
487App.activeDraftCommand = self
488self.featureName = "Arc_3Points"
489
490# Reset the values
491self.points = []
492self.normal = None
493self.tracker = trackers.arcTracker()
494self.tracker.autoinvert = False
495
496# Set up the working plane and launch the Snapper
497# with the indicated callbacks: one for when the user clicks
498# on the 3D view, and another for when the user moves the pointer.
499import WorkingPlane
500WorkingPlane.get_working_plane()
501
502Gui.Snapper.getPoint(callback=self.getPoint,
503movecallback=self.drawArc)
504Gui.Snapper.ui.sourceCmd = self
505Gui.Snapper.ui.setTitle(title=translate("draft", "Arc by 3 points"),
506icon="Draft_Arc_3Points")
507Gui.Snapper.ui.continueCmd.show()
508
509def getPoint(self, point, info):
510"""Get the point by clicking on the 3D view.
511
512Every time the user clicks on the 3D view this method is run.
513In this case, a point is appended to the list of points,
514and the tracker is updated.
515The object is finally created when three points are picked.
516
517Parameters
518----------
519point: Base::Vector
520The point selected in the 3D view.
521
522info: str
523Some information obtained about the point passed by the Snapper.
524"""
525# If there is not point, the command was cancelled
526# so the command exits.
527if not point:
528return None
529
530# Avoid adding the same point twice
531if point not in self.points:
532self.points.append(point)
533
534if len(self.points) < 3:
535# If one or two points were picked, set up again the Snapper
536# to get further points, but update the `last` property
537# with the last selected point.
538#
539# When two points are selected then we can turn on
540# the arc tracker to show the preview of the final curve.
541if len(self.points) == 2:
542self.tracker.on()
543Gui.Snapper.getPoint(last=self.points[-1],
544callback=self.getPoint,
545movecallback=self.drawArc)
546Gui.Snapper.ui.sourceCmd = self
547Gui.Snapper.ui.setTitle(title=translate("draft", "Arc by 3 points"),
548icon="Draft_Arc_3Points")
549Gui.Snapper.ui.continueCmd.show()
550
551else:
552# If three points were already picked in the 3D view
553# proceed with creating the final object.
554# Draw a simple `Part::Feature` if the parameter is `True`.
555Gui.addModule("Draft")
556_cmd = "Draft.make_arc_3points(["
557_cmd += "FreeCAD." + str(self.points[0])
558_cmd += ", FreeCAD." + str(self.points[1])
559_cmd += ", FreeCAD." + str(self.points[2])
560_cmd += "], primitive=" + str(params.get_param("UsePartPrimitives")) + ")"
561_cmd_list = ["circle = " + _cmd,
562"Draft.autogroup(circle)"]
563if params.get_param("UsePartPrimitives"):
564_cmd_list.append("Draft.select(circle)")
565_cmd_list.append("FreeCAD.ActiveDocument.recompute()")
566self.commit(translate("draft", "Create Arc by 3 points"), _cmd_list)
567self.finish(cont=None)
568
569def drawArc(self, point, info):
570"""Draw preview arc when we move the pointer in the 3D view.
571
572It uses the `gui_trackers.arcTracker.setBy3Points` method.
573
574Parameters
575----------
576point: Base::Vector
577The dynamic point passed by the callback
578as we move the pointer in the 3D view.
579
580info: str
581Some information obtained from the point by the Snapper.
582"""
583if len(self.points) == 2:
584if point.sub(self.points[1]).Length > 0.001:
585self.tracker.setBy3Points(self.points[0],
586self.points[1],
587point)
588
589def finish(self, cont=False):
590"""Terminate the operation.
591
592Parameters
593----------
594cont: bool or None, optional
595Restart (continue) the command if `True`, or if `None` and
596`ui.continueMode` is `True`.
597"""
598App.activeDraftCommand = None
599self.tracker.finalize()
600super().finish()
601if cont or (cont is None and Gui.Snapper.ui and Gui.Snapper.ui.continueMode):
602self.Activated()
603
604
605Draft_Arc_3Points = Arc_3Points
606Gui.addCommand('Draft_Arc_3Points', Arc_3Points())
607
608
609class ArcGroup:
610"""Gui Command group for the Arc tools."""
611
612def GetResources(self):
613"""Set icon, menu and tooltip."""
614return {'MenuText': QT_TRANSLATE_NOOP("Draft_ArcTools", "Arc tools"),
615'ToolTip': QT_TRANSLATE_NOOP("Draft_ArcTools", "Create various types of circular arcs.")}
616
617def GetCommands(self):
618"""Return a tuple of commands in the group."""
619return ('Draft_Arc', 'Draft_Arc_3Points')
620
621def IsActive(self):
622"""Return True when this command should be available.
623
624It is `True` when there is a document.
625"""
626if Gui.ActiveDocument:
627return True
628else:
629return False
630
631
632Gui.addCommand('Draft_ArcTools', ArcGroup())
633
634## @}
635