FreeCAD
483 строки · 24.0 Кб
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 stretch Draft objects.
26
27It works with rectangles, wires, b-splines, bezier curves, and sketches.
28It essentially moves the points that are located within a selection area,
29while keeping other points intact. This means the lines tied by the points
30that were moved are 'stretched'.
31"""
32## @package gui_stretch
33# \ingroup draftguitools
34# \brief Provides GUI tools to stretch Draft objects.
35
36## \addtogroup draftguitools
37# @{
38from PySide.QtCore import QT_TRANSLATE_NOOP
39
40import FreeCAD as App
41import FreeCADGui as Gui
42import Draft_rc
43import DraftVecUtils
44import draftutils.utils as utils
45import draftguitools.gui_base_original as gui_base_original
46import draftguitools.gui_tool_utils as gui_tool_utils
47import draftguitools.gui_trackers as trackers
48
49from draftutils.messages import _msg, _toolmsg
50from draftutils.translate import translate
51
52# The module is used to prevent complaints from code checkers (flake8)
53True if Draft_rc.__name__ else False
54
55
56class Stretch(gui_base_original.Modifier):
57"""Gui Command for the Stretch tool."""
58
59def GetResources(self):
60"""Set icon, menu and tooltip."""
61
62return {'Pixmap': 'Draft_Stretch',
63'Accel': "S, H",
64'MenuText': QT_TRANSLATE_NOOP("Draft_Stretch", "Stretch"),
65'ToolTip': QT_TRANSLATE_NOOP("Draft_Stretch", "Stretches the selected objects.\nSelect an object, then draw a rectangle to pick the vertices that will be stretched,\nthen draw a line to specify the distance and direction of stretching.")}
66
67def Activated(self):
68"""Execute when the command is called."""
69super().Activated(name="Stretch")
70self.rectracker = None
71self.nodetracker = None
72if self.ui:
73if not Gui.Selection.getSelection():
74self.ui.selectUi(on_close_call=self.finish)
75_msg(translate("draft", "Select an object to stretch"))
76self.call = \
77self.view.addEventCallback("SoEvent",
78gui_tool_utils.selectObject)
79else:
80self.proceed()
81
82def proceed(self):
83"""Proceed with execution of the command after proper selection."""
84if self.call:
85self.view.removeEventCallback("SoEvent", self.call)
86supported = ["Rectangle", "Wire", "BSpline", "BezCurve", "Sketch"]
87self.sel = []
88for obj in Gui.Selection.getSelection():
89if utils.getType(obj) in supported:
90self.sel.append([obj, App.Placement()])
91elif hasattr(obj, "Base"):
92if obj.Base:
93if utils.getType(obj.Base) in supported:
94self.sel.append([obj.Base, obj.Placement])
95elif utils.getType(obj.Base) in ["Offset2D", "Array"]:
96base = None
97if hasattr(obj.Base, "Source") and obj.Base.Source:
98base = obj.Base.Source
99elif hasattr(obj.Base, "Base") and obj.Base.Base:
100base = obj.Base.Base
101if base:
102if utils.getType(base) in supported:
103self.sel.append([base, obj.Placement.multiply(obj.Base.Placement)])
104elif hasattr(obj.Base, "Base"):
105if obj.Base.Base:
106if utils.getType(obj.Base.Base) in supported:
107self.sel.append([obj.Base.Base, obj.Placement.multiply(obj.Base.Placement)])
108elif utils.getType(obj) in ["Offset2D", "Array"]:
109base = None
110if hasattr(obj, "Source") and obj.Source:
111base = obj.Source
112elif hasattr(obj, "Base") and obj.Base:
113base = obj.Base
114if base:
115if utils.getType(base) in supported:
116self.sel.append([base, obj.Placement])
117if self.ui and self.sel:
118self.step = 1
119self.refpoint = None
120self.ui.pointUi(title=translate("draft", self.featureName), icon="Draft_Stretch")
121self.call = self.view.addEventCallback("SoEvent", self.action)
122self.rectracker = trackers.rectangleTracker(dotted=True,
123scolor=(0.0, 0.0, 1.0),
124swidth=2)
125self.nodetracker = []
126self.displacement = None
127_toolmsg(translate("draft", "Pick first point of selection rectangle"))
128
129def action(self, arg):
130"""Handle the 3D scene events.
131
132This is installed as an EventCallback in the Inventor view.
133
134Parameters
135----------
136arg: dict
137Dictionary with strings that indicates the type of event received
138from the 3D view.
139"""
140if arg["Type"] == "SoKeyboardEvent":
141if arg["Key"] == "ESCAPE":
142self.finish()
143elif arg["Type"] == "SoLocation2Event": # mouse movement detection
144point, ctrlPoint, info = gui_tool_utils.getPoint(self, arg)
145if self.step == 2:
146self.rectracker.update(point)
147gui_tool_utils.redraw3DView()
148elif arg["Type"] == "SoMouseButtonEvent":
149if arg["State"] == "DOWN" and arg["Button"] == "BUTTON1":
150if arg["Position"] == self.pos:
151# clicked twice on the same point
152self.finish()
153else:
154point, ctrlPoint, info = gui_tool_utils.getPoint(self, arg)
155self.addPoint(point)
156
157def addPoint(self, point):
158"""Add point to defined selection rectangle."""
159if self.step == 1:
160# first rctangle point
161_toolmsg(translate("draft", "Pick opposite point "
162"of selection rectangle"))
163self.ui.setRelative(-1)
164self.rectracker.setorigin(point)
165self.rectracker.on()
166if self.planetrack:
167self.planetrack.set(point)
168self.step = 2
169elif self.step == 2:
170# second rectangle point
171_toolmsg(translate("draft", "Pick start point of displacement"))
172self.ui.setRelative(-2)
173self.rectracker.off()
174nodes = []
175self.ops = []
176for sel in self.sel:
177o = sel[0]
178vispla = sel[1]
179tp = utils.getType(o)
180if tp in ["Wire", "BSpline", "BezCurve"]:
181np = []
182iso = False
183for p in o.Points:
184p = o.Placement.multVec(p)
185p = vispla.multVec(p)
186isi = self.rectracker.isInside(p)
187np.append(isi)
188if isi:
189iso = True
190nodes.append(p)
191if iso:
192self.ops.append([o, np])
193elif tp in ["Rectangle"]:
194p1 = App.Vector(0, 0, 0)
195p2 = App.Vector(o.Length.Value, 0, 0)
196p3 = App.Vector(o.Length.Value, o.Height.Value, 0)
197p4 = App.Vector(0, o.Height.Value, 0)
198np = []
199iso = False
200for p in [p1, p2, p3, p4]:
201p = o.Placement.multVec(p)
202p = vispla.multVec(p)
203isi = self.rectracker.isInside(p)
204np.append(isi)
205if isi:
206iso = True
207nodes.append(p)
208if iso:
209self.ops.append([o, np])
210elif tp in ["Sketch"]:
211np = []
212iso = False
213for p in o.Shape.Vertexes:
214p = vispla.multVec(p.Point)
215isi = self.rectracker.isInside(p)
216np.append(isi)
217if isi:
218iso = True
219nodes.append(p)
220if iso:
221self.ops.append([o, np])
222else:
223p = o.Placement.Base
224p = vispla.multVec(p)
225if self.rectracker.isInside(p):
226self.ops.append([o])
227nodes.append(p)
228for n in nodes:
229nt = trackers.editTracker(n, inactive=True)
230nt.on()
231self.nodetracker.append(nt)
232self.step = 3
233elif self.step == 3:
234# first point of displacement line
235_toolmsg(translate("draft", "Pick end point of displacement"))
236self.displacement = point
237# print("first point:", point)
238self.node = [point]
239self.step = 4
240elif self.step == 4:
241# print("second point:", point)
242self.displacement = point.sub(self.displacement)
243self.doStretch()
244if self.point:
245self.ui.redraw()
246
247def numericInput(self, numx, numy, numz):
248"""Validate the entry fields in the user interface.
249
250This function is called by the toolbar or taskpanel interface
251when valid x, y, and z have been entered in the input fields.
252"""
253point = App.Vector(numx, numy, numz)
254self.addPoint(point)
255
256def finish(self, cont=False):
257"""Terminate the operation of the command. and clean up."""
258self.end_callbacks(self.call)
259if self.rectracker:
260self.rectracker.finalize()
261if self.nodetracker:
262for n in self.nodetracker:
263n.finalize()
264super().finish()
265
266def doStretch(self):
267"""Do the actual stretching once the points are selected."""
268commitops = []
269if self.displacement:
270if self.displacement.Length > 0:
271_doc = "FreeCAD.ActiveDocument."
272# print("displacement: ", self.displacement)
273
274# TODO: break this section into individual functions
275# depending on the type of object (wire, curve, sketch,
276# rectangle, etc.) that is selected, and use variables
277# with common strings to avoid repeating
278# the same information every time, for example, the `_doc`
279# variable.
280# This is necessary to reduce the number of indentation levels
281# and make the code easier to read.
282for ops in self.ops:
283tp = utils.getType(ops[0])
284_rot = ops[0].Placement.Rotation
285localdisp = _rot.inverted().multVec(self.displacement)
286if tp in ["Wire", "BSpline", "BezCurve"]:
287pts = []
288for i in range(len(ops[1])):
289if ops[1][i] is False:
290pts.append(ops[0].Points[i])
291else:
292pts.append(ops[0].Points[i].add(localdisp))
293pts = str(pts).replace("Vector ", "FreeCAD.Vector")
294_cmd = _doc + ops[0].Name + ".Points=" + pts
295commitops.append(_cmd)
296elif tp in ["Sketch"]:
297baseverts = [ops[0].Shape.Vertexes[i].Point for i in range(len(ops[1])) if ops[1][i]]
298for i in range(ops[0].GeometryCount):
299j = 0
300while True:
301try:
302p = ops[0].getPoint(i, j)
303except ValueError:
304break
305else:
306p = ops[0].Placement.multVec(p)
307r = None
308for bv in baseverts:
309if DraftVecUtils.isNull(p.sub(bv)):
310_cmd = _doc
311_cmd += ops[0].Name
312_cmd += ".movePoint"
313_cmd += "("
314_cmd += str(i) + ", "
315_cmd += str(j) + ", "
316_cmd += "FreeCAD." + str(localdisp) + ", "
317_cmd += "True"
318_cmd += ")"
319commitops.append(_cmd)
320r = bv
321break
322if r:
323baseverts.remove(r)
324j += 1
325elif tp in ["Rectangle"]:
326p1 = App.Vector(0, 0, 0)
327p2 = App.Vector(ops[0].Length.Value, 0, 0)
328p3 = App.Vector(ops[0].Length.Value,
329ops[0].Height.Value,
3300)
331p4 = App.Vector(0, ops[0].Height.Value, 0)
332if ops[1] == [False, True, True, False]:
333optype = 1
334elif ops[1] == [False, False, True, True]:
335optype = 2
336elif ops[1] == [True, False, False, True]:
337optype = 3
338elif ops[1] == [True, True, False, False]:
339optype = 4
340else:
341optype = 0
342# print("length:", ops[0].Length,
343# "height:", ops[0].Height,
344# " - ", ops[1],
345# " - ", self.displacement)
346done = False
347if optype > 0:
348v1 = ops[0].Placement.multVec(p2).sub(ops[0].Placement.multVec(p1))
349a1 = round(self.displacement.getAngle(v1), 4)
350v2 = ops[0].Placement.multVec(p4).sub(ops[0].Placement.multVec(p1))
351a2 = round(self.displacement.getAngle(v2), 4)
352# check if the displacement is along one
353# of the rectangle directions
354if a1 == 0: # 0 degrees
355if optype == 1:
356if ops[0].Length.Value >= 0:
357d = ops[0].Length.Value + self.displacement.Length
358else:
359d = ops[0].Length.Value - self.displacement.Length
360_cmd = _doc
361_cmd += ops[0].Name + ".Length=" + str(d)
362commitops.append(_cmd)
363done = True
364elif optype == 3:
365if ops[0].Length.Value >= 0:
366d = ops[0].Length.Value - self.displacement.Length
367else:
368d = ops[0].Length.Value + self.displacement.Length
369_cmd = _doc + ops[0].Name
370_cmd += ".Length=" + str(d)
371_pl = _doc + ops[0].Name
372_pl += ".Placement.Base=FreeCAD."
373_pl += str(ops[0].Placement.Base.add(self.displacement))
374commitops.append(_cmd)
375commitops.append(_pl)
376done = True
377elif a1 == 3.1416: # pi radians, 180 degrees
378if optype == 1:
379if ops[0].Length.Value >= 0:
380d = ops[0].Length.Value - self.displacement.Length
381else:
382d = ops[0].Length.Value + self.displacement.Length
383_cmd = _doc + ops[0].Name
384_cmd += ".Length=" + str(d)
385commitops.append(_cmd)
386done = True
387elif optype == 3:
388if ops[0].Length.Value >= 0:
389d = ops[0].Length.Value + self.displacement.Length
390else:
391d = ops[0].Length.Value - self.displacement.Length
392_cmd = _doc + ops[0].Name
393_cmd += ".Length=" + str(d)
394_pl = _doc + ops[0].Name
395_pl += ".Placement.Base=FreeCAD."
396_pl += str(ops[0].Placement.Base.add(self.displacement))
397commitops.append(_cmd)
398commitops.append(_pl)
399done = True
400elif a2 == 0: # 0 degrees
401if optype == 2:
402if ops[0].Height.Value >= 0:
403d = ops[0].Height.Value + self.displacement.Length
404else:
405d = ops[0].Height.Value - self.displacement.Length
406_cmd = _doc + ops[0].Name
407_cmd += ".Height=" + str(d)
408commitops.append(_cmd)
409done = True
410elif optype == 4:
411if ops[0].Height.Value >= 0:
412d = ops[0].Height.Value - self.displacement.Length
413else:
414d = ops[0].Height.Value + self.displacement.Length
415_cmd = _doc + ops[0].Name
416_cmd += ".Height=" + str(d)
417_pl = _doc + ops[0].Name
418_pl += ".Placement.Base=FreeCAD."
419_pl += str(ops[0].Placement.Base.add(self.displacement))
420commitops.append(_cmd)
421commitops.append(_pl)
422done = True
423elif a2 == 3.1416: # pi radians, 180 degrees
424if optype == 2:
425if ops[0].Height.Value >= 0:
426d = ops[0].Height.Value - self.displacement.Length
427else:
428d = ops[0].Height.Value + self.displacement.Length
429_cmd = _doc + ops[0].Name
430_cmd += ".Height=" + str(d)
431commitops.append(_cmd)
432done = True
433elif optype == 4:
434if ops[0].Height.Value >= 0:
435d = ops[0].Height.Value + self.displacement.Length
436else:
437d = ops[0].Height.Value - self.displacement.Length
438_cmd = _doc + ops[0].Name
439_cmd += ".Height=" + str(d)
440_pl = _doc + ops[0].Name
441_pl += ".Placement.Base=FreeCAD."
442_pl += str(ops[0].Placement.Base.add(self.displacement))
443commitops.append(_cmd)
444commitops.append(_pl)
445done = True
446if not done:
447# otherwise create a wire copy and stretch it instead
448_msg(translate("draft", "Turning one Rectangle into a Wire"))
449pts = []
450vts = ops[0].Shape.Vertexes
451for i in range(4):
452if ops[1][i] == False:
453pts.append(vts[i].Point)
454else:
455pts.append(vts[i].Point.add(self.displacement))
456pts = str(pts).replace("Vector ", "FreeCAD.Vector")
457_cmd = "Draft.make_wire"
458_cmd += "(" + pts + ", closed=True, "
459_cmd += "face=" + str(ops[0].MakeFace)
460_cmd += ")"
461_format = "Draft.formatObject"
462_format += "(w, "
463_format += _doc + ops[0].Name
464_format += ")"
465_hide = _doc + ops[0].Name + ".ViewObject.hide()"
466commitops.append("w = " + _cmd)
467commitops.append(_format)
468commitops.append(_hide)
469else:
470_pl = _doc + ops[0].Name
471_pl += ".Placement.Base=FreeCAD."
472_pl += str(ops[0].Placement.Base.add(self.displacement))
473commitops.append(_pl)
474if commitops:
475commitops.append("FreeCAD.ActiveDocument.recompute()")
476Gui.addModule("Draft")
477self.commit(translate("draft", "Stretch"), commitops)
478self.finish()
479
480
481Gui.addCommand('Draft_Stretch', Stretch())
482
483## @}
484