diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 00a65d4f64..04edf400b6 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -112,6 +112,7 @@ SET(Modifier_tools draftguitools/gui_upgrade.py draftguitools/gui_downgrade.py draftguitools/gui_trimex.py + draftguitools/gui_scale.py ) SET(Draft_GUI_tools diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 92123371c7..80985b4656 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -94,7 +94,6 @@ from draftguitools.gui_lineslope import Draft_Slope import draftguitools.gui_arrays import draftguitools.gui_annotationstyleeditor # import DraftFillet -import drafttaskpanels.task_scale as task_scale # --------------------------------------------------------------------------- # Preflight stuff @@ -184,240 +183,7 @@ from draftguitools.gui_split import Split from draftguitools.gui_upgrade import Upgrade from draftguitools.gui_downgrade import Downgrade from draftguitools.gui_trimex import Trimex - - -class Scale(Modifier): - '''The Draft_Scale FreeCAD command definition. - This tool scales the selected objects from a base point.''' - - def GetResources(self): - return {'Pixmap' : 'Draft_Scale', - 'Accel' : "S, C", - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_Scale", "Scale"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_Scale", "Scales the selected objects from a base point. CTRL to snap, SHIFT to constrain, ALT to copy")} - - def Activated(self): - self.name = translate("draft","Scale", utf8_decode=True) - Modifier.Activated(self,self.name) - if not self.ui: - return - self.ghosts = [] - self.get_object_selection() - - def get_object_selection(self): - if FreeCADGui.Selection.getSelection(): - return self.proceed() - self.ui.selectUi() - FreeCAD.Console.PrintMessage(translate("draft", "Select an object to scale")+"\n") - self.call = self.view.addEventCallback("SoEvent",selectObject) - - def proceed(self): - if self.call: - self.view.removeEventCallback("SoEvent", self.call) - self.selected_objects = FreeCADGui.Selection.getSelection() - self.selected_objects = Draft.getGroupContents(self.selected_objects) - self.selected_subelements = FreeCADGui.Selection.getSelectionEx() - self.refs = [] - self.ui.pointUi(self.name) - self.ui.modUi() - self.ui.xValue.setFocus() - self.ui.xValue.selectAll() - self.pickmode = False - self.task = None - self.call = self.view.addEventCallback("SoEvent", self.action) - FreeCAD.Console.PrintMessage(translate("draft", "Pick base point")+"\n") - - def set_ghosts(self): - if self.ui.isSubelementMode.isChecked(): - return self.set_subelement_ghosts() - self.ghosts = [trackers.ghostTracker(self.selected_objects)] - - def set_subelement_ghosts(self): - import Part - for object in self.selected_subelements: - for subelement in object.SubObjects: - if isinstance(subelement, Part.Vertex) \ - or isinstance(subelement, Part.Edge): - self.ghosts.append(trackers.ghostTracker(subelement)) - - def pickRef(self): - self.pickmode = True - if self.node: - self.node = self.node[:1] # remove previous picks - FreeCAD.Console.PrintMessage(translate("draft", "Pick reference distance from base point")+"\n") - self.call = self.view.addEventCallback("SoEvent",self.action) - - def action(self,arg): - """scene event handler""" - if arg["Type"] == "SoKeyboardEvent" and arg["Key"] == "ESCAPE": - self.finish() - elif arg["Type"] == "SoLocation2Event": - self.handle_mouse_move_event(arg) - elif arg["Type"] == "SoMouseButtonEvent" \ - and arg["State"] == "DOWN" \ - and (arg["Button"] == "BUTTON1") \ - and self.point: - self.handle_mouse_click_event() - - def handle_mouse_move_event(self, arg): - for ghost in self.ghosts: - ghost.off() - self.point, ctrlPoint, info = getPoint(self, arg, sym=True) - - def handle_mouse_click_event(self): - if not self.ghosts: - self.set_ghosts() - self.numericInput(self.point.x, self.point.y, self.point.z) - - def scale(self): - self.delta = Vector(self.task.xValue.value(), self.task.yValue.value(), self.task.zValue.value()) - self.center = self.node[0] - if self.task.isSubelementMode.isChecked(): - self.scale_subelements() - elif self.task.isClone.isChecked(): - self.scale_with_clone() - else: - self.scale_object() - self.finish() - - def scale_subelements(self): - try: - if self.task.isCopy.isChecked(): - self.commit(translate("draft", "Copy"), self.build_copy_subelements_command()) - else: - self.commit(translate("draft", "Scale"), self.build_scale_subelements_command()) - except: - FreeCAD.Console.PrintError(translate("draft", "Some subelements could not be scaled.")) - - def scale_with_clone(self): - if self.task.relative.isChecked(): - self.delta = FreeCAD.DraftWorkingPlane.getGlobalCoords(self.delta) - objects = '[' + ','.join(['FreeCAD.ActiveDocument.' + object.Name for object in self.selected_objects]) + ']' - FreeCADGui.addModule("Draft") - self.commit(translate("draft","Copy") if self.task.isCopy.isChecked() else translate("draft","Scale"), - ['clone = Draft.clone('+objects+',forcedraft=True)', - 'clone.Scale = '+DraftVecUtils.toString(self.delta), - 'FreeCAD.ActiveDocument.recompute()']) - - def build_copy_subelements_command(self): - import Part - command = [] - arguments = [] - for object in self.selected_subelements: - for index, subelement in enumerate(object.SubObjects): - if not isinstance(subelement, Part.Edge): - continue - arguments.append('[FreeCAD.ActiveDocument.{}, {}, {}, {}]'.format( - object.ObjectName, - int(object.SubElementNames[index][len("Edge"):])-1, - DraftVecUtils.toString(self.delta), - DraftVecUtils.toString(self.center))) - command.append('Draft.copyScaledEdges([{}])'.format(','.join(arguments))) - command.append('FreeCAD.ActiveDocument.recompute()') - return command - - def build_scale_subelements_command(self): - import Part - command = [] - for object in self.selected_subelements: - for index, subelement in enumerate(object.SubObjects): - if isinstance(subelement, Part.Vertex): - command.append('Draft.scaleVertex(FreeCAD.ActiveDocument.{}, {}, {}, {})'.format( - object.ObjectName, - int(object.SubElementNames[index][len("Vertex"):])-1, - DraftVecUtils.toString(self.delta), - DraftVecUtils.toString(self.center))) - elif isinstance(subelement, Part.Edge): - command.append('Draft.scaleEdge(FreeCAD.ActiveDocument.{}, {}, {}, {})'.format( - object.ObjectName, - int(object.SubElementNames[index][len("Edge"):])-1, - DraftVecUtils.toString(self.delta), - DraftVecUtils.toString(self.center))) - command.append('FreeCAD.ActiveDocument.recompute()') - return command - - def is_scalable(self,obj): - t = Draft.getType(obj) - if t in ["Rectangle","Wire","Annotation","BSpline"]: - # TODO - support more types in Draft.scale - return True - else: - return False - - def scale_object(self): - if self.task.relative.isChecked(): - self.delta = FreeCAD.DraftWorkingPlane.getGlobalCoords(self.delta) - goods = [] - bads = [] - for obj in self.selected_objects: - if self.is_scalable(obj): - goods.append(obj) - else: - bads.append(obj) - if bads: - if len(bads) == 1: - m = translate("draft","Unable to scale object")+": "+bads[0].Label - else: - m = translate("draft","Unable to scale objects")+": "+",".join([o.Label for o in bads]) - m += " - "+translate("draft","This object type cannot be scaled directly. Please use the clone method.")+"\n" - FreeCAD.Console.PrintError(m) - if goods: - objects = '[' + ','.join(['FreeCAD.ActiveDocument.' + obj.Name for obj in goods]) + ']' - FreeCADGui.addModule("Draft") - self.commit(translate("draft","Copy" if self.task.isCopy.isChecked() else "Scale"), - ['Draft.scale('+objects+',scale='+DraftVecUtils.toString(self.delta)+',center='+DraftVecUtils.toString(self.center)+',copy='+str(self.task.isCopy.isChecked())+')', - 'FreeCAD.ActiveDocument.recompute()']) - - def scaleGhost(self,x,y,z,rel): - delta = Vector(x,y,z) - if rel: - delta = FreeCAD.DraftWorkingPlane.getGlobalCoords(delta) - for ghost in self.ghosts: - ghost.scale(delta) - # calculate a correction factor depending on the scaling center - corr = Vector(self.node[0].x,self.node[0].y,self.node[0].z) - corr.scale(delta.x,delta.y,delta.z) - corr = (corr.sub(self.node[0])).negative() - for ghost in self.ghosts: - ghost.move(corr) - ghost.on() - - def numericInput(self,numx,numy,numz): - """this function gets called by the toolbar when a valid base point has been entered""" - self.point = Vector(numx,numy,numz) - self.node.append(self.point) - if not self.pickmode: - if not self.ghosts: - self.set_ghosts() - self.ui.offUi() - if self.call: - self.view.removeEventCallback("SoEvent",self.call) - self.task = task_scale.ScaleTaskPanel() - self.task.sourceCmd = self - ToDo.delay(FreeCADGui.Control.showDialog, self.task) - ToDo.delay(self.task.xValue.selectAll, None) - ToDo.delay(self.task.xValue.setFocus, None) - for ghost in self.ghosts: - ghost.on() - elif len(self.node) == 2: - FreeCAD.Console.PrintMessage(translate("draft", "Pick new distance from base point")+"\n") - elif len(self.node) == 3: - if hasattr(FreeCADGui,"Snapper"): - FreeCADGui.Snapper.off() - if self.call: - self.view.removeEventCallback("SoEvent",self.call) - d1 = (self.node[1].sub(self.node[0])).Length - d2 = (self.node[2].sub(self.node[0])).Length - #print d2,"/",d1,"=",d2/d1 - if hasattr(self,"task"): - if self.task: - self.task.lock.setChecked(True) - self.task.setValue(d2/d1) - - def finish(self,closed=False,cont=False): - Modifier.finish(self) - for ghost in self.ghosts: - ghost.finalize() +from draftguitools.gui_scale import Scale class Drawing(Modifier): @@ -975,7 +741,6 @@ from draftguitools.gui_snaps import ShowSnapBar # drawing commands # modification commands -FreeCADGui.addCommand('Draft_Scale',Scale()) FreeCADGui.addCommand('Draft_Drawing',Drawing()) FreeCADGui.addCommand('Draft_WireToBSpline',WireToBSpline()) FreeCADGui.addCommand('Draft_Draft2Sketch',Draft2Sketch()) diff --git a/src/Mod/Draft/draftguitools/gui_scale.py b/src/Mod/Draft/draftguitools/gui_scale.py new file mode 100644 index 0000000000..9540544f68 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_scale.py @@ -0,0 +1,407 @@ +# *************************************************************************** +# * (c) 2009, 2010 Yorik van Havre * +# * (c) 2009, 2010 Ken Cline * +# * (c) 2020 Eliud Cabrera Castillo * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +"""Provides tools for scaling objects with the Draft Workbench. + +The scale operation can also be done with subelements. + +The subelements operations only really work with polylines (Wires) +because internally the functions `scaleVertex` and `scaleEdge` +only work with polylines that have a `Points` property. +""" +## @package gui_scale +# \ingroup DRAFT +# \brief Provides tools for scaling objects with the Draft Workbench. + +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc +import DraftVecUtils +import draftutils.utils as utils +import draftutils.todo as todo +import draftguitools.gui_base_original as gui_base_original +import draftguitools.gui_tool_utils as gui_tool_utils +import draftguitools.gui_trackers as trackers +import drafttaskpanels.task_scale as task_scale +from draftutils.messages import _msg, _err +from draftutils.translate import translate + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class Scale(gui_base_original.Modifier): + """Gui Command for the Scale tool. + + This tool scales the selected objects from a base point. + """ + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = ("Scales the selected objects from a base point.\n" + "CTRL to snap, SHIFT to constrain, ALT to copy.") + + return {'Pixmap': 'Draft_Scale', + 'Accel': "S, C", + 'MenuText': QT_TRANSLATE_NOOP("Draft_Scale", "Scale"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_Scale", _tip)} + + def Activated(self): + """Execute when the command is called.""" + self.name = translate("draft", "Scale") + super().Activated(name=self.name) + if not self.ui: + return + self.ghosts = [] + self.get_object_selection() + + def get_object_selection(self): + """Get object selection and proceed if succesful.""" + if Gui.Selection.getSelection(): + return self.proceed() + self.ui.selectUi() + _msg(translate("draft", "Select an object to scale")) + self.call = self.view.addEventCallback("SoEvent", + gui_tool_utils.selectObject) + + def proceed(self): + """Proceed with execution of the command after selection.""" + if self.call: + self.view.removeEventCallback("SoEvent", self.call) + + self.selected_objects = Gui.Selection.getSelection() + self.selected_objects = utils.getGroupContents(self.selected_objects) + self.selected_subelements = Gui.Selection.getSelectionEx() + self.refs = [] + self.ui.pointUi(self.name) + self.ui.modUi() + self.ui.xValue.setFocus() + self.ui.xValue.selectAll() + self.pickmode = False + self.task = None + self.call = self.view.addEventCallback("SoEvent", self.action) + _msg(translate("draft", "Pick base point")) + + def set_ghosts(self): + """Set the previews of the objects to scale.""" + if self.ui.isSubelementMode.isChecked(): + return self.set_subelement_ghosts() + self.ghosts = [trackers.ghostTracker(self.selected_objects)] + + def set_subelement_ghosts(self): + """Set the previews of the subelements from an object to scale.""" + import Part + + for object in self.selected_subelements: + for subelement in object.SubObjects: + if isinstance(subelement, (Part.Vertex, Part.Edge)): + self.ghosts.append(trackers.ghostTracker(subelement)) + + def pickRef(self): + """Pick a point of reference.""" + self.pickmode = True + if self.node: + self.node = self.node[:1] # remove previous picks + _msg(translate("draft", "Pick reference distance from base point")) + self.call = self.view.addEventCallback("SoEvent", self.action) + + def action(self, arg): + """Handle the 3D scene events. + + This is installed as an EventCallback in the Inventor view. + + Parameters + ---------- + arg: dict + Dictionary with strings that indicates the type of event received + from the 3D view. + """ + if arg["Type"] == "SoKeyboardEvent" and arg["Key"] == "ESCAPE": + self.finish() + elif arg["Type"] == "SoLocation2Event": + self.handle_mouse_move_event(arg) + elif (arg["Type"] == "SoMouseButtonEvent" + and arg["State"] == "DOWN" + and arg["Button"] == "BUTTON1" + and self.point): + self.handle_mouse_click_event() + + def handle_mouse_move_event(self, arg): + """Handle the mouse event of movement.""" + for ghost in self.ghosts: + ghost.off() + (self.point, + ctrlPoint, info) = gui_tool_utils.getPoint(self, arg, sym=True) + + def handle_mouse_click_event(self): + """Handle the mouse click event.""" + if not self.ghosts: + self.set_ghosts() + self.numericInput(self.point.x, self.point.y, self.point.z) + + def scale(self): + """Perform the scale of the object. + + Scales the subelements, or with a clone, or just general scaling. + """ + self.delta = App.Vector(self.task.xValue.value(), + self.task.yValue.value(), + self.task.zValue.value()) + self.center = self.node[0] + if self.task.isSubelementMode.isChecked(): + self.scale_subelements() + elif self.task.isClone.isChecked(): + self.scale_with_clone() + else: + self.scale_object() + self.finish() + + def scale_subelements(self): + """Scale only the subelements if the appropriate option is set. + + The subelements operations only really work with polylines (Wires) + because internally the functions `scaleVertex` and `scaleEdge` + only work with polylines that have a `Points` property. + + BUG: the code should not cause an error. It should check that + the selected object is not a rectangle or another object + that can't be used with `scaleVertex` and `scaleEdge`. + """ + try: + if self.task.isCopy.isChecked(): + self.commit(translate("draft", "Copy"), + self.build_copy_subelements_command()) + else: + self.commit(translate("draft", "Scale"), + self.build_scale_subelements_command()) + except Exception: + _err(translate("draft", "Some subelements could not be scaled.")) + + def scale_with_clone(self): + """Scale with clone.""" + if self.task.relative.isChecked(): + self.delta = App.DraftWorkingPlane.getGlobalCoords(self.delta) + + Gui.addModule("Draft") + + _doc = 'FreeCAD.ActiveDocument.' + _selected = self.selected_objects + + objects = '[' + objects += ', '.join([_doc + obj.Name for obj in _selected]) + objects += ']' + + if self.task.isCopy.isChecked(): + _cmd_name = translate("draft", "Copy") + else: + _cmd_name = translate("draft", "Scale") + + _cmd = 'Draft.clone' + _cmd += '(' + _cmd += objects + ', ' + _cmd += 'forcedraft=True' + _cmd += ')' + _cmd_list = ['clone = ' + _cmd, + 'clone.Scale = ' + DraftVecUtils.toString(self.delta), + 'FreeCAD.ActiveDocument.recompute()'] + self.commit(_cmd_name, _cmd_list) + + def build_copy_subelements_command(self): + """Build the string to commit to copy the subelements.""" + import Part + + command = [] + arguments = [] + E = len("Edge") + for obj in self.selected_subelements: + for index, subelement in enumerate(obj.SubObjects): + if not isinstance(subelement, Part.Edge): + continue + _edge_index = int(obj.SubElementNames[index][E:]) - 1 + _cmd = '[' + _cmd += 'FreeCAD.ActiveDocument.' + _cmd += obj.ObjectName + ', ' + _cmd += str(_edge_index) + ', ' + _cmd += DraftVecUtils.toString(self.delta) + ', ' + _cmd += DraftVecUtils.toString(self.center) + _cmd += ']' + arguments.append(_cmd) + all_args = ', '.join(arguments) + command.append('Draft.copyScaledEdges([' + all_args + '])') + command.append('FreeCAD.ActiveDocument.recompute()') + return command + + def build_scale_subelements_command(self): + """Build the strings to commit to scale the subelements.""" + import Part + + command = [] + V = len("Vertex") + E = len("Edge") + for obj in self.selected_subelements: + for index, subelement in enumerate(obj.SubObjects): + if isinstance(subelement, Part.Vertex): + _vertex_index = int(obj.SubElementNames[index][V:]) - 1 + _cmd = 'Draft.scaleVertex' + _cmd += '(' + _cmd += 'FreeCAD.ActiveDocument.' + _cmd += obj.ObjectName + ', ' + _cmd += str(_vertex_index) + ', ' + _cmd += DraftVecUtils.toString(self.delta) + ', ' + _cmd += DraftVecUtils.toString(self.center) + _cmd += ')' + command.append(_cmd) + elif isinstance(subelement, Part.Edge): + _edge_index = int(obj.SubElementNames[index][E:]) - 1 + _cmd = 'Draft.scaleEdge' + _cmd += '(' + _cmd += 'FreeCAD.ActiveDocument.' + _cmd += obj.ObjectName + ', ' + _cmd += str(_edge_index) + ', ' + _cmd += DraftVecUtils.toString(self.delta) + ', ' + _cmd += DraftVecUtils.toString(self.center) + _cmd += ')' + command.append(_cmd) + command.append('FreeCAD.ActiveDocument.recompute()') + return command + + def is_scalable(self, obj): + """Return True only for the supported objects. + + Currently it only supports `Rectangle`, `Wire`, `Annotation` + and `BSpline`. + """ + t = utils.getType(obj) + if t in ["Rectangle", "Wire", "Annotation", "BSpline"]: + # TODO: support more types in Draft.scale + return True + else: + return False + + def scale_object(self): + """Scale the object.""" + if self.task.relative.isChecked(): + self.delta = App.DraftWorkingPlane.getGlobalCoords(self.delta) + goods = [] + bads = [] + for obj in self.selected_objects: + if self.is_scalable(obj): + goods.append(obj) + else: + bads.append(obj) + if bads: + if len(bads) == 1: + m = translate("draft", "Unable to scale object: ") + m += bads[0].Label + else: + m = translate("draft", "Unable to scale objects: ") + m += ", ".join([o.Label for o in bads]) + m += " - " + translate("draft", + "This object type cannot be scaled " + "directly. Please use the clone method.") + _err(m) + if goods: + _doc = 'FreeCAD.ActiveDocument.' + objects = '[' + objects += ', '.join([_doc + obj.Name for obj in goods]) + objects += ']' + Gui.addModule("Draft") + + if self.task.isCopy.isChecked(): + _cmd_name = translate("draft", "Copy") + else: + _cmd_name = translate("draft", "Scale") + + _cmd = 'Draft.scale' + _cmd += '(' + _cmd += objects + ', ' + _cmd += 'scale=' + DraftVecUtils.toString(self.delta) + ', ' + _cmd += 'center=' + DraftVecUtils.toString(self.center) + ', ' + _cmd += 'copy=' + str(self.task.isCopy.isChecked()) + _cmd += ')' + _cmd_list = ['ss = ' + _cmd, + 'FreeCAD.ActiveDocument.recompute()'] + self.commit(_cmd_name, _cmd_list) + + def scaleGhost(self, x, y, z, rel): + """Scale the preview of the object.""" + delta = App.Vector(x, y, z) + if rel: + delta = App.DraftWorkingPlane.getGlobalCoords(delta) + for ghost in self.ghosts: + ghost.scale(delta) + # calculate a correction factor depending on the scaling center + corr = App.Vector(self.node[0].x, self.node[0].y, self.node[0].z) + corr.scale(delta.x, delta.y, delta.z) + corr = (corr.sub(self.node[0])).negative() + for ghost in self.ghosts: + ghost.move(corr) + ghost.on() + + def numericInput(self, numx, numy, numz): + """Validate the entry fields in the user interface. + + This function is called by the toolbar or taskpanel interface + when valid x, y, and z have been entered in the input fields. + """ + self.point = App.Vector(numx, numy, numz) + self.node.append(self.point) + if not self.pickmode: + if not self.ghosts: + self.set_ghosts() + self.ui.offUi() + if self.call: + self.view.removeEventCallback("SoEvent", self.call) + self.task = task_scale.ScaleTaskPanel() + self.task.sourceCmd = self + todo.ToDo.delay(Gui.Control.showDialog, self.task) + todo.ToDo.delay(self.task.xValue.selectAll, None) + todo.ToDo.delay(self.task.xValue.setFocus, None) + for ghost in self.ghosts: + ghost.on() + elif len(self.node) == 2: + _msg(translate("draft", "Pick new distance from base point")) + elif len(self.node) == 3: + if hasattr(Gui, "Snapper"): + Gui.Snapper.off() + if self.call: + self.view.removeEventCallback("SoEvent", self.call) + d1 = (self.node[1].sub(self.node[0])).Length + d2 = (self.node[2].sub(self.node[0])).Length + # print ("d2/d1 = {}".format(d2/d1)) + if hasattr(self, "task"): + if self.task: + self.task.lock.setChecked(True) + self.task.setValue(d2/d1) + + def finish(self, closed=False, cont=False): + """Terminate the operation.""" + super().finish() + for ghost in self.ghosts: + ghost.finalize() + + +Gui.addCommand('Draft_Scale', Scale())