FreeCAD
426 строк · 17.7 Кб
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 rotate objects in the 3D space."""
26## @package gui_rotate
27# \ingroup draftguitools
28# \brief Provides GUI tools to rotate objects in the 3D space.
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_rc
38import DraftVecUtils
39import draftutils.groups as groups
40import draftutils.todo as todo
41import draftguitools.gui_base_original as gui_base_original
42import draftguitools.gui_tool_utils as gui_tool_utils
43import draftguitools.gui_trackers as trackers
44
45from FreeCAD import Units as U
46from draftutils.messages import _msg, _err, _toolmsg
47from draftutils.translate import translate
48
49# The module is used to prevent complaints from code checkers (flake8)
50True if Draft_rc.__name__ else False
51
52
53class Rotate(gui_base_original.Modifier):
54"""Gui Command for the Rotate tool."""
55
56def GetResources(self):
57"""Set icon, menu and tooltip."""
58_tip = ()
59
60return {'Pixmap': 'Draft_Rotate',
61'Accel': "R, O",
62'MenuText': QT_TRANSLATE_NOOP("Draft_Rotate", "Rotate"),
63'ToolTip': QT_TRANSLATE_NOOP("Draft_Rotate", "Rotates the selected objects. Choose the center of rotation, then the initial angle, and then the final angle.\nIf the \"copy\" option is active, it will create rotated copies.\nCTRL to snap, SHIFT to constrain. Hold ALT and click to create a copy with each click.")}
64
65def Activated(self):
66"""Execute when the command is called."""
67super().Activated(name="Rotate")
68if not self.ui:
69return
70self.ghosts = []
71self.arctrack = None
72self.get_object_selection()
73
74def get_object_selection(self):
75"""Get the object selection."""
76if Gui.Selection.getSelection():
77return self.proceed()
78self.ui.selectUi(on_close_call=self.finish)
79_msg(translate("draft", "Select an object to rotate"))
80self.call = \
81self.view.addEventCallback("SoEvent", gui_tool_utils.selectObject)
82
83def proceed(self):
84"""Continue with the command after a selection has been made."""
85if self.call:
86self.view.removeEventCallback("SoEvent", self.call)
87self.selected_objects = Gui.Selection.getSelection()
88self.selected_objects = \
89groups.get_group_contents(self.selected_objects,
90addgroups=True,
91spaces=True,
92noarchchild=True)
93self.selected_subelements = Gui.Selection.getSelectionEx()
94self.step = 0
95self.center = None
96self.ui.rotateSetCenterUi()
97self.arctrack = trackers.arcTracker()
98self.call = self.view.addEventCallback("SoEvent", self.action)
99_toolmsg(translate("draft", "Pick rotation center"))
100
101def action(self, arg):
102"""Handle the 3D scene events.
103
104This is installed as an EventCallback in the Inventor view.
105
106Parameters
107----------
108arg: dict
109Dictionary with strings that indicates the type of event received
110from the 3D view.
111"""
112if arg["Type"] == "SoKeyboardEvent" and arg["Key"] == "ESCAPE":
113self.finish()
114elif arg["Type"] == "SoLocation2Event":
115self.handle_mouse_move_event(arg)
116elif (arg["Type"] == "SoMouseButtonEvent"
117and arg["State"] == "DOWN"
118and arg["Button"] == "BUTTON1"):
119self.handle_mouse_click_event(arg)
120
121def handle_mouse_move_event(self, arg):
122"""Handle the mouse when moving."""
123for ghost in self.ghosts:
124ghost.off()
125self.point, ctrlPoint, info = gui_tool_utils.getPoint(self, arg)
126# this is to make sure radius is what you see on screen
127if self.center and DraftVecUtils.dist(self.point, self.center):
128viewdelta = DraftVecUtils.project(self.point.sub(self.center),
129self.wp.axis)
130if not DraftVecUtils.isNull(viewdelta):
131self.point = self.point.add(viewdelta.negative())
132if self.extendedCopy:
133if not gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key()):
134self.step = 3
135self.finish()
136if self.step == 0:
137pass
138elif self.step == 1:
139currentrad = DraftVecUtils.dist(self.point, self.center)
140if currentrad != 0:
141angle = DraftVecUtils.angle(self.wp.u,
142self.point.sub(self.center),
143self.wp.axis)
144else:
145angle = 0
146self.ui.setRadiusValue(math.degrees(angle), unit="Angle")
147self.firstangle = angle
148self.ui.radiusValue.setFocus()
149self.ui.radiusValue.selectAll()
150elif self.step == 2:
151currentrad = DraftVecUtils.dist(self.point, self.center)
152if currentrad != 0:
153angle = DraftVecUtils.angle(self.wp.u,
154self.point.sub(self.center),
155self.wp.axis)
156else:
157angle = 0
158if angle < self.firstangle:
159sweep = (2 * math.pi - self.firstangle) + angle
160else:
161sweep = angle - self.firstangle
162self.arctrack.setApertureAngle(sweep)
163for ghost in self.ghosts:
164ghost.rotate(self.wp.axis, sweep)
165ghost.on()
166self.ui.setRadiusValue(math.degrees(sweep), 'Angle')
167self.ui.radiusValue.setFocus()
168self.ui.radiusValue.selectAll()
169gui_tool_utils.redraw3DView()
170
171def handle_mouse_click_event(self, arg):
172"""Handle the mouse when the first button is clicked."""
173if not self.point:
174return
175if self.step == 0:
176self.set_center()
177elif self.step == 1:
178self.set_start_point()
179else:
180self.set_rotation_angle(arg)
181
182def set_center(self):
183"""Set the center of the rotation."""
184if not self.ghosts:
185self.set_ghosts()
186self.center = self.point
187self.node = [self.point]
188self.ui.radiusUi()
189self.ui.radiusValue.setText(U.Quantity(0, U.Angle).UserString)
190self.ui.hasFill.hide()
191self.ui.labelRadius.setText(translate("draft", "Base angle"))
192self.ui.radiusValue.setToolTip(translate("draft", "The base angle you wish to start the rotation from"))
193self.arctrack.setCenter(self.center)
194for ghost in self.ghosts:
195ghost.center(self.center)
196self.step = 1
197_toolmsg(translate("draft", "Pick base angle"))
198if self.planetrack:
199self.planetrack.set(self.point)
200
201def set_start_point(self):
202"""Set the starting point of the rotation."""
203self.ui.labelRadius.setText(translate("draft", "Rotation"))
204self.ui.radiusValue.setToolTip(translate("draft", "The amount of rotation you wish to perform.\nThe final angle will be the base angle plus this amount."))
205self.rad = DraftVecUtils.dist(self.point, self.center)
206self.arctrack.on()
207self.arctrack.setStartPoint(self.point)
208for ghost in self.ghosts:
209ghost.on()
210self.step = 2
211_toolmsg(translate("draft", "Pick rotation angle"))
212
213def set_rotation_angle(self, arg):
214"""Set the rotation angle."""
215
216# currentrad = DraftVecUtils.dist(self.point, self.center)
217angle = self.point.sub(self.center).getAngle(self.wp.u)
218_v = DraftVecUtils.project(self.point.sub(self.center), self.wp.v)
219if _v.getAngle(self.wp.v) > 1:
220angle = -angle
221if angle < self.firstangle:
222self.angle = (2 * math.pi - self.firstangle) + angle
223else:
224self.angle = angle - self.firstangle
225self.rotate(self.ui.isCopy.isChecked()
226or gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key()))
227if gui_tool_utils.hasMod(arg, gui_tool_utils.get_mod_alt_key()):
228self.extendedCopy = True
229else:
230self.finish(cont=None)
231
232def set_ghosts(self):
233"""Set the ghost to display."""
234for ghost in self.ghosts:
235ghost.remove()
236if self.ui.isSubelementMode.isChecked():
237self.ghosts = self.get_subelement_ghosts()
238else:
239self.ghosts = [trackers.ghostTracker(self.selected_objects)]
240if self.center:
241for ghost in self.ghosts:
242ghost.center(self.center)
243
244def get_subelement_ghosts(self):
245"""Get ghost for the subelements (vertices, edges)."""
246import Part
247
248ghosts = []
249for sel in Gui.Selection.getSelectionEx("", 0):
250for sub in sel.SubElementNames if sel.SubElementNames else [""]:
251if "Vertex" in sub or "Edge" in sub:
252shape = Part.getShape(sel.Object, sub, needSubElement=True, retType=0)
253ghosts.append(trackers.ghostTracker(shape))
254return ghosts
255
256def finish(self, cont=False):
257"""Terminate the operation.
258
259Parameters
260----------
261cont: bool or None, optional
262Restart (continue) the command if `True`, or if `None` and
263`ui.continueMode` is `True`.
264"""
265self.end_callbacks(self.call)
266if self.arctrack:
267self.arctrack.finalize()
268for ghost in self.ghosts:
269ghost.finalize()
270super().finish()
271if cont or (cont is None and self.ui and self.ui.continueMode):
272todo.ToDo.delayAfter(self.Activated, [])
273
274def rotate(self, is_copy=False):
275"""Perform the rotation of the subelements or the entire object."""
276if self.ui.isSubelementMode.isChecked():
277self.rotate_subelements(is_copy)
278else:
279self.rotate_object(is_copy)
280
281def rotate_subelements(self, is_copy):
282"""Rotate the subelements."""
283Gui.addModule("Draft")
284try:
285if is_copy:
286self.commit(translate("draft", "Copy"),
287self.build_copy_subelements_command())
288else:
289self.commit(translate("draft", "Rotate"),
290self.build_rotate_subelements_command())
291except Exception:
292_err(translate("draft", "Some subelements could not be moved."))
293
294def build_copy_subelements_command(self):
295"""Build the string to commit to copy the subelements."""
296import Part
297
298command = []
299arguments = []
300E = len("Edge")
301for obj in self.selected_subelements:
302for index, subelement in enumerate(obj.SubObjects):
303if not isinstance(subelement, Part.Edge):
304continue
305_edge_index = int(obj.SubElementNames[index][E:]) - 1
306_cmd = '['
307_cmd += 'FreeCAD.ActiveDocument.'
308_cmd += obj.ObjectName + ', '
309_cmd += str(_edge_index) + ', '
310_cmd += str(math.degrees(self.angle)) + ', '
311_cmd += DraftVecUtils.toString(self.center) + ', '
312_cmd += DraftVecUtils.toString(self.wp.axis)
313_cmd += ']'
314arguments.append(_cmd)
315
316all_args = ', '.join(arguments)
317command.append('Draft.copy_rotated_edges([' + all_args + '])')
318command.append('FreeCAD.ActiveDocument.recompute()')
319return command
320
321def build_rotate_subelements_command(self):
322"""Build the string to commit to rotate the subelements."""
323import Part
324
325command = []
326V = len("Vertex")
327E = len("Edge")
328for obj in self.selected_subelements:
329for index, subelement in enumerate(obj.SubObjects):
330if isinstance(subelement, Part.Vertex):
331_vertex_index = int(obj.SubElementNames[index][V:]) - 1
332_cmd = 'Draft.rotate_vertex'
333_cmd += '('
334_cmd += 'FreeCAD.ActiveDocument.'
335_cmd += obj.ObjectName + ', '
336_cmd += str(_vertex_index) + ', '
337_cmd += str(math.degrees(self.angle)) + ', '
338_cmd += DraftVecUtils.toString(self.center) + ', '
339_cmd += DraftVecUtils.toString(self.wp.axis)
340_cmd += ')'
341command.append(_cmd)
342elif isinstance(subelement, Part.Edge):
343_edge_index = int(obj.SubElementNames[index][E:]) - 1
344_cmd = 'Draft.rotate_edge'
345_cmd += '('
346_cmd += 'FreeCAD.ActiveDocument.'
347_cmd += obj.ObjectName + ', '
348_cmd += str(_edge_index) + ', '
349_cmd += str(math.degrees(self.angle)) + ', '
350_cmd += DraftVecUtils.toString(self.center) + ', '
351_cmd += DraftVecUtils.toString(self.wp.axis)
352_cmd += ')'
353command.append(_cmd)
354command.append('FreeCAD.ActiveDocument.recompute()')
355return command
356
357def rotate_object(self, is_copy):
358"""Move the object."""
359_doc = 'FreeCAD.ActiveDocument.'
360_selected = self.selected_objects
361
362objects = '['
363objects += ','.join([_doc + obj.Name for obj in _selected])
364objects += ']'
365
366_cmd = 'Draft.rotate'
367_cmd += '('
368_cmd += objects + ', '
369_cmd += str(math.degrees(self.angle)) + ', '
370_cmd += DraftVecUtils.toString(self.center) + ', '
371_cmd += 'axis=' + DraftVecUtils.toString(self.wp.axis) + ', '
372_cmd += 'copy=' + str(is_copy)
373_cmd += ')'
374_cmd_list = [_cmd,
375'FreeCAD.ActiveDocument.recompute()']
376
377_mode = "Copy" if is_copy else "Rotate"
378Gui.addModule("Draft")
379self.commit(translate("draft", _mode),
380_cmd_list)
381
382def numericInput(self, numx, numy, numz):
383"""Validate the entry fields in the user interface.
384
385This function is called by the toolbar or taskpanel interface
386when valid x, y, and z have been entered in the input fields.
387"""
388self.center = App.Vector(numx, numy, numz)
389self.node = [self.center]
390self.arctrack.setCenter(self.center)
391for ghost in self.ghosts:
392ghost.center(self.center)
393self.ui.radiusUi()
394self.ui.hasFill.hide()
395self.ui.labelRadius.setText(translate("draft", "Base angle"))
396self.ui.radiusValue.setToolTip(translate("draft", "The base angle you wish to start the rotation from"))
397self.ui.radiusValue.setText(U.Quantity(0, U.Angle).UserString)
398self.step = 1
399_toolmsg(translate("draft", "Pick base angle"))
400
401def numericRadius(self, rad):
402"""Validate the radius entry field in the user interface.
403
404This function is called by the toolbar or taskpanel interface
405when a valid radius has been entered in the input field.
406"""
407if self.step == 1:
408self.ui.labelRadius.setText(translate("draft", "Rotation"))
409self.ui.radiusValue.setToolTip(translate("draft", "The amount of rotation you wish to perform.\nThe final angle will be the base angle plus this amount."))
410self.ui.radiusValue.setText(U.Quantity(0, U.Angle).UserString)
411self.firstangle = math.radians(rad)
412self.arctrack.setStartAngle(self.firstangle)
413self.arctrack.on()
414for ghost in self.ghosts:
415ghost.on()
416self.step = 2
417_toolmsg(translate("draft", "Pick rotation angle"))
418else:
419self.angle = math.radians(rad)
420self.rotate(self.ui.isCopy.isChecked())
421self.finish(cont=None)
422
423
424Gui.addCommand('Draft_Rotate', Rotate())
425
426## @}
427