From 186af3d05d9ca2ee8c7a780d7ed3db2bb91edc53 Mon Sep 17 00:00:00 2001 From: wandererfan Date: Wed, 8 Apr 2020 21:02:26 -0400 Subject: [PATCH 001/142] [TD]fix preference key for SectionEdges --- src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui index 8c9b8c1cea..60ab9bc0c4 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui @@ -251,10 +251,10 @@ Then you need to increase the tile limit. Show Section Edges - ShowUnits + ShowSectionEdges - /Mod/TechDraw/Dimensions + /Mod/TechDraw/General From b2a7cb93032f9033148715e24dbd63a0ba18c464 Mon Sep 17 00:00:00 2001 From: wandererfan Date: Thu, 9 Apr 2020 14:59:35 -0400 Subject: [PATCH 002/142] [TD]expose SymbolScale preference --- src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui | 187 ++++++++++-------- src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp | 2 + 2 files changed, 111 insertions(+), 78 deletions(-) diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui index 5829ebe238..49206852bd 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui @@ -7,7 +7,7 @@ 0 0 440 - 450 + 532 @@ -404,15 +404,48 @@ Each unit is approx. 0.1 mm wide - - - - Vertex Scale + + + + + 0 + 0 + + + + + 174 + 0 + + + + + 0 + 0 + + + + Tolerance font size adjustment. Multiplier of dimension font size. + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 0.500000000000000 + + + TolSizeAdjust + + + Mod/TechDraw/Dimensions - - + + 0 @@ -426,34 +459,26 @@ Each unit is approx. 0.1 mm wide - Scale of vertex dots. Multiplier of line width. - - - + Size of template field click handles Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - 5.000000000000000 + 3.000000000000000 - VertexScale + TemplateDotSize Mod/TechDraw/General - - - - - true - - + + - Center Mark Scale + Vertex Scale @@ -504,6 +529,52 @@ Each unit is approx. 0.1 mm wide + + + + + 0 + 0 + + + + + 174 + 0 + + + + Scale of vertex dots. Multiplier of line width. + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 5.000000000000000 + + + VertexScale + + + Mod/TechDraw/General + + + + + + + + true + + + + Center Mark Scale + + + @@ -528,46 +599,6 @@ Each unit is approx. 0.1 mm wide - - - - - 0 - 0 - - - - - 174 - 0 - - - - - 0 - 0 - - - - Tolerance font size adjustment. Multiplier of dimension font size. - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 0.500000000000000 - - - TolSizeAdjust - - - Mod/TechDraw/Dimensions - - - @@ -575,34 +606,29 @@ Each unit is approx. 0.1 mm wide - - - - - 0 - 0 - - - - - 174 - 0 - + + + + Welding Symbol Scale + + + + - Size of template field click handles + Multiplier for size of welding symbols Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - 3.000000000000000 + 1.250000000000000 - TemplateDotSize + SymbolFactor - Mod/TechDraw/General + Mod/TechDraw/Decorations @@ -646,6 +672,11 @@ Each unit is approx. 0.1 mm wide + + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
Gui::PrefComboBox QComboBox diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp index 2a24864609..a726891675 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp @@ -69,6 +69,7 @@ void DlgPrefsTechDraw2Imp::saveSettings() pdsbEdgeFuzz->onSave(); pdsbMarkFuzz->onSave(); pdsbTemplateMark->onSave(); + pdsbSymbolScale->onSave(); } void DlgPrefsTechDraw2Imp::loadSettings() @@ -85,6 +86,7 @@ void DlgPrefsTechDraw2Imp::loadSettings() pdsbEdgeFuzz->onRestore(); pdsbMarkFuzz->onRestore(); pdsbTemplateMark->onRestore(); + pdsbSymbolScale->onRestore(); } /** From 13cc36e73d34e71b1c3e59fbee1f2362f3bf6b2b Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Fri, 6 Mar 2020 01:55:40 -0600 Subject: [PATCH 003/142] Draft: gui_ and task_circulararray cleanup --- .../Draft/draftguitools/gui_circulararray.py | 19 +- .../drafttaskpanels/task_circulararray.py | 480 +++++++++++------- 2 files changed, 302 insertions(+), 197 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_circulararray.py b/src/Mod/Draft/draftguitools/gui_circulararray.py index 955230df14..cc229af358 100644 --- a/src/Mod/Draft/draftguitools/gui_circulararray.py +++ b/src/Mod/Draft/draftguitools/gui_circulararray.py @@ -20,7 +20,7 @@ # * USA * # * * # *************************************************************************** -"""Provides the Draft CircularArray tool.""" +"""Provides the Draft CircularArray GuiCommand.""" ## @package gui_circulararray # \ingroup DRAFT # \brief This module provides the Draft CircularArray tool. @@ -31,13 +31,15 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files +from draftutils.messages import _msg, _log +from draftutils.translate import _tr from draftguitools import gui_base from drafttaskpanels import task_circulararray import draftutils.todo as todo # The module is used to prevent complaints from code checkers (flake8) -True if Draft_rc.__name__ else False +bool(Draft_rc.__name__) class GuiCommandCircularArray(gui_base.GuiCommandBase): @@ -45,7 +47,7 @@ class GuiCommandCircularArray(gui_base.GuiCommandBase): def __init__(self): super().__init__() - self.command_name = "CircularArray" + self.command_name = "Circular array" self.location = None self.mouse_event = None self.view = None @@ -56,14 +58,15 @@ class GuiCommandCircularArray(gui_base.GuiCommandBase): def GetResources(self): """Set icon, menu and tooltip.""" - _msg = ("Creates copies of a selected object, " + _tip = ("Creates copies of a selected object, " "and places the copies in a circular pattern.\n" "The properties of the array can be further modified after " "the new object is created, including turning it into " "a different type of array.") + d = {'Pixmap': 'Draft_CircularArray', 'MenuText': QT_TRANSLATE_NOOP("Draft", "Circular array"), - 'ToolTip': QT_TRANSLATE_NOOP("Draft", _msg)} + 'ToolTip': QT_TRANSLATE_NOOP("Draft", _tip)} return d def Activated(self): @@ -72,6 +75,10 @@ class GuiCommandCircularArray(gui_base.GuiCommandBase): We add callbacks that connect the 3D view with the widgets of the task panel. """ + _log("GuiCommand: {}".format(_tr(self.command_name))) + _msg("{}".format(16*"-")) + _msg("GuiCommand: {}".format(_tr(self.command_name))) + self.location = coin.SoLocation2Event.getClassTypeId() self.mouse_event = coin.SoMouseButtonEvent.getClassTypeId() self.view = Draft.get3DView() diff --git a/src/Mod/Draft/drafttaskpanels/task_circulararray.py b/src/Mod/Draft/drafttaskpanels/task_circulararray.py index d1e7d4327d..13bab39ea1 100644 --- a/src/Mod/Draft/drafttaskpanels/task_circulararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_circulararray.py @@ -1,9 +1,3 @@ -"""This module provides the task panel for the Draft CircularArray tool. -""" -## @package task_circulararray -# \ingroup DRAFT -# \brief This module provides the task panel code for the CircularArray tool. - # *************************************************************************** # * (c) 2019 Eliud Cabrera Castillo * # * * @@ -26,183 +20,256 @@ # * USA * # * * # *************************************************************************** +"""Provides the task panel code for the Draft CircularArray tool.""" +## @package task_circulararray +# \ingroup DRAFT +# \brief This module provides the task panel code for the CircularArray tool. + +import PySide.QtGui as QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui -# import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files import DraftVecUtils +from draftutils.messages import _msg, _wrn, _err, _log +from draftutils.translate import _tr +from FreeCAD import Units as U -import PySide.QtCore as QtCore -import PySide.QtGui as QtGui -from PySide.QtCore import QT_TRANSLATE_NOOP -# import DraftTools -from DraftGui import translate -# from DraftGui import displayExternal - -_Quantity = App.Units.Quantity - - -def _Msg(text, end="\n"): - """Print message with newline""" - App.Console.PrintMessage(text + end) - - -def _Wrn(text, end="\n"): - """Print warning with newline""" - App.Console.PrintWarning(text + end) - - -def _tr(text): - """Function to translate with the context set""" - return translate("Draft", text) - - -# So the resource file doesn't trigger errors from code checkers (flake8) -True if Draft_rc.__name__ else False +# The module is used to prevent complaints from code checkers (flake8) +bool(Draft_rc.__name__) class TaskPanelCircularArray: """TaskPanel code for the CircularArray command. The names of the widgets are defined in the `.ui` file. - In this class all those widgets are automatically created - under the name `self.form.` + This `.ui` file `must` be loaded into an attribute + called `self.form` so that it is loaded into the task panel correctly. + + In this class all widgets are automatically created + as `self.form.`. The `.ui` file may use special FreeCAD widgets such as `Gui::InputField` (based on `QLineEdit`) and `Gui::QuantitySpinBox` (based on `QAbstractSpinBox`). See the Doxygen documentation of the corresponding files in `src/Gui/`, for example, `InputField.h` and `QuantitySpinBox.h`. + + Attributes + ---------- + source_command: gui_base.GuiCommandBase + This attribute holds a reference to the calling class + of this task panel. + This parent class, which is derived from `gui_base.GuiCommandBase`, + is responsible for calling this task panel, for installing + certain callbacks, and for removing them. + + It also delays the execution of the internal creation commands + by using the `draftutils.todo.ToDo` class. + + See Also + -------- + * https://forum.freecadweb.org/viewtopic.php?f=10&t=40007 + * https://forum.freecadweb.org/viewtopic.php?t=5374#p43038 """ def __init__(self): + self.name = "Circular array" + _log(_tr("Task panel:") + "{}".format(_tr(self.name))) + + # The .ui file must be loaded into an attribute + # called `self.form` so that it is displayed in the task panel. ui_file = ":/ui/TaskPanel_CircularArray.ui" self.form = Gui.PySideUic.loadUi(ui_file) - self.name = self.form.windowTitle() icon_name = "Draft_CircularArray" svg = ":/icons/" + icon_name pix = QtGui.QPixmap(svg) icon = QtGui.QIcon.fromTheme(icon_name, QtGui.QIcon(svg)) self.form.setWindowIcon(icon) + self.form.setWindowTitle(_tr(self.name)) + self.form.label_icon.setPixmap(pix.scaled(32, 32)) - start_distance = _Quantity(1000.0, App.Units.Length) - distance_unit = start_distance.getUserPreferred()[2] - self.form.spinbox_r_distance.setProperty('rawValue', - 2 * start_distance.Value) - self.form.spinbox_r_distance.setProperty('unit', distance_unit) - self.form.spinbox_tan_distance.setProperty('rawValue', - start_distance.Value) - self.form.spinbox_tan_distance.setProperty('unit', distance_unit) + # ------------------------------------------------------------------- + # Default values for the internal function, + # and for the task panel interface + start_distance = U.Quantity(50.0, App.Units.Length) + length_unit = start_distance.getUserPreferred()[2] self.r_distance = 2 * start_distance.Value self.tan_distance = start_distance.Value - self.form.spinbox_number.setValue(3) - self.form.spinbox_symmetry.setValue(1) + self.form.spinbox_r_distance.setProperty('rawValue', + self.r_distance) + self.form.spinbox_r_distance.setProperty('unit', length_unit) + self.form.spinbox_tan_distance.setProperty('rawValue', + self.tan_distance) + self.form.spinbox_tan_distance.setProperty('unit', length_unit) - self.number = self.form.spinbox_number.value() - self.symmetry = self.form.spinbox_symmetry.value() + self.number = 3 + self.symmetry = 1 + self.form.spinbox_number.setValue(self.number) + self.form.spinbox_symmetry.setValue(self.symmetry) + + # TODO: the axis is currently fixed, it should be editable + # or selectable from the task panel self.axis = App.Vector(0, 0, 1) - start_point = _Quantity(0.0, App.Units.Length) + start_point = U.Quantity(0.0, App.Units.Length) length_unit = start_point.getUserPreferred()[2] - self.form.input_c_x.setProperty('rawValue', start_point.Value) + + self.center = App.Vector(start_point.Value, + start_point.Value, + start_point.Value) + + self.form.input_c_x.setProperty('rawValue', self.center.x) self.form.input_c_x.setProperty('unit', length_unit) - self.form.input_c_y.setProperty('rawValue', start_point.Value) + self.form.input_c_y.setProperty('rawValue', self.center.y) self.form.input_c_y.setProperty('unit', length_unit) - self.form.input_c_z.setProperty('rawValue', start_point.Value) + self.form.input_c_z.setProperty('rawValue', self.center.z) self.form.input_c_z.setProperty('unit', length_unit) - self.valid_input = True - self.c_x_str = "" - self.c_y_str = "" - self.c_z_str = "" - self.center = App.Vector(0, 0, 0) + self.fuse = False + self.use_link = True - # Old style for Qt4 - # QtCore.QObject.connect(self.form.button_reset, - # QtCore.SIGNAL("clicked()"), - # self.reset_point) - # New style for Qt5 - self.form.button_reset.clicked.connect(self.reset_point) + self.form.checkbox_fuse.setChecked(self.fuse) + self.form.checkbox_link.setChecked(self.use_link) + # ------------------------------------------------------------------- + + # Some objects need to be selected before we can execute the function. + self.selection = None + + # This is used to test the input of the internal function. + # It should be changed to True before we can execute the function. + self.valid_input = False + + self.set_widget_callbacks() + + self.tr_true = QT_TRANSLATE_NOOP("Draft", "True") + self.tr_false = QT_TRANSLATE_NOOP("Draft", "False") # The mask is not used at the moment, but could be used in the future # by a callback to restrict the coordinates of the pointer. self.mask = "" - # When the checkbox changes, change the fuse value - self.fuse = False - QtCore.QObject.connect(self.form.checkbox_fuse, - QtCore.SIGNAL("stateChanged(int)"), - self.set_fuse) + def set_widget_callbacks(self): + """Set up the callbacks (slots) for the widget signals.""" + # New style for Qt5 + self.form.button_reset.clicked.connect(self.reset_point) - self.use_link = False - QtCore.QObject.connect(self.form.checkbox_link, - QtCore.SIGNAL("stateChanged(int)"), - self.set_link) + # When the checkbox changes, change the internal value + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) + + # Old style for Qt4, avoid! + # QtCore.QObject.connect(self.form.button_reset, + # QtCore.SIGNAL("clicked()"), + # self.reset_point) + # QtCore.QObject.connect(self.form.checkbox_fuse, + # QtCore.SIGNAL("stateChanged(int)"), + # self.set_fuse) + # QtCore.QObject.connect(self.form.checkbox_link, + # QtCore.SIGNAL("stateChanged(int)"), + # self.set_link) def accept(self): - """Function that executes when clicking the OK button""" - selection = Gui.Selection.getSelection() - self.number = self.form.spinbox_number.value() + """Execute when clicking the OK button or Enter key.""" + self.selection = Gui.Selection.getSelection() - tan_d_str = self.form.spinbox_tan_distance.text() - self.tan_distance = _Quantity(tan_d_str).Value - self.valid_input = self.validate_input(selection, + (self.r_distance, + self.tan_distance) = self.get_distances() + + (self.number, + self.symmetry) = self.get_number_symmetry() + + self.axis = self.get_axis() + self.center = self.get_center() + + self.valid_input = self.validate_input(self.selection, + self.r_distance, + self.tan_distance, self.number, - self.tan_distance) + self.symmetry, + self.axis, + self.center) if self.valid_input: - self.create_object(selection) - self.print_messages(selection) + self.create_object() + self.print_messages() self.finish() - def validate_input(self, selection, number, tan_distance): - """Check that the input is valid""" + def validate_input(self, selection, + r_distance, tan_distance, + number, symmetry, + axis, center): + """Check that the input is valid. + + Some values may not need to be checked because + the interface may not allow to input wrong data. + """ if not selection: - _Wrn(_tr("At least one element must be selected")) + _err(_tr("At least one element must be selected.")) return False + if number < 2: - _Wrn(_tr("Number of elements must be at least 2")) + _err(_tr("Number of layers must be at least 2.")) return False - # Todo: each of the elements of the selection could be tested, - # not only the first one. - if selection[0].isDerivedFrom("App::FeaturePython"): - _Wrn(_tr("Selection is not suitable for array")) - _Wrn(_tr("Object:") + " {}".format(selection[0].Label)) + + # TODO: this should handle multiple objects. + # Each of the elements of the selection should be tested. + obj = selection[0] + if obj.isDerivedFrom("App::FeaturePython"): + _err(_tr("Selection is not suitable for array.")) + _err(_tr("Object:") + " {}".format(selection[0].Label)) return False + + if r_distance == 0: + _wrn(_tr("Radial distance is zero. " + "Resulting array may not look correct.")) + elif r_distance < 0: + _wrn(_tr("Radial distance is negative. " + "It is made positive to proceed.")) + self.r_distance = abs(r_distance) + if tan_distance == 0: - _Wrn(_tr("Tangential distance cannot be zero")) + _err(_tr("Tangential distance cannot be zero.")) return False - return True + elif tan_distance < 0: + _wrn(_tr("Tangential distance is negative. " + "It is made positive to proceed.")) + self.tan_distance = abs(tan_distance) - def create_object(self, selection): - """Create the actual object""" - r_d_str = self.form.spinbox_r_distance.text() - tan_d_str = self.form.spinbox_tan_distance.text() - self.r_distance = _Quantity(r_d_str).Value - self.tan_distance = _Quantity(tan_d_str).Value - - self.number = self.form.spinbox_number.value() - self.symmetry = self.form.spinbox_symmetry.value() - self.center = self.set_point() - - if len(selection) == 1: - sel_obj = selection[0] - else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] + # The other arguments are not tested but they should be present. + if symmetry and axis and center: + pass self.fuse = self.form.checkbox_fuse.isChecked() self.use_link = self.form.checkbox_link.isChecked() + return True + + def create_object(self): + """Create the new object. + + At this stage we already tested that the input is correct + so the necessary attributes are already set. + Then we proceed with the internal function to create the new object. + """ + if len(self.selection) == 1: + sel_obj = self.selection[0] + else: + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] # This creates the object immediately # obj = Draft.makeArray(sel_obj, - # self.center, self.angle, self.number) + # self.r_distance, self.tan_distance, + # self.axis, self.center, + # self.number, self.symmetry, + # self.use_link) # if obj: # obj.Fuse = self.fuse @@ -210,96 +277,120 @@ class TaskPanelCircularArray: # of this class, the GuiCommand. # This is needed to schedule geometry manipulation # that would crash Coin3D if done in the event callback. - _cmd = "obj = Draft.makeArray(" - _cmd += "FreeCAD.ActiveDocument." + sel_obj.Name + ", " - _cmd += "arg1=" + str(self.r_distance) + ", " - _cmd += "arg2=" + str(self.tan_distance) + ", " - _cmd += "arg3=" + DraftVecUtils.toString(self.axis) + ", " - _cmd += "arg4=" + DraftVecUtils.toString(self.center) + ", " - _cmd += "arg5=" + str(self.number) + ", " - _cmd += "arg6=" + str(self.symmetry) + ", " + _cmd = "draftobjects.circulararray.make_circular_array" + _cmd += "(" + _cmd += "App.ActiveDocument." + sel_obj.Name + ", " + _cmd += "r_distance=" + str(self.r_distance) + ", " + _cmd += "tan_distance=" + str(self.tan_distance) + ", " + _cmd += "number=" + str(self.number) + ", " + _cmd += "symmetry=" + str(self.symmetry) + ", " + _cmd += "axis=" + DraftVecUtils.toString(self.axis) + ", " + _cmd += "center=" + DraftVecUtils.toString(self.center) + ", " _cmd += "use_link=" + str(self.use_link) _cmd += ")" - _cmd_list = ["FreeCADGui.addModule('Draft')", - _cmd, + _cmd_list = ["Gui.addModule('Draft')", + "Gui.addModule('draftobjects.circulararray')", + "obj = " + _cmd, "obj.Fuse = " + str(self.fuse), "Draft.autogroup(obj)", - "FreeCAD.ActiveDocument.recompute()"] - self.source_command.commit("Circular array", _cmd_list) + "App.ActiveDocument.recompute()"] - def set_point(self): - """Assign the values to the center""" - self.c_x_str = self.form.input_c_x.text() - self.c_y_str = self.form.input_c_y.text() - self.c_z_str = self.form.input_c_z.text() - center = App.Vector(_Quantity(self.c_x_str).Value, - _Quantity(self.c_y_str).Value, - _Quantity(self.c_z_str).Value) + # We commit the command list through the parent command + self.source_command.commit(_tr(self.name), _cmd_list) + + def get_distances(self): + """Get the distance parameters from the widgets.""" + r_d_str = self.form.spinbox_r_distance.text() + tan_d_str = self.form.spinbox_tan_distance.text() + return (U.Quantity(r_d_str).Value, + U.Quantity(tan_d_str).Value) + + def get_number_symmetry(self): + """Get the number and symmetry parameters from the widgets.""" + number = self.form.spinbox_number.value() + symmetry = self.form.spinbox_symmetry.value() + return number, symmetry + + def get_center(self): + """Get the value of the center from the widgets.""" + c_x_str = self.form.input_c_x.text() + c_y_str = self.form.input_c_y.text() + c_z_str = self.form.input_c_z.text() + center = App.Vector(U.Quantity(c_x_str).Value, + U.Quantity(c_y_str).Value, + U.Quantity(c_z_str).Value) return center + def get_axis(self): + """Get the axis that will be used for the array. NOT IMPLEMENTED. + + It should consider a second selection of an edge or wire to use + as an axis. + """ + return self.axis + def reset_point(self): - """Reset the point to the original distance""" + """Reset the center point to the original distance.""" self.form.input_c_x.setProperty('rawValue', 0) self.form.input_c_y.setProperty('rawValue', 0) self.form.input_c_z.setProperty('rawValue', 0) - self.center = self.set_point() - _Msg(_tr("Center reset:") + self.center = self.get_center() + _msg(_tr("Center reset:") + " ({0}, {1}, {2})".format(self.center.x, self.center.y, self.center.z)) - def print_fuse_state(self): - """Print the state translated""" - if self.fuse: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_fuse_state(self, fuse): + """Print the fuse state translated.""" + if fuse: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Fuse:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Fuse:") + " {}".format(state)) def set_fuse(self): - """This function is called when the fuse checkbox changes""" + """Execute as a callback when the fuse checkbox changes.""" self.fuse = self.form.checkbox_fuse.isChecked() - self.print_fuse_state() + self.print_fuse_state(self.fuse) - def print_link_state(self): - """Print the state translated""" - if self.use_link: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_link_state(self, use_link): + """Print the link state translated.""" + if use_link: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Use Link object:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Create Link array:") + " {}".format(state)) def set_link(self): - """This function is called when the fuse checkbox changes""" + """Execute as a callback when the link checkbox changes.""" self.use_link = self.form.checkbox_link.isChecked() - self.print_link_state() + self.print_link_state(self.use_link) - def print_messages(self, selection): - """Print messages about the operation""" - if len(selection) == 1: - sel_obj = selection[0] + def print_messages(self): + """Print messages about the operation.""" + if len(self.selection) == 1: + sel_obj = self.selection[0] else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] - _Msg("{}".format(16*"-")) - _Msg("{}".format(self.name)) - _Msg(_tr("Object:") + " {}".format(sel_obj.Label)) - _Msg(_tr("Radial distance:") + " {}".format(self.r_distance)) - _Msg(_tr("Tangential distance:") + " {}".format(self.tan_distance)) - _Msg(_tr("Number of circular layers:") + " {}".format(self.number)) - _Msg(_tr("Symmetry parameter:") + " {}".format(self.symmetry)) - _Msg(_tr("Center of rotation:") + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] + _msg(_tr("Object:") + " {}".format(sel_obj.Label)) + _msg(_tr("Radial distance:") + " {}".format(self.r_distance)) + _msg(_tr("Tangential distance:") + " {}".format(self.tan_distance)) + _msg(_tr("Number of circular layers:") + " {}".format(self.number)) + _msg(_tr("Symmetry parameter:") + " {}".format(self.symmetry)) + _msg(_tr("Center of rotation:") + " ({0}, {1}, {2})".format(self.center.x, self.center.y, self.center.z)) - self.print_fuse_state() - self.print_link_state() + self.print_fuse_state(self.fuse) + self.print_link_state(self.use_link) def display_point(self, point=None, plane=None, mask=None): - """Displays the coordinates in the x, y, and z widgets. + """Display the coordinates in the x, y, and z widgets. This function should be used in a Coin callback so that the coordinate values are automatically updated when the @@ -307,21 +398,21 @@ class TaskPanelCircularArray: This was copied from `DraftGui.py` but needs to be improved for this particular command. - point : + point: Base::Vector3 is a vector that arrives by the callback. - plane : + plane: WorkingPlane is a `WorkingPlane` instance, for example, `App.DraftWorkingPlane`. It is not used at the moment, but could be used to set up the grid. - mask : + mask: str is a string that specifies which coordinate is being edited. It is used to restrict edition of a single coordinate. It is not used at the moment but could be used with a callback. """ # Get the coordinates to display - dp = None + d_p = None if point: - dp = point + d_p = point # Set the widgets to the value of the mouse pointer. # @@ -336,25 +427,28 @@ class TaskPanelCircularArray: # sbx = self.form.spinbox_c_x # sby = self.form.spinbox_c_y # sbz = self.form.spinbox_c_z - if dp: + if d_p: if self.mask in ('y', 'z'): - # sbx.setText(displayExternal(dp.x, None, 'Length')) - self.form.input_c_x.setProperty('rawValue', dp.x) + # sbx.setText(displayExternal(d_p.x, None, 'Length')) + self.form.input_c_x.setProperty('rawValue', d_p.x) else: - # sbx.setText(displayExternal(dp.x, None, 'Length')) - self.form.input_c_x.setProperty('rawValue', dp.x) + # sbx.setText(displayExternal(d_p.x, None, 'Length')) + self.form.input_c_x.setProperty('rawValue', d_p.x) if self.mask in ('x', 'z'): - # sby.setText(displayExternal(dp.y, None, 'Length')) - self.form.input_c_y.setProperty('rawValue', dp.y) + # sby.setText(displayExternal(d_p.y, None, 'Length')) + self.form.input_c_y.setProperty('rawValue', d_p.y) else: - # sby.setText(displayExternal(dp.y, None, 'Length')) - self.form.input_c_y.setProperty('rawValue', dp.y) + # sby.setText(displayExternal(d_p.y, None, 'Length')) + self.form.input_c_y.setProperty('rawValue', d_p.y) if self.mask in ('x', 'y'): - # sbz.setText(displayExternal(dp.z, None, 'Length')) - self.form.input_c_z.setProperty('rawValue', dp.z) + # sbz.setText(displayExternal(d_p.z, None, 'Length')) + self.form.input_c_z.setProperty('rawValue', d_p.z) else: - # sbz.setText(displayExternal(dp.z, None, 'Length')) - self.form.input_c_z.setProperty('rawValue', dp.z) + # sbz.setText(displayExternal(d_p.z, None, 'Length')) + self.form.input_c_z.setProperty('rawValue', d_p.z) + + if plane: + pass # Set masks if (mask == "x") or (self.mask == "x"): @@ -379,7 +473,7 @@ class TaskPanelCircularArray: self.set_focus() def set_focus(self, key=None): - """Set the focus on the widget that receives the key signal""" + """Set the focus on the widget that receives the key signal.""" if key is None or key == "x": self.form.input_c_x.setFocus() self.form.input_c_x.selectAll() @@ -391,12 +485,16 @@ class TaskPanelCircularArray: self.form.input_c_z.selectAll() def reject(self): - """Function that executes when clicking the Cancel button""" - _Msg(_tr("Aborted:") + " {}".format(self.name)) + """Execute when clicking the Cancel button or pressing Escape.""" + _msg(_tr("Aborted:") + " {}".format(_tr(self.name))) self.finish() def finish(self): - """Function that runs at the end after OK or Cancel""" + """Finish the command, after accept or reject. + + It finally calls the parent class to execute + the delayed functions, and perform cleanup. + """ # App.ActiveDocument.commitTransaction() Gui.ActiveDocument.resetEdit() # Runs the parent command to complete the call From 8cbb5992084dac776059a64f5d3da4e63eed4680 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sat, 7 Mar 2020 22:44:28 -0600 Subject: [PATCH 004/142] Draft: circulararray .ui file, Link array by default Also small additions to the tooltips. --- .../Resources/ui/TaskPanel_CircularArray.ui | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/Mod/Draft/Resources/ui/TaskPanel_CircularArray.ui b/src/Mod/Draft/Resources/ui/TaskPanel_CircularArray.ui index ece65b12ee..b70551ce8b 100644 --- a/src/Mod/Draft/Resources/ui/TaskPanel_CircularArray.ui +++ b/src/Mod/Draft/Resources/ui/TaskPanel_CircularArray.ui @@ -7,7 +7,7 @@ 0 0 445 - 488 + 511
@@ -54,7 +54,8 @@ - The coordinates of the point through which the axis of rotation passes. + The coordinates of the point through which the axis of rotation passes. +Change the direction of the axis itself in the property editor. Center of rotation @@ -127,7 +128,7 @@ - Reset the coordinates of the center of rotation + Reset the coordinates of the center of rotation. Reset point @@ -142,7 +143,8 @@ - If checked, the resulting objects in the array will be fused if they touch each other + If checked, the resulting objects in the array will be fused if they touch each other. +This only works if "Link array" is off. Fuse @@ -152,10 +154,14 @@ - If checked, the resulting objects in the array will be Links instead of simple copies + If checked, the resulting object will be a "Link array" instead of a regular array. +A Link array is more efficient when creating multiple copies, but it cannot be fused together. - Use Links + Link array + + + true @@ -166,7 +172,8 @@ - Distance from one element in the array to the next element in the same layer. It cannot be zero. + Distance from one element in one ring of the array to the next element in the same ring. +It cannot be zero. Tangential distance @@ -176,7 +183,8 @@ - Distance from one element in the array to the next element in the same layer. It cannot be zero. + Distance from one element in one ring of the array to the next element in the same ring. +It cannot be zero. @@ -189,7 +197,7 @@ - Distance from the center of the array to the outer layers + Distance from one layer of objects to the next layer of objects. Radial distance @@ -199,7 +207,7 @@ - Distance from the center of the array to the outer layers + Distance from one layer of objects to the next layer of objects. @@ -212,7 +220,10 @@ - Number that controls how the objects will be distributed + The number of symmetry lines in the circular array. + + + 1 1 @@ -222,7 +233,11 @@ - Number of circular arrays to create, including a copy of the original object. It must be at least 2. + Number of circular layers or rings to create, including a copy of the original object. +It must be at least 2. + + + 2 3 @@ -232,7 +247,8 @@ - Number of circular arrays to create, including a copy of the original object. It must be at least 2. + Number of circular layers or rings to create, including a copy of the original object. +It must be at least 2. Number of circular layers @@ -242,7 +258,7 @@ - Number that controls how the objects will be distributed + The number of symmetry lines in the circular array. Symmetry From 48619ad6e9298d290e08419133e1a8a2872b75e5 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sat, 7 Mar 2020 23:55:47 -0600 Subject: [PATCH 005/142] Draft: gui_ and task_orthoarray cleanup --- src/Mod/Draft/draftguitools/gui_orthoarray.py | 17 +- .../Draft/drafttaskpanels/task_orthoarray.py | 464 ++++++++++-------- 2 files changed, 274 insertions(+), 207 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_orthoarray.py b/src/Mod/Draft/draftguitools/gui_orthoarray.py index fdd2f0b4f3..07f8537dd7 100644 --- a/src/Mod/Draft/draftguitools/gui_orthoarray.py +++ b/src/Mod/Draft/draftguitools/gui_orthoarray.py @@ -31,13 +31,15 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files +from draftutils.messages import _msg, _log +from draftutils.translate import _tr from draftguitools import gui_base from drafttaskpanels import task_orthoarray import draftutils.todo as todo # The module is used to prevent complaints from code checkers (flake8) -True if Draft_rc.__name__ else False +bool(Draft_rc.__name__) class GuiCommandOrthoArray(gui_base.GuiCommandBase): @@ -45,7 +47,7 @@ class GuiCommandOrthoArray(gui_base.GuiCommandBase): def __init__(self): super().__init__() - self.command_name = "OrthoArray" + self.command_name = "Orthogonal array" # self.location = None self.mouse_event = None self.view = None @@ -56,14 +58,15 @@ class GuiCommandOrthoArray(gui_base.GuiCommandBase): def GetResources(self): """Set icon, menu and tooltip.""" - _msg = ("Creates copies of a selected object, " + _tip = ("Creates copies of a selected object, " "and places the copies in an orthogonal pattern.\n" "The properties of the array can be further modified after " "the new object is created, including turning it into " "a different type of array.") + d = {'Pixmap': 'Draft_Array', 'MenuText': QT_TRANSLATE_NOOP("Draft", "Array"), - 'ToolTip': QT_TRANSLATE_NOOP("Draft", _msg)} + 'ToolTip': QT_TRANSLATE_NOOP("Draft", _tip)} return d def Activated(self): @@ -72,6 +75,10 @@ class GuiCommandOrthoArray(gui_base.GuiCommandBase): We add callbacks that connect the 3D view with the widgets of the task panel. """ + _log("GuiCommand: {}".format(_tr(self.command_name))) + _msg("{}".format(16*"-")) + _msg("GuiCommand: {}".format(_tr(self.command_name))) + # self.location = coin.SoLocation2Event.getClassTypeId() self.mouse_event = coin.SoMouseButtonEvent.getClassTypeId() self.view = Draft.get3DView() diff --git a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py index 1eea177091..27906ae887 100644 --- a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py +++ b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py @@ -1,8 +1,3 @@ -"""Provide the task panel for the Draft OrthoArray tool.""" -## @package task_orthoarray -# \ingroup DRAFT -# \brief Provide the task panel for the Draft OrthoArray tool. - # *************************************************************************** # * (c) 2020 Eliud Cabrera Castillo * # * * @@ -25,175 +20,226 @@ # * USA * # * * # *************************************************************************** - -import FreeCAD as App -import FreeCADGui as Gui -# import Draft -import Draft_rc -import DraftVecUtils +"""Provides the task panel for the Draft OrthoArray tool.""" +## @package task_orthoarray +# \ingroup DRAFT +# \brief Provide the task panel for the Draft OrthoArray tool. import PySide.QtGui as QtGui from PySide.QtCore import QT_TRANSLATE_NOOP -# import DraftTools -from draftutils.translate import translate -# from DraftGui import displayExternal -_Quantity = App.Units.Quantity +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc # include resources, icons, ui files +import DraftVecUtils +from draftutils.messages import _msg, _err, _log +from draftutils.translate import _tr +from FreeCAD import Units as U - -def _Msg(text, end="\n"): - """Print message with newline.""" - App.Console.PrintMessage(text + end) - - -def _Wrn(text, end="\n"): - """Print warning with newline.""" - App.Console.PrintWarning(text + end) - - -def _tr(text): - """Translate with the context set.""" - return translate("Draft", text) - - -# So the resource file doesn't trigger errors from code checkers (flake8) -True if Draft_rc else False +# The module is used to prevent complaints from code checkers (flake8) +bool(Draft_rc.__name__) class TaskPanelOrthoArray: - """TaskPanel for the OrthoArray command. + """TaskPanel code for the OrthoArray command. The names of the widgets are defined in the `.ui` file. - In this class all those widgets are automatically created - under the name `self.form.` + This `.ui` file `must` be loaded into an attribute + called `self.form` so that it is loaded into the task panel correctly. + + In this class all widgets are automatically created + as `self.form.`. The `.ui` file may use special FreeCAD widgets such as `Gui::InputField` (based on `QLineEdit`) and `Gui::QuantitySpinBox` (based on `QAbstractSpinBox`). See the Doxygen documentation of the corresponding files in `src/Gui/`, for example, `InputField.h` and `QuantitySpinBox.h`. + + Attributes + ---------- + source_command: gui_base.GuiCommandBase + This attribute holds a reference to the calling class + of this task panel. + This parent class, which is derived from `gui_base.GuiCommandBase`, + is responsible for calling this task panel, for installing + certain callbacks, and for removing them. + + It also delays the execution of the internal creation commands + by using the `draftutils.todo.ToDo` class. + + See Also + -------- + * https://forum.freecadweb.org/viewtopic.php?f=10&t=40007 + * https://forum.freecadweb.org/viewtopic.php?t=5374#p43038 """ def __init__(self): + self.name = "Orthogonal array" + _log(_tr("Task panel:") + "{}".format(_tr(self.name))) + + # The .ui file must be loaded into an attribute + # called `self.form` so that it is displayed in the task panel. ui_file = ":/ui/TaskPanel_OrthoArray.ui" self.form = Gui.PySideUic.loadUi(ui_file) - self.name = self.form.windowTitle() icon_name = "Draft_Array" svg = ":/icons/" + icon_name pix = QtGui.QPixmap(svg) icon = QtGui.QIcon.fromTheme(icon_name, QtGui.QIcon(svg)) self.form.setWindowIcon(icon) + self.form.setWindowTitle(_tr(self.name)) + self.form.label_icon.setPixmap(pix.scaled(32, 32)) - start_x = _Quantity(100.0, App.Units.Length) + # ------------------------------------------------------------------- + # Default values for the internal function, + # and for the task panel interface + start_x = U.Quantity(100.0, App.Units.Length) start_y = start_x start_z = start_x - start_zero = _Quantity(0.0, App.Units.Length) length_unit = start_x.getUserPreferred()[2] - self.form.input_X_x.setProperty('rawValue', start_x.Value) + self.v_x = App.Vector(start_x.Value, 0, 0) + self.v_y = App.Vector(0, start_y.Value, 0) + self.v_z = App.Vector(0, 0, start_z.Value) + + self.form.input_X_x.setProperty('rawValue', self.v_x.x) self.form.input_X_x.setProperty('unit', length_unit) - self.form.input_X_y.setProperty('rawValue', start_zero.Value) + self.form.input_X_y.setProperty('rawValue', self.v_x.y) self.form.input_X_y.setProperty('unit', length_unit) - self.form.input_X_z.setProperty('rawValue', start_zero.Value) + self.form.input_X_z.setProperty('rawValue', self.v_x.z) self.form.input_X_z.setProperty('unit', length_unit) - self.form.input_Y_x.setProperty('rawValue', start_zero.Value) + self.form.input_Y_x.setProperty('rawValue', self.v_y.x) self.form.input_Y_x.setProperty('unit', length_unit) - self.form.input_Y_y.setProperty('rawValue', start_y.Value) + self.form.input_Y_y.setProperty('rawValue', self.v_y.y) self.form.input_Y_y.setProperty('unit', length_unit) - self.form.input_Y_z.setProperty('rawValue', start_zero.Value) + self.form.input_Y_z.setProperty('rawValue', self.v_y.z) self.form.input_Y_z.setProperty('unit', length_unit) - self.form.input_Z_x.setProperty('rawValue', start_zero.Value) + self.form.input_Z_x.setProperty('rawValue', self.v_z.x) self.form.input_Z_x.setProperty('unit', length_unit) - self.form.input_Z_y.setProperty('rawValue', start_zero.Value) + self.form.input_Z_y.setProperty('rawValue', self.v_z.y) self.form.input_Z_y.setProperty('unit', length_unit) - self.form.input_Z_z.setProperty('rawValue', start_z.Value) + self.form.input_Z_z.setProperty('rawValue', self.v_z.z) self.form.input_Z_z.setProperty('unit', length_unit) - self.v_X = App.Vector(100, 0, 0) - self.v_Y = App.Vector(0, 100, 0) - self.v_Z = App.Vector(0, 0, 100) + self.n_x = 2 + self.n_y = 2 + self.n_z = 1 - # Old style for Qt4, avoid! - # QtCore.QObject.connect(self.form.button_reset, - # QtCore.SIGNAL("clicked()"), - # self.reset_point) + self.form.spinbox_n_X.setValue(self.n_x) + self.form.spinbox_n_Y.setValue(self.n_y) + self.form.spinbox_n_Z.setValue(self.n_z) + + self.fuse = False + self.use_link = True + + self.form.checkbox_fuse.setChecked(self.fuse) + self.form.checkbox_link.setChecked(self.use_link) + # ------------------------------------------------------------------- + + # Some objects need to be selected before we can execute the function. + self.selection = None + + # This is used to test the input of the internal function. + # It should be changed to True before we can execute the function. + self.valid_input = False + + self.set_widget_callbacks() + + self.tr_true = QT_TRANSLATE_NOOP("Draft", "True") + self.tr_false = QT_TRANSLATE_NOOP("Draft", "False") + + def set_widget_callbacks(self): + """Set up the callbacks (slots) for the widget signals.""" # New style for Qt5 self.form.button_reset_X.clicked.connect(lambda: self.reset_v("X")) self.form.button_reset_Y.clicked.connect(lambda: self.reset_v("Y")) self.form.button_reset_Z.clicked.connect(lambda: self.reset_v("Z")) - self.n_X = 2 - self.n_Y = 2 - self.n_Z = 1 - - self.form.spinbox_n_X.setValue(self.n_X) - self.form.spinbox_n_Y.setValue(self.n_Y) - self.form.spinbox_n_Z.setValue(self.n_Z) - - self.valid_input = False - - # When the checkbox changes, change the fuse value - self.fuse = False + # When the checkbox changes, change the internal value self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) - - self.use_link = False self.form.checkbox_link.stateChanged.connect(self.set_link) + # Old style for Qt4, avoid! + # QtCore.QObject.connect(self.form.button_reset, + # QtCore.SIGNAL("clicked()"), + # self.reset_point) + def accept(self): - """Execute when clicking the OK button.""" - selection = Gui.Selection.getSelection() - n_X = self.form.spinbox_n_X.value() - n_Y = self.form.spinbox_n_Y.value() - n_Z = self.form.spinbox_n_Z.value() - self.valid_input = self.validate_input(selection, - n_X, - n_Y, - n_Z) + """Execute when clicking the OK button or Enter key.""" + self.selection = Gui.Selection.getSelection() + + (self.v_x, + self.v_y, + self.v_z) = self.get_intervals() + + (self.n_x, + self.n_y, + self.n_z) = self.get_numbers() + + self.valid_input = self.validate_input(self.selection, + self.v_x, self.v_y, self.v_z, + self.n_x, self.n_y, self.n_z) if self.valid_input: - self.create_object(selection) - self.print_messages(selection) + self.create_object() + self.print_messages() self.finish() - def validate_input(self, selection, n_X, n_Y, n_Z): - """Check that the input is valid.""" + def validate_input(self, selection, + v_x, v_y, v_z, + n_x, n_y, n_z): + """Check that the input is valid. + + Some values may not need to be checked because + the interface may not allow to input wrong data. + """ if not selection: - _Wrn(_tr("At least one element must be selected")) + _err(_tr("At least one element must be selected.")) return False - if n_X < 1 or n_Y < 1 or n_Z < 1: - _Wrn(_tr("Number of elements must be at least 1")) + + if n_x < 1 or n_y < 1 or n_z < 1: + _err(_tr("Number of elements must be at least 1.")) return False - # Todo: each of the elements of the selection could be tested, - # not only the first one. + + # TODO: this should handle multiple objects. + # Each of the elements of the selection should be tested. obj = selection[0] if obj.isDerivedFrom("App::FeaturePython"): - _Wrn(_tr("Selection is not suitable for array")) - _Wrn(_tr("Object:") + " {0} ({1})".format(obj.Label, obj.TypeId)) + _err(_tr("Selection is not suitable for array.")) + _err(_tr("Object:") + " {0} ({1})".format(obj.Label, obj.TypeId)) return False - return True - def create_object(self, selection): - """Create the actual object.""" - self.v_X, self.v_Y, self.v_Z = self.set_intervals() - self.n_X, self.n_Y, self.n_Z = self.set_numbers() - - if len(selection) == 1: - sel_obj = selection[0] - else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] + # The other arguments are not tested but they should be present. + if v_x and v_y and v_z: + pass self.fuse = self.form.checkbox_fuse.isChecked() self.use_link = self.form.checkbox_link.isChecked() + return True + + def create_object(self): + """Create the new object. + + At this stage we already tested that the input is correct + so the necessary attributes are already set. + Then we proceed with the internal function to create the new object. + """ + if len(self.selection) == 1: + sel_obj = self.selection[0] + else: + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] # This creates the object immediately # obj = Draft.makeArray(sel_obj, - # self.v_X, self.v_Y, self.v_Z, - # self.n_X, self.n_Y, self.n_Z) + # self.v_x, self.v_y, self.v_z, + # self.n_x, self.n_y, self.n_z, + # self.use_link) # if obj: # obj.Fuse = self.fuse @@ -201,146 +247,160 @@ class TaskPanelOrthoArray: # of this class, the GuiCommand. # This is needed to schedule geometry manipulation # that would crash Coin3D if done in the event callback. - _cmd = "obj = Draft.makeArray(" - _cmd += "FreeCAD.ActiveDocument." + sel_obj.Name + ", " - _cmd += "arg1=" + DraftVecUtils.toString(self.v_X) + ", " - _cmd += "arg2=" + DraftVecUtils.toString(self.v_Y) + ", " - _cmd += "arg3=" + DraftVecUtils.toString(self.v_Z) + ", " - _cmd += "arg4=" + str(self.n_X) + ", " - _cmd += "arg5=" + str(self.n_Y) + ", " - _cmd += "arg6=" + str(self.n_Z) + ", " + _cmd = "draftobjects.orthoarray.make_ortho_array" + _cmd += "(" + _cmd += "App.ActiveDocument." + sel_obj.Name + ", " + _cmd += "v_x=" + DraftVecUtils.toString(self.v_x) + ", " + _cmd += "v_y=" + DraftVecUtils.toString(self.v_y) + ", " + _cmd += "v_z=" + DraftVecUtils.toString(self.v_z) + ", " + _cmd += "n_x=" + str(self.n_x) + ", " + _cmd += "n_y=" + str(self.n_y) + ", " + _cmd += "n_z=" + str(self.n_z) + ", " _cmd += "use_link=" + str(self.use_link) _cmd += ")" - _cmd_list = ["FreeCADGui.addModule('Draft')", - _cmd, + _cmd_list = ["Gui.addModule('Draft')", + "Gui.addModule('draftobjects.orthoarray')", + "obj = " + _cmd, "obj.Fuse = " + str(self.fuse), "Draft.autogroup(obj)", - "FreeCAD.ActiveDocument.recompute()"] - self.source_command.commit("Ortho array", _cmd_list) + "App.ActiveDocument.recompute()"] - def set_numbers(self): - """Assign the number of elements.""" - self.n_X = self.form.spinbox_n_X.value() - self.n_Y = self.form.spinbox_n_Y.value() - self.n_Z = self.form.spinbox_n_Z.value() - return self.n_X, self.n_Y, self.n_Z + # We commit the command list through the parent command + self.source_command.commit(_tr(self.name), _cmd_list) - def set_intervals(self): - """Assign the interval vectors.""" - v_X_x_str = self.form.input_X_x.text() - v_X_y_str = self.form.input_X_y.text() - v_X_z_str = self.form.input_X_z.text() - self.v_X = App.Vector(_Quantity(v_X_x_str).Value, - _Quantity(v_X_y_str).Value, - _Quantity(v_X_z_str).Value) + def get_numbers(self): + """Get the number of elements from the widgets.""" + return (self.form.spinbox_n_X.value(), + self.form.spinbox_n_Y.value(), + self.form.spinbox_n_Z.value()) - v_Y_x_str = self.form.input_Y_x.text() - v_Y_y_str = self.form.input_Y_y.text() - v_Y_z_str = self.form.input_Y_z.text() - self.v_Y = App.Vector(_Quantity(v_Y_x_str).Value, - _Quantity(v_Y_y_str).Value, - _Quantity(v_Y_z_str).Value) + def get_intervals(self): + """Get the interval vectors from the widgets.""" + v_x_x_str = self.form.input_X_x.text() + v_x_y_str = self.form.input_X_y.text() + v_x_z_str = self.form.input_X_z.text() + v_x = App.Vector(U.Quantity(v_x_x_str).Value, + U.Quantity(v_x_y_str).Value, + U.Quantity(v_x_z_str).Value) - v_Z_x_str = self.form.input_Z_x.text() - v_Z_y_str = self.form.input_Z_y.text() - v_Z_z_str = self.form.input_Z_z.text() - self.v_Z = App.Vector(_Quantity(v_Z_x_str).Value, - _Quantity(v_Z_y_str).Value, - _Quantity(v_Z_z_str).Value) - return self.v_X, self.v_Y, self.v_Z + v_y_x_str = self.form.input_Y_x.text() + v_y_y_str = self.form.input_Y_y.text() + v_y_z_str = self.form.input_Y_z.text() + v_y = App.Vector(U.Quantity(v_y_x_str).Value, + U.Quantity(v_y_y_str).Value, + U.Quantity(v_y_z_str).Value) + + v_z_x_str = self.form.input_Z_x.text() + v_z_y_str = self.form.input_Z_y.text() + v_z_z_str = self.form.input_Z_z.text() + v_z = App.Vector(U.Quantity(v_z_x_str).Value, + U.Quantity(v_z_y_str).Value, + U.Quantity(v_z_z_str).Value) + return v_x, v_y, v_z def reset_v(self, interval): - """Reset the interval to zero distance.""" + """Reset the interval to zero distance. + + Parameters + ---------- + interval: str + Either "X", "Y", "Z", to reset the interval vector + for that direction. + """ if interval == "X": self.form.input_X_x.setProperty('rawValue', 100) self.form.input_X_y.setProperty('rawValue', 0) self.form.input_X_z.setProperty('rawValue', 0) - _Msg(_tr("Interval X reset:") - + " ({0}, {1}, {2})".format(self.v_X.x, - self.v_X.y, - self.v_X.z)) + self.v_x, self.v_y, self.v_z = self.get_intervals() + _msg(_tr("Interval X reset:") + + " ({0}, {1}, {2})".format(self.v_x.x, + self.v_x.y, + self.v_x.z)) elif interval == "Y": self.form.input_Y_x.setProperty('rawValue', 0) self.form.input_Y_y.setProperty('rawValue', 100) self.form.input_Y_z.setProperty('rawValue', 0) - _Msg(_tr("Interval Y reset:") - + " ({0}, {1}, {2})".format(self.v_Y.x, - self.v_Y.y, - self.v_Y.z)) + self.v_x, self.v_y, self.v_z = self.get_intervals() + _msg(_tr("Interval Y reset:") + + " ({0}, {1}, {2})".format(self.v_y.x, + self.v_y.y, + self.v_y.z)) elif interval == "Z": self.form.input_Z_x.setProperty('rawValue', 0) self.form.input_Z_y.setProperty('rawValue', 0) self.form.input_Z_z.setProperty('rawValue', 100) - _Msg(_tr("Interval Z reset:") - + " ({0}, {1}, {2})".format(self.v_Z.x, - self.v_Z.y, - self.v_Z.z)) + self.v_x, self.v_y, self.v_z = self.get_intervals() + _msg(_tr("Interval Z reset:") + + " ({0}, {1}, {2})".format(self.v_z.x, + self.v_z.y, + self.v_z.z)) - self.n_X, self.n_Y, self.n_Z = self.set_intervals() - - def print_fuse_state(self): - """Print the state translated.""" - if self.fuse: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_fuse_state(self, fuse): + """Print the fuse state translated.""" + if fuse: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Fuse:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Fuse:") + " {}".format(state)) def set_fuse(self): - """Run callback when the fuse checkbox changes.""" + """Execute as a callback when the fuse checkbox changes.""" self.fuse = self.form.checkbox_fuse.isChecked() - self.print_fuse_state() + self.print_fuse_state(self.fuse) - def print_link_state(self): - """Print the state translated.""" - if self.use_link: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_link_state(self, use_link): + """Print the link state translated.""" + if use_link: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Use Link object:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Create Link array:") + " {}".format(state)) def set_link(self): - """Run callback when the link checkbox changes.""" + """Execute as a callback when the link checkbox changes.""" self.use_link = self.form.checkbox_link.isChecked() - self.print_link_state() + self.print_link_state(self.use_link) - def print_messages(self, selection): + def print_messages(self): """Print messages about the operation.""" - if len(selection) == 1: - sel_obj = selection[0] + if len(self.selection) == 1: + sel_obj = self.selection[0] else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] - _Msg("{}".format(16*"-")) - _Msg("{}".format(self.name)) - _Msg(_tr("Object:") + " {}".format(sel_obj.Label)) - _Msg(_tr("Number of X elements:") + " {}".format(self.n_X)) - _Msg(_tr("Interval X:") - + " ({0}, {1}, {2})".format(self.v_X.x, - self.v_X.y, - self.v_X.z)) - _Msg(_tr("Number of Y elements:") + " {}".format(self.n_Y)) - _Msg(_tr("Interval Y:") - + " ({0}, {1}, {2})".format(self.v_Y.x, - self.v_Y.y, - self.v_Y.z)) - _Msg(_tr("Number of Z elements:") + " {}".format(self.n_Z)) - _Msg(_tr("Interval Z:") - + " ({0}, {1}, {2})".format(self.v_Z.x, - self.v_Z.y, - self.v_Z.z)) - self.print_fuse_state() - self.print_link_state() + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] + _msg(_tr("Object:") + " {}".format(sel_obj.Label)) + _msg(_tr("Number of X elements:") + " {}".format(self.n_x)) + _msg(_tr("Interval X:") + + " ({0}, {1}, {2})".format(self.v_x.x, + self.v_x.y, + self.v_x.z)) + _msg(_tr("Number of Y elements:") + " {}".format(self.n_y)) + _msg(_tr("Interval Y:") + + " ({0}, {1}, {2})".format(self.v_y.x, + self.v_y.y, + self.v_y.z)) + _msg(_tr("Number of Z elements:") + " {}".format(self.n_z)) + _msg(_tr("Interval Z:") + + " ({0}, {1}, {2})".format(self.v_z.x, + self.v_z.y, + self.v_z.z)) + self.print_fuse_state(self.fuse) + self.print_link_state(self.use_link) def reject(self): - """Run when clicking the Cancel button.""" - _Msg(_tr("Aborted:") + " {}".format(self.name)) + """Execute when clicking the Cancel button or pressing Escape.""" + _msg(_tr("Aborted:") + " {}".format(_tr(self.name))) self.finish() def finish(self): - """Run at the end after OK or Cancel.""" + """Finish the command, after accept or reject. + + It finally calls the parent class to execute + the delayed functions, and perform cleanup. + """ # App.ActiveDocument.commitTransaction() Gui.ActiveDocument.resetEdit() # Runs the parent command to complete the call From da066410ab6f972c144894cd2633b7bc94a747f2 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 9 Mar 2020 13:01:55 -0600 Subject: [PATCH 006/142] Draft: orthoarray .ui file, Link array by default --- .../Resources/ui/TaskPanel_OrthoArray.ui | 181 ++++++++++-------- 1 file changed, 102 insertions(+), 79 deletions(-) diff --git a/src/Mod/Draft/Resources/ui/TaskPanel_OrthoArray.ui b/src/Mod/Draft/Resources/ui/TaskPanel_OrthoArray.ui index 52541fe2f9..d4e8889bcc 100644 --- a/src/Mod/Draft/Resources/ui/TaskPanel_OrthoArray.ui +++ b/src/Mod/Draft/Resources/ui/TaskPanel_OrthoArray.ui @@ -41,10 +41,12 @@ - Distance between the elements in the Z direction. Normally, only the Z value is necessary; the other two values can give an additional shift in their respective directions. + Distance between the elements in the Z direction. +Normally, only the Z value is necessary; the other two values can give an additional shift in their respective directions. +Negative values will result in copies produced in the negative direction. - Interval Z + Z intervals @@ -117,7 +119,7 @@ - Reset the distances + Reset the distances. Reset Z @@ -132,7 +134,8 @@ - If checked, the resulting objects in the array will be fused if they touch each other + If checked, the resulting objects in the array will be fused if they touch each other. +This only works if "Link array" is off. Fuse @@ -142,10 +145,14 @@ - If checked, the resulting objects in the array will be Links instead of simple copies + If checked, the resulting object will be a "Link array" instead of a regular array. +A Link array is more efficient when creating multiple copies, but it cannot be fused together. - Use Links + Link array + + + true @@ -164,73 +171,6 @@ - - - - Number of elements in the array in the specified direction, including a copy of the original object. The number must be at least 1 in each direction. - - - Number of elements - - - - - - - - X - - - - - - - Z - - - - - - - Y - - - - - - - 1 - - - 2 - - - - - - - 1 - - - 2 - - - - - - - 1 - - - 1 - - - - - - - - @@ -241,10 +181,12 @@ - Distance between the elements in the X direction. Normally, only the X value is necessary; the other two values can give an additional shift in their respective directions. + Distance between the elements in the X direction. +Normally, only the X value is necessary; the other two values can give an additional shift in their respective directions. +Negative values will result in copies produced in the negative direction. - Interval X + X intervals @@ -317,7 +259,7 @@ - Reset the distances + Reset the distances. Reset X @@ -330,10 +272,12 @@ - Distance between the elements in the Y direction. Normally, only the Y value is necessary; the other two values can give an additional shift in their respective directions. + Distance between the elements in the Y direction. +Normally, only the Y value is necessary; the other two values can give an additional shift in their respective directions. +Negative values will result in copies produced in the negative direction. - Interval Y + Y intervals @@ -406,7 +350,7 @@ - Reset the distances + Reset the distances. Reset Y @@ -416,6 +360,74 @@ + + + + Number of elements in the array in the specified direction, including a copy of the original object. +The number must be at least 1 in each direction. + + + Number of elements + + + + + + + + X + + + + + + + Z + + + + + + + Y + + + + + + + 1 + + + 2 + + + + + + + 1 + + + 2 + + + + + + + 1 + + + 1 + + + + + + + + @@ -429,10 +441,21 @@ + spinbox_n_X + spinbox_n_Y + spinbox_n_Z input_X_x input_X_y input_X_z button_reset_X + input_Y_x + input_Y_y + input_Y_z + button_reset_Y + input_Z_x + input_Z_y + input_Z_z + button_reset_Z checkbox_fuse checkbox_link From c5e5f901e919fa559a699a100e533a0990e32931 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 9 Mar 2020 19:13:45 -0600 Subject: [PATCH 007/142] Draft: gui_ and task_polararray cleanup --- src/Mod/Draft/draftguitools/gui_polararray.py | 19 +- .../Draft/drafttaskpanels/task_polararray.py | 381 +++++++++++------- 2 files changed, 238 insertions(+), 162 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_polararray.py b/src/Mod/Draft/draftguitools/gui_polararray.py index 0c277c5bcf..f31ed2bf01 100644 --- a/src/Mod/Draft/draftguitools/gui_polararray.py +++ b/src/Mod/Draft/draftguitools/gui_polararray.py @@ -20,7 +20,7 @@ # * USA * # * * # *************************************************************************** -"""Provides the Draft PolarArray tool.""" +"""Provides the Draft PolarArray GuiCommand.""" ## @package gui_polararray # \ingroup DRAFT # \brief This module provides the Draft PolarArray tool. @@ -31,13 +31,15 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files +from draftutils.messages import _msg, _log +from draftutils.translate import _tr from draftguitools import gui_base from drafttaskpanels import task_polararray import draftutils.todo as todo # The module is used to prevent complaints from code checkers (flake8) -True if Draft_rc.__name__ else False +bool(Draft_rc.__name__) class GuiCommandPolarArray(gui_base.GuiCommandBase): @@ -45,7 +47,7 @@ class GuiCommandPolarArray(gui_base.GuiCommandBase): def __init__(self): super().__init__() - self.command_name = "PolarArray" + self.command_name = "Polar array" self.location = None self.mouse_event = None self.view = None @@ -56,14 +58,15 @@ class GuiCommandPolarArray(gui_base.GuiCommandBase): def GetResources(self): """Set icon, menu and tooltip.""" - _msg = ("Creates copies of a selected object, " + _tip = ("Creates copies of a selected object, " "and places the copies in a polar pattern.\n" "The properties of the array can be further modified after " "the new object is created, including turning it into " "a different type of array.") + d = {'Pixmap': 'Draft_PolarArray', 'MenuText': QT_TRANSLATE_NOOP("Draft", "Polar array"), - 'ToolTip': QT_TRANSLATE_NOOP("Draft", _msg)} + 'ToolTip': QT_TRANSLATE_NOOP("Draft", _tip)} return d def Activated(self): @@ -72,6 +75,10 @@ class GuiCommandPolarArray(gui_base.GuiCommandBase): We add callbacks that connect the 3D view with the widgets of the task panel. """ + _log("GuiCommand: {}".format(_tr(self.command_name))) + _msg("{}".format(16*"-")) + _msg("GuiCommand: {}".format(_tr(self.command_name))) + self.location = coin.SoLocation2Event.getClassTypeId() self.mouse_event = coin.SoMouseButtonEvent.getClassTypeId() self.view = Draft.get3DView() diff --git a/src/Mod/Draft/drafttaskpanels/task_polararray.py b/src/Mod/Draft/drafttaskpanels/task_polararray.py index 042a18a8a3..a3bd08107f 100644 --- a/src/Mod/Draft/drafttaskpanels/task_polararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_polararray.py @@ -1,9 +1,3 @@ -"""This module provides the task panel for the Draft PolarArray tool. -""" -## @package task_polararray -# \ingroup DRAFT -# \brief This module provides the task panel code for the PolarArray tool. - # *************************************************************************** # * (c) 2019 Eliud Cabrera Castillo * # * * @@ -26,163 +20,220 @@ # * USA * # * * # *************************************************************************** +"""Provides the task panel for the Draft PolarArray tool.""" +## @package task_polararray +# \ingroup DRAFT +# \brief This module provides the task panel code for the PolarArray tool. + +import PySide.QtGui as QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui -# import Draft -import Draft_rc +import Draft_rc # include resources, icons, ui files import DraftVecUtils +from draftutils.messages import _msg, _wrn, _err, _log +from draftutils.translate import _tr +from FreeCAD import Units as U -import PySide.QtCore as QtCore -import PySide.QtGui as QtGui -from PySide.QtCore import QT_TRANSLATE_NOOP -# import DraftTools -from DraftGui import translate -# from DraftGui import displayExternal - -_Quantity = App.Units.Quantity - - -def _Msg(text, end="\n"): - """Print message with newline""" - App.Console.PrintMessage(text + end) - - -def _Wrn(text, end="\n"): - """Print warning with newline""" - App.Console.PrintWarning(text + end) - - -def _tr(text): - """Function to translate with the context set""" - return translate("Draft", text) - - -# So the resource file doesn't trigger errors from code checkers (flake8) -True if Draft_rc.__name__ else False +# The module is used to prevent complaints from code checkers (flake8) +bool(Draft_rc.__name__) class TaskPanelPolarArray: - """TaskPanel for the PolarArray command. + """TaskPanel code for the PolarArray command. The names of the widgets are defined in the `.ui` file. - In this class all those widgets are automatically created - under the name `self.form.` + This `.ui` file `must` be loaded into an attribute + called `self.form` so that it is loaded into the task panel correctly. + + In this class all widgets are automatically created + as `self.form.`. The `.ui` file may use special FreeCAD widgets such as `Gui::InputField` (based on `QLineEdit`) and `Gui::QuantitySpinBox` (based on `QAbstractSpinBox`). See the Doxygen documentation of the corresponding files in `src/Gui/`, for example, `InputField.h` and `QuantitySpinBox.h`. + + Attributes + ---------- + source_command: gui_base.GuiCommandBase + This attribute holds a reference to the calling class + of this task panel. + This parent class, which is derived from `gui_base.GuiCommandBase`, + is responsible for calling this task panel, for installing + certain callbacks, and for removing them. + + It also delays the execution of the internal creation commands + by using the `draftutils.todo.ToDo` class. + + See Also + -------- + * https://forum.freecadweb.org/viewtopic.php?f=10&t=40007 + * https://forum.freecadweb.org/viewtopic.php?t=5374#p43038 """ def __init__(self): + self.name = "Polar array" + _log(_tr("Task panel:") + "{}".format(_tr(self.name))) + + # The .ui file must be loaded into an attribute + # called `self.form` so that it is displayed in the task panel. ui_file = ":/ui/TaskPanel_PolarArray.ui" self.form = Gui.PySideUic.loadUi(ui_file) - self.name = self.form.windowTitle() icon_name = "Draft_PolarArray" svg = ":/icons/" + icon_name pix = QtGui.QPixmap(svg) icon = QtGui.QIcon.fromTheme(icon_name, QtGui.QIcon(svg)) self.form.setWindowIcon(icon) + self.form.setWindowTitle(_tr(self.name)) + self.form.label_icon.setPixmap(pix.scaled(32, 32)) - start_angle = _Quantity(180.0, App.Units.Angle) + # ------------------------------------------------------------------- + # Default values for the internal function, + # and for the task panel interface + start_angle = U.Quantity(360.0, App.Units.Angle) angle_unit = start_angle.getUserPreferred()[2] - self.form.spinbox_angle.setProperty('rawValue', start_angle.Value) - self.form.spinbox_angle.setProperty('unit', angle_unit) - self.form.spinbox_number.setValue(4) - self.angle_str = self.form.spinbox_angle.text() self.angle = start_angle.Value + self.number = 5 - self.number = self.form.spinbox_number.value() + self.form.spinbox_angle.setProperty('rawValue', self.angle) + self.form.spinbox_angle.setProperty('unit', angle_unit) - start_point = _Quantity(0.0, App.Units.Length) + self.form.spinbox_number.setValue(self.number) + + start_point = U.Quantity(0.0, App.Units.Length) length_unit = start_point.getUserPreferred()[2] - self.form.input_c_x.setProperty('rawValue', start_point.Value) + + self.center = App.Vector(start_point.Value, + start_point.Value, + start_point.Value) + + self.form.input_c_x.setProperty('rawValue', self.center.x) self.form.input_c_x.setProperty('unit', length_unit) - self.form.input_c_y.setProperty('rawValue', start_point.Value) + self.form.input_c_y.setProperty('rawValue', self.center.y) self.form.input_c_y.setProperty('unit', length_unit) - self.form.input_c_z.setProperty('rawValue', start_point.Value) + self.form.input_c_z.setProperty('rawValue', self.center.z) self.form.input_c_z.setProperty('unit', length_unit) - self.valid_input = True - self.c_x_str = "" - self.c_y_str = "" - self.c_z_str = "" - self.center = App.Vector(0, 0, 0) + self.fuse = False + self.use_link = True - # Old style for Qt4 - # QtCore.QObject.connect(self.form.button_reset, - # QtCore.SIGNAL("clicked()"), - # self.reset_point) - # New style for Qt5 - self.form.button_reset.clicked.connect(self.reset_point) + self.form.checkbox_fuse.setChecked(self.fuse) + self.form.checkbox_link.setChecked(self.use_link) + # ------------------------------------------------------------------- + + # Some objects need to be selected before we can execute the function. + self.selection = None + + # This is used to test the input of the internal function. + # It should be changed to True before we can execute the function. + self.valid_input = False + + self.set_widget_callbacks() + + self.tr_true = QT_TRANSLATE_NOOP("Draft", "True") + self.tr_false = QT_TRANSLATE_NOOP("Draft", "False") # The mask is not used at the moment, but could be used in the future # by a callback to restrict the coordinates of the pointer. self.mask = "" - # When the checkbox changes, change the fuse value - self.fuse = False - QtCore.QObject.connect(self.form.checkbox_fuse, - QtCore.SIGNAL("stateChanged(int)"), - self.set_fuse) + def set_widget_callbacks(self): + """Set up the callbacks (slots) for the widget signals.""" + # New style for Qt5 + self.form.button_reset.clicked.connect(self.reset_point) - self.use_link = False - QtCore.QObject.connect(self.form.checkbox_link, - QtCore.SIGNAL("stateChanged(int)"), - self.set_link) + # When the checkbox changes, change the internal value + self.form.checkbox_fuse.stateChanged.connect(self.set_fuse) + self.form.checkbox_link.stateChanged.connect(self.set_link) + + # Old style for Qt4, avoid! + # QtCore.QObject.connect(self.form.button_reset, + # QtCore.SIGNAL("clicked()"), + # self.reset_point) def accept(self): - """Function that executes when clicking the OK button""" - selection = Gui.Selection.getSelection() - self.number = self.form.spinbox_number.value() - self.valid_input = self.validate_input(selection, - self.number) + """Execute when clicking the OK button or Enter key.""" + self.selection = Gui.Selection.getSelection() + + (self.number, + self.angle) = self.get_number_angle() + + self.center = self.get_center() + + self.valid_input = self.validate_input(self.selection, + self.number, + self.angle, + self.center) if self.valid_input: - self.create_object(selection) - self.print_messages(selection) + self.create_object() + self.print_messages() self.finish() - def validate_input(self, selection, number): - """Check that the input is valid""" + def validate_input(self, selection, + number, angle, center): + """Check that the input is valid. + + Some values may not need to be checked because + the interface may not allow to input wrong data. + """ if not selection: - _Wrn(_tr("At least one element must be selected")) + _err(_tr("At least one element must be selected.")) return False + + # TODO: this should handle multiple objects. + # Each of the elements of the selection should be tested. + obj = selection[0] + if obj.isDerivedFrom("App::FeaturePython"): + _err(_tr("Selection is not suitable for array.")) + _err(_tr("Object:") + " {}".format(selection[0].Label)) + return False + if number < 2: - _Wrn(_tr("Number of elements must be at least 2")) + _err(_tr("Number of elements must be at least 2.")) return False - # Todo: each of the elements of the selection could be tested, - # not only the first one. - if selection[0].isDerivedFrom("App::FeaturePython"): - _Wrn(_tr("Selection is not suitable for array")) - _Wrn(_tr("Object:") + " {}".format(selection[0].Label)) - return False - return True - def create_object(self, selection): - """Create the actual object""" - self.angle_str = self.form.spinbox_angle.text() - self.angle = _Quantity(self.angle_str).Value + if angle > 360: + _wrn(_tr("The angle is above 360 degrees. " + "It is set to this value to proceed.")) + self.angle = 360 + elif angle < -360: + _wrn(_tr("The angle is below -360 degrees. " + "It is set to this value to proceed.")) + self.angle = -360 - self.center = self.set_point() - - if len(selection) == 1: - sel_obj = selection[0] - else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] + # The other arguments are not tested but they should be present. + if center: + pass self.fuse = self.form.checkbox_fuse.isChecked() self.use_link = self.form.checkbox_link.isChecked() + return True + + def create_object(self): + """Create the new object. + + At this stage we already tested that the input is correct + so the necessary attributes are already set. + Then we proceed with the internal function to create the new object. + """ + if len(self.selection) == 1: + sel_obj = self.selection[0] + else: + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] # This creates the object immediately # obj = Draft.makeArray(sel_obj, - # self.center, self.angle, self.number) + # self.center, self.angle, self.number, + # self.use_link) # if obj: # obj.Fuse = self.fuse @@ -190,91 +241,102 @@ class TaskPanelPolarArray: # of this class, the GuiCommand. # This is needed to schedule geometry manipulation # that would crash Coin3D if done in the event callback. - _cmd = "obj = Draft.makeArray(" - _cmd += "FreeCAD.ActiveDocument." + sel_obj.Name + ", " - _cmd += "arg1=" + DraftVecUtils.toString(self.center) + ", " - _cmd += "arg2=" + str(self.angle) + ", " - _cmd += "arg3=" + str(self.number) + ", " + _cmd = "draftobjects.polararray.make_polar_array" + _cmd += "(" + _cmd += "App.ActiveDocument." + sel_obj.Name + ", " + _cmd += "number=" + str(self.number) + ", " + _cmd += "angle=" + str(self.angle) + ", " + _cmd += "center=" + DraftVecUtils.toString(self.center) + ", " _cmd += "use_link=" + str(self.use_link) _cmd += ")" - _cmd_list = ["FreeCADGui.addModule('Draft')", - _cmd, + _cmd_list = ["Gui.addModule('Draft')", + "Gui.addModule('draftobjects.polararray')", + "obj = " + _cmd, "obj.Fuse = " + str(self.fuse), "Draft.autogroup(obj)", - "FreeCAD.ActiveDocument.recompute()"] - self.source_command.commit("Polar array", _cmd_list) + "App.ActiveDocument.recompute()"] - def set_point(self): - """Assign the values to the center""" - self.c_x_str = self.form.input_c_x.text() - self.c_y_str = self.form.input_c_y.text() - self.c_z_str = self.form.input_c_z.text() - center = App.Vector(_Quantity(self.c_x_str).Value, - _Quantity(self.c_y_str).Value, - _Quantity(self.c_z_str).Value) + # We commit the command list through the parent command + self.source_command.commit(_tr(self.name), _cmd_list) + + def get_number_angle(self): + """Get the number and angle parameters from the widgets.""" + number = self.form.spinbox_number.value() + + angle_str = self.form.spinbox_angle.text() + angle = U.Quantity(angle_str).Value + return number, angle + + def get_center(self): + """Get the value of the center from the widgets.""" + c_x_str = self.form.input_c_x.text() + c_y_str = self.form.input_c_y.text() + c_z_str = self.form.input_c_z.text() + center = App.Vector(U.Quantity(c_x_str).Value, + U.Quantity(c_y_str).Value, + U.Quantity(c_z_str).Value) return center def reset_point(self): - """Reset the point to the original distance""" + """Reset the center point to the original distance.""" self.form.input_c_x.setProperty('rawValue', 0) self.form.input_c_y.setProperty('rawValue', 0) self.form.input_c_z.setProperty('rawValue', 0) - self.center = self.set_point() - _Msg(_tr("Center reset:") + self.center = self.get_center() + _msg(_tr("Center reset:") + " ({0}, {1}, {2})".format(self.center.x, self.center.y, self.center.z)) - def print_fuse_state(self): - """Print the state translated""" - if self.fuse: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_fuse_state(self, fuse): + """Print the fuse state translated.""" + if fuse: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Fuse:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Fuse:") + " {}".format(state)) def set_fuse(self): - """This function is called when the fuse checkbox changes""" + """Execute as a callback when the fuse checkbox changes.""" self.fuse = self.form.checkbox_fuse.isChecked() - self.print_fuse_state() + self.print_fuse_state(self.fuse) - def print_link_state(self): - """Print the state translated""" - if self.use_link: - translated_state = QT_TRANSLATE_NOOP("Draft", "True") + def print_link_state(self, use_link): + """Print the link state translated.""" + if use_link: + state = self.tr_true else: - translated_state = QT_TRANSLATE_NOOP("Draft", "False") - _Msg(_tr("Use Link object:") + " {}".format(translated_state)) + state = self.tr_false + _msg(_tr("Create Link array:") + " {}".format(state)) def set_link(self): - """This function is called when the fuse checkbox changes""" + """Execute as a callback when the link checkbox changes.""" self.use_link = self.form.checkbox_link.isChecked() - self.print_link_state() + self.print_link_state(self.use_link) - def print_messages(self, selection): - """Print messages about the operation""" - if len(selection) == 1: - sel_obj = selection[0] + def print_messages(self): + """Print messages about the operation.""" + if len(self.selection) == 1: + sel_obj = self.selection[0] else: - # This can be changed so a compound of multiple - # selected objects is produced - sel_obj = selection[0] - _Msg("{}".format(16*"-")) - _Msg("{}".format(self.name)) - _Msg(_tr("Object:") + " {}".format(sel_obj.Label)) - _Msg(_tr("Start angle:") + " {}".format(self.angle_str)) - _Msg(_tr("Number of elements:") + " {}".format(self.number)) - _Msg(_tr("Center of rotation:") + # TODO: this should handle multiple objects. + # For example, it could take the shapes of all objects, + # make a compound and then use it as input for the array function. + sel_obj = self.selection[0] + _msg(_tr("Object:") + " {}".format(sel_obj.Label)) + _msg(_tr("Number of elements:") + " {}".format(self.number)) + _msg(_tr("Polar angle:") + " {}".format(self.angle)) + _msg(_tr("Center of rotation:") + " ({0}, {1}, {2})".format(self.center.x, self.center.y, self.center.z)) - self.print_fuse_state() - self.print_link_state() + self.print_fuse_state(self.fuse) + self.print_link_state(self.use_link) def display_point(self, point=None, plane=None, mask=None): - """Displays the coordinates in the x, y, and z widgets. + """Display the coordinates in the x, y, and z widgets. This function should be used in a Coin callback so that the coordinate values are automatically updated when the @@ -331,6 +393,9 @@ class TaskPanelPolarArray: # sbz.setText(displayExternal(dp.z, None, 'Length')) self.form.input_c_z.setProperty('rawValue', dp.z) + if plane: + pass + # Set masks if (mask == "x") or (self.mask == "x"): self.form.input_c_x.setEnabled(True) @@ -354,7 +419,7 @@ class TaskPanelPolarArray: self.set_focus() def set_focus(self, key=None): - """Set the focus on the widget that receives the key signal""" + """Set the focus on the widget that receives the key signal.""" if key is None or key == "x": self.form.input_c_x.setFocus() self.form.input_c_x.selectAll() @@ -366,12 +431,16 @@ class TaskPanelPolarArray: self.form.input_c_z.selectAll() def reject(self): - """Function that executes when clicking the Cancel button""" - _Msg(_tr("Aborted:") + " {}".format(self.name)) + """Execute when clicking the Cancel button or pressing Escape.""" + _msg(_tr("Aborted:") + " {}".format(_tr(self.name))) self.finish() def finish(self): - """Function that runs at the end after OK or Cancel""" + """Finish the command, after accept or reject. + + It finally calls the parent class to execute + the delayed functions, and perform cleanup. + """ # App.ActiveDocument.commitTransaction() Gui.ActiveDocument.resetEdit() # Runs the parent command to complete the call From e1c31bf9274534c7629ce96d9dca9759d67bc682 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sat, 28 Mar 2020 21:15:43 -0600 Subject: [PATCH 008/142] Draft: polararray .ui file, Link array by default --- .../Resources/ui/TaskPanel_PolarArray.ui | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/Mod/Draft/Resources/ui/TaskPanel_PolarArray.ui b/src/Mod/Draft/Resources/ui/TaskPanel_PolarArray.ui index 5c85cf69f3..0994d58d79 100644 --- a/src/Mod/Draft/Resources/ui/TaskPanel_PolarArray.ui +++ b/src/Mod/Draft/Resources/ui/TaskPanel_PolarArray.ui @@ -54,7 +54,8 @@ - The coordinates of the point through which the axis of rotation passes. + The coordinates of the point through which the axis of rotation passes. +Change the direction of the axis itself in the property editor. Center of rotation @@ -127,7 +128,7 @@ - Reset the coordinates of the center of rotation + Reset the coordinates of the center of rotation. Reset point @@ -142,7 +143,8 @@ - If checked, the resulting objects in the array will be fused if they touch each other + If checked, the resulting objects in the array will be fused if they touch each other. +This only works if "Link array" is off. Fuse @@ -152,10 +154,14 @@ - If checked, the resulting objects in the array will be Links instead of simple copies + If checked, the resulting object will be a "Link array" instead of a regular array. +A Link array is more efficient when creating multiple copies, but it cannot be fused together. - Use Links + Link array + + + true @@ -166,7 +172,9 @@ - Sweeping angle of the polar distribution + Sweeping angle of the polar distribution. +A negative angle produces a polar pattern in the opposite direction. +The maximum absolute value is 360 degrees. Polar angle @@ -176,20 +184,29 @@ - Sweeping angle of the polar distribution + Sweeping angle of the polar distribution. +A negative angle produces a polar pattern in the opposite direction. +The maximum absolute value is 360 degrees. + + -360.000000000000000 + + + 360.000000000000000 + - 180.000000000000000 + 360.000000000000000 - Number of elements in the array, including a copy of the original object. It must be at least 2. + Number of elements in the array, including a copy of the original object. +It must be at least 2. Number of elements @@ -199,10 +216,14 @@ - Number of elements in the array, including a copy of the original object. It must be at least 2. + Number of elements in the array, including a copy of the original object. +It must be at least 2. + + + 2 - 4 + 5 From 4d0c0d2d734c0c22bd569ef867ae720079d5240f Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sat, 28 Mar 2020 21:16:55 -0600 Subject: [PATCH 009/142] Draft: parameters to control array options in the task panel Use the value of the parameters `Draft_array_fuse` and `Draft_array_Link` to set the default value of the `Fuse` and `Link array` checkboxes in the task panels. These default to `False` and `True`, respectively. Whenever the user toggles a checkbox the new value of the parameter is stored so that when the command is used again the last state of the checkbox is remembered. --- src/Mod/Draft/drafttaskpanels/task_circulararray.py | 7 +++++-- src/Mod/Draft/drafttaskpanels/task_orthoarray.py | 7 +++++-- src/Mod/Draft/drafttaskpanels/task_polararray.py | 7 +++++-- src/Mod/Draft/draftutils/utils.py | 3 ++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Mod/Draft/drafttaskpanels/task_circulararray.py b/src/Mod/Draft/drafttaskpanels/task_circulararray.py index 13bab39ea1..bcab6bf807 100644 --- a/src/Mod/Draft/drafttaskpanels/task_circulararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_circulararray.py @@ -32,6 +32,7 @@ import FreeCAD as App import FreeCADGui as Gui import Draft_rc # include resources, icons, ui files import DraftVecUtils +import draftutils.utils as utils from draftutils.messages import _msg, _wrn, _err, _log from draftutils.translate import _tr from FreeCAD import Units as U @@ -132,8 +133,8 @@ class TaskPanelCircularArray: self.form.input_c_z.setProperty('rawValue', self.center.z) self.form.input_c_z.setProperty('unit', length_unit) - self.fuse = False - self.use_link = True + self.fuse = utils.get_param("Draft_array_fuse", False) + self.use_link = utils.get_param("Draft_array_Link", True) self.form.checkbox_fuse.setChecked(self.fuse) self.form.checkbox_link.setChecked(self.use_link) @@ -354,6 +355,7 @@ class TaskPanelCircularArray: """Execute as a callback when the fuse checkbox changes.""" self.fuse = self.form.checkbox_fuse.isChecked() self.print_fuse_state(self.fuse) + utils.set_param("Draft_array_fuse", self.fuse) def print_link_state(self, use_link): """Print the link state translated.""" @@ -367,6 +369,7 @@ class TaskPanelCircularArray: """Execute as a callback when the link checkbox changes.""" self.use_link = self.form.checkbox_link.isChecked() self.print_link_state(self.use_link) + utils.set_param("Draft_array_Link", self.use_link) def print_messages(self): """Print messages about the operation.""" diff --git a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py index 27906ae887..554d124599 100644 --- a/src/Mod/Draft/drafttaskpanels/task_orthoarray.py +++ b/src/Mod/Draft/drafttaskpanels/task_orthoarray.py @@ -32,6 +32,7 @@ import FreeCAD as App import FreeCADGui as Gui import Draft_rc # include resources, icons, ui files import DraftVecUtils +import draftutils.utils as utils from draftutils.messages import _msg, _err, _log from draftutils.translate import _tr from FreeCAD import Units as U @@ -133,8 +134,8 @@ class TaskPanelOrthoArray: self.form.spinbox_n_Y.setValue(self.n_y) self.form.spinbox_n_Z.setValue(self.n_z) - self.fuse = False - self.use_link = True + self.fuse = utils.get_param("Draft_array_fuse", False) + self.use_link = utils.get_param("Draft_array_Link", True) self.form.checkbox_fuse.setChecked(self.fuse) self.form.checkbox_link.setChecked(self.use_link) @@ -348,6 +349,7 @@ class TaskPanelOrthoArray: """Execute as a callback when the fuse checkbox changes.""" self.fuse = self.form.checkbox_fuse.isChecked() self.print_fuse_state(self.fuse) + utils.set_param("Draft_array_fuse", self.fuse) def print_link_state(self, use_link): """Print the link state translated.""" @@ -361,6 +363,7 @@ class TaskPanelOrthoArray: """Execute as a callback when the link checkbox changes.""" self.use_link = self.form.checkbox_link.isChecked() self.print_link_state(self.use_link) + utils.set_param("Draft_array_Link", self.use_link) def print_messages(self): """Print messages about the operation.""" diff --git a/src/Mod/Draft/drafttaskpanels/task_polararray.py b/src/Mod/Draft/drafttaskpanels/task_polararray.py index a3bd08107f..48d55c2a53 100644 --- a/src/Mod/Draft/drafttaskpanels/task_polararray.py +++ b/src/Mod/Draft/drafttaskpanels/task_polararray.py @@ -32,6 +32,7 @@ import FreeCAD as App import FreeCADGui as Gui import Draft_rc # include resources, icons, ui files import DraftVecUtils +import draftutils.utils as utils from draftutils.messages import _msg, _wrn, _err, _log from draftutils.translate import _tr from FreeCAD import Units as U @@ -120,8 +121,8 @@ class TaskPanelPolarArray: self.form.input_c_z.setProperty('rawValue', self.center.z) self.form.input_c_z.setProperty('unit', length_unit) - self.fuse = False - self.use_link = True + self.fuse = utils.get_param("Draft_array_fuse", False) + self.use_link = utils.get_param("Draft_array_Link", True) self.form.checkbox_fuse.setChecked(self.fuse) self.form.checkbox_link.setChecked(self.use_link) @@ -302,6 +303,7 @@ class TaskPanelPolarArray: """Execute as a callback when the fuse checkbox changes.""" self.fuse = self.form.checkbox_fuse.isChecked() self.print_fuse_state(self.fuse) + utils.set_param("Draft_array_fuse", self.fuse) def print_link_state(self, use_link): """Print the link state translated.""" @@ -315,6 +317,7 @@ class TaskPanelPolarArray: """Execute as a callback when the link checkbox changes.""" self.use_link = self.form.checkbox_link.isChecked() self.print_link_state(self.use_link) + utils.set_param("Draft_array_Link", self.use_link) def print_messages(self): """Print messages about the operation.""" diff --git a/src/Mod/Draft/draftutils/utils.py b/src/Mod/Draft/draftutils/utils.py index b13d3e3d87..ee93388f47 100644 --- a/src/Mod/Draft/draftutils/utils.py +++ b/src/Mod/Draft/draftutils/utils.py @@ -157,7 +157,8 @@ def get_param_type(param): "SvgLinesBlack", "dxfStdSize", "showSnapBar", "hideSnapBar", "alwaysShowGrid", "renderPolylineWidth", "showPlaneTracker", "UsePartPrimitives", - "DiscretizeEllipses", "showUnit"): + "DiscretizeEllipses", "showUnit", + "Draft_array_fuse", "Draft_array_Link"): return "bool" elif param in ("color", "constructioncolor", "snapcolor", "gridColor"): From aed8c9140b82a0894f13d7518148593c286fd936 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Fri, 10 Apr 2020 14:31:14 +0200 Subject: [PATCH 010/142] Arch: Export ortho arrays to IFC --- src/Mod/Arch/exportIFC.py | 242 +++++++++++++++++++++++++++----------- 1 file changed, 176 insertions(+), 66 deletions(-) diff --git a/src/Mod/Arch/exportIFC.py b/src/Mod/Arch/exportIFC.py index f0b3c71cbe..596b137331 100644 --- a/src/Mod/Arch/exportIFC.py +++ b/src/Mod/Arch/exportIFC.py @@ -137,7 +137,8 @@ def getPreferences(): 'ADD_DEFAULT_STOREY': p.GetBool("IfcAddDefaultStorey",False), 'ADD_DEFAULT_BUILDING': p.GetBool("IfcAddDefaultBuilding",True), 'IFC_UNIT': u, - 'SCALE_FACTOR': f + 'SCALE_FACTOR': f, + 'GET_STANDARD': p.GetBool("getStandardType",False) } return preferences @@ -162,6 +163,8 @@ def export(exportList,filename,colors=None,preferences=None): FreeCAD.Console.PrintMessage("Visit https://www.freecadweb.org/wiki/Arch_IFC to learn how to install it\n") return + # process template + version = FreeCAD.Version() owner = FreeCAD.ActiveDocument.CreatedBy email = '' @@ -171,11 +174,10 @@ def export(exportList,filename,colors=None,preferences=None): email = s[1].strip(">") global template template = ifctemplate.replace("$version",version[0]+"."+version[1]+" build "+version[2]) - getstd = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetBool("getStandardType",False) if hasattr(ifcopenshell,"schema_identifier"): schema = ifcopenshell.schema_identifier elif hasattr(ifcopenshell,"version") and (float(ifcopenshell.version[:3]) >= 0.6): - # v0.6 allows to set our own schema + # v0.6 onwards allows to set our own schema schema = ["IFC4", "IFC2X3"][FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetInt("IfcVersion",0)] else: schema = "IFC2X3" @@ -196,6 +198,9 @@ def export(exportList,filename,colors=None,preferences=None): of.write(template) of.close() os.close(templatefilehandle) + + # create IFC file + global ifcfile, surfstyles, clones, sharedobjects, profiledefs, shapedefs ifcfile = ifcopenshell.open(templatefile) ifcfile = exportIFCHelper.writeUnits(ifcfile,preferences["IFC_UNIT"]) @@ -211,13 +216,17 @@ def export(exportList,filename,colors=None,preferences=None): if obj.Shape: if obj.Shape.Edges and (not obj.Shape.Faces): annotations.append(obj) + # clean objects list of unwanted types + objectslist = [obj for obj in objectslist if obj not in annotations] objectslist = Arch.pruneIncluded(objectslist,strict=True) objectslist = [obj for obj in objectslist if Draft.getType(obj) not in ["Dimension","Material","MaterialContainer","WorkingPlaneProxy"]] if preferences['FULL_PARAMETRIC']: objectslist = Arch.getAllChildren(objectslist) + # create project and context + contextCreator = exportIFCHelper.ContextCreator(ifcfile, objectslist) context = contextCreator.model_view_subcontext project = contextCreator.project @@ -227,6 +236,8 @@ def export(exportList,filename,colors=None,preferences=None): decl = Draft.getObjectsOfType(objectslist, "Site")[0].Declination.getValueAs(FreeCAD.Units.Radian) contextCreator.model_context.TrueNorth.DirectionRatios = (math.cos(decl+math.pi/2), math.sin(decl+math.pi/2)) + # define holders for the different types we create + products = {} # { Name: IfcEntity, ... } subproducts = {} # { Name: IfcEntity, ... } for storing additions/subtractions and other types of subcomponents of a product surfstyles = {} # { (r,g,b): IfcEntity, ... } @@ -267,33 +278,57 @@ def export(exportList,filename,colors=None,preferences=None): # getting generic data - name = obj.Label - if six.PY2: - name = name.encode("utf8") - description = obj.Description if hasattr(obj,"Description") else "" - if six.PY2: - description = description.encode("utf8") - - # getting uid - - uid = None - if hasattr(obj,"IfcData"): - if "IfcUID" in obj.IfcData.keys(): - uid = str(obj.IfcData["IfcUID"]) - if not uid: - uid = ifcopenshell.guid.new() - # storing the uid for further use - if preferences['STORE_UID'] and hasattr(obj,"IfcData"): - d = obj.IfcData - d["IfcUID"] = uid - obj.IfcData = d - + name = getText("Name",obj) + description = getText("Description",obj) + uid = getUID(obj,preferences) ifctype = getIfcTypeFromObj(obj) if ifctype == "IfcGroup": groups[obj.Name] = [o.Name for o in obj.Group] continue + # handle assemblies (arrays, app::parts, references, etc...) + + assemblyElements = [] + + if ifctype == "IfcArray": + if obj.ArrayType == "ortho": + clonedeltas = [] + for i in range(obj.NumberX): + clonedeltas.append(obj.Placement.Base+(i*obj.IntervalX)) + for j in range(obj.NumberY): + if j > 0: + clonedeltas.append(obj.Placement.Base+(i*obj.IntervalX)+(j*obj.IntervalY)) + for k in range(obj.NumberZ): + if k > 0: + clonedeltas.append(obj.Placement.Base+(i*obj.IntervalX)+(j*obj.IntervalY)+(k*obj.IntervalZ)) + #print("clonedeltas:",clonedeltas) + for delta in clonedeltas: + representation,placement,shapetype = getRepresentation( + ifcfile, + context, + obj.Base, + forcebrep=(getBrepFlag(obj.Base,preferences)), + colors=colors, + preferences=preferences, + forceclone=delta + ) + subproduct = createProduct( + ifcfile, + obj.Base, + getIfcTypeFromObj(obj.Base), + getUID(obj.Base,preferences), + history, + getText("Name",obj.Base), + getText("Description",obj.Base), + placement, + representation, + preferences, + schema) + + assemblyElements.append(subproduct) + ifctype = "IfcElementAssembly" + # export grids if ifctype in ["IfcAxis","IfcAxisSystem","IfcGrid"]: @@ -356,61 +391,55 @@ def export(exportList,filename,colors=None,preferences=None): if ifctype not in ArchIFCSchema.IfcProducts.keys(): ifctype = "IfcBuildingElementProxy" - # getting the "Force BREP" flag - - brepflag = False - if hasattr(obj,"IfcData"): - if "FlagForceBrep" in obj.IfcData.keys(): - if obj.IfcData["FlagForceBrep"] == "True": - brepflag = True - # getting the representation representation,placement,shapetype = getRepresentation( ifcfile, context, obj, - forcebrep=(brepflag or preferences['FORCE_BREP']), + forcebrep=(getBrepFlag(obj,preferences)), colors=colors, preferences=preferences ) - if getstd: + if preferences['GET_STANDARD']: if isStandardCase(obj,ifctype): ifctype += "StandardCase" - if preferences['DEBUG']: print(str(count).ljust(3)," : ", ifctype, " (",shapetype,") : ",name) - - # setting the arguments - - kwargs = { - "GlobalId": uid, - "OwnerHistory": history, - "Name": name, - "Description": description, - "ObjectPlacement": placement, - "Representation": representation - } - if ifctype == "IfcSite": - kwargs.update({ - "RefLatitude":dd2dms(obj.Latitude), - "RefLongitude":dd2dms(obj.Longitude), - "RefElevation":obj.Elevation.Value*preferences['SCALE_FACTOR'], - "SiteAddress":buildAddress(obj,ifcfile), - "CompositionType": "ELEMENT" - }) - if schema == "IFC2X3": - kwargs = exportIFC2X3Attributes(obj, kwargs, preferences['SCALE_FACTOR']) - else: - kwargs = exportIfcAttributes(obj, kwargs, preferences['SCALE_FACTOR']) + if preferences['DEBUG']: + print(str(count).ljust(3)," : ", ifctype, " (",shapetype,") : ",name) # creating the product - #print(obj.Label," : ",ifctype," : ",kwargs) - product = getattr(ifcfile,"create"+ifctype)(**kwargs) + product = createProduct( + ifcfile, + obj, + ifctype, + uid, + history, + name, + description, + placement, + representation, + preferences, + schema) + products[obj.Name] = product if ifctype in ["IfcBuilding","IfcBuildingStorey","IfcSite","IfcSpace"]: spatialelements[obj.Name] = product + # gather assembly subelements + + if assemblyElements: + ifcfile.createIfcRelAggregates( + ifcopenshell.guid.new(), + history, + 'Assembly', + '', + products[obj.Name], + assemblyElements + ) + if preferences['DEBUG']: print(" aggregating",len(assemblyElements),"object(s)") + # additions if hasattr(obj,"Additions") and (shapetype in ["extrusion","no shape"]): @@ -1743,9 +1772,10 @@ def getProfile(ifcfile,p): return profile -def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tessellation=1,colors=None,preferences=None): +def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tessellation=1,colors=None,preferences=None,forceclone=False): - """returns an IfcShapeRepresentation object or None""" + """returns an IfcShapeRepresentation object or None. forceclone can be False (does nothing), + "store" or True (stores the object as clone base) or a Vector (creates a clone)""" import Part import DraftGeomUtils @@ -1756,20 +1786,27 @@ def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tess shapetype = "no shape" tostore = False subplacement = None + skipshape = False # check for clones - if (not subtraction) and (not forcebrep): + if ((not subtraction) and (not forcebrep)) or forceclone: + if forceclone: + if not obj.Name in clones: + clones[obj.Name] = [] for k,v in clones.items(): if (obj.Name == k) or (obj.Name in v): if k in sharedobjects: # base shape already exists repmap = sharedobjects[k] pla = obj.getGlobalPlacement() + pos = FreeCAD.Vector(pla.Base) + if isinstance(forceclone,FreeCAD.Vector): + pos += forceclone axis1 = ifcbin.createIfcDirection(tuple(pla.Rotation.multVec(FreeCAD.Vector(1,0,0)))) axis2 = ifcbin.createIfcDirection(tuple(pla.Rotation.multVec(FreeCAD.Vector(0,1,0)))) axis3 = ifcbin.createIfcDirection(tuple(pla.Rotation.multVec(FreeCAD.Vector(0,0,1)))) - origin = ifcbin.createIfcCartesianPoint(tuple(FreeCAD.Vector(pla.Base).multiply(preferences['SCALE_FACTOR']))) + origin = ifcbin.createIfcCartesianPoint(tuple(pos.multiply(preferences['SCALE_FACTOR']))) transf = ifcbin.createIfcCartesianTransformationOperator3D(axis1,axis2,origin,1.0,axis3) mapitem = ifcfile.createIfcMappedItem(repmap,transf) shapes = [mapitem] @@ -1783,7 +1820,11 @@ def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tess if obj.isDerivedFrom("Part::Feature") and (len(obj.Shape.Solids) > 1) and hasattr(obj,"Axis") and obj.Axis: forcebrep = True - if (not shapes) and (not forcebrep): + # specific cases that must ignore their own shape + if Draft.getType(obj) in ["Array"]: + skipshape = True + + if (not shapes) and (not forcebrep) and (not skipshape): profile = None ev = FreeCAD.Vector() if hasattr(obj,"Proxy"): @@ -1858,7 +1899,7 @@ def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tess solidType = "SweptSolid" shapetype = "extrusion" - if not shapes: + if (not shapes) and (not skipshape): # check if we keep a null shape (additions-only object) @@ -2106,3 +2147,72 @@ def getRepresentation(ifcfile,context,obj,forcebrep=False,subtraction=False,tess productdef = ifcfile.createIfcProductDefinitionShape(None,None,[representation]) return productdef,placement,shapetype + + +def getBrepFlag(obj,preferences): + """returns True if the object must be exported as BREP""" + brepflag = False + if preferences['FORCE_BREP']: + return True + if hasattr(obj,"IfcData"): + if "FlagForceBrep" in obj.IfcData.keys(): + if obj.IfcData["FlagForceBrep"] == "True": + brepflag = True + return brepflag + + +def createProduct(ifcfile,obj,ifctype,uid,history,name,description,placement,representation,preferences,schema): + """creates a product in the given IFC file""" + + kwargs = { + "GlobalId": uid, + "OwnerHistory": history, + "Name": name, + "Description": description, + "ObjectPlacement": placement, + "Representation": representation + } + if ifctype == "IfcSite": + kwargs.update({ + "RefLatitude":dd2dms(obj.Latitude), + "RefLongitude":dd2dms(obj.Longitude), + "RefElevation":obj.Elevation.Value*preferences['SCALE_FACTOR'], + "SiteAddress":buildAddress(obj,ifcfile), + "CompositionType": "ELEMENT" + }) + if schema == "IFC2X3": + kwargs = exportIFC2X3Attributes(obj, kwargs, preferences['SCALE_FACTOR']) + else: + kwargs = exportIfcAttributes(obj, kwargs, preferences['SCALE_FACTOR']) + product = getattr(ifcfile,"create"+ifctype)(**kwargs) + return product + + +def getUID(obj,preferences): + """gets or creates an UUID for an object""" + + uid = None + if hasattr(obj,"IfcData"): + if "IfcUID" in obj.IfcData.keys(): + uid = str(obj.IfcData["IfcUID"]) + if not uid: + uid = ifcopenshell.guid.new() + # storing the uid for further use + if preferences['STORE_UID'] and hasattr(obj,"IfcData"): + d = obj.IfcData + d["IfcUID"] = uid + obj.IfcData = d + return uid + + +def getText(field,obj): + """Returns the value of a text property of an object""" + + result = "" + if field == "Name": + field = "Label" + if hasattr(obj,field): + result = getattr(obj,field) + if six.PY2: + result = result.encode("utf8") + return result From ba7b32e6bcc8b93252d171fbfba98826e25c7f3b Mon Sep 17 00:00:00 2001 From: wandererfan Date: Wed, 8 Apr 2020 21:03:16 -0400 Subject: [PATCH 011/142] [Draft]support BSplineCurve in getNormal --- src/Mod/Draft/DraftGeomUtils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Mod/Draft/DraftGeomUtils.py b/src/Mod/Draft/DraftGeomUtils.py index cd6c9c1b68..6c2cd6ed2b 100644 --- a/src/Mod/Draft/DraftGeomUtils.py +++ b/src/Mod/Draft/DraftGeomUtils.py @@ -1178,6 +1178,17 @@ def isReallyClosed(wire): if DraftVecUtils.equals(v1,v2): return True return False +def getSplineNormal(edge): + """Find the normal of a BSpline edge""" + startPoint = edge.valueAt(edge.FirstParameter) + endPoint = edge.valueAt(edge.LastParameter) + midParameter = edge.FirstParameter + (edge.LastParameter - edge.FirstParameter)/2 + midPoint = edge.valueAt(midParameter) + v1 = midPoint - startPoint + v2 = midPoint - endPoint + n = v1.cross(v2) + n.normalize() + return n def getNormal(shape): """Find the normal of a shape, if possible.""" @@ -1189,11 +1200,18 @@ def getNormal(shape): elif shape.ShapeType == "Edge": if geomType(shape.Edges[0]) in ["Circle","Ellipse"]: n = shape.Edges[0].Curve.Axis + elif geomType(edge) == "BSplineCurve" or \ + geomType(edge) == "BezierCurve": + n = getSplineNormal(edge) else: for e in shape.Edges: if geomType(e) in ["Circle","Ellipse"]: n = e.Curve.Axis break + elif geomType(e) == "BSplineCurve" or \ + geomType(e) == "BezierCurve": + n = getSplineNormal(e) + break e1 = vec(shape.Edges[0]) for i in range(1,len(shape.Edges)): e2 = vec(shape.Edges[i]) From bc9569515a029b2c6fd2ae537f12544806d95591 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Sat, 11 Apr 2020 17:05:59 +0200 Subject: [PATCH 012/142] Add size parameter for syntax highlighting of GCode editor --- src/Mod/Path/PathScripts/PathInspect.py | 17 +++++++++++++++-- src/Mod/Path/PathScripts/PostUtils.py | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathInspect.py b/src/Mod/Path/PathScripts/PathInspect.py index 448bc6b685..19cc291a78 100644 --- a/src/Mod/Path/PathScripts/PathInspect.py +++ b/src/Mod/Path/PathScripts/PathInspect.py @@ -112,7 +112,6 @@ class GCodeEditorDialog(QtGui.QDialog): font.setPointSize(p.GetInt("FontSize", 10)) self.editor.setFont(font) self.editor.setText("G01 X55 Y4.5 F300.0") - self.highlighter = GCodeHighlighter(self.editor.document()) layout.addWidget(self.editor) # Note @@ -192,11 +191,25 @@ class GCodeEditorDialog(QtGui.QDialog): def show(obj): "show(obj): shows the G-code data of the given Path object in a dialog" - + + prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") + # default Max Highlighter Size = 512 Ko + defaultMHS = 512 * 1024 + mhs = prefs.GetUnsigned('inspecteditorMaxHighlighterSize', defaultMHS) + if hasattr(obj, "Path"): if obj.Path: dia = GCodeEditorDialog(obj.Path) dia.editor.setText(obj.Path.toGCode()) + gcodeSize = len(dia.editor.toPlainText()) + if (gcodeSize <= mhs): + # because of poor performance, syntax highlighting is + # limited to mhs octets (default 512 KB). + # It seems than the response time curve has an inflexion near 500 KB + # beyond 500 KB, the response time increases exponentially. + dia.highlighter = GCodeHighlighter(dia.editor.document()) + else: + FreeCAD.Console.PrintMessage(translate("Path", "GCode size too big ({} o), disabling syntax highlighter.".format(gcodeSize))) result = dia.exec_() # exec_() returns 0 or 1 depending on the button pressed (Ok or # Cancel) diff --git a/src/Mod/Path/PathScripts/PostUtils.py b/src/Mod/Path/PathScripts/PostUtils.py index 26f6ba491e..5acadafc79 100644 --- a/src/Mod/Path/PathScripts/PostUtils.py +++ b/src/Mod/Path/PathScripts/PostUtils.py @@ -78,7 +78,6 @@ class GCodeEditorDialog(QtGui.QDialog): font.setPointSize(10) self.editor.setFont(font) self.editor.setText("G01 X55 Y4.5 F300.0") - self.highlighter = GCodeHighlighter(self.editor.document()) layout.addWidget(self.editor) # OK and Cancel buttons @@ -134,8 +133,23 @@ def fmt(num,dec,units): def editor(gcode): '''pops up a handy little editor to look at the code output ''' + + prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") + # default Max Highlighter Size = 512 Ko + defaultMHS = 512 * 1024 + mhs = prefs.GetUnsigned('inspecteditorMaxHighlighterSize', defaultMHS) + dia = GCodeEditorDialog() dia.editor.setText(gcode) + gcodeSize = len(dia.editor.toPlainText()) + if (gcodeSize <= mhs): + # because of poor performance, syntax highlighting is + # limited to mhs octets (default 512 KB). + # It seems than the response time curve has an inflexion near 500 KB + # beyond 500 KB, the response time increases exponentially. + dia.highlighter = GCodeHighlighter(dia.editor.document()) + else: + FreeCAD.Console.PrintMessage(translate("Path", "GCode size too big ({} o), disabling syntax highlighter.".format(gcodeSize))) result = dia.exec_() if result: # If user selected 'OK' get modified G Code final = dia.editor.toPlainText() From a05ffd499366ab12fa1b43f37db73f4f1e9c332d Mon Sep 17 00:00:00 2001 From: WandererFan Date: Fri, 10 Apr 2020 21:30:57 -0400 Subject: [PATCH 013/142] [TD]apply global placement --- src/Mod/TechDraw/App/ShapeExtractor.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Mod/TechDraw/App/ShapeExtractor.cpp b/src/Mod/TechDraw/App/ShapeExtractor.cpp index 35b4303449..29fac29def 100644 --- a/src/Mod/TechDraw/App/ShapeExtractor.cpp +++ b/src/Mod/TechDraw/App/ShapeExtractor.cpp @@ -415,16 +415,18 @@ Base::Vector3d ShapeExtractor::getLocation3dFromFeat(App::DocumentObject* obj) // if (isDraftPoint(obj) { // //Draft Points are not necc. Part::PartFeature?? // //if Draft option "use part primitives" is not set are Draft points still PartFeature? -// Base::Vector3d featPos = features[i]->(Placement.getValue()).Position(); Part::Feature* pf = dynamic_cast(obj); if (pf != nullptr) { - TopoDS_Shape ts = pf->Shape.getValue(); + Part::TopoShape pts = pf->Shape.getShape(); + pts.setPlacement(pf->globalPlacement()); + TopoDS_Shape ts = pts.getShape(); if (ts.ShapeType() == TopAbs_VERTEX) { TopoDS_Vertex v = TopoDS::Vertex(ts); result = DrawUtil::vertex2Vector(v); } } + // Base::Console().Message("SE::getLocation3dFromFeat - returns: %s\n", // DrawUtil::formatVector(result).c_str()); return result; From 15318f4957ffc384e89ef0ced2dc757e8f5b9922 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Sun, 12 Apr 2020 06:33:34 +0100 Subject: [PATCH 014/142] Deburr: signal for update when values are changed --- src/Mod/Path/PathScripts/PathDeburrGui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathDeburrGui.py b/src/Mod/Path/PathScripts/PathDeburrGui.py index ed25c644fa..6334b6e9fc 100644 --- a/src/Mod/Path/PathScripts/PathDeburrGui.py +++ b/src/Mod/Path/PathScripts/PathDeburrGui.py @@ -101,6 +101,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): signals.append(self.form.joinRound.clicked) signals.append(self.form.coolantController.currentIndexChanged) signals.append(self.form.direction.currentIndexChanged) + signals.append(self.form.value_W.valueChanged) + signals.append(self.form.value_h.valueChanged) return signals def registerSignalHandlers(self, obj): From dd7e3ec729ca936c52813fb738acf4bbe42a4b63 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Sun, 12 Apr 2020 08:04:32 +0100 Subject: [PATCH 015/142] Ensure join type is shown --- .../Gui/Resources/panels/PageOpDeburrEdit.ui | 553 ++++++++++-------- 1 file changed, 318 insertions(+), 235 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpDeburrEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpDeburrEdit.ui index 452e6ab7e3..31eb02df70 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpDeburrEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpDeburrEdit.ui @@ -6,14 +6,14 @@ 0 0 - 399 - 333 + 321 + 529 Form - + @@ -31,6 +31,24 @@ + + + 0 + 0 + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + Tool Controller @@ -45,6 +63,24 @@ + + + 0 + 0 + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + Coolant Mode @@ -60,254 +96,301 @@ + + + + 6 + + + 12 + + + 12 + + + + + + 0 + 0 + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + + + Direction + + + + + + + <html><head/><body><p>The direction in which the profile is performed, clockwise or counter clockwise.</p></body></html> + + + CW + + + 0 + + + + CW + + + + + CCW + + + + + + - - - - - - - 0 - - - 0 - - - 0 - - - 9 - - - - - - QFormLayout::AllNonFixedFieldsGrow - - - - - W = - - + + + + + + 125 + 0 + + + + + 16777215 + 16777215 + + + + + + + QLayout::SetDefaultConstraint + + + + + + + + + + 50 + 0 + + + + W = + + + + + + + <html><head/><body><p>Width of chamfer cut.</p></body></html> + + + mm + + + + - - - - <html><head/><body><p>Width of chamfer cut.</p></body></html> - - - mm - - + + + + + + + 50 + 0 + + + + h = + + + + + + + <html><head/><body><p>Extra depth of tool immersion.</p></body></html> + + + mm + + + + - - - - h = - - - - - - - <html><head/><body><p>Extra depth of tool immersion.</p></body></html> - - - mm - - - - - - - - - - - - - Join: - - - - - + + - Qt::Horizontal + Qt::Vertical + + + QSizePolicy::Fixed - 40 - 20 + 20 + 10 - - - - <html><head/><body><p>Miter joint</p></body></html> - - - - - - true - - - true - - - - - - - <html><head/><body><p>Round joint</p></body></html> - - - - - - true - - - true - - - true - - + + + + + + + 50 + 0 + + + + Join: + + + + + + + <html><head/><body><p>Round joint</p></body></html> + + + + + + true + + + true + + + true + + + + + + + <html><head/><body><p>Miter joint</p></body></html> + + + + + + true + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + - - - - - - - - - - - - 0 - 0 - - - - - 150 - 150 - - - - - 150 - 150 - - - - TextLabel - - - true - - - Qt::AlignCenter - - - - - - - Qt::Vertical - - - - 20 - 40 - - - + + + + + Qt::Vertical + + + + 20 + 40 + + + + + - - - - - - - - - 9 - - - 9 - - - 9 - - - 9 - - - 8 - - - - - <html><head/><body><p>The direction in which the profile is performed, clockwise or counter clockwise.</p></body></html> - - - CW - - - 0 - - - - CW - + + + + + + + + + + + + 0 + 0 + + + + + 150 + 150 + + + + + 150 + 150 + + + + TextLabel + + + true + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + - - - CCW - - - - - - - - - 0 - 0 - - - - Direction - - - - - - - Qt::Horizontal - - - QSizePolicy::Preferred - - - - 30 - 20 - - - - - - + + + + From 4045b2fa3cbd574467aa2b90b2e813158696a196 Mon Sep 17 00:00:00 2001 From: wandererfan Date: Sat, 11 Apr 2020 15:50:24 -0400 Subject: [PATCH 016/142] [TD]"<" symbol embedded in html --- src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp b/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp index 02c25ce9a4..64ff8f49a7 100644 --- a/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp +++ b/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp @@ -158,10 +158,19 @@ void QGIViewAnnotation::drawAnnotation() if (it != annoText.begin()) { ss << "
"; } - std::string u8String = Base::Tools::escapedUnicodeToUtf8(*it); -// what madness turns \' into \\\\\'? - std::string apos = std::regex_replace((u8String), std::regex("\\\\\'"), "'"); - ss << apos; + //TODO: there is still a bug here. entering "'" works, save and restore works, but edit after + // save and restore brings "\'" back into text. manually deleting the "\" fixes it until the next + // save/restore/edit cycle. + // a guess is that the editor for propertyStringList is too enthusiastic about substituting. + // the substituting might be necessary for using the strings in Python. + // ' doesn't seem to help in this case. + + std::string u8String = Base::Tools::escapedUnicodeToUtf8(*it); //from \x??\x?? to real utf8 + std::string apos = std::regex_replace((u8String), std::regex("\\\\"), ""); //remove doubles. + apos = std::regex_replace((apos), std::regex("\\'"), "'"); //replace escaped apos + //"less than" symbol chops off line. need to use html sub. + std::string lt = std::regex_replace((apos), std::regex("<"), "<"); + ss << lt; } ss << "

\n\n "; From 7dc60feab0239d9d37336376a535ce3134e25af3 Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Mon, 13 Apr 2020 14:33:11 +0800 Subject: [PATCH 017/142] Path: fix path sort --- src/Mod/Path/App/Area.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Mod/Path/App/Area.cpp b/src/Mod/Path/App/Area.cpp index 1badf749da..c0f775879b 100644 --- a/src/Mod/Path/App/Area.cpp +++ b/src/Mod/Path/App/Area.cpp @@ -1189,10 +1189,8 @@ static int foreachSubshape(const TopoDS_Shape &shape, BRep_Builder builder; TopoDS_Compound comp; builder.MakeCompound(comp); - for(auto &s : openShapes) { - for(TopExp_Explorer it(s,TopAbs_EDGE); it.More(); it.Next()) - builder.Add(comp,s); - } + for(auto &s : openShapes) + builder.Add(comp,s); func(comp, TopAbs_COMPOUND); return TopAbs_COMPOUND; } From 75623131fa809ec509e5a0c1800ccf1beb866669 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sat, 14 Mar 2020 00:34:41 -0600 Subject: [PATCH 018/142] Draft: new ShapeString icon for the tree view --- src/Mod/Draft/Draft.py | 2 + src/Mod/Draft/Resources/Draft.qrc | 1 + .../icons/Draft_ShapeString_tree.svg | 270 ++++++++++++++++++ 3 files changed, 273 insertions(+) create mode 100644 src/Mod/Draft/Resources/icons/Draft_ShapeString_tree.svg diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 2f0aea0d45..5d0c5d140a 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -3299,6 +3299,8 @@ class _ViewProviderDraft: return ":/icons/Draft_N-Polygon.svg" elif tp in ('Circle', 'Ellipse', 'BSpline', 'BezCurve', 'Fillet'): return ":/icons/Draft_N-Curve.svg" + elif tp in ("ShapeString"): + return ":/icons/Draft_ShapeString_tree.svg" else: return ":/icons/Draft_Draft.svg" diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index fc9da4a2e6..0b8b5145ee 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -66,6 +66,7 @@ icons/Draft_SelectGroup.svg icons/Draft_SelectPlane.svg icons/Draft_ShapeString.svg + icons/Draft_ShapeString_tree.svg icons/Draft_Slope.svg icons/Draft_Snap.svg icons/Draft_Split.svg diff --git a/src/Mod/Draft/Resources/icons/Draft_ShapeString_tree.svg b/src/Mod/Draft/Resources/icons/Draft_ShapeString_tree.svg new file mode 100644 index 0000000000..a71d1bb7bf --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_ShapeString_tree.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +   + + + + + image/svg+xml + + + + Mon Apr 15 13:25:25 2013 -0400 + + + [vocx] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_ShapeString_tree.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson, [wandererfan] + + + + + S + letter + + + A capital letter S, slightly italicized; color variation + + + + From 8afb0379bdc3e4626b0f4716d4555dff4a41bc93 Mon Sep 17 00:00:00 2001 From: lorenz Date: Sat, 11 Apr 2020 11:16:05 +0200 Subject: [PATCH 019/142] Draft: dwg-export: allow overwriting of files --- src/Mod/Draft/importDWG.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Draft/importDWG.py b/src/Mod/Draft/importDWG.py index 54dd50d35f..2820d02fca 100644 --- a/src/Mod/Draft/importDWG.py +++ b/src/Mod/Draft/importDWG.py @@ -261,7 +261,7 @@ def convertToDwg(dxffilename, dwgfilename): import shutil if shutil.which("dxf2dwg"): - proc = subprocess.Popen(("dxf2dwg", dxffilename, "-o", dwgfilename)) + proc = subprocess.Popen(("dxf2dwg", dxffilename, "-y", "-o", dwgfilename)) proc.communicate() return dwgfilename From f692494ada71dbe613042f59b99aa7f45c63f56f Mon Sep 17 00:00:00 2001 From: Jean-Marie Verdun Date: Fri, 10 Apr 2020 21:06:04 -0400 Subject: [PATCH 020/142] Use "simpler" naming convention per user request --- src/Mod/Cloud/App/AppCloud.cpp | 168 ++++++++++++++++----------------- src/Mod/Cloud/App/AppCloud.h | 62 ++++++------ 2 files changed, 115 insertions(+), 115 deletions(-) diff --git a/src/Mod/Cloud/App/AppCloud.cpp b/src/Mod/Cloud/App/AppCloud.cpp index e669c67cf9..ef53719043 100644 --- a/src/Mod/Cloud/App/AppCloud.cpp +++ b/src/Mod/Cloud/App/AppCloud.cpp @@ -62,46 +62,46 @@ PyMOD_INIT_FUNC(Cloud) PyMOD_Return(mod); } -Py::Object Cloud::Module::sCloudUrl(const Py::Tuple& args) +Py::Object Cloud::Module::sCloudURL(const Py::Tuple& args) { - char *Url; - if (!PyArg_ParseTuple(args.ptr(), "et","utf-8",&Url)) // convert args: Python->C + char *URL; + if (!PyArg_ParseTuple(args.ptr(), "et","utf-8",&URL)) // convert args: Python->C return Py::None(); - if (this->Url.getStrValue() != Url) { - this->Url.setValue(Url); + if (this->URL.getStrValue() != URL) { + this->URL.setValue(URL); } return Py::None(); } -Py::Object Cloud::Module::sCloudAccessKey(const Py::Tuple& args) +Py::Object Cloud::Module::sCloudTokenAuth(const Py::Tuple& args) { - char *AccessKey; - if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &AccessKey)) // convert args: Python->C + char *TokenAuth; + if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &TokenAuth)) // convert args: Python->C return Py::None(); - if (this->AccessKey.getStrValue() != AccessKey) { - this->AccessKey.setValue(AccessKey); + if (this->TokenAuth.getStrValue() != TokenAuth) { + this->TokenAuth.setValue(TokenAuth); } return Py::None(); } -Py::Object Cloud::Module::sCloudSecretKey(const Py::Tuple& args) +Py::Object Cloud::Module::sCloudTokenSecret(const Py::Tuple& args) { - char *SecretKey; - if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &SecretKey)) // convert args: Python->C + char *TokenSecret; + if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &TokenSecret)) // convert args: Python->C return Py::None(); - if (this->SecretKey.getStrValue() != SecretKey) { - this->SecretKey.setValue(SecretKey); + if (this->TokenSecret.getStrValue() != TokenSecret) { + this->TokenSecret.setValue(TokenSecret); } return Py::None(); } -Py::Object Cloud::Module::sCloudTcpPort(const Py::Tuple& args) +Py::Object Cloud::Module::sCloudTCPPort(const Py::Tuple& args) { - char *TcpPort; - if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &TcpPort)) // convert args: Python->C + char *TCPPort; + if (!PyArg_ParseTuple(args.ptr(), "et","utf-8", &TCPPort)) // convert args: Python->C return Py::None(); - if (this->TcpPort.getStrValue() != TcpPort) { - this->TcpPort.setValue(TcpPort); + if (this->TCPPort.getStrValue() != TCPPort) { + this->TCPPort.setValue(TCPPort); } return Py::None(); } @@ -207,7 +207,7 @@ void Cloud::CloudWriter::createBucket() char path[1024]; sprintf(path, "/%s/", this->Bucket); - RequestData = Cloud::ComputeDigestAmzS3v2("PUT", "application/xml", path, this->SecretKey, NULL, 0); + RequestData = Cloud::ComputeDigestAmzS3v2("PUT", "application/xml", path, this->TokenSecret, NULL, 0); // Let's build the Header and call to curl curl_global_init(CURL_GLOBAL_ALL); @@ -220,22 +220,22 @@ void Cloud::CloudWriter::createBucket() if ( curl ) { struct curl_slist *chunk = NULL; - char Url[256]; + char URL[256]; // Let's build our own header - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); - // Lets build the Url for our Curl call + // Lets build the URL for our Curl call - sprintf(Url,"%s:%s/%s/", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/", this->URL,this->TCPPort, this->Bucket); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); curl_easy_setopt(curl, CURLOPT_PUT, 1L); @@ -336,14 +336,14 @@ char *Cloud::MD5Sum(const char *ptr, long size) return(output); } -struct curl_slist *Cloud::BuildHeaderAmzS3v2(const char *Url, const char *TcpPort, const char *PublicKey, struct Cloud::AmzData *Data) +struct curl_slist *Cloud::BuildHeaderAmzS3v2(const char *URL, const char *TCPPort, const char *PublicKey, struct Cloud::AmzData *Data) { char header_data[1024]; struct curl_slist *chunk = NULL; // Build the Host: entry - sprintf(header_data,"Host: %s:%s", Url, TcpPort); + sprintf(header_data,"Host: %s:%s", URL, TCPPort); chunk = curl_slist_append(chunk, header_data); // Build the Date entry @@ -375,7 +375,7 @@ struct curl_slist *Cloud::BuildHeaderAmzS3v2(const char *Url, const char *TcpPor return chunk; } -Cloud::CloudWriter::CloudWriter(const char* Url, const char* AccessKey, const char* SecretKey, const char* TcpPort, const char* Bucket) +Cloud::CloudWriter::CloudWriter(const char* URL, const char* TokenAuth, const char* TokenSecret, const char* TCPPort, const char* Bucket) { struct Cloud::AmzData *RequestData; CURL *curl; @@ -383,15 +383,15 @@ Cloud::CloudWriter::CloudWriter(const char* Url, const char* AccessKey, const ch std::string s; - this->Url=Url; - this->AccessKey=AccessKey; - this->SecretKey=SecretKey; - this->TcpPort=TcpPort; + this->URL=URL; + this->TokenAuth=TokenAuth; + this->TokenSecret=TokenSecret; + this->TCPPort=TCPPort; this->Bucket=Bucket; this->FileName=""; char path[1024]; sprintf(path,"/%s/", this->Bucket); - RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/xml", path, this->SecretKey, NULL, 0); + RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/xml", path, this->TokenSecret, NULL, 0); // Let's build the Header and call to curl curl_global_init(CURL_GLOBAL_ALL); curl = curl_easy_init(); @@ -403,21 +403,21 @@ Cloud::CloudWriter::CloudWriter(const char* Url, const char* AccessKey, const ch { // Let's build our own header struct curl_slist *chunk = NULL; - char Url[256]; - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + char URL[256]; + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); - // Lets build the Url for our Curl call + // Lets build the URL for our Curl call - sprintf(Url,"%s:%s/%s/", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/", this->URL,this->TCPPort, this->Bucket); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_StdString); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); @@ -557,7 +557,7 @@ Cloud::CloudReader::~CloudReader() { } -Cloud::CloudReader::CloudReader(const char* Url, const char* AccessKey, const char* SecretKey, const char* TcpPort, const char* Bucket) +Cloud::CloudReader::CloudReader(const char* URL, const char* TokenAuth, const char* TokenSecret, const char* TCPPort, const char* Bucket) { struct Cloud::AmzData *RequestData; CURL *curl; @@ -565,10 +565,10 @@ Cloud::CloudReader::CloudReader(const char* Url, const char* AccessKey, const ch bool GetBucketContentList=true; - this->Url=Url; - this->AccessKey=AccessKey; - this->SecretKey=SecretKey; - this->TcpPort=TcpPort; + this->URL=URL; + this->TokenAuth=TokenAuth; + this->TokenSecret=TokenSecret; + this->TCPPort=TCPPort; this->Bucket=Bucket; char path[1024]; @@ -584,7 +584,7 @@ Cloud::CloudReader::CloudReader(const char* Url, const char* AccessKey, const ch while ( GetBucketContentList ) { std::string s; - RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/xml", path, this->SecretKey, NULL, 0); + RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/xml", path, this->TokenSecret, NULL, 0); curl = curl_easy_init(); #ifdef ALLOW_SELF_SIGNED_CERTIFICATE curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); @@ -594,22 +594,22 @@ Cloud::CloudReader::CloudReader(const char* Url, const char* AccessKey, const ch { // Let's build our own header struct curl_slist *chunk = NULL; - char Url[256]; - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + char URL[256]; + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); if ( strlen(NextFileName) == 0 ) - sprintf(Url,"%s:%s/%s/?list-type=2", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/?list-type=2", this->URL,this->TCPPort, this->Bucket); else - sprintf(Url,"%s:%s/%s/?list-type=2&continuation-token=%s", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/?list-type=2&continuation-token=%s", this->URL,this->TCPPort, this->Bucket, NextFileName); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_StdString); @@ -669,7 +669,7 @@ void Cloud::CloudReader::DownloadFile(Cloud::CloudReader::FileEntry *entry) // We must get the directory content char path[1024]; sprintf(path, "/%s/%s", this->Bucket, entry->FileName); - RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/octet-stream", path, this->SecretKey, NULL, 0); + RequestData = Cloud::ComputeDigestAmzS3v2("GET", "application/octet-stream", path, this->TokenSecret, NULL, 0); // Let's build the Header and call to curl curl_global_init(CURL_GLOBAL_ALL); @@ -681,20 +681,20 @@ void Cloud::CloudReader::DownloadFile(Cloud::CloudReader::FileEntry *entry) if ( curl ) { struct curl_slist *chunk = NULL; - char Url[256]; + char URL[256]; // Let's build our own header - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); - sprintf(Url,"%s:%s/%s/%s", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/%s", this->URL,this->TCPPort, this->Bucket, entry->FileName); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWrite_CallbackFunc_StdString); @@ -783,7 +783,7 @@ void Cloud::CloudWriter::pushCloud(const char *FileName, const char *data, long char path[1024]; sprintf(path, "/%s/%s", this->Bucket, FileName); - RequestData = Cloud::ComputeDigestAmzS3v2("PUT", "application/octet-stream", path, this->SecretKey, data, size); + RequestData = Cloud::ComputeDigestAmzS3v2("PUT", "application/octet-stream", path, this->TokenSecret, data, size); // Let's build the Header and call to curl curl_global_init(CURL_GLOBAL_ALL); @@ -795,23 +795,23 @@ void Cloud::CloudWriter::pushCloud(const char *FileName, const char *data, long if ( curl ) { struct curl_slist *chunk = NULL; - char Url[256]; + char URL[256]; // Let's build our own header - std::string strUrl(this->Url); - eraseSubStr(strUrl,"http://"); - eraseSubStr(strUrl,"https://"); + std::string strURL(this->URL); + eraseSubStr(strURL,"http://"); + eraseSubStr(strURL,"https://"); - chunk = Cloud::BuildHeaderAmzS3v2( strUrl.c_str(), this->TcpPort, this->AccessKey, RequestData); + chunk = Cloud::BuildHeaderAmzS3v2( strURL.c_str(), this->TCPPort, this->TokenAuth, RequestData); delete RequestData; curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); - // Lets build the Url for our Curl call + // Lets build the URL for our Curl call - sprintf(Url,"%s:%s/%s/%s", this->Url,this->TcpPort, + sprintf(URL,"%s:%s/%s/%s", this->URL,this->TCPPort, this->Bucket,FileName); - curl_easy_setopt(curl, CURLOPT_URL, Url); + curl_easy_setopt(curl, CURLOPT_URL, URL); // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); @@ -898,10 +898,10 @@ bool Cloud::Module::cloudSave(const char *BucketName) if ( strcmp(BucketName, doc->Label.getValue()) != 0 ) doc->Label.setValue(BucketName); - Cloud::CloudWriter mywriter((const char*)this->Url.getStrValue().c_str(), - (const char*)this->AccessKey.getStrValue().c_str(), - (const char*)this->SecretKey.getStrValue().c_str(), - (const char*)this->TcpPort.getStrValue().c_str(), + Cloud::CloudWriter mywriter((const char*)this->URL.getStrValue().c_str(), + (const char*)this->TokenAuth.getStrValue().c_str(), + (const char*)this->TokenSecret.getStrValue().c_str(), + (const char*)this->TCPPort.getStrValue().c_str(), BucketName); mywriter.putNextEntry("Document.xml"); @@ -972,10 +972,10 @@ bool Cloud::Module::cloudRestore (const char *BucketName) std::stringstream oss; - Cloud::CloudReader myreader((const char*)this->Url.getStrValue().c_str(), - (const char*)this->AccessKey.getStrValue().c_str(), - (const char*)this->SecretKey.getStrValue().c_str(), - (const char*)this->TcpPort.getStrValue().c_str(), + Cloud::CloudReader myreader((const char*)this->URL.getStrValue().c_str(), + (const char*)this->TokenAuth.getStrValue().c_str(), + (const char*)this->TokenSecret.getStrValue().c_str(), + (const char*)this->TCPPort.getStrValue().c_str(), BucketName); // we shall pass there the initial Document.xml file diff --git a/src/Mod/Cloud/App/AppCloud.h b/src/Mod/Cloud/App/AppCloud.h index 63ee9e852a..23dcc75967 100644 --- a/src/Mod/Cloud/App/AppCloud.h +++ b/src/Mod/Cloud/App/AppCloud.h @@ -58,13 +58,13 @@ struct AmzData { void eraseSubStr(std::string & Str, const std::string & toErase); size_t CurlWrite_CallbackFunc_StdString(void *contents, size_t size, size_t nmemb, std::string *s); struct AmzData *ComputeDigestAmzS3v2(char *operation, char *data_type, const char *target, const char *Secret, const char *ptr, long size); -struct curl_slist *BuildHeaderAmzS3v2(const char *Url, const char *TcpPort, const char *PublicKey, struct AmzData *Data); +struct curl_slist *BuildHeaderAmzS3v2(const char *URL, const char *TCPPort, const char *PublicKey, struct AmzData *Data); char *MD5Sum(const char *ptr, long size); class CloudAppExport CloudReader { public: - CloudReader(const char* Url, const char* AccessKey, const char* SecretKey, const char* TcpPort, const char* Bucket); + CloudReader(const char* URL, const char* AccessKey, const char* SecretKey, const char* TCPPort, const char* Bucket); virtual ~CloudReader(); int file=0; int continuation=0; @@ -86,10 +86,10 @@ public: protected: std::list FileList; char* NextFileName; - const char* Url; - const char* TcpPort; - const char* AccessKey; - const char* SecretKey; + const char* URL; + const char* TCPPort; + const char* TokenAuth; + const char* TokenSecret; const char* Bucket; }; @@ -98,28 +98,28 @@ class Module : public Py::ExtensionModule public: Module() : Py::ExtensionModule("Cloud") { - add_varargs_method("cloudurl",&Module::sCloudUrl, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("URL",&Module::sCloudURL, + "URL(string) -- Connect to a Cloud Storage service." ); - add_varargs_method("cloudaccesskey",&Module::sCloudAccessKey, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("TokenAuth",&Module::sCloudTokenAuth, + "TokenAuth(string) -- Token Authorization string." ); - add_varargs_method("cloudsecretkey",&Module::sCloudSecretKey, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("TokenSecret",&Module::sCloudTokenSecret, + "TokenSecret(string) -- Token Secret string." ); - add_varargs_method("cloudtcpport",&Module::sCloudTcpPort, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("TCPPort",&Module::sCloudTCPPort, + "TCPPort(string) -- Port number." ); - add_varargs_method("cloudsave",&Module::sCloudSave, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("Save",&Module::sCloudSave, + "Save(string) -- Save the active document to the Cloud." ); - add_varargs_method("cloudrestore",&Module::sCloudRestore, - "cloudurl(string) -- Connect to a Cloud Storage service." + add_varargs_method("Restore",&Module::sCloudRestore, + "Restore(string) -- Restore to the active document from the Cloud." ); initialize("This module is the Cloud module."); // register with Python @@ -127,18 +127,18 @@ public: virtual ~Module() {} - App::PropertyString Url; - App::PropertyString TcpPort; - App::PropertyString AccessKey; - App::PropertyString SecretKey; + App::PropertyString URL; + App::PropertyString TCPPort; + App::PropertyString TokenAuth; + App::PropertyString TokenSecret; bool cloudSave(const char* BucketName); bool cloudRestore(const char* BucketName); private: - Py::Object sCloudUrl (const Py::Tuple& args); - Py::Object sCloudAccessKey (const Py::Tuple& args); - Py::Object sCloudSecretKey (const Py::Tuple& args); - Py::Object sCloudTcpPort (const Py::Tuple& args); + Py::Object sCloudURL (const Py::Tuple& args); + Py::Object sCloudTokenAuth (const Py::Tuple& args); + Py::Object sCloudTokenSecret (const Py::Tuple& args); + Py::Object sCloudTCPPort (const Py::Tuple& args); Py::Object sCloudSave (const Py::Tuple& args); Py::Object sCloudRestore (const Py::Tuple& args); @@ -158,7 +158,7 @@ class CloudAppExport CloudWriter : public Base::Writer public: int print=0; char errorCode[1024]=""; - CloudWriter(const char* Url, const char* AccessKey, const char* SecretKey, const char* TcpPort, const char* Bucket); + CloudWriter(const char* URL, const char* TokenAuth, const char* TokenSecret, const char* TCPPort, const char* Bucket); virtual ~CloudWriter(); void pushCloud(const char *FileName, const char *data, long size); void putNextEntry(const char* file); @@ -173,10 +173,10 @@ public: protected: std::string FileName; - const char* Url; - const char* TcpPort; - const char* AccessKey; - const char* SecretKey; + const char* URL; + const char* TCPPort; + const char* TokenAuth; + const char* TokenSecret; const char* Bucket; std::stringstream FileStream; }; From c2a2effac24d88877f7b0cf73bdcfb551f629232 Mon Sep 17 00:00:00 2001 From: Sebastian Bachmann Date: Thu, 9 Apr 2020 20:05:50 +0200 Subject: [PATCH 021/142] Resolve SyntaxWarning literal comparison in py3.8 Comparison with literals should be done using != and == and not 'is not' and 'is'. Found the files using: find . -name \*.py -exec pylint --disable=all --enable=R0123 --score=no {} \; Python 3.8 prints out SyntaxWarnings when reading the files, this would happen for example on every installation. --- src/Mod/Fem/femexamples/manager.py | 4 ++-- src/Mod/Fem/feminout/importFenicsMesh.py | 2 +- src/Mod/Fem/feminout/writeFenicsXDMF.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mod/Fem/femexamples/manager.py b/src/Mod/Fem/femexamples/manager.py index 8a499620b4..21ca3f2204 100644 --- a/src/Mod/Fem/femexamples/manager.py +++ b/src/Mod/Fem/femexamples/manager.py @@ -67,7 +67,7 @@ def run_analysis(doc, base_name, filepath=""): # print([obj.Name for obj in doc.Objects]) # filepath - if filepath is "": + if filepath == "": filepath = join(gettmp(), "FEM_examples") if not exists(filepath): makedirs(filepath) @@ -78,7 +78,7 @@ def run_analysis(doc, base_name, filepath=""): from femtools.femutils import is_derived_from if ( is_derived_from(m, "Fem::FemSolverObjectPython") - and m.Proxy.Type is not "Fem::FemSolverCalculixCcxTools" + and m.Proxy.Type != "Fem::FemSolverCalculixCcxTools" ): solver = m break diff --git a/src/Mod/Fem/feminout/importFenicsMesh.py b/src/Mod/Fem/feminout/importFenicsMesh.py index 2425c40131..4c4fd8643e 100644 --- a/src/Mod/Fem/feminout/importFenicsMesh.py +++ b/src/Mod/Fem/feminout/importFenicsMesh.py @@ -186,7 +186,7 @@ def export(objectslist, fileString, group_values_dict_nogui=None): writeFenicsXML.write_fenics_mesh_xml(obj, fileString) elif fileExtension.lower() == ".xdmf": mesh_groups = importToolsFem.get_FemMeshObjectMeshGroups(obj) - if mesh_groups is not (): + if mesh_groups != (): # if there are groups found, make task panel available if GuiUp if FreeCAD.GuiUp == 1: panel = WriteXDMFTaskPanel(obj, fileString) diff --git a/src/Mod/Fem/feminout/writeFenicsXDMF.py b/src/Mod/Fem/feminout/writeFenicsXDMF.py index cd3a4fb6bd..4d82b2007a 100644 --- a/src/Mod/Fem/feminout/writeFenicsXDMF.py +++ b/src/Mod/Fem/feminout/writeFenicsXDMF.py @@ -317,7 +317,7 @@ def write_fenics_mesh_xdmf( fem_mesh = fem_mesh_obj.FemMesh gmshgroups = get_FemMeshObjectMeshGroups(fem_mesh_obj) - if gmshgroups is not (): + if gmshgroups != (): Console.PrintMessage("found mesh groups\n") for g in gmshgroups: From 27a475d84b7c6368814c30fdd479087788716ad9 Mon Sep 17 00:00:00 2001 From: wmayer Date: Mon, 13 Apr 2020 13:32:32 +0200 Subject: [PATCH 022/142] Tools: [skip ci] implement method to get commit number, date and branch name from sha --- src/Tools/SubWCRev.py | 71 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/Tools/SubWCRev.py b/src/Tools/SubWCRev.py index 3c6a0fd8a0..c2b931b525 100644 --- a/src/Tools/SubWCRev.py +++ b/src/Tools/SubWCRev.py @@ -127,6 +127,75 @@ class BazaarControl(VersionControl): def printInfo(self): print("bazaar") +class DebianGitHub(VersionControl): + #https://gist.github.com/0penBrain/7be59a48aba778c955d992aa69e524c5 + #https://gist.github.com/yershalom/a7c08f9441d1aadb13777bce4c7cdc3b + #https://github.community/t5/GitHub-API-Development-and/How-to-get-all-branches-which-contain-a-commit-from-SHA-using/td-p/25006 + def extractInfo(self, srcdir, bindir): + try: + f = open(srcdir+"/debian/git-build-recipe.manifest") + except: + return False + + # Read the first two lines + recipe = f.readline() + commit = f.readline() + f.close() + + import requests + base_url = "https://api.github.com" + owner = "FreeCAD" + repo = "FreeCAD" + sha = commit[commit.rfind(':') + 1 : -1] + request_url = "{}/repos/{}/{}/commits?per_page=1&sha={}".format(base_url, owner, repo, sha) + + commit_req = requests.get(request_url) + if not commit_req.ok: + return False + + commit_date = commit_req.headers.get('last-modified') + self.hash = sha + + try: + # Try to convert into the same format as GitControl + t = time.strptime(commit_date, "%a, %d %b %Y %H:%M:%S GMT") + self.date = ("%d/%02d/%02d %02d:%02d:%02d") % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) + except: + self.date = commit_date + + self.branch = "master" + + # Try to determine the branch of the sha + # There is no function of the rest API of GH but with the url below we get HTML code + branch_url = "https://github.com/{}/{}/branch_commits/{}".format(owner, repo, sha) + branch_req = requests.get(branch_url) + if branch_req.ok: + html = branch_req.text + pattern = "
  • ") + 1 + end = link.find("<", start) + self.branch = link[start:end] + + self.url = "git://github.com/{}/{}.git {}".format(owner, repo, self.branch) + link = commit_req.headers.get("link") + beg = link.rfind("&page=") + 6 + end = link.rfind(">") + self.rev = link[beg:end] + " (GitHub)" + return True + + def writeVersion(self, lines): + content = VersionControl.writeVersion(self, lines) + content.append('// Git relevant stuff\n') + content.append('#define FCRepositoryHash "%s"\n' % (self.hash)) + content.append('#define FCRepositoryBranch "%s"\n' % (self.branch)) + return content + + def printInfo(self): + print("Debian/GitHub") + class GitControl(VersionControl): #http://www.hermanradtke.com/blog/canonical-version-numbers-with-git/ #http://blog.marcingil.com/2011/11/creating-build-numbers-using-git-commits/ @@ -374,7 +443,7 @@ def main(): if o in ("-b", "--bindir"): bindir = a - vcs=[GitControl(), BazaarControl(), Subversion(), MercurialControl(), DebianChangelog(), UnknownControl()] + vcs=[GitControl(), DebianGitHub(), BazaarControl(), Subversion(), MercurialControl(), DebianChangelog(), UnknownControl()] for i in vcs: if i.extractInfo(srcdir, bindir): # Open the template file and the version file From e537972ef95457279c4d1c679c60749664fe6370 Mon Sep 17 00:00:00 2001 From: wandererfan Date: Sun, 12 Apr 2020 09:43:22 -0400 Subject: [PATCH 023/142] [TD]GlobalPlacement for loose 2D objects --- src/Mod/TechDraw/App/ShapeExtractor.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Mod/TechDraw/App/ShapeExtractor.cpp b/src/Mod/TechDraw/App/ShapeExtractor.cpp index 29fac29def..febe6fcd06 100644 --- a/src/Mod/TechDraw/App/ShapeExtractor.cpp +++ b/src/Mod/TechDraw/App/ShapeExtractor.cpp @@ -73,17 +73,24 @@ std::vector ShapeExtractor::getShapes2d(const std::vector objs = gex->Group.getValues(); for (auto& d: objs) { if (is2dObject(d)) { - auto shape = Part::Feature::getShape(d); - if(!shape.IsNull()) { - shapes2d.push_back(shape); + if (d->getTypeId().isDerivedFrom(Part::Feature::getClassTypeId())) { + //need to apply global placement here. ??? because 2d shapes (Points so far) + //don't get gp from Part::feature::getShape() ???? + const Part::Feature* pf = static_cast(d); + Part::TopoShape ts = pf->Shape.getShape(); + ts.setPlacement(pf->globalPlacement()); + shapes2d.push_back(ts.getShape()); } } } } else { if (is2dObject(l)) { - auto shape = Part::Feature::getShape(l); - if(!shape.IsNull()) { - shapes2d.push_back(shape); + if (l->getTypeId().isDerivedFrom(Part::Feature::getClassTypeId())) { + //need to apply placement here + const Part::Feature* pf = static_cast(l); + Part::TopoShape ts = pf->Shape.getShape(); + ts.setPlacement(pf->globalPlacement()); + shapes2d.push_back(ts.getShape()); } } } @@ -110,6 +117,7 @@ TopoDS_Shape ShapeExtractor::getShapes(const std::vector l if(!shape.IsNull()) { // BRepTools::Write(shape, "DVPgetShape.brep"); //debug if (shape.ShapeType() > TopAbs_COMPSOLID) { //simple shape + //do we need to apply placement here too?? sourceShapes.push_back(shape); } else { //complex shape std::vector drawable = extractDrawableShapes(shape); From 9e57caf2ecc7f7268f70b92f016bfccd8ebcab59 Mon Sep 17 00:00:00 2001 From: donovaly Date: Mon, 13 Apr 2020 19:10:57 +0200 Subject: [PATCH 024/142] [TD] sanitize Detail view dialog - fix dialog layout (was broken for Windows) and simplify it - enable so immediately the changes you make in the dialog -> necessary to fine-tune the right position --- src/Mod/TechDraw/Gui/TaskDetail.cpp | 18 +- src/Mod/TechDraw/Gui/TaskDetail.ui | 443 +++++++++++++--------------- 2 files changed, 208 insertions(+), 253 deletions(-) diff --git a/src/Mod/TechDraw/Gui/TaskDetail.cpp b/src/Mod/TechDraw/Gui/TaskDetail.cpp index 270b8fb612..8d44dc974b 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.cpp +++ b/src/Mod/TechDraw/Gui/TaskDetail.cpp @@ -97,7 +97,7 @@ TaskDetail::TaskDetail(TechDraw::DrawViewPart* baseFeat): m_baseName = m_baseFeat->getNameInDocument(); m_doc = m_baseFeat->getDocument(); - m_pageName = m_basePage->getNameInDocument(); + m_pageName = m_basePage->getNameInDocument(); ui->setupUi(this); @@ -114,12 +114,14 @@ TaskDetail::TaskDetail(TechDraw::DrawViewPart* baseFeat): connect(ui->pbDragger, SIGNAL(clicked(bool)), this, SLOT(onDraggerClicked(bool))); - connect(ui->qsbX, SIGNAL(editingFinished()), + connect(ui->qsbX, SIGNAL(valueChanged(double)), this, SLOT(onXEdit())); - connect(ui->qsbY, SIGNAL(editingFinished()), + connect(ui->qsbY, SIGNAL(valueChanged(double)), this, SLOT(onYEdit())); - connect(ui->qsbRadius, SIGNAL(editingFinished()), + connect(ui->qsbRadius, SIGNAL(valueChanged(double)), this, SLOT(onRadiusEdit())); + connect(ui->aeReference, SIGNAL(textChanged(QString)), + this, SLOT(onReferenceEdit())); m_ghost = new QGIGhostHighlight(); m_scene->addItem(m_ghost); @@ -182,13 +184,13 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): connect(ui->pbDragger, SIGNAL(clicked(bool)), this, SLOT(onDraggerClicked(bool))); - connect(ui->qsbX, SIGNAL(editingFinished()), + connect(ui->qsbX, SIGNAL(valueChanged(double)), this, SLOT(onXEdit())); - connect(ui->qsbY, SIGNAL(editingFinished()), + connect(ui->qsbY, SIGNAL(valueChanged(double)), this, SLOT(onYEdit())); - connect(ui->qsbRadius, SIGNAL(editingFinished()), + connect(ui->qsbRadius, SIGNAL(valueChanged(double)), this, SLOT(onRadiusEdit())); - connect(ui->aeReference, SIGNAL(editingFinished()), + connect(ui->aeReference, SIGNAL(textChanged(QString)), this, SLOT(onReferenceEdit())); m_ghost = new QGIGhostHighlight(); diff --git a/src/Mod/TechDraw/Gui/TaskDetail.ui b/src/Mod/TechDraw/Gui/TaskDetail.ui index bf7d9798f3..0aa0a2fe21 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.ui +++ b/src/Mod/TechDraw/Gui/TaskDetail.ui @@ -6,12 +6,12 @@ 0 0 - 381 - 405 + 304 + 244 - + 0 0 @@ -29,252 +29,205 @@ :/icons/actions/techdraw-DetailView.svg:/icons/actions/techdraw-DetailView.svg - + - - - - 0 - 0 - + + + + + false + + + false + + + Qt::NoFocus + + + false + + + + + + + Base View + + + + + + + Detail View + + + + + + + false + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Click to drag detail highlight to new position + + + Drag Highlight + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal - - - 300 - 300 - - - - - 300 - 300 - - - - QFrame::Box - - - QFrame::Raised - - - - - - - - - - false - - - false - - - Qt::NoFocus - - - false - - - - - - - Base View - - - - - - - Detail View - - - - - - - false - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Click to drag detail highlight to new position - - - Drag Highlight - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Qt::Horizontal - - - - - - - - - size of detail view - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 10.000000000000000 - - - - - - - X - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Y - - - - - - - - - - - - - - x position of detail highlight within view - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 0.000000000000000 - - - - - - - y position of detail highlight within view - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - Radius - - - - - - - Reference - - - - - - - Detail identifier - - - 1 - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + + + + + + size of detail view + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 10.000000000000000 + + + + + + + X + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Y + + + + + + + + + + + + + + x position of detail highlight within view + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 0.000000000000000 + + + + + + + y position of detail highlight within view + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Radius + + + + + + + Reference + + + + + + + Detail identifier + + + 1 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + From 7fe094ac02880e94fc73dd93a1ba31b72f238053 Mon Sep 17 00:00:00 2001 From: Eric Trombly Date: Thu, 9 Apr 2020 18:13:02 -0500 Subject: [PATCH 025/142] add lazyloader support lazy_loader is copied to Ext now, modified external imports to lazy_load add a few more imports to be lazy loaded, think the install path is correct now [TD]"<" symbol embedded in html revert changes to path modules for testing use lazyloader in PathAreaOp.py add back in deferred loading temp change to print error message in tests temp change to print error message in tests add _init__.py to lazy_loader make install in CMakeLists.txt one line --- src/3rdParty/CMakeLists.txt | 2 + src/3rdParty/lazy_loader/CMakeLists.txt | 12 ++++ src/3rdParty/lazy_loader/__init__.py | 0 src/3rdParty/lazy_loader/lazy_loader.py | 60 +++++++++++++++++++ src/Mod/Path/PathScripts/PathAreaOp.py | 7 ++- .../Path/PathScripts/PathCircularHoleBase.py | 11 ++-- src/Mod/Path/PathScripts/PathDeburr.py | 5 +- .../Path/PathScripts/PathDressupDogbone.py | 7 ++- .../Path/PathScripts/PathDressupDragknife.py | 5 +- .../PathScripts/PathDressupHoldingTags.py | 5 +- .../Path/PathScripts/PathDressupRampEntry.py | 5 +- src/Mod/Path/PathScripts/PathDressupTag.py | 7 ++- .../Path/PathScripts/PathDressupZCorrect.py | 5 +- src/Mod/Path/PathScripts/PathEngrave.py | 7 ++- src/Mod/Path/PathScripts/PathEngraveBase.py | 5 +- src/Mod/Path/PathScripts/PathGeom.py | 5 +- src/Mod/Path/PathScripts/PathGetPoint.py | 5 +- src/Mod/Path/PathScripts/PathJob.py | 7 ++- src/Mod/Path/PathScripts/PathJobGui.py | 7 ++- src/Mod/Path/PathScripts/PathMillFace.py | 5 +- src/Mod/Path/PathScripts/PathOp.py | 5 +- src/Mod/Path/PathScripts/PathOpTools.py | 5 +- src/Mod/Path/PathScripts/PathPocket.py | 5 +- src/Mod/Path/PathScripts/PathPocketShape.py | 9 ++- .../Path/PathScripts/PathPocketShapeGui.py | 5 +- .../Path/PathScripts/PathProfileContour.py | 7 ++- src/Mod/Path/PathScripts/PathProfileEdges.py | 9 ++- src/Mod/Path/PathScripts/PathProfileFaces.py | 7 ++- src/Mod/Path/PathScripts/PathSimulatorGui.py | 7 ++- src/Mod/Path/PathScripts/PathStock.py | 5 +- src/Mod/Path/PathScripts/PathSurface.py | 9 ++- src/Mod/Path/PathScripts/PathToolBit.py | 5 +- .../Path/PathScripts/PathToolControllerGui.py | 5 +- src/Mod/Path/PathScripts/PathUtils.py | 9 ++- src/Mod/Path/PathScripts/PathWaterline.py | 9 ++- src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp | 17 ++++-- 36 files changed, 233 insertions(+), 57 deletions(-) create mode 100644 src/3rdParty/lazy_loader/CMakeLists.txt create mode 100644 src/3rdParty/lazy_loader/__init__.py create mode 100644 src/3rdParty/lazy_loader/lazy_loader.py diff --git a/src/3rdParty/CMakeLists.txt b/src/3rdParty/CMakeLists.txt index b509ba7814..7bde0b1ac7 100644 --- a/src/3rdParty/CMakeLists.txt +++ b/src/3rdParty/CMakeLists.txt @@ -2,3 +2,5 @@ if (BUILD_SMESH AND NOT FREECAD_USE_EXTERNAL_SMESH) add_subdirectory(salomesmesh) endif() + +add_subdirectory(lazy_loader) \ No newline at end of file diff --git a/src/3rdParty/lazy_loader/CMakeLists.txt b/src/3rdParty/lazy_loader/CMakeLists.txt new file mode 100644 index 0000000000..7ed973983c --- /dev/null +++ b/src/3rdParty/lazy_loader/CMakeLists.txt @@ -0,0 +1,12 @@ +SET(lazy_loader + lazy_loader.py + __init__.py +) +add_custom_target(lazy_loader ALL SOURCES + ${lazy_loader} +) +SET_PYTHON_PREFIX_SUFFIX(lazy_loader) + +fc_copy_sources(lazy_loader "${CMAKE_BINARY_DIR}/Ext/lazy_loader" ${lazy_loader}) +install (FILES ${lazy_loader} DESTINATION "${CMAKE_INSTALL_PREFIX}/Ext/lazy_loader") + diff --git a/src/3rdParty/lazy_loader/__init__.py b/src/3rdParty/lazy_loader/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/3rdParty/lazy_loader/lazy_loader.py b/src/3rdParty/lazy_loader/lazy_loader.py new file mode 100644 index 0000000000..2a443fc30c --- /dev/null +++ b/src/3rdParty/lazy_loader/lazy_loader.py @@ -0,0 +1,60 @@ +# Copyright 2015 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""A LazyLoader class.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import importlib +import types + + +class LazyLoader(types.ModuleType): + """Lazily import a module, mainly to avoid pulling in large dependencies. + + `contrib`, and `ffmpeg` are examples of modules that are large and not always + needed, and this allows them to only be loaded when they are used. + """ + + # The lint error here is incorrect. + def __init__(self, local_name, parent_module_globals, name, warning=None): # pylint: disable=super-on-old-class + self._local_name = local_name + self._parent_module_globals = parent_module_globals + self._warning = warning + + super(LazyLoader, self).__init__(name) + + def _load(self): + """Load the module and insert it into the parent's globals.""" + # Import the target module and insert it into the parent's namespace + module = importlib.import_module(self.__name__) + self._parent_module_globals[self._local_name] = module + + # Update this object's dict so that if someone keeps a reference to the + # LazyLoader, lookups are efficient (__getattr__ is only called on lookups + # that fail). + self.__dict__.update(module.__dict__) + + return module + + def __getattr__(self, item): + module = self._load() + return getattr(module, item) + + def __dir__(self): + module = self._load() + return dir(module) \ No newline at end of file diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index 3ccb4a39d6..b40e932eb8 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -28,9 +28,12 @@ import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils import PathScripts.PathGeom as PathGeom -import Draft import math -import Part + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') # from PathScripts.PathUtils import waiting_effects from PySide import QtCore diff --git a/src/Mod/Path/PathScripts/PathCircularHoleBase.py b/src/Mod/Path/PathScripts/PathCircularHoleBase.py index 570d7c09e8..09e57a770d 100644 --- a/src/Mod/Path/PathScripts/PathCircularHoleBase.py +++ b/src/Mod/Path/PathScripts/PathCircularHoleBase.py @@ -28,10 +28,7 @@ # * * # *************************************************************************** -import ArchPanel import FreeCAD -import DraftGeomUtils -import Part import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils @@ -39,8 +36,14 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore import PathScripts.PathGeom as PathGeom +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') + import math -import Draft if FreeCAD.GuiUp: import FreeCADGui diff --git a/src/Mod/Path/PathScripts/PathDeburr.py b/src/Mod/Path/PathScripts/PathDeburr.py index aa1b1e6d21..26f0fe2acc 100644 --- a/src/Mod/Path/PathScripts/PathDeburr.py +++ b/src/Mod/Path/PathScripts/PathDeburr.py @@ -24,7 +24,6 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathEngraveBase as PathEngraveBase import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp @@ -33,6 +32,10 @@ import math from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path Deburr Operation" __author__ = "sliptonic (Brad Collette), Schildkroet" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathDressupDogbone.py b/src/Mod/Path/PathScripts/PathDressupDogbone.py index 80e697a691..8af09df11d 100644 --- a/src/Mod/Path/PathScripts/PathDressupDogbone.py +++ b/src/Mod/Path/PathScripts/PathDressupDogbone.py @@ -22,10 +22,8 @@ # * * # *************************************************************************** from __future__ import print_function -import DraftGeomUtils import FreeCAD import math -import Part import Path import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom @@ -35,6 +33,11 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +DraftDraftGeomUtils = LazyLoader('DraftDraftGeomUtils', globals(), 'DraftDraftGeomUtils') +Part = LazyLoader('Part', globals(), 'Part') + LOG_MODULE = PathLog.thisModule() PathLog.setLevel(PathLog.Level.NOTICE, LOG_MODULE) diff --git a/src/Mod/Path/PathScripts/PathDressupDragknife.py b/src/Mod/Path/PathScripts/PathDressupDragknife.py index 927c07fc44..38badc176d 100644 --- a/src/Mod/Path/PathScripts/PathDressupDragknife.py +++ b/src/Mod/Path/PathScripts/PathDressupDragknife.py @@ -27,9 +27,12 @@ import FreeCAD import Path from PySide import QtCore import math -import DraftVecUtils as D import PathScripts.PathUtils as PathUtils +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +D = LazyLoader('DraftVecUtils', globals(), 'DraftVecUtils') + __doc__ = """Dragknife Dressup object and FreeCAD command""" if FreeCAD.GuiUp: diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py index e1dd5daba6..d619f0fd13 100644 --- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py +++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py @@ -22,7 +22,6 @@ # * * # *************************************************************************** import FreeCAD -import Part import Path import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom @@ -36,6 +35,10 @@ from PathScripts.PathDressupTagPreferences import HoldingTagPreferences from PathScripts.PathUtils import waiting_effects from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) #PathLog.trackModule() diff --git a/src/Mod/Path/PathScripts/PathDressupRampEntry.py b/src/Mod/Path/PathScripts/PathDressupRampEntry.py index ad363b984b..1255472d23 100644 --- a/src/Mod/Path/PathScripts/PathDressupRampEntry.py +++ b/src/Mod/Path/PathScripts/PathDressupRampEntry.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD import Path -import Part import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog @@ -32,6 +31,10 @@ import math from PathScripts import PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + if FreeCAD.GuiUp: import FreeCADGui diff --git a/src/Mod/Path/PathScripts/PathDressupTag.py b/src/Mod/Path/PathScripts/PathDressupTag.py index 403210fbd3..a610e7939a 100644 --- a/src/Mod/Path/PathScripts/PathDressupTag.py +++ b/src/Mod/Path/PathScripts/PathDressupTag.py @@ -22,14 +22,17 @@ # * * # *************************************************************************** import FreeCAD -import DraftGeomUtils -import Part import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils import math +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') +Part = LazyLoader('Part', globals(), 'Part') + from PathScripts.PathDressupTagPreferences import HoldingTagPreferences from PySide import QtCore diff --git a/src/Mod/Path/PathScripts/PathDressupZCorrect.py b/src/Mod/Path/PathScripts/PathDressupZCorrect.py index b3e766d89e..8f26281d66 100644 --- a/src/Mod/Path/PathScripts/PathDressupZCorrect.py +++ b/src/Mod/Path/PathScripts/PathDressupZCorrect.py @@ -27,7 +27,6 @@ # *************************************************************************** import FreeCAD import FreeCADGui -import Part import Path import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog @@ -35,6 +34,10 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore, QtGui +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + """Z Depth Correction Dressup. This dressup takes a probe file as input and does bilinear interpolation of the Zdepths to correct for a surface which is not parallel to the milling table/bed. The probe file should conform to the format specified by the linuxcnc G38 probe logging: 9-number coordinate consisting of XYZABCUVW http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g38 """ diff --git a/src/Mod/Path/PathScripts/PathEngrave.py b/src/Mod/Path/PathScripts/PathEngrave.py index 8063e73484..f1e2d4cdae 100644 --- a/src/Mod/Path/PathScripts/PathEngrave.py +++ b/src/Mod/Path/PathScripts/PathEngrave.py @@ -22,9 +22,7 @@ # * * # *************************************************************************** -import ArchPanel import FreeCAD -import Part import Path import PathScripts.PathEngraveBase as PathEngraveBase import PathScripts.PathLog as PathLog @@ -33,6 +31,11 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Part = LazyLoader('Part', globals(), 'Part') + __doc__ = "Class and implementation of Path Engrave operation" PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) diff --git a/src/Mod/Path/PathScripts/PathEngraveBase.py b/src/Mod/Path/PathScripts/PathEngraveBase.py index 459e60a839..7606ec9b58 100644 --- a/src/Mod/Path/PathScripts/PathEngraveBase.py +++ b/src/Mod/Path/PathScripts/PathEngraveBase.py @@ -22,7 +22,6 @@ # * * # *************************************************************************** -import DraftGeomUtils import Path import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog @@ -30,6 +29,10 @@ import PathScripts.PathOp as PathOp import PathScripts.PathOpTools as PathOpTools import copy +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') + from PySide import QtCore __doc__ = "Base class for all ops in the engrave family." diff --git a/src/Mod/Path/PathScripts/PathGeom.py b/src/Mod/Path/PathScripts/PathGeom.py index fba8d1ddb1..5782fa747f 100644 --- a/src/Mod/Path/PathScripts/PathGeom.py +++ b/src/Mod/Path/PathScripts/PathGeom.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD -import Part import Path import PathScripts.PathLog as PathLog import math @@ -31,6 +30,10 @@ import math from FreeCAD import Vector from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "PathGeom - geometry utilities for Path" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathGetPoint.py b/src/Mod/Path/PathScripts/PathGetPoint.py index fc54f69498..93ac673fc2 100644 --- a/src/Mod/Path/PathScripts/PathGetPoint.py +++ b/src/Mod/Path/PathScripts/PathGetPoint.py @@ -22,11 +22,14 @@ # * * # *************************************************************************** -import Draft import FreeCAD import FreeCADGui import PathScripts.PathLog as PathLog +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Draft = LazyLoader('Draft', globals(), 'Draft') + from PySide import QtCore, QtGui from pivy import coin diff --git a/src/Mod/Path/PathScripts/PathJob.py b/src/Mod/Path/PathScripts/PathJob.py index c57291b470..2225c2fa9f 100644 --- a/src/Mod/Path/PathScripts/PathJob.py +++ b/src/Mod/Path/PathScripts/PathJob.py @@ -22,8 +22,6 @@ # * * # *************************************************************************** -import ArchPanel -import Draft import FreeCAD import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog @@ -34,6 +32,11 @@ import PathScripts.PathToolController as PathToolController import PathScripts.PathUtil as PathUtil import json +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Draft = LazyLoader('Draft', globals(), 'Draft') + from PathScripts.PathPostProcessor import PostProcessor from PySide import QtCore diff --git a/src/Mod/Path/PathScripts/PathJobGui.py b/src/Mod/Path/PathScripts/PathJobGui.py index 186b2066bd..0b033cc4a9 100644 --- a/src/Mod/Path/PathScripts/PathJobGui.py +++ b/src/Mod/Path/PathScripts/PathJobGui.py @@ -22,8 +22,6 @@ # * * # *************************************************************************** -import Draft -import DraftVecUtils import FreeCAD import FreeCADGui import PathScripts.PathJob as PathJob @@ -42,6 +40,11 @@ import PathScripts.PathUtils as PathUtils import math import traceback +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Draft = LazyLoader('Draft', globals(), 'Draft') +DraftVecUtils = LazyLoader('DraftVecUtils', globals(), 'DraftVecUtils') + from PySide import QtCore, QtGui from collections import Counter from contextlib import contextmanager diff --git a/src/Mod/Path/PathScripts/PathMillFace.py b/src/Mod/Path/PathScripts/PathMillFace.py index ed1fcfcbb0..b2f2f699c3 100644 --- a/src/Mod/Path/PathScripts/PathMillFace.py +++ b/src/Mod/Path/PathScripts/PathMillFace.py @@ -25,7 +25,6 @@ from __future__ import print_function import FreeCAD -import Part import PathScripts.PathLog as PathLog import PathScripts.PathPocketBase as PathPocketBase import PathScripts.PathUtils as PathUtils @@ -33,6 +32,10 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore import numpy +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path Mill Face Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathOp.py b/src/Mod/Path/PathScripts/PathOp.py index 41297bf474..fab24c28c2 100644 --- a/src/Mod/Path/PathScripts/PathOp.py +++ b/src/Mod/Path/PathScripts/PathOp.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD -import Part import Path import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog @@ -33,6 +32,10 @@ import PathScripts.PathUtils as PathUtils from PathScripts.PathUtils import waiting_effects from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Base class for all operations." __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathOpTools.py b/src/Mod/Path/PathScripts/PathOpTools.py index 50a0484c36..40e7105ca5 100644 --- a/src/Mod/Path/PathScripts/PathOpTools.py +++ b/src/Mod/Path/PathScripts/PathOpTools.py @@ -23,13 +23,16 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import math from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "PathOpTools - Tools for Path operations." __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathPocket.py b/src/Mod/Path/PathScripts/PathPocket.py index 355178ff52..27fd1a9441 100644 --- a/src/Mod/Path/PathScripts/PathPocket.py +++ b/src/Mod/Path/PathScripts/PathPocket.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathPocketBase as PathPocketBase @@ -31,6 +30,10 @@ import PathScripts.PathUtils as PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path 3D Pocket Operation" __author__ = "Yorik van Havre " __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py index 7c98f71d9c..6cc24e5ba4 100644 --- a/src/Mod/Path/PathScripts/PathPocketShape.py +++ b/src/Mod/Path/PathScripts/PathPocketShape.py @@ -24,15 +24,18 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathPocketBase as PathPocketBase import PathScripts.PathUtils as PathUtils -import TechDraw import math -import Draft + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') +TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw') from PySide import QtCore diff --git a/src/Mod/Path/PathScripts/PathPocketShapeGui.py b/src/Mod/Path/PathScripts/PathPocketShapeGui.py index c498e64750..f085d062c1 100644 --- a/src/Mod/Path/PathScripts/PathPocketShapeGui.py +++ b/src/Mod/Path/PathScripts/PathPocketShapeGui.py @@ -24,7 +24,6 @@ import FreeCAD import FreeCADGui -import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog @@ -35,6 +34,10 @@ import PathScripts.PathPocketBaseGui as PathPocketBaseGui from PySide import QtCore, QtGui from pivy import coin +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path Pocket Shape Operation UI" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathProfileContour.py b/src/Mod/Path/PathScripts/PathProfileContour.py index 8a08ceeea1..5aaeb8c56d 100644 --- a/src/Mod/Path/PathScripts/PathProfileContour.py +++ b/src/Mod/Path/PathScripts/PathProfileContour.py @@ -24,9 +24,7 @@ from __future__ import print_function -import ArchPanel import FreeCAD -import Part import Path import PathScripts.PathProfileBase as PathProfileBase import PathScripts.PathLog as PathLog @@ -34,6 +32,11 @@ import PathScripts.PathLog as PathLog from PathScripts import PathUtils from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Part = LazyLoader('Part', globals(), 'Part') + FreeCAD.setLogLevel('Path.Area', 0) PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) diff --git a/src/Mod/Path/PathScripts/PathProfileEdges.py b/src/Mod/Path/PathScripts/PathProfileEdges.py index e01ee25d4c..515e0de5dd 100644 --- a/src/Mod/Path/PathScripts/PathProfileEdges.py +++ b/src/Mod/Path/PathScripts/PathProfileEdges.py @@ -23,18 +23,21 @@ # *************************************************************************** import FreeCAD -import Part import Path import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathProfileBase as PathProfileBase import PathScripts.PathUtils as PathUtils -import DraftGeomUtils -import Draft import math import PySide +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) # PathLog.trackModule(PathLog.thisModule()) diff --git a/src/Mod/Path/PathScripts/PathProfileFaces.py b/src/Mod/Path/PathScripts/PathProfileFaces.py index 6f2233b215..281d848699 100644 --- a/src/Mod/Path/PathScripts/PathProfileFaces.py +++ b/src/Mod/Path/PathScripts/PathProfileFaces.py @@ -23,9 +23,7 @@ # * * # *************************************************************************** -import ArchPanel import FreeCAD -import Part import Path import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp @@ -35,6 +33,11 @@ import numpy from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Path Profile Faces Operation" __author__ = "sliptonic (Brad Collette), Schildkroet" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathSimulatorGui.py b/src/Mod/Path/PathScripts/PathSimulatorGui.py index 50014e0272..428dcc394c 100644 --- a/src/Mod/Path/PathScripts/PathSimulatorGui.py +++ b/src/Mod/Path/PathScripts/PathSimulatorGui.py @@ -1,6 +1,4 @@ import FreeCAD -import Mesh -import Part import Path import PathScripts.PathDressup as PathDressup import PathScripts.PathGeom as PathGeom @@ -13,6 +11,11 @@ from FreeCAD import Vector, Base _filePath = os.path.dirname(os.path.abspath(__file__)) +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Mesh = LazyLoader('Mesh', globals(), 'Mesh') +Part = LazyLoader('Part', globals(), 'Part') + if FreeCAD.GuiUp: import FreeCADGui from PySide import QtGui, QtCore diff --git a/src/Mod/Path/PathScripts/PathStock.py b/src/Mod/Path/PathScripts/PathStock.py index dfb15658a0..e1ab825627 100644 --- a/src/Mod/Path/PathScripts/PathStock.py +++ b/src/Mod/Path/PathScripts/PathStock.py @@ -23,13 +23,16 @@ '''used to create material stock around a machined part- for visualization ''' import FreeCAD -import Part import PathScripts.PathIconViewProvider as PathIconViewProvider import PathScripts.PathLog as PathLog import math from PySide import QtCore +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) #PathLog.trackModule(PathLog.thisModule()) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 9e63b6f359..40cd771398 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -30,7 +30,6 @@ from __future__ import print_function import FreeCAD -import MeshPart import Path import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils @@ -39,8 +38,12 @@ import PathScripts.PathOp as PathOp from PySide import QtCore import time import math -import Part -import Draft + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') if FreeCAD.GuiUp: import FreeCADGui diff --git a/src/Mod/Path/PathScripts/PathToolBit.py b/src/Mod/Path/PathScripts/PathToolBit.py index eeae4a70ae..7863e31df7 100644 --- a/src/Mod/Path/PathScripts/PathToolBit.py +++ b/src/Mod/Path/PathScripts/PathToolBit.py @@ -23,7 +23,6 @@ # *************************************************************************** import FreeCAD -import Part import PathScripts.PathGeom as PathGeom import PathScripts.PathLog as PathLog import PathScripts.PathPreferences as PathPreferences @@ -36,6 +35,10 @@ import math import os import zipfile +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + __title__ = "Tool bits." __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" diff --git a/src/Mod/Path/PathScripts/PathToolControllerGui.py b/src/Mod/Path/PathScripts/PathToolControllerGui.py index 3a05fbe301..f71658172a 100644 --- a/src/Mod/Path/PathScripts/PathToolControllerGui.py +++ b/src/Mod/Path/PathScripts/PathToolControllerGui.py @@ -24,7 +24,6 @@ import FreeCAD import FreeCADGui -import Part import PathScripts import PathScripts.PathGui as PathGui import PathScripts.PathLog as PathLog @@ -34,6 +33,10 @@ import PathScripts.PathUtil as PathUtil from PySide import QtCore, QtGui +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + # Qt translation handling def translate(context, text, disambig=None): return QtCore.QCoreApplication.translate(context, text, disambig) diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py index 0ea9c90c30..29dfcdb2bd 100644 --- a/src/Mod/Path/PathScripts/PathUtils.py +++ b/src/Mod/Path/PathScripts/PathUtils.py @@ -23,21 +23,24 @@ # *************************************************************************** '''PathUtils -common functions used in PathScripts for filtering, sorting, and generating gcode toolpath data ''' import FreeCAD -import Part import Path import PathScripts import PathScripts.PathGeom as PathGeom -import TechDraw import math import numpy -from DraftGeomUtils import geomType from FreeCAD import Vector from PathScripts import PathJob from PathScripts import PathLog from PySide import QtCore from PySide import QtGui +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +geomType = LazyLoader('DraftDraftGeomUtils', globals(), 'DraftDraftGeomUtils.geomType') +Part = LazyLoader('Part', globals(), 'Part') +TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw') + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) #PathLog.trackModule(PathLog.thisModule()) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index 0362680580..c1c8b66cb6 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -26,7 +26,6 @@ from __future__ import print_function import FreeCAD -import MeshPart import Path import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils @@ -35,8 +34,12 @@ import PathScripts.PathOp as PathOp from PySide import QtCore import time import math -import Part -import Draft + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') if FreeCAD.GuiUp: import FreeCADGui diff --git a/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp b/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp index 02c25ce9a4..64ff8f49a7 100644 --- a/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp +++ b/src/Mod/TechDraw/Gui/QGIViewAnnotation.cpp @@ -158,10 +158,19 @@ void QGIViewAnnotation::drawAnnotation() if (it != annoText.begin()) { ss << "
    "; } - std::string u8String = Base::Tools::escapedUnicodeToUtf8(*it); -// what madness turns \' into \\\\\'? - std::string apos = std::regex_replace((u8String), std::regex("\\\\\'"), "'"); - ss << apos; + //TODO: there is still a bug here. entering "'" works, save and restore works, but edit after + // save and restore brings "\'" back into text. manually deleting the "\" fixes it until the next + // save/restore/edit cycle. + // a guess is that the editor for propertyStringList is too enthusiastic about substituting. + // the substituting might be necessary for using the strings in Python. + // ' doesn't seem to help in this case. + + std::string u8String = Base::Tools::escapedUnicodeToUtf8(*it); //from \x??\x?? to real utf8 + std::string apos = std::regex_replace((u8String), std::regex("\\\\"), ""); //remove doubles. + apos = std::regex_replace((apos), std::regex("\\'"), "'"); //replace escaped apos + //"less than" symbol chops off line. need to use html sub. + std::string lt = std::regex_replace((apos), std::regex("<"), "<"); + ss << lt; } ss << "

    \n\n "; From d91bd53e10c9945ab7c0599c576c9e38425adf39 Mon Sep 17 00:00:00 2001 From: wmayer Date: Tue, 14 Apr 2020 09:37:45 +0200 Subject: [PATCH 026/142] Part: [skip ci] include missing header file --- src/Mod/Part/App/AppPartPy.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Part/App/AppPartPy.cpp b/src/Mod/Part/App/AppPartPy.cpp index da506351e5..1210c81d55 100644 --- a/src/Mod/Part/App/AppPartPy.cpp +++ b/src/Mod/Part/App/AppPartPy.cpp @@ -62,6 +62,7 @@ # include # include # include +# include # include # include # include From 940af87f69779befe9cffc735a77589f68c4c642 Mon Sep 17 00:00:00 2001 From: donovaly Date: Tue, 14 Apr 2020 02:45:37 +0200 Subject: [PATCH 027/142] [Arch] split IFC dialog --- src/Mod/Arch/InitGui.py | 1 + src/Mod/Arch/Resources/Arch.qrc | 1 + .../Resources/ui/preferences-ifc-export.ui | 359 ++++++++++++++++++ src/Mod/Arch/Resources/ui/preferences-ifc.ui | 298 +-------------- src/Mod/Arch/exportIFC.py | 2 +- src/Mod/Draft/importDXF.py | 2 +- 6 files changed, 375 insertions(+), 288 deletions(-) create mode 100644 src/Mod/Arch/Resources/ui/preferences-ifc-export.ui diff --git a/src/Mod/Arch/InitGui.py b/src/Mod/Arch/InitGui.py index 53d154c1b9..3791fba309 100644 --- a/src/Mod/Arch/InitGui.py +++ b/src/Mod/Arch/InitGui.py @@ -180,6 +180,7 @@ FreeCADGui.addWorkbench(ArchWorkbench) import Arch_rc from PySide.QtCore import QT_TRANSLATE_NOOP FreeCADGui.addPreferencePage(":/ui/preferences-ifc.ui", QT_TRANSLATE_NOOP("Draft", "Import-Export")) +FreeCADGui.addPreferencePage(":/ui/preferences-ifc-export.ui", QT_TRANSLATE_NOOP("Draft", "Import-Export")) FreeCADGui.addPreferencePage(":/ui/preferences-dae.ui", QT_TRANSLATE_NOOP("Draft", "Import-Export")) FreeCAD.__unit_test__ += ["TestArch"] diff --git a/src/Mod/Arch/Resources/Arch.qrc b/src/Mod/Arch/Resources/Arch.qrc index ce9f1a9864..fa7a65c2a0 100644 --- a/src/Mod/Arch/Resources/Arch.qrc +++ b/src/Mod/Arch/Resources/Arch.qrc @@ -112,6 +112,7 @@ ui/preferences-archdefaults.ui ui/preferences-dae.ui ui/preferences-ifc.ui + ui/preferences-ifc-export.ui translations/Arch_af.qm translations/Arch_ar.qm translations/Arch_ca.qm diff --git a/src/Mod/Arch/Resources/ui/preferences-ifc-export.ui b/src/Mod/Arch/Resources/ui/preferences-ifc-export.ui new file mode 100644 index 0000000000..3d1df06a58 --- /dev/null +++ b/src/Mod/Arch/Resources/ui/preferences-ifc-export.ui @@ -0,0 +1,359 @@ + + + Gui::Dialog::DlgSettingsArch + + + + 0 + 0 + 463 + 421 + + + + IFC-Export + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + Show this dialog when exporting + + + ifcShowDialog + + + Mod/Arch + + + + + + + Export options + + + + + + Some IFC viewers don't like objects exported as extrusions. +Use this to force all objects to be exported as BREP geometry. + + + Force export as Brep + + + ifcExportAsBrep + + + Mod/Arch + + + + + + + Use triangulation options set in the DAE options page + + + Use DAE triangulation options + + + ifcUseDaeOptions + + + Mod/Arch + + + + + + + Curved shapes that cannot be represented as curves in IFC +are decomposed into flat facets. +If this is checked, additional calculation is done to join coplanar facets. + + + Join coplanar facets when triangulating + + + ifcJoinCoplanarFacets + + + Mod/Arch + + + + + + + When exporting objects without unique ID (UID), the generated UID +will be stored inside the FreeCAD object for reuse next time that object +is exported. This leads to smaller differences between file versions. + + + Store IFC unique ID in FreeCAD objects + + + true + + + ifcStoreUid + + + Mod/Arch + + + + + + + IFCOpenShell is a library that allows to import IFC files. +Its serializer functionality allows to give it an OCC shape and it will +produce adequate IFC geometry: NURBS, faceted, or anything else. +Note: The serializer is still an experimental feature! + + + Use IfcOpenShell serializer if available + + + ifcSerialize + + + Mod/Arch + + + + + + + 2D objects will be exported as IfcAnnotation + + + Export 2D objects as IfcAnnotations + + + true + + + ifcExport2D + + + Mod/Arch + + + + + + + All FreeCAD object properties will be stored inside the exported objects, +allowing to recreate a full parametric model on reimport. + + + Export full FreeCAD parametric model + + + IfcExportFreeCADProperties + + + Mod/Arch + + + + + + + When possible, similar entities will be used only once in the file if possible. +This can reduce the file size a lot, but will make it less easily readable. + + + Reuse similar entities + + + true + + + ifcCompress + + + Mod/Arch + + + + + + + When possible, IFC objects that are extruded rectangles will be +exported as IfcRectangleProfileDef. +However, some other applications might have problems importing that entity. +If this is your case, you can disable this and then all profiles will be exported as IfcArbitraryClosedProfileDef. + + + Disable IfcRectangleProfileDef + + + DisableIfcRectangleProfileDef + + + Mod/Arch + + + + + + + Some IFC types such as IfcWall or IfcBeam have special standard versions +like IfcWallStandardCase or IfcBeamStandardCase. +If this option is turned on, FreeCAD will automatically export such objects +as standard cases when the necessary conditions are met. + + + Auto-detect and export as standard cases when applicable + + + getStandardCase + + + Mod/Arch + + + + + + + If no site is found in the FreeCAD document, a default one will be added. +A site is not mandatory but a common practice is to have at least one in the file. + + + Add default site if one is not found in the document + + + IfcAddDefaultSite + + + Mod/Arch + + + + + + + If no building is found in the FreeCAD document, a default one will be added. +Warning: The IFC standard asks for at least one building in each file. By turning this option off, you will produce a non-standard IFC file. +However, at FreeCAD, we believe having a building should not be mandatory, and this option is there to have a chance to demonstrate our point of view. + + + Add default building if one is not found in the document (no standard) + + + true + + + IfcAddDefaultBuilding + + + Mod/Arch + + + + + + + If no building storey is found in the FreeCAD document, a default one will be added. +A building storey is not mandatory but a common practice to have at least one in the file. + + + Add default building storey if one is not found in the document + + + IfcAddDefaultStorey + + + Mod/Arch + + + + + + + + + IFC file units + + + + + + + The units you want your IFC file to be exported to. Note that IFC file are ALWAYS written in metric units. Imperial units are only a conversion applied on top of it. But some BIM applications will use this to choose which unit to work with when opening the file. + + + ifcUnit + + + Mod/Arch + + + + Metric + + + + + Imperial + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + qPixmapFromMimeSource + + + Gui::PrefCheckBox + QCheckBox +
    Gui/PrefWidgets.h
    +
    + + Gui::PrefComboBox + QComboBox +
    Gui/PrefWidgets.h
    +
    +
    + + +
    diff --git a/src/Mod/Arch/Resources/ui/preferences-ifc.ui b/src/Mod/Arch/Resources/ui/preferences-ifc.ui index 28835df005..089594c571 100644 --- a/src/Mod/Arch/Resources/ui/preferences-ifc.ui +++ b/src/Mod/Arch/Resources/ui/preferences-ifc.ui @@ -7,7 +7,7 @@ 0 0 463 - 937 + 495 @@ -17,13 +17,22 @@ 6 - + + 9 + + + 9 + + + 9 + + 9 - Show this dialog when importing and exporting + Show this dialog when importing ifcShowDialog @@ -388,289 +397,6 @@ FreeCAD object properties
    - - - - Export options - - - - - - Some IFC viewers don't like objects exported as extrusions. -Use this to force all objects to be exported as BREP geometry. - - - Force export as Brep - - - ifcExportAsBrep - - - Mod/Arch - - - - - - - Use triangulation options set in the DAE options page - - - Use DAE triangulation options - - - ifcUseDaeOptions - - - Mod/Arch - - - - - - - Curved shapes that cannot be represented as curves in IFC -are decomposed into flat facets. -If this is checked, additional calculation is done to join coplanar facets. - - - Join coplanar facets when triangulating - - - ifcJoinCoplanarFacets - - - Mod/Arch - - - - - - - When exporting objects without unique ID (UID), the generated UID -will be stored inside the FreeCAD object for reuse next time that object -is exported. This leads to smaller differences between file versions. - - - Store IFC unique ID in FreeCAD objects - - - true - - - ifcStoreUid - - - Mod/Arch - - - - - - - IFCOpenShell is a library that allows to import IFC files. -Its serializer functionality allows to give it an OCC shape and it will -produce adequate IFC geometry: NURBS, faceted, or anything else. -Note: The serializer is still an experimental feature! - - - Use IfcOpenShell serializer if available - - - ifcSerialize - - - Mod/Arch - - - - - - - 2D objects will be exported as IfcAnnotation - - - Export 2D objects as IfcAnnotations - - - true - - - ifcExport2D - - - Mod/Arch - - - - - - - All FreeCAD object properties will be stored inside the exported objects, -allowing to recreate a full parametric model on reimport. - - - Export full FreeCAD parametric model - - - IfcExportFreeCADProperties - - - Mod/Arch - - - - - - - When possible, similar entities will be used only once in the file if possible. -This can reduce the file size a lot, but will make it less easily readable. - - - Reuse similar entities - - - true - - - ifcCompress - - - Mod/Arch - - - - - - - When possible, IFC objects that are extruded rectangles will be -exported as IfcRectangleProfileDef. -However, some other applications might have problems importing that entity. -If this is your case, you can disable this and then all profiles will be exported as IfcArbitraryClosedProfileDef. - - - Disable IfcRectangleProfileDef - - - DisableIfcRectangleProfileDef - - - Mod/Arch - - - - - - - Some IFC types such as IfcWall or IfcBeam have special standard versions -like IfcWallStandardCase or IfcBeamStandardCase. -If this option is turned on, FreeCAD will automatically export such objects -as standard cases when the necessary conditions are met. - - - Auto-detect and export as standard cases when applicable - - - getStandardCase - - - Mod/Arch - - - - - - - If no site is found in the FreeCAD document, a default one will be added. -A site is not mandatory but a common practice is to have at least one in the file. - - - Add default site if one is not found in the document - - - IfcAddDefaultSite - - - Mod/Arch - - - - - - - If no building is found in the FreeCAD document, a default one will be added. -Warning: The IFC standard asks for at least one building in each file. By turning this option off, you will produce a non-standard IFC file. -However, at FreeCAD, we believe having a building should not be mandatory, and this option is there to have a chance to demonstrate our point of view. - - - Add default building if one is not found in the document (no standard) - - - true - - - IfcAddDefaultBuilding - - - Mod/Arch - - - - - - - If no building storey is found in the FreeCAD document, a default one will be added. -A building storey is not mandatory but a common practice to have at least one in the file. - - - Add default building storey if one is not found in the document - - - IfcAddDefaultStorey - - - Mod/Arch - - - - - - - - - IFC file units - - - - - - - The units you want your IFC file to be exported to. Note that IFC file are ALWAYS written in metric units. Imperial units are only a conversion applied on top of it. But some BIM applications will use this to choose which unit to work with when opening the file. - - - ifcUnit - - - Mod/Arch - - - - Metric - - - - - Imperial - - - - - - - - - diff --git a/src/Mod/Arch/exportIFC.py b/src/Mod/Arch/exportIFC.py index 596b137331..8baf610739 100644 --- a/src/Mod/Arch/exportIFC.py +++ b/src/Mod/Arch/exportIFC.py @@ -113,7 +113,7 @@ def getPreferences(): if FreeCAD.GuiUp and p.GetBool("ifcShowDialog",False): import FreeCADGui - FreeCADGui.showPreferences("Import-Export",0) + FreeCADGui.showPreferences("Import-Export",1) ifcunit = p.GetInt("ifcUnit",0) f = 0.001 u = "metre" diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 760b3a8654..e972cb8816 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -4169,7 +4169,7 @@ def readPreferences(): # reading parameters p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") if FreeCAD.GuiUp and p.GetBool("dxfShowDialog", False): - FreeCADGui.showPreferences("Import-Export", 2) + FreeCADGui.showPreferences("Import-Export", 3) global dxfCreatePart, dxfCreateDraft, dxfCreateSketch global dxfDiscretizeCurves, dxfStarBlocks global dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth From 890809bcb0a298cc1f56aea8d7dd77af76222250 Mon Sep 17 00:00:00 2001 From: wmayer Date: Tue, 14 Apr 2020 12:38:53 +0200 Subject: [PATCH 028/142] [skip ci]: make SubWCRef not to fail if internet connection is blocked --- src/Tools/SubWCRev.py | 66 +++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/Tools/SubWCRev.py b/src/Tools/SubWCRev.py index c2b931b525..bd9897c9c1 100644 --- a/src/Tools/SubWCRev.py +++ b/src/Tools/SubWCRev.py @@ -147,43 +147,55 @@ class DebianGitHub(VersionControl): owner = "FreeCAD" repo = "FreeCAD" sha = commit[commit.rfind(':') + 1 : -1] - request_url = "{}/repos/{}/{}/commits?per_page=1&sha={}".format(base_url, owner, repo, sha) - - commit_req = requests.get(request_url) - if not commit_req.ok: - return False - - commit_date = commit_req.headers.get('last-modified') self.hash = sha + try: + request_url = "{}/repos/{}/{}/commits?per_page=1&sha={}".format(base_url, owner, repo, sha) + commit_req = requests.get(request_url) + if not commit_req.ok: + return False + + commit_date = commit_req.headers.get('last-modified') + + except: + # if connection fails then use the date of the file git-build-recipe.manifest + commit_date = recipe[recipe.rfind('~') + 1 : -1] + + try: # Try to convert into the same format as GitControl t = time.strptime(commit_date, "%a, %d %b %Y %H:%M:%S GMT") - self.date = ("%d/%02d/%02d %02d:%02d:%02d") % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) + commit_date = ("%d/%02d/%02d %02d:%02d:%02d") % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) except: - self.date = commit_date + t = time.strptime(commit_date, "%Y%m%d%H%M") + commit_date = ("%d/%02d/%02d %02d:%02d:%02d") % (t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec) - self.branch = "master" + self.date = commit_date + self.branch = "unknown" - # Try to determine the branch of the sha - # There is no function of the rest API of GH but with the url below we get HTML code - branch_url = "https://github.com/{}/{}/branch_commits/{}".format(owner, repo, sha) - branch_req = requests.get(branch_url) - if branch_req.ok: - html = branch_req.text - pattern = "
  • ") + 1 - end = link.find("<", start) - self.branch = link[start:end] + try: + # Try to determine the branch of the sha + # There is no function of the rest API of GH but with the url below we get HTML code + branch_url = "https://github.com/{}/{}/branch_commits/{}".format(owner, repo, sha) + branch_req = requests.get(branch_url) + if branch_req.ok: + html = branch_req.text + pattern = "
  • ") + 1 + end = link.find("<", start) + self.branch = link[start:end] + + link = commit_req.headers.get("link") + beg = link.rfind("&page=") + 6 + end = link.rfind(">") + self.rev = link[beg:end] + " (GitHub)" + except: + pass self.url = "git://github.com/{}/{}.git {}".format(owner, repo, self.branch) - link = commit_req.headers.get("link") - beg = link.rfind("&page=") + 6 - end = link.rfind(">") - self.rev = link[beg:end] + " (GitHub)" return True def writeVersion(self, lines): From 2c11e0f4cd03ea336b7fd5423d44cc1c1105c279 Mon Sep 17 00:00:00 2001 From: wmayer Date: Tue, 14 Apr 2020 12:57:07 +0200 Subject: [PATCH 029/142] TechDraw: [skip ci] fix -Winconsistent-missing-override --- src/Mod/TechDraw/Gui/QGIGhostHighlight.h | 4 ++-- src/Mod/TechDraw/Gui/QGIHighlight.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Mod/TechDraw/Gui/QGIGhostHighlight.h b/src/Mod/TechDraw/Gui/QGIGhostHighlight.h index 0f86095a79..de58d1c9aa 100644 --- a/src/Mod/TechDraw/Gui/QGIGhostHighlight.h +++ b/src/Mod/TechDraw/Gui/QGIGhostHighlight.h @@ -42,8 +42,8 @@ public: explicit QGIGhostHighlight(); ~QGIGhostHighlight(); - enum {Type = QGraphicsItem::UserType + 177}; - int type() const { return Type;} + enum {Type = QGraphicsItem::UserType + 177}; + int type() const override { return Type;} void setInteractive(bool state); void setRadius(double r); diff --git a/src/Mod/TechDraw/Gui/QGIHighlight.h b/src/Mod/TechDraw/Gui/QGIHighlight.h index 4b3a9f02e5..0e36945edf 100644 --- a/src/Mod/TechDraw/Gui/QGIHighlight.h +++ b/src/Mod/TechDraw/Gui/QGIHighlight.h @@ -50,8 +50,8 @@ public: explicit QGIHighlight(); ~QGIHighlight(); - enum {Type = QGraphicsItem::UserType + 176}; - int type() const { return Type;} + enum {Type = QGraphicsItem::UserType + 176}; + int type() const override { return Type;} virtual void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, @@ -60,7 +60,7 @@ public: void setBounds(double x1,double y1,double x2,double y2); void setReference(char* sym); void setFont(QFont f, double fsize); - virtual void draw(); + virtual void draw() override; void setInteractive(bool state); protected: From 97c8eff825963737c52cf3bd15515b6b7caf81b2 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Tue, 14 Apr 2020 15:35:46 +0200 Subject: [PATCH 030/142] Draft: warn the user if offset direction is not set --- src/Mod/Draft/DraftTools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 569c8d0704..415ea413c8 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -3067,6 +3067,8 @@ class Offset(Modifier): ['Draft.offset(FreeCAD.ActiveDocument.'+self.sel.Name+','+d+',copy='+str(copymode)+',occ='+str(occmode)+')', 'FreeCAD.ActiveDocument.recompute()']) self.finish() + else: + FreeCAD.Console.PrintError(translate("draft","Offset direction is not defined. Please move the mouse on either side of the object first to indicate a direction")+"/n") class Stretch(Modifier): From 3eff81df354f2eb611934ef93ea85e4b1995e309 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Tue, 14 Apr 2020 15:49:36 +0200 Subject: [PATCH 031/142] Arch: Fixed louvre width/spacing property --- src/Mod/Arch/ArchWindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Arch/ArchWindow.py b/src/Mod/Arch/ArchWindow.py index 9fe8773919..8c2029a505 100644 --- a/src/Mod/Arch/ArchWindow.py +++ b/src/Mod/Arch/ArchWindow.py @@ -1226,7 +1226,7 @@ class _Window(ArchComponent.Component): bb.enlarge(10) step = obj.LouvreWidth.Value+obj.LouvreSpacing.Value if step < bb.ZLength: - box = Part.makeBox(bb.XLength,bb.YLength,obj.LouvreWidth.Value) + box = Part.makeBox(bb.XLength,bb.YLength,obj.LouvreSpacing.Value) boxes = [] for i in range(int(bb.ZLength/step)+1): b = box.copy() From 66c362a8b4b141a4ee75cfda52c4b83dacf11ace Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Tue, 14 Apr 2020 16:01:29 +0200 Subject: [PATCH 032/142] Draft: Fixed div by zero error in snapping --- src/Mod/Draft/DraftGeomUtils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Draft/DraftGeomUtils.py b/src/Mod/Draft/DraftGeomUtils.py index 6c2cd6ed2b..eb13163993 100644 --- a/src/Mod/Draft/DraftGeomUtils.py +++ b/src/Mod/Draft/DraftGeomUtils.py @@ -305,7 +305,7 @@ def findIntersection(edge1, edge2, except: return [] norm3 = vec1.cross(vec2) - if not DraftVecUtils.isNull(norm3) : + if not DraftVecUtils.isNull(norm3) and (norm3.x+norm3.y+norm3.z != 0): k = ((pt3.z-pt1.z)*(vec2.x-vec2.y)+(pt3.y-pt1.y)*(vec2.z-vec2.x)+ \ (pt3.x-pt1.x)*(vec2.y-vec2.z))/(norm3.x+norm3.y+norm3.z) vec1.scale(k,k,k) From 30e07229e197a972416906ad250e655ef1d785fb Mon Sep 17 00:00:00 2001 From: wandererfan Date: Fri, 10 Apr 2020 17:31:28 -0400 Subject: [PATCH 033/142] [App]convenience getter for PropertyXLinkList --- src/App/PropertyLinks.cpp | 8 ++++++++ src/App/PropertyLinks.h | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/App/PropertyLinks.cpp b/src/App/PropertyLinks.cpp index 390a773a79..b936c9665e 100644 --- a/src/App/PropertyLinks.cpp +++ b/src/App/PropertyLinks.cpp @@ -4337,6 +4337,14 @@ void PropertyXLinkList::setPyObject(PyObject *value) PropertyXLinkSubList::setPyObject(value); } +//for consistency with PropertyLinkList +const std::vector PropertyXLinkList::getValues(void) const +{ + std::vector xLinks; + getLinks(xLinks); + return(xLinks); +} + //************************************************************************** // PropertyXLinkContainer //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/src/App/PropertyLinks.h b/src/App/PropertyLinks.h index 9dfa74e8ec..183cfcbfba 100644 --- a/src/App/PropertyLinks.h +++ b/src/App/PropertyLinks.h @@ -1309,6 +1309,9 @@ public: virtual PyObject *getPyObject(void) override; virtual void setPyObject(PyObject *) override; + + //for consistency with PropertyLinkList + const std::vector getValues(void) const; }; From 4104a0e2632def82e66d1e89f1be5509ea4a3f80 Mon Sep 17 00:00:00 2001 From: wandererfan Date: Fri, 10 Apr 2020 17:32:13 -0400 Subject: [PATCH 034/142] [TD]Use PropertyXLinkList for external Sources --- src/Mod/TechDraw/App/DrawProjGroup.cpp | 41 ++++++++++--- src/Mod/TechDraw/App/DrawProjGroup.h | 9 ++- src/Mod/TechDraw/App/DrawProjGroupItem.cpp | 2 + src/Mod/TechDraw/App/DrawViewPart.cpp | 32 ++++++++-- src/Mod/TechDraw/App/DrawViewPart.h | 4 ++ src/Mod/TechDraw/Gui/Command.cpp | 71 +++++++++++----------- 6 files changed, 112 insertions(+), 47 deletions(-) diff --git a/src/Mod/TechDraw/App/DrawProjGroup.cpp b/src/Mod/TechDraw/App/DrawProjGroup.cpp index c63dd05d3c..6724e6cc85 100644 --- a/src/Mod/TechDraw/App/DrawProjGroup.cpp +++ b/src/Mod/TechDraw/App/DrawProjGroup.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -73,6 +74,7 @@ DrawProjGroup::DrawProjGroup(void) : ADD_PROPERTY_TYPE(Source ,(0), group, App::Prop_None,"Shape to view"); Source.setScope(App::LinkScope::Global); Source.setAllowExternal(true); + ADD_PROPERTY_TYPE(XSource ,(0),group,App::Prop_None,"External 3D Shape to view"); ADD_PROPERTY_TYPE(Anchor, (0), group, App::Prop_None, "The root view to align projections with"); Anchor.setScope(App::LinkScope::Global); @@ -93,15 +95,28 @@ DrawProjGroup::~DrawProjGroup() { } + +//TODO: this duplicates code in DVP +std::vector DrawProjGroup::getAllSources(void) const +{ +// Base::Console().Message("DPG::getAllSources()\n"); + const std::vector links = Source.getValues(); + std::vector xLinks; + XSource.getLinks(xLinks); + std::vector result = links; + if (!xLinks.empty()) { + result.insert(result.end(), xLinks.begin(), xLinks.end()); + } + return result; +} + + void DrawProjGroup::onChanged(const App::Property* prop) { //TODO: For some reason, when the projection type is changed, the isometric views show change appropriately, but the orthographic ones don't... Or vice-versa. WF: why would you change from 1st to 3rd in mid drawing? //if group hasn't been added to page yet, can't scale or distribute projItems TechDraw::DrawPage *page = getPage(); if (!isRestoring() && page) { - if (prop == &Source) { - //nothing in particular - } if (prop == &Scale) { if (!m_lockScale) { updateChildrenScale(); @@ -112,7 +127,8 @@ void DrawProjGroup::onChanged(const App::Property* prop) updateChildrenEnforce(); } - if (prop == &Source) { + if ( (prop == &Source) || + (prop == &XSource) ) { updateChildrenSource(); } @@ -156,7 +172,7 @@ App::DocumentObjectExecReturn *DrawProjGroup::execute(void) return DrawViewCollection::execute(); } - std::vector docObjs = Source.getValues(); + std::vector docObjs = getAllSources(); if (docObjs.empty()) { return DrawViewCollection::execute(); } @@ -188,6 +204,7 @@ short DrawProjGroup::mustExecute() const if (!isRestoring()) { result = Views.isTouched() || Source.isTouched() || + XSource.isTouched() || Scale.isTouched() || ScaleType.isTouched() || ProjectionType.isTouched() || @@ -428,6 +445,11 @@ App::DocumentObject * DrawProjGroup::addProjection(const char *viewProjType) view->Label.setValue(viewProjType); addView(view); //from DrawViewCollection view->Source.setValues(Source.getValues()); +// std::vector xLinks; +// XSource.getLinks(xLinks); +// view->XSource.setValues(xLinks); + view->XSource.setValues(XSource.getValues()); + // the Scale is already set by DrawView view->Type.setValue(viewProjType); if (strcmp(viewProjType, "Front") != 0 ) { //not Front! @@ -957,8 +979,13 @@ void DrawProjGroup::updateChildrenSource(void) Base::Console().Log("PROBLEM - DPG::updateChildrenSource - non DPGI entry in Views! %s\n", getNameInDocument()); throw Base::TypeError("Error: projection in DPG list is not a DPGI!"); - } else if (view->Source.getValues() != Source.getValues()) { - view->Source.setValues(Source.getValues()); + } else { + if (view->Source.getValues() != Source.getValues()) { + view->Source.setValues(Source.getValues()); + } + if (view->XSource.getValues() != XSource.getValues()) { + view->XSource.setValues(XSource.getValues()); + } } } } diff --git a/src/Mod/TechDraw/App/DrawProjGroup.h b/src/Mod/TechDraw/App/DrawProjGroup.h index 23d5367104..e9985dcf56 100644 --- a/src/Mod/TechDraw/App/DrawProjGroup.h +++ b/src/Mod/TechDraw/App/DrawProjGroup.h @@ -27,6 +27,8 @@ # include #include #include +#include + #include #include @@ -55,7 +57,9 @@ public: DrawProjGroup(); ~DrawProjGroup(); - App::PropertyLinkList Source; + App::PropertyLinkList Source; + App::PropertyXLinkList XSource; + App::PropertyEnumeration ProjectionType; App::PropertyBool AutoDistribute; @@ -134,6 +138,9 @@ public: void autoPositionChildren(void); void updateChildrenEnforce(void); + std::vector getAllSources(void) const; + + protected: void onChanged(const App::Property* prop) override; diff --git a/src/Mod/TechDraw/App/DrawProjGroupItem.cpp b/src/Mod/TechDraw/App/DrawProjGroupItem.cpp index 36ac741e83..320b719e4d 100644 --- a/src/Mod/TechDraw/App/DrawProjGroupItem.cpp +++ b/src/Mod/TechDraw/App/DrawProjGroupItem.cpp @@ -86,6 +86,7 @@ short DrawProjGroupItem::mustExecute() const result = (Direction.isTouched() || XDirection.isTouched() || Source.isTouched() || + XSource.isTouched() || Scale.isTouched()); } @@ -174,6 +175,7 @@ void DrawProjGroupItem::autoPosition() void DrawProjGroupItem::onDocumentRestored() { +// Base::Console().Message("DPGI::onDocumentRestored() - %s\n", getNameInDocument()); App::DocumentObjectExecReturn* rc = DrawProjGroupItem::execute(); if (rc) { delete rc; diff --git a/src/Mod/TechDraw/App/DrawViewPart.cpp b/src/Mod/TechDraw/App/DrawViewPart.cpp index e1f95e7d4b..09e791ad7c 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.cpp +++ b/src/Mod/TechDraw/App/DrawViewPart.cpp @@ -143,6 +143,8 @@ DrawViewPart::DrawViewPart(void) : ADD_PROPERTY_TYPE(Source ,(0),group,App::Prop_None,"3D Shape to view"); Source.setScope(App::LinkScope::Global); Source.setAllowExternal(true); + ADD_PROPERTY_TYPE(XSource ,(0),group,App::Prop_None,"External 3D Shape to view"); + ADD_PROPERTY_TYPE(Direction ,(0.0,-1.0,0.0), group,App::Prop_None,"Projection Plane normal. The direction you are looking from."); @@ -181,7 +183,7 @@ std::vector DrawViewPart::getSourceShape2d(void) const { // Base::Console().Message("DVP::getSourceShape2d()\n"); std::vector result; - const std::vector& links = Source.getValues(); + const std::vector& links = getAllSources(); result = ShapeExtractor::getShapes2d(links); return result; } @@ -189,8 +191,9 @@ std::vector DrawViewPart::getSourceShape2d(void) const TopoDS_Shape DrawViewPart::getSourceShape(void) const { +// Base::Console().Message("DVP::getSourceShape()\n"); TopoDS_Shape result; - const std::vector& links = Source.getValues(); + const std::vector& links = getAllSources(); if (links.empty()) { bool isRestoring = getDocument()->testStatus(App::Document::Status::Restoring); if (isRestoring) { @@ -208,8 +211,10 @@ TopoDS_Shape DrawViewPart::getSourceShape(void) const TopoDS_Shape DrawViewPart::getSourceShapeFused(void) const { +// Base::Console().Message("DVP::getSourceShapeFused()\n"); TopoDS_Shape result; - const std::vector& links = Source.getValues(); +// const std::vector& links = Source.getValues(); + const std::vector& links = getAllSources(); if (links.empty()) { bool isRestoring = getDocument()->testStatus(App::Document::Status::Restoring); if (isRestoring) { @@ -225,6 +230,20 @@ TopoDS_Shape DrawViewPart::getSourceShapeFused(void) const return result; } +std::vector DrawViewPart::getAllSources(void) const +{ +// Base::Console().Message("DVP::getAllSources()\n"); + const std::vector links = Source.getValues(); + std::vector xLinks = XSource.getValues(); +// std::vector xLinks; +// XSource.getLinks(xLinks); + + std::vector result = links; + if (!xLinks.empty()) { + result.insert(result.end(), xLinks.begin(), xLinks.end()); + } + return result; +} App::DocumentObjectExecReturn *DrawViewPart::execute(void) { @@ -232,10 +251,13 @@ App::DocumentObjectExecReturn *DrawViewPart::execute(void) if (!keepUpdated()) { return App::DocumentObject::StdReturn; } + +// Base::Console().Message("DVP::execute - Source: %d XSource: %d\n", +// Source.getValues().size(), XSource.getValues().size()); App::Document* doc = getDocument(); bool isRestoring = doc->testStatus(App::Document::Status::Restoring); - const std::vector& links = Source.getValues(); + const std::vector& links = getAllSources(); if (links.empty()) { if (isRestoring) { Base::Console().Warning("DVP::execute - No Sources (but document is restoring) - %s\n", @@ -246,7 +268,6 @@ App::DocumentObjectExecReturn *DrawViewPart::execute(void) } return App::DocumentObject::StdReturn; } - std::vector sources = Source.getValues(); TopoDS_Shape shape = getSourceShape(); if (shape.IsNull()) { @@ -307,6 +328,7 @@ short DrawViewPart::mustExecute() const if (!isRestoring()) { result = (Direction.isTouched() || Source.isTouched() || + XSource.isTouched() || Perspective.isTouched() || Focus.isTouched() || Rotation.isTouched() || diff --git a/src/Mod/TechDraw/App/DrawViewPart.h b/src/Mod/TechDraw/App/DrawViewPart.h index 1c679be64e..c366e44823 100644 --- a/src/Mod/TechDraw/App/DrawViewPart.h +++ b/src/Mod/TechDraw/App/DrawViewPart.h @@ -93,6 +93,7 @@ public: virtual ~DrawViewPart(); App::PropertyLinkList Source; + App::PropertyXLinkList XSource; App::PropertyVector Direction; //TODO: Rename to YAxisDirection or whatever this actually is (ProjectionDirection) App::PropertyVector XDirection; App::PropertyBool Perspective; @@ -202,6 +203,9 @@ public: void removeAllReferencesFromGeom(); void resetReferenceVerts(); + std::vector getAllSources(void) const; + + protected: bool checkXDirection(void) const; diff --git a/src/Mod/TechDraw/Gui/Command.cpp b/src/Mod/TechDraw/Gui/Command.cpp index 89b680d198..7a82f0210f 100644 --- a/src/Mod/TechDraw/Gui/Command.cpp +++ b/src/Mod/TechDraw/Gui/Command.cpp @@ -30,28 +30,30 @@ #include -#include -#include -#include #include #include -#include #include +#include #include -#include #include +#include +#include +#include #include #include +#include #include +#include +#include #include #include #include #include #include #include -#include -#include #include +#include +#include #include #include @@ -304,6 +306,7 @@ void CmdTechDrawView::activated(int iMsg) //set projection direction from selected Face //use first object with a face selected std::vector shapes; + std::vector xShapes; App::DocumentObject* partObj = nullptr; std::string faceName; int resolve = 1; //mystery @@ -317,22 +320,33 @@ void CmdTechDrawView::activated(int iMsg) if (obj->isDerivedFrom(TechDraw::DrawPage::getClassTypeId()) ) { continue; } + if ( obj->isDerivedFrom(App::LinkElement::getClassTypeId()) || + obj->isDerivedFrom(App::LinkGroup::getClassTypeId()) || + obj->isDerivedFrom(App::Link::getClassTypeId()) ) { + xShapes.push_back(obj); + continue; + } + //not a Link and not null. assume to be drawable. Undrawables will be + // skipped later. if (obj != nullptr) { shapes.push_back(obj); } if(partObj != nullptr) { continue; } + //don't know if this works for an XLink for(auto& sub : sel.getSubNames()) { if (TechDraw::DrawUtil::getGeomTypeFromName(sub) == "Face") { faceName = sub; + // partObj = obj; break; } } } - if ((shapes.empty())) { + if ( shapes.empty() && + xShapes.empty() ) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong selection"), QObject::tr("No Shapes, Groups or Links in this selection")); return; @@ -350,6 +364,7 @@ void CmdTechDrawView::activated(int iMsg) throw Base::TypeError("CmdTechDrawView DVP not found\n"); } dvp->Source.setValues(shapes); + dvp->XSource.setValues(xShapes); doCommand(Doc,"App.activeDocument().%s.addView(App.activeDocument().%s)",PageName.c_str(),FeatName.c_str()); if (faceName.size()) { std::pair dirs = DrawGuiUtil::getProjDirFromFace(partObj,faceName); @@ -450,26 +465,6 @@ void CmdTechDrawSectionView::activated(int iMsg) return; } TechDraw::DrawViewPart* dvp = static_cast(*baseObj.begin()); -// std::string BaseName = dvp->getNameInDocument(); -// std::string PageName = page->getNameInDocument(); -// double baseScale = dvp->getScale(); - -// Gui::WaitCursor wc; -// openCommand("Create view"); -// std::string FeatName = getUniqueObjectName("Section"); - -// doCommand(Doc,"App.activeDocument().addObject('TechDraw::DrawViewSection','%s')",FeatName.c_str()); - -// App::DocumentObject *docObj = getDocument()->getObject(FeatName.c_str()); -// TechDraw::DrawViewSection* dsv = dynamic_cast(docObj); -// if (!dsv) { -// throw Base::TypeError("CmdTechDrawSectionView DVS not found\n"); -// } -// dsv->Source.setValues(dvp->Source.getValues()); -// doCommand(Doc,"App.activeDocument().%s.BaseView = App.activeDocument().%s",FeatName.c_str(),BaseName.c_str()); -// doCommand(Doc,"App.activeDocument().%s.ScaleType = App.activeDocument().%s.ScaleType",FeatName.c_str(),BaseName.c_str()); -// doCommand(Doc,"App.activeDocument().%s.addView(App.activeDocument().%s)",PageName.c_str(),FeatName.c_str()); -// doCommand(Doc,"App.activeDocument().%s.Scale = %0.6f",FeatName.c_str(),baseScale); Gui::Control().showDialog(new TaskDlgSectionView(dvp)); updateActive(); //ok here since dialog doesn't call doc.recompute() @@ -568,6 +563,7 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) //set projection direction from selected Face //use first object with a face selected std::vector shapes; + std::vector xShapes; App::DocumentObject* partObj = nullptr; std::string faceName; int resolve = 1; //mystery @@ -577,14 +573,19 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) resolve, single); for (auto& sel: selection) { -// for(auto &sel : getSelection().getSelectionEx(0,App::DocumentObject::getClassTypeId(),false)) { auto obj = sel.getObject(); if (obj->isDerivedFrom(TechDraw::DrawPage::getClassTypeId()) ) { continue; } -// if(!obj || inlist.count(obj)) //?????? -// continue; - if (obj != nullptr) { //can this happen? + if ( obj->isDerivedFrom(App::LinkElement::getClassTypeId()) || + obj->isDerivedFrom(App::LinkGroup::getClassTypeId()) || + obj->isDerivedFrom(App::Link::getClassTypeId()) ) { + xShapes.push_back(obj); + continue; + } + //not a Link and not null. assume to be drawable. Undrawables will be + // skipped later. + if (obj != nullptr) { shapes.push_back(obj); } if(partObj != nullptr) { @@ -598,9 +599,10 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) } } } - if (shapes.empty()) { + if ( shapes.empty() && + xShapes.empty() ) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong selection"), - QObject::tr("No Shapes or Groups in this selection")); + QObject::tr("No Shapes, Groups or Links in this selection")); return; } @@ -618,6 +620,7 @@ void CmdTechDrawProjectionGroup::activated(int iMsg) App::DocumentObject *docObj = getDocument()->getObject(multiViewName.c_str()); auto multiView( static_cast(docObj) ); multiView->Source.setValues(shapes); + multiView->XSource.setValues(xShapes); doCommand(Doc,"App.activeDocument().%s.addProjection('Front')",multiViewName.c_str()); if (faceName.size()) { From 855fa1adc60d9f7a5a35d17ba9b6f1d22a637d6a Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Tue, 14 Apr 2020 16:51:54 +0200 Subject: [PATCH 035/142] Arch: Support App::Parts in IFC export --- src/Mod/Arch/exportIFC.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Mod/Arch/exportIFC.py b/src/Mod/Arch/exportIFC.py index 8baf610739..699d6a516b 100644 --- a/src/Mod/Arch/exportIFC.py +++ b/src/Mod/Arch/exportIFC.py @@ -329,6 +329,32 @@ def export(exportList,filename,colors=None,preferences=None): assemblyElements.append(subproduct) ifctype = "IfcElementAssembly" + elif ifctype == "IfcApp::Part": + for subobj in [FreeCAD.ActiveDocument.getObject(n[:-1]) for n in obj.getSubObjects()]: + representation,placement,shapetype = getRepresentation( + ifcfile, + context, + subobj, + forcebrep=(getBrepFlag(subobj,preferences)), + colors=colors, + preferences=preferences + ) + subproduct = createProduct( + ifcfile, + subobj, + getIfcTypeFromObj(subobj), + getUID(subobj,preferences), + history, + getText("Name",subobj), + getText("Description",subobj), + placement, + representation, + preferences, + schema) + + assemblyElements.append(subproduct) + ifctype = "IfcElementAssembly" + # export grids if ifctype in ["IfcAxis","IfcAxisSystem","IfcGrid"]: From 7bd2ec425eb2f5acc9fa37be9d3263dc3af722af Mon Sep 17 00:00:00 2001 From: Eric Trombly Date: Tue, 14 Apr 2020 10:44:15 -0500 Subject: [PATCH 036/142] add docstring to __init__.py --- src/3rdParty/lazy_loader/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/3rdParty/lazy_loader/__init__.py b/src/3rdParty/lazy_loader/__init__.py index e69de29bb2..db8200b5bc 100644 --- a/src/3rdParty/lazy_loader/__init__.py +++ b/src/3rdParty/lazy_loader/__init__.py @@ -0,0 +1,13 @@ +""" +LazyLoader will defer import of a module until first usage. Usage: +from lazy_loader.lazy_loader import LazyLoader +numpy = LazyLoader("numpy", globals(), "numpy") + +or + +whatever = LazyLoader("module", globals(), "module.whatever") + +or to replicate import module as something + +something = LazyLoader("module", globals(), "module") +""" \ No newline at end of file From a150f8a54f64c5757a389e413431e215319e48f6 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Tue, 14 Apr 2020 17:56:18 +0200 Subject: [PATCH 037/142] Arch: Fixed IFC4 export of surface styles --- src/Mod/Arch/exportIFC.py | 40 ++++++++++++++++----------------- src/Mod/Arch/exportIFCHelper.py | 7 ++++-- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/Mod/Arch/exportIFC.py b/src/Mod/Arch/exportIFC.py index 699d6a516b..cafeb9fd4e 100644 --- a/src/Mod/Arch/exportIFC.py +++ b/src/Mod/Arch/exportIFC.py @@ -140,6 +140,14 @@ def getPreferences(): 'SCALE_FACTOR': f, 'GET_STANDARD': p.GetBool("getStandardType",False) } + if hasattr(ifcopenshell,"schema_identifier"): + schema = ifcopenshell.schema_identifier + elif hasattr(ifcopenshell,"version") and (float(ifcopenshell.version[:3]) >= 0.6): + # v0.6 onwards allows to set our own schema + schema = ["IFC4", "IFC2X3"][FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetInt("IfcVersion",0)] + else: + schema = "IFC2X3" + preferences["SCHEMA"] = schema return preferences @@ -152,9 +160,6 @@ def export(exportList,filename,colors=None,preferences=None): colors is an optional dictionary of objName:shapeColorTuple or objName:diffuseColorList elements to be used in non-GUI mode if you want to be able to export colors.""" - if preferences is None: - preferences = getPreferences() - try: global ifcopenshell import ifcopenshell @@ -163,6 +168,9 @@ def export(exportList,filename,colors=None,preferences=None): FreeCAD.Console.PrintMessage("Visit https://www.freecadweb.org/wiki/Arch_IFC to learn how to install it\n") return + if preferences is None: + preferences = getPreferences() + # process template version = FreeCAD.Version() @@ -174,15 +182,8 @@ def export(exportList,filename,colors=None,preferences=None): email = s[1].strip(">") global template template = ifctemplate.replace("$version",version[0]+"."+version[1]+" build "+version[2]) - if hasattr(ifcopenshell,"schema_identifier"): - schema = ifcopenshell.schema_identifier - elif hasattr(ifcopenshell,"version") and (float(ifcopenshell.version[:3]) >= 0.6): - # v0.6 onwards allows to set our own schema - schema = ["IFC4", "IFC2X3"][FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetInt("IfcVersion",0)] - else: - schema = "IFC2X3" - if preferences['DEBUG']: print("Exporting an",schema,"file...") - template = template.replace("$ifcschema",schema) + if preferences['DEBUG']: print("Exporting an",preferences['SCHEMA'],"file...") + template = template.replace("$ifcschema",preferences['SCHEMA']) template = template.replace("$owner",owner) template = template.replace("$company",FreeCAD.ActiveDocument.Company) template = template.replace("$email",email) @@ -323,8 +324,7 @@ def export(exportList,filename,colors=None,preferences=None): getText("Description",obj.Base), placement, representation, - preferences, - schema) + preferences) assemblyElements.append(subproduct) ifctype = "IfcElementAssembly" @@ -349,8 +349,7 @@ def export(exportList,filename,colors=None,preferences=None): getText("Description",subobj), placement, representation, - preferences, - schema) + preferences) assemblyElements.append(subproduct) ifctype = "IfcElementAssembly" @@ -446,8 +445,7 @@ def export(exportList,filename,colors=None,preferences=None): description, placement, representation, - preferences, - schema) + preferences) products[obj.Name] = product if ifctype in ["IfcBuilding","IfcBuildingStorey","IfcSite","IfcSpace"]: @@ -1170,7 +1168,7 @@ def export(exportList,filename,colors=None,preferences=None): rgb = tuple([float(f) for f in m.Material[colorslot].strip("()").split(",")]) break if rgb: - psa = ifcbin.createIfcPresentationStyleAssignment(l,rgb[0],rgb[1],rgb[2]) + psa = ifcbin.createIfcPresentationStyleAssignment(l,rgb[0],rgb[1],rgb[2],ifc4=(preferences["SCHEMA"]=="IFC4")) isi = ifcfile.createIfcStyledItem(None,[psa],None) isr = ifcfile.createIfcStyledRepresentation(context,"Style","Material",[isi]) imd = ifcfile.createIfcMaterialDefinitionRepresentation(None,None,[isr],mat) @@ -2187,7 +2185,7 @@ def getBrepFlag(obj,preferences): return brepflag -def createProduct(ifcfile,obj,ifctype,uid,history,name,description,placement,representation,preferences,schema): +def createProduct(ifcfile,obj,ifctype,uid,history,name,description,placement,representation,preferences): """creates a product in the given IFC file""" kwargs = { @@ -2206,7 +2204,7 @@ def createProduct(ifcfile,obj,ifctype,uid,history,name,description,placement,rep "SiteAddress":buildAddress(obj,ifcfile), "CompositionType": "ELEMENT" }) - if schema == "IFC2X3": + if preferences['SCHEMA'] == "IFC2X3": kwargs = exportIFC2X3Attributes(obj, kwargs, preferences['SCALE_FACTOR']) else: kwargs = exportIfcAttributes(obj, kwargs, preferences['SCALE_FACTOR']) diff --git a/src/Mod/Arch/exportIFCHelper.py b/src/Mod/Arch/exportIFCHelper.py index c82c75778c..11ff3b313f 100644 --- a/src/Mod/Arch/exportIFCHelper.py +++ b/src/Mod/Arch/exportIFCHelper.py @@ -370,7 +370,7 @@ class recycler: self.sstyles[key] = c return c - def createIfcPresentationStyleAssignment(self,name,r,g,b,t=0): + def createIfcPresentationStyleAssignment(self,name,r,g,b,t=0,ifc4=False): if name: key = name+str((r,g,b,t)) else: @@ -380,7 +380,10 @@ class recycler: return self.psas[key] else: iss = self.createIfcSurfaceStyle(name,r,g,b,t) - c = self.ifcfile.createIfcPresentationStyleAssignment([iss]) + if ifc4: + c = iss + else: + c = self.ifcfile.createIfcPresentationStyleAssignment([iss]) if self.compress: self.psas[key] = c return c From 7d9cfeb2c5032aa5edbae6cf10213aaec3ee97f9 Mon Sep 17 00:00:00 2001 From: Abdullah Tahiri Date: Tue, 14 Apr 2020 15:42:48 +0200 Subject: [PATCH 038/142] Sketcher: Fix trim ================== https://forum.freecadweb.org/viewtopic.php?p=387303#p387303 1. Trim had a bug that the type of the constraint on the second point was equal to the first one regardless of the situation. 2. Trim did not have support for checking whether points were close to the edge and relied on preexisting constraints. --- src/Mod/Sketcher/App/SketchObject.cpp | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 443840ab3f..eeb4061446 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -1939,6 +1939,17 @@ int SketchObject::trim(int GeoId, const Base::Vector3d& point) secondPos = constr->FirstPos; } }; + + auto isPointAtPosition = [this] (int GeoId1, PointPos pos1, Base::Vector3d point) { + + Base::Vector3d pp = getPoint(GeoId1,pos1); + + if( (point-pp).Length() < Precision::Confusion() ) + return true; + + return false; + + }; Part::Geometry *geo = geomlist[GeoId]; if (geo->getTypeId() == Part::GeomLineSegment::getClassTypeId()) { @@ -2128,6 +2139,26 @@ int SketchObject::trim(int GeoId, const Base::Vector3d& point) PointPos secondPos1 = Sketcher::none, secondPos2 = Sketcher::none; ConstraintType constrType1 = Sketcher::PointOnObject, constrType2 = Sketcher::PointOnObject; + + // check first if start and end points are within a confusion tolerance + if(isPointAtPosition(GeoId1, Sketcher::start, point1)) { + constrType1 = Sketcher::Coincident; + secondPos1 = Sketcher::start; + } + else if(isPointAtPosition(GeoId1, Sketcher::end, point1)) { + constrType1 = Sketcher::Coincident; + secondPos1 = Sketcher::end; + } + + if(isPointAtPosition(GeoId2, Sketcher::start, point2)) { + constrType2 = Sketcher::Coincident; + secondPos2 = Sketcher::start; + } + else if(isPointAtPosition(GeoId2, Sketcher::end, point2)) { + constrType2 = Sketcher::Coincident; + secondPos2 = Sketcher::end; + } + for (std::vector::const_iterator it=constraints.begin(); it != constraints.end(); ++it) { Constraint *constr = *(it); @@ -2162,6 +2193,7 @@ int SketchObject::trim(int GeoId, const Base::Vector3d& point) newConstr->SecondPos = Sketcher::none; // Add Second Constraint + newConstr->Type = constrType2; newConstr->First = GeoId; newConstr->FirstPos = end; newConstr->Second = GeoId2; From 2d29ac5fc1e5a259a7179573c7310887d2d44e6e Mon Sep 17 00:00:00 2001 From: wandererfan Date: Tue, 14 Apr 2020 10:14:02 -0400 Subject: [PATCH 039/142] [TD]Fix Detail dragger for ProjGroup --- src/Mod/TechDraw/Gui/TaskDetail.cpp | 68 +++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/src/Mod/TechDraw/Gui/TaskDetail.cpp b/src/Mod/TechDraw/Gui/TaskDetail.cpp index 8d44dc974b..1388159ff5 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.cpp +++ b/src/Mod/TechDraw/Gui/TaskDetail.cpp @@ -46,6 +46,8 @@ #include #include #include +#include +#include #include #include @@ -202,6 +204,7 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): TaskDetail::~TaskDetail() { + m_ghost->deleteLater(); //this might not exist if scene is destroyed before TaskDetail is deleted? delete ui; } @@ -326,8 +329,10 @@ void TaskDetail::editByHighlight() return; } + double scale = getBaseFeat()->getScale(); m_scene->clearSelection(); m_ghost->setSelected(true); + m_ghost->setRadius(ui->qsbRadius->rawValue() * scale); m_ghost->setPos(getAnchorScene()); m_ghost->draw(); m_ghost->show(); @@ -341,12 +346,25 @@ void TaskDetail::onHighlightMoved(QPointF dragEnd) ui->pbDragger->setEnabled(true); double scale = getBaseFeat()->getScale(); - double x = Rez::guiX(getBaseFeat()->X.getValue()) * scale; - double y = Rez::guiX(getBaseFeat()->Y.getValue()) * scale; - QPointF basePosScene(x, -y); //base position in scene coords + double x = Rez::guiX(getBaseFeat()->X.getValue()); + double y = Rez::guiX(getBaseFeat()->Y.getValue()); + DrawViewPart* dvp = getBaseFeat(); + DrawProjGroupItem* dpgi = dynamic_cast(dvp); + if (dpgi != nullptr) { + DrawProjGroup* dpg = dpgi->getPGroup(); + if (dpg == nullptr) { + Base::Console().Message("TD::getAnchorScene - projection group is confused\n"); + //TODO::throw something. + return; + } + x += Rez::guiX(dpg->X.getValue()); + y += Rez::guiX(dpg->Y.getValue()); + } + + QPointF basePosScene(x, -y); //base position in scene coords QPointF anchorDisplace = dragEnd - basePosScene; - QPointF newAnchorPos = Rez::appX(anchorDisplace) / scale; + QPointF newAnchorPos = Rez::appX(anchorDisplace / scale); updateUi(newAnchorPos); updateDetail(); @@ -430,17 +448,39 @@ void TaskDetail::updateDetail() //get the current Anchor highlight position in scene coords QPointF TaskDetail::getAnchorScene() { - TechDraw::DrawViewPart* dvp = getBaseFeat(); - TechDraw::DrawViewDetail* dvd = getDetailFeat(); - + DrawViewPart* dvp = getBaseFeat(); + DrawProjGroupItem* dpgi = dynamic_cast(dvp); + DrawViewDetail* dvd = getDetailFeat(); Base::Vector3d anchorPos = dvd->AnchorPoint.getValue(); - double x = dvp->X.getValue(); - double y = dvp->Y.getValue(); - Base::Vector3d basePos(x, y, 0.0); - Base::Vector3d netPos = basePos + anchorPos; - netPos = Rez::guiX(netPos * dvp->getScale()); + anchorPos.y = -anchorPos.y; + Base::Vector3d basePos; + double scale = 1; - QPointF qAnchor(netPos.x, - netPos.y); + if (dpgi == nullptr) { //base is normal view + double x = dvp->X.getValue(); + double y = dvp->Y.getValue(); + basePos = Base::Vector3d (x, -y, 0.0); + scale = dvp->getScale(); + } else { //part of projection group + + DrawProjGroup* dpg = dpgi->getPGroup(); + if (dpg == nullptr) { + Base::Console().Message("TD::getAnchorScene - projection group is confused\n"); + //TODO::throw something. + return QPointF(0.0, 0.0); + } + double x = dpg->X.getValue(); + x += dpgi->X.getValue(); + double y = dpg->Y.getValue(); + y += dpgi->Y.getValue(); + basePos = Base::Vector3d(x, -y, 0.0); + scale = dpgi->getScale(); + } + + Base::Vector3d xyScene = Rez::guiX(basePos); + Base::Vector3d anchorOffsetScene = Rez::guiX(anchorPos) * scale; + Base::Vector3d netPos = xyScene + anchorOffsetScene; + QPointF qAnchor(netPos.x, netPos.y); return qAnchor; } @@ -496,6 +536,7 @@ bool TaskDetail::accept() Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); if (!doc) return false; + m_ghost->hide(); getDetailFeat()->requestPaint(); getBaseFeat()->requestPaint(); Gui::Command::doCommand(Gui::Command::Gui,"Gui.ActiveDocument.resetEdit()"); @@ -509,6 +550,7 @@ bool TaskDetail::reject() Gui::Document* doc = Gui::Application::Instance->getDocument(m_basePage->getDocument()); if (!doc) return false; + m_ghost->hide(); if (m_mode == CREATEMODE) { if (m_created) { Gui::Command::doCommand(Gui::Command::Gui,"App.activeDocument().removeObject('%s')", From 843d39495709ed2daff51aebbfeba665a9260894 Mon Sep 17 00:00:00 2001 From: wmayer Date: Wed, 15 Apr 2020 10:39:46 +0200 Subject: [PATCH 040/142] Tools: [skip ci] move import of requests module into try/except block --- src/Tools/SubWCRev.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/SubWCRev.py b/src/Tools/SubWCRev.py index bd9897c9c1..9795450226 100644 --- a/src/Tools/SubWCRev.py +++ b/src/Tools/SubWCRev.py @@ -142,7 +142,6 @@ class DebianGitHub(VersionControl): commit = f.readline() f.close() - import requests base_url = "https://api.github.com" owner = "FreeCAD" repo = "FreeCAD" @@ -150,6 +149,7 @@ class DebianGitHub(VersionControl): self.hash = sha try: + import requests request_url = "{}/repos/{}/{}/commits?per_page=1&sha={}".format(base_url, owner, repo, sha) commit_req = requests.get(request_url) if not commit_req.ok: From 9dac36eec9514d02420db59f1ce4bf56653c7041 Mon Sep 17 00:00:00 2001 From: Bernd Hahnebach Date: Tue, 14 Apr 2020 14:01:36 +0200 Subject: [PATCH 041/142] FEM: self weight object, set to not shown in property editor --- src/Mod/Fem/femobjects/_FemConstraintSelfWeight.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Mod/Fem/femobjects/_FemConstraintSelfWeight.py b/src/Mod/Fem/femobjects/_FemConstraintSelfWeight.py index 1e5d3e5917..95f47f8653 100644 --- a/src/Mod/Fem/femobjects/_FemConstraintSelfWeight.py +++ b/src/Mod/Fem/femobjects/_FemConstraintSelfWeight.py @@ -66,3 +66,9 @@ class _FemConstraintSelfWeight(FemConstraint.Proxy): obj.Gravity_x = 0.0 obj.Gravity_y = 0.0 obj.Gravity_z = -1.0 + + # https://wiki.freecadweb.org/Scripted_objects#Property_Type + # https://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=20#p109709 + # https://forum.freecadweb.org/viewtopic.php?t=25524 + # obj.setEditorMode("References", 1) # read only in PropertyEditor, but writeable by Python + obj.setEditorMode("References", 2) # do not show in Editor From f0061fadeb014914e715b07f3a691858e01d4c9a Mon Sep 17 00:00:00 2001 From: Bernd Hahnebach Date: Wed, 15 Apr 2020 08:17:29 +0200 Subject: [PATCH 042/142] FEM: group meshing, fix retriving group elements in rare cases --- src/Mod/Fem/femmesh/meshtools.py | 45 +++++++++++++++++++------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/Mod/Fem/femmesh/meshtools.py b/src/Mod/Fem/femmesh/meshtools.py index a35a4d82d8..97b353baec 100644 --- a/src/Mod/Fem/femmesh/meshtools.py +++ b/src/Mod/Fem/femmesh/meshtools.py @@ -1846,21 +1846,36 @@ def get_analysis_group_elements( aAnalysis, aPart ): - """ all Reference shapes of all Analysis member are searched in the Shape of aPart. - If found in shape they are added to a dict - {ConstraintName : ["ShapeType of the Elements"], [ElementID, ElementID, ...], ...} """ + all Reference shapes of all Analysis member are searched in the Shape of aPart. + If found in shape they are added to a dict + {ConstraintName : ["ShapeType of the Elements"], [ElementID, ElementID, ...], ...} + """ + from femtools.femutils import is_of_type group_elements = {} # { name : [element, element, ... , element]} empty_references = [] + # find the objects with empty references, if there are more than one of this type + # they are for all shapes not in the references of the other objects + # ATM: empty references if there are more than one obj of this type are allowed for: + # solid meshes: material + # face meshes: materials, ShellThickness + # edge meshes: material, BeamSection/FluidSection + # BTW: some constraints do have empty references in any case (ex. constraint self weight) for m in aAnalysis.Group: - if hasattr(m, "References") and "ReadOnly" not in m.getEditorMode("References"): - # some C++ Constraints have a not used References Property - # it is set to Hidden in ReadOnly and PropertyEditor - if m.References: + if hasattr(m, "References"): + if len(m.References) > 0: grp_ele = get_reference_group_elements(m, aPart) group_elements[grp_ele[0]] = grp_ele[1] - else: - FreeCAD.Console.PrintMessage(" Empty reference: " + m.Name + "\n") + elif ( + len(m.References) == 0 + and ( + is_of_type(m, "Fem::Material") + # TODO test and implement ElementGeometry1D and ElementGeometry2D + # or is_of_type(m, "Fem::ElementGeometry1D") + # or is_of_type(m, "Fem::ElementGeometry2D") + ) + ): + FreeCAD.Console.PrintMessage(" Empty reference: {}\n".format(m.Name)) empty_references.append(m) if empty_references: if len(empty_references) == 1: @@ -1876,11 +1891,8 @@ def get_analysis_group_elements( FreeCAD.Console.PrintMessage( "We are going to try to get the empty material references anyway.\n" ) - # FemElementGeometry2D, ElementGeometry1D and - # FemElementFluid1D could have empty references, - # but on solid meshes only materials should have empty references for er in empty_references: - FreeCAD.Console.PrintMessage(er.Name + "\n") + FreeCAD.Console.PrintMessage("{}\n".format(er.Name)) group_elements = get_anlysis_empty_references_group_elements( group_elements, aAnalysis, @@ -1990,12 +2002,9 @@ def get_anlysis_empty_references_group_elements( aAnalysis, aShape ): - """get the elementIDs if the Reference shape is empty + """ + get the elementIDs if the Reference shape is empty see get_analysis_group_elements() for more information - on solid meshes only material objects could have an - empty reference without there being something wrong! - face meshes could have empty ShellThickness and - edge meshes could have empty BeamSection/FluidSection """ # FreeCAD.Console.PrintMessage("{}\n".format(group_elements)) material_ref_shapes = [] From 8fbfeffe13ab51f92b9dfc581ed4a6ee7abb1499 Mon Sep 17 00:00:00 2001 From: Przemo Firszt Date: Tue, 14 Apr 2020 09:09:07 +0100 Subject: [PATCH 043/142] Switch freecad.spec from OCE to opencascade (OCCT) Signed-off-by: Przemo Firszt --- package/fedora/freecad.spec | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/package/fedora/freecad.spec b/package/fedora/freecad.spec index 837a82381e..396b63e695 100644 --- a/package/fedora/freecad.spec +++ b/package/fedora/freecad.spec @@ -10,8 +10,6 @@ %global plugins Drawing Fem FreeCAD Image Import Inspection Mesh MeshPart Part Points QtUnit Raytracing ReverseEngineering Robot Sketcher Start Web PartDesignGui _PartDesign Path PathGui Spreadsheet SpreadsheetGui area DraftUtils DraftUtils libDriver libDriverDAT libDriverSTL libDriverUNV libMEFISTO2 libSMDS libSMESH libSMESHDS libStdMeshers Measure TechDraw TechDrawGui libarea-native Surface SurfaceGui PathSimulator # Some configuration options for other environments -# rpmbuild --with=occ: Compile using OpenCASCADE instead of OCE -%global occ %{?_with_occ: 1} %{?!_with_occ: 0} # rpmbuild --with=bundled_zipios: use bundled version of zipios++ %global bundled_zipios %{?_with_bundled_zipios: 1} %{?!_with_bundled_zipios: 0} # rpmbuild --without=bundled_pycxx: don't use bundled version of pycxx @@ -50,13 +48,7 @@ BuildRequires: git BuildRequires: Coin3-devel BuildRequires: Inventor-devel -%if %{occ} -BuildRequires: OpenCASCADE-devel -%else -BuildRequires: OCE-devel -BuildRequires: OCE-draw -%endif - +BuildRequires: opencascade-devel BuildRequires: boost-devel BuildRequires: boost-python3-devel BuildRequires: eigen3-devel @@ -210,9 +202,7 @@ LDFLAGS='-Wl,--as-needed -Wl,--no-undefined'; export LDFLAGS -DCOIN3D_INCLUDE_DIR=%{_includedir}/Coin3 \ -DCOIN3D_DOC_PATH=%{_datadir}/Coin3/Coin \ -DFREECAD_USE_EXTERNAL_PIVY=TRUE \ -%if %{occ} -DUSE_OCC=TRUE \ -%endif %if ! %{bundled_smesh} -DFREECAD_USE_EXTERNAL_SMESH=TRUE \ -DSMESH_FOUND=TRUE \ From f1a2063a14b2b2826dfa5be09219f161ab268c2f Mon Sep 17 00:00:00 2001 From: Przemo Firszt Date: Tue, 14 Apr 2020 09:09:47 +0100 Subject: [PATCH 044/142] Remove conditionals for older fedora versions from freecad.spec Signed-off-by: Przemo Firszt --- package/fedora/freecad.spec | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package/fedora/freecad.spec b/package/fedora/freecad.spec index 396b63e695..7d7702518c 100644 --- a/package/fedora/freecad.spec +++ b/package/fedora/freecad.spec @@ -71,19 +71,15 @@ BuildRequires: netgen-mesher-devel-private BuildRequires: python3-pivy BuildRequires: mesa-libEGL-devel BuildRequires: pcl-devel -%if 0%{?fedora} > 29 BuildRequires: pyside2-tools -%endif BuildRequires: python3 BuildRequires: python3-devel BuildRequires: python3-matplotlib %if ! %{bundled_pycxx} BuildRequires: python3-pycxx-devel %endif -%if 0%{?fedora} > 29 BuildRequires: python3-pyside2-devel BuildRequires: python3-shiboken2-devel -%endif BuildRequires: qt5-devel BuildRequires: qt5-qtwebkit-devel %if ! %{bundled_smesh} From 57328a136e2ca51cb0a07bcabbb3df83c21cd9cb Mon Sep 17 00:00:00 2001 From: donovaly Date: Mon, 13 Apr 2020 23:42:02 +0200 Subject: [PATCH 045/142] [TD] split too long preferences dialogs - as once discussed 2 of the dialogs are too long for smaller screens. This commits splits the "Dimensions" dialog to "Dimensions" and "Annotation". --- src/Mod/TechDraw/Gui/AppTechDrawGui.cpp | 6 +- src/Mod/TechDraw/Gui/CMakeLists.txt | 15 +- ...Draw3.ui => DlgPrefsTechDrawAnnotation.ui} | 541 +-------------- ....cpp => DlgPrefsTechDrawAnnotationImp.cpp} | 61 +- .../Gui/DlgPrefsTechDrawAnnotationImp.h | 51 ++ .../Gui/DlgPrefsTechDrawDimensions.ui | 617 ++++++++++++++++++ .../Gui/DlgPrefsTechDrawDimensionsImp.cpp | 119 ++++ ...3Imp.h => DlgPrefsTechDrawDimensionsImp.h} | 19 +- 8 files changed, 825 insertions(+), 604 deletions(-) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw3.ui => DlgPrefsTechDrawAnnotation.ui} (63%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw3Imp.cpp => DlgPrefsTechDrawAnnotationImp.cpp} (66%) create mode 100644 src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.h create mode 100644 src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensions.ui create mode 100644 src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.cpp rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw3Imp.h => DlgPrefsTechDrawDimensionsImp.h} (80%) diff --git a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp index b8833756f9..810708cfd5 100644 --- a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp +++ b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp @@ -41,7 +41,8 @@ #include "DlgPrefsTechDraw1Imp.h" #include "DlgPrefsTechDraw2Imp.h" -#include "DlgPrefsTechDraw3Imp.h" +#include "DlgPrefsTechDrawAnnotationImp.h" +#include "DlgPrefsTechDrawDimensionsImp.h" #include "DlgPrefsTechDraw4Imp.h" #include "DlgPrefsTechDraw5Imp.h" #include "ViewProviderPage.h" @@ -151,7 +152,8 @@ PyMOD_INIT_FUNC(TechDrawGui) // register preferences pages new Gui::PrefPageProducer ("TechDraw"); //General new Gui::PrefPageProducer ("TechDraw"); //Scale - new Gui::PrefPageProducer ("TechDraw"); //Dimensions + new Gui::PrefPageProducer ("TechDraw"); //Annotation + new Gui::PrefPageProducer("TechDraw"); //Dimensions new Gui::PrefPageProducer ("TechDraw"); //HLR new Gui::PrefPageProducer ("TechDraw"); //Advanced diff --git a/src/Mod/TechDraw/Gui/CMakeLists.txt b/src/Mod/TechDraw/Gui/CMakeLists.txt index 43ab8e5eeb..35edc2ce17 100644 --- a/src/Mod/TechDraw/Gui/CMakeLists.txt +++ b/src/Mod/TechDraw/Gui/CMakeLists.txt @@ -46,7 +46,8 @@ set(TechDrawGui_MOC_HDRS TaskProjGroup.h DlgPrefsTechDraw1Imp.h DlgPrefsTechDraw2Imp.h - DlgPrefsTechDraw3Imp.h + DlgPrefsTechDrawAnnotationImp.h + DlgPrefsTechDrawDimensionsImp.h DlgPrefsTechDraw4Imp.h DlgPrefsTechDraw5Imp.h TaskLinkDim.h @@ -86,7 +87,8 @@ endif() set(TechDrawGui_UIC_SRCS DlgPrefsTechDraw1.ui DlgPrefsTechDraw2.ui - DlgPrefsTechDraw3.ui + DlgPrefsTechDrawAnnotation.ui + DlgPrefsTechDrawDimensions.ui DlgPrefsTechDraw4.ui DlgPrefsTechDraw5.ui TaskProjGroup.ui @@ -151,9 +153,12 @@ SET(TechDrawGui_SRCS DlgPrefsTechDraw2.ui DlgPrefsTechDraw2Imp.cpp DlgPrefsTechDraw2Imp.h - DlgPrefsTechDraw3.ui - DlgPrefsTechDraw3Imp.cpp - DlgPrefsTechDraw3Imp.h + DlgPrefsTechDrawAnnotation.ui + DlgPrefsTechDrawAnnotationImp.cpp + DlgPrefsTechDrawAnnotationImp.h + DlgPrefsTechDrawDimensions.ui + DlgPrefsTechDrawDimensionsImp.cpp + DlgPrefsTechDrawDimensionsImp.h DlgPrefsTechDraw4.ui DlgPrefsTechDraw4Imp.cpp DlgPrefsTechDraw4Imp.h diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui similarity index 63% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui index c2d313e355..088c917a31 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotation.ui @@ -1,13 +1,13 @@ - TechDrawGui::DlgPrefsTechDraw3Imp - + TechDrawGui::DlgPrefsTechDrawAnnotationImp + 0 0 460 - 790 + 460 @@ -17,374 +17,9 @@ - Dimensions + Annotation - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - Dimensions - - - - - - - - - 0 - 0 - - - - Standard and Style - - - - - - - - 0 - 0 - - - - - 184 - 22 - - - - Standard to be used for dimensional values - - - StandardAndStyle - - - /Mod/TechDraw/Dimensions - - - - ISO Oriented - - - - - ISO Referencing - - - - - ASME Inlined - - - - - ASME Referencing - - - - - - - - - 0 - 0 - - - - Use system setting for number of decimals - - - Use Global Decimals - - - true - - - UseGlobalDecimals - - - /Mod/TechDraw/Dimensions - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - Append unit to dimension values - - - Show Units - - - ShowUnits - - - /Mod/TechDraw/Dimensions - - - - - - - Alternate Decimals - - - - - - - false - - - - 0 - 0 - - - - - 0 - 22 - - - - Number of decimals if 'Use Global Decimals' is not used - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - AltDecimals - - - /Mod/TechDraw/Dimensions - - - - - - - - true - - - - Font Size - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - Dimension text font size - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 4.000000000000000 - - - FontSize - - - /Mod/TechDraw/Dimensions - - - - - - - Diameter Symbol - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - - 12 - - - - Character used to indicate diameter dimensions - - - ⌀ - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - DiameterSymbol - - - /Mod/TechDraw/Dimensions - - - - - - - - true - - - - Arrow Style - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - Arrowhead style - - - -1 - - - ArrowStyle - - - Mod/TechDraw/Dimensions - - - - - - - - true - - - - Arrow Size - - - - - - - - 0 - 0 - - - - - 0 - 22 - - - - Arrowhead size - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 5.000000000000000 - - - ArrowSize - - - Mod/TechDraw/Dimensions - - - - - - - - + @@ -1201,148 +836,6 @@ - - - - - 0 - 0 - - - - - 0 - 85 - - - - - 16777215 - 500 - - - - - 0 - 500 - - - - Conventions - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Projection Group Angle - - - - - - - - 0 - 0 - - - - - 184 - 0 - - - - Use first- or third-angle mutliview projection convention - - - ProjectionAngle - - - Mod/TechDraw/General - - - - First - - - - - Third - - - - - - - - Hidden Line Style - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - Style for hidden lines - - - HiddenLine - - - Mod/TechDraw/General - - - - Continuous - - - - :/icons/continuous-line.svg:/icons/continuous-line.svg - - - - - Dashed - - - - :/icons/dash-line.svg:/icons/dash-line.svg - - - - - - - - - @@ -1383,11 +876,6 @@ QWidget
    Gui/QuantitySpinBox.h
    - - Gui::PrefSpinBox - QSpinBox -
    Gui/PrefWidgets.h
    -
    Gui::PrefCheckBox QCheckBox @@ -1412,22 +900,5 @@ - - - cbGlobalDecimals - toggled(bool) - sbAltDecimals - setDisabled(bool) - - - 108 - 71 - - - 425 - 124 - - - - +
    diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.cpp similarity index 66% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.cpp index 624527ce47..22eb09e6d3 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.cpp @@ -1,6 +1,6 @@ /*************************************************************************** - * Copyright (c) 2015 FreeCAD Developers * - * Author: WandererFan * + * Copyright (c) 2020 FreeCAD Developers * + * Author: Uwe Stöhr * * Based on src/Mod/FEM/Gui/DlgSettingsFEMImp.cpp * * * * This file is part of the FreeCAD CAx development system. * @@ -31,105 +31,75 @@ #include #include "DrawGuiUtil.h" -#include "DlgPrefsTechDraw3Imp.h" +#include "DlgPrefsTechDrawAnnotationImp.h" using namespace TechDrawGui; using namespace TechDraw; -DlgPrefsTechDraw3Imp::DlgPrefsTechDraw3Imp( QWidget* parent ) +DlgPrefsTechDrawAnnotationImp::DlgPrefsTechDrawAnnotationImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); - plsb_FontSize->setUnit(Base::Unit::Length); - plsb_FontSize->setMinimum(0); - plsb_ArrowSize->setUnit(Base::Unit::Length); - plsb_ArrowSize->setMinimum(0); pdsbBalloonKink->setUnit(Base::Unit::Length); pdsbBalloonKink->setMinimum(0); } -DlgPrefsTechDraw3Imp::~DlgPrefsTechDraw3Imp() +DlgPrefsTechDrawAnnotationImp::~DlgPrefsTechDrawAnnotationImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw3Imp::saveSettings() +void DlgPrefsTechDrawAnnotationImp::saveSettings() { cbAutoHoriz->onSave(); - cbGlobalDecimals->onSave(); - cbHiddenLineStyle->onSave(); cbPrintCenterMarks->onSave(); - cbProjAngle->onSave(); cbPyramidOrtho->onSave(); cbSectionLineStd->onSave(); cbShowCenterMarks->onSave(); - cbShowUnits->onSave(); - leDiameter->onSave(); leLineGroup->onSave(); - pcbArrow->onSave(); pcbBalloonArrow->onSave(); pcbBalloonShape->onSave(); pcbCenterStyle->onSave(); pcbMatting->onSave(); pcbSectionStyle->onSave(); - pcbStandardAndStyle->onSave(); pdsbBalloonKink->onSave(); - plsb_ArrowSize->onSave(); - plsb_FontSize->onSave(); - sbAltDecimals->onSave(); cbCutSurface->onSave(); pcbHighlightStyle->onSave(); } -void DlgPrefsTechDraw3Imp::loadSettings() +void DlgPrefsTechDrawAnnotationImp::loadSettings() { //set defaults for Quantity widgets if property not found //Quantity widgets do not use preset value since they are based on //QAbstractSpinBox double kinkDefault = 5.0; pdsbBalloonKink->setValue(kinkDefault); - double arrowDefault = 5.0; - plsb_ArrowSize->setValue(arrowDefault); - double fontDefault = 4.0; - plsb_FontSize->setValue(fontDefault); cbAutoHoriz->onRestore(); - cbGlobalDecimals->onRestore(); - cbHiddenLineStyle->onRestore(); cbPrintCenterMarks->onRestore(); - cbProjAngle->onRestore(); cbPyramidOrtho->onRestore(); cbSectionLineStd->onRestore(); cbShowCenterMarks->onRestore(); - cbShowUnits->onRestore(); - leDiameter->onRestore(); leLineGroup->onRestore(); - pcbArrow->onRestore(); pcbBalloonArrow->onRestore(); pcbBalloonShape->onRestore(); pcbCenterStyle->onRestore(); pcbMatting->onRestore(); pcbSectionStyle->onRestore(); - pcbStandardAndStyle->onRestore(); pdsbBalloonKink->onRestore(); - plsb_ArrowSize->onRestore(); - plsb_FontSize->onRestore(); - sbAltDecimals->onRestore(); cbCutSurface->onRestore(); pcbHighlightStyle->onRestore(); DrawGuiUtil::loadArrowBox(pcbBalloonArrow); pcbBalloonArrow->setCurrentIndex(prefBalloonArrow()); - DrawGuiUtil::loadArrowBox(pcbArrow); - pcbArrow->setCurrentIndex(prefArrowStyle()); } /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw3Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawAnnotationImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -141,7 +111,7 @@ void DlgPrefsTechDraw3Imp::changeEvent(QEvent *e) } } -int DlgPrefsTechDraw3Imp::prefBalloonArrow(void) const +int DlgPrefsTechDrawAnnotationImp::prefBalloonArrow(void) const { Base::Reference hGrp = App::GetApplication().GetUserParameter(). GetGroup("BaseApp")->GetGroup("Preferences")-> @@ -150,15 +120,4 @@ int DlgPrefsTechDraw3Imp::prefBalloonArrow(void) const return end; } -int DlgPrefsTechDraw3Imp::prefArrowStyle(void) const -{ - Base::Reference hGrp = App::GetApplication().GetUserParameter(). - GetGroup("BaseApp")->GetGroup("Preferences")-> - GetGroup("Mod/TechDraw/Dimensions"); - int style = hGrp->GetInt("ArrowStyle", 0); - return style; -} - - - -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.h new file mode 100644 index 0000000000..22b9ed283c --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAnnotationImp.h @@ -0,0 +1,51 @@ + /************************************************************************** + * Copyright (c) 2020 FreeCAD Developers * + * Author: Uwe Stöhr * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPANNOTATION_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPANNOTATION_H + +#include +#include + +namespace TechDrawGui { + +class DlgPrefsTechDrawAnnotationImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawAnnotationImp +{ + Q_OBJECT + +public: + DlgPrefsTechDrawAnnotationImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawAnnotationImp(); + +protected: + void saveSettings(); + void loadSettings(); + void changeEvent(QEvent *e); + + int prefBalloonArrow(void) const; +}; + +} // namespace TechDrawGui + +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPANNOTATION_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensions.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensions.ui new file mode 100644 index 0000000000..7246bbd49f --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensions.ui @@ -0,0 +1,617 @@ + + + TechDrawGui::DlgPrefsTechDrawDimensionsImp + + + + 0 + 0 + 460 + 425 + + + + + 0 + 0 + + + + Dimensions + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Dimensions + + + + + + + + + 0 + 0 + + + + Standard and Style + + + + + + + + 0 + 0 + + + + + 184 + 22 + + + + Standard to be used for dimensional values + + + StandardAndStyle + + + /Mod/TechDraw/Dimensions + + + + ISO Oriented + + + + + ISO Referencing + + + + + ASME Inlined + + + + + ASME Referencing + + + + + + + + + 0 + 0 + + + + Use system setting for number of decimals + + + Use Global Decimals + + + true + + + UseGlobalDecimals + + + /Mod/TechDraw/Dimensions + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + Append unit to dimension values + + + Show Units + + + ShowUnits + + + /Mod/TechDraw/Dimensions + + + + + + + Alternate Decimals + + + + + + + false + + + + 0 + 0 + + + + + 0 + 22 + + + + Number of decimals if 'Use Global Decimals' is not used + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 2 + + + AltDecimals + + + /Mod/TechDraw/Dimensions + + + + + + + + true + + + + Font Size + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + Dimension text font size + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 4.000000000000000 + + + FontSize + + + /Mod/TechDraw/Dimensions + + + + + + + Diameter Symbol + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + + 12 + + + + Character used to indicate diameter dimensions + + + ⌀ + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + DiameterSymbol + + + /Mod/TechDraw/Dimensions + + + + + + + + true + + + + Arrow Style + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + Arrowhead style + + + -1 + + + ArrowStyle + + + Mod/TechDraw/Dimensions + + + + + + + + true + + + + Arrow Size + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + Arrowhead size + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 5.000000000000000 + + + ArrowSize + + + Mod/TechDraw/Dimensions + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 85 + + + + + 16777215 + 500 + + + + + 0 + 500 + + + + Conventions + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Projection Group Angle + + + + + + + + 0 + 0 + + + + + 184 + 0 + + + + Use first- or third-angle mutliview projection convention + + + ProjectionAngle + + + Mod/TechDraw/General + + + + First + + + + + Third + + + + + + + + Hidden Line Style + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Style for hidden lines + + + HiddenLine + + + Mod/TechDraw/General + + + + Continuous + + + + :/icons/continuous-line.svg:/icons/continuous-line.svg + + + + + Dashed + + + + :/icons/dash-line.svg:/icons/dash-line.svg + + + + + + + + + + + + + + 12 + true + + + + QFrame::Box + + + Items in italics are default values for new objects. They have no effect on existing objects. + + + true + + + + + + + Qt::Vertical + + + + 20 + 20 + + + + + + + + + Gui::QuantitySpinBox + QWidget +
    Gui/QuantitySpinBox.h
    +
    + + Gui::PrefSpinBox + QSpinBox +
    Gui/PrefWidgets.h
    +
    + + Gui::PrefCheckBox + QCheckBox +
    Gui/PrefWidgets.h
    +
    + + Gui::PrefComboBox + QComboBox +
    Gui/PrefWidgets.h
    +
    + + Gui::PrefLineEdit + QLineEdit +
    Gui/PrefWidgets.h
    +
    + + Gui::PrefUnitSpinBox + Gui::QuantitySpinBox +
    Gui/PrefWidgets.h
    +
    +
    + + + + + + cbGlobalDecimals + toggled(bool) + sbAltDecimals + setDisabled(bool) + + + 108 + 71 + + + 425 + 124 + + + + +
    diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.cpp new file mode 100644 index 0000000000..bb35d48211 --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.cpp @@ -0,0 +1,119 @@ +/*************************************************************************** + * Copyright (c) 2015 FreeCAD Developers * + * Author: WandererFan * + * Based on src/Mod/FEM/Gui/DlgSettingsFEMImp.cpp * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include + +#include +#include + +#include "DrawGuiUtil.h" +#include "DlgPrefsTechDrawDimensionsImp.h" + + +using namespace TechDrawGui; +using namespace TechDraw; + + +DlgPrefsTechDrawDimensionsImp::DlgPrefsTechDrawDimensionsImp( QWidget* parent ) + : PreferencePage( parent ) +{ + this->setupUi(this); + plsb_FontSize->setUnit(Base::Unit::Length); + plsb_FontSize->setMinimum(0); + plsb_ArrowSize->setUnit(Base::Unit::Length); + plsb_ArrowSize->setMinimum(0); +} + +DlgPrefsTechDrawDimensionsImp::~DlgPrefsTechDrawDimensionsImp() +{ + // no need to delete child widgets, Qt does it all for us +} + +void DlgPrefsTechDrawDimensionsImp::saveSettings() +{ + cbGlobalDecimals->onSave(); + cbHiddenLineStyle->onSave(); + cbProjAngle->onSave(); + cbShowUnits->onSave(); + leDiameter->onSave(); + pcbArrow->onSave(); + pcbStandardAndStyle->onSave(); + plsb_ArrowSize->onSave(); + plsb_FontSize->onSave(); + sbAltDecimals->onSave(); +} + +void DlgPrefsTechDrawDimensionsImp::loadSettings() +{ + //set defaults for Quantity widgets if property not found + //Quantity widgets do not use preset value since they are based on + //QAbstractSpinBox + double arrowDefault = 5.0; + plsb_ArrowSize->setValue(arrowDefault); + double fontDefault = 4.0; + plsb_FontSize->setValue(fontDefault); + + cbGlobalDecimals->onRestore(); + cbHiddenLineStyle->onRestore(); + cbProjAngle->onRestore(); + cbShowUnits->onRestore(); + leDiameter->onRestore(); + pcbArrow->onRestore(); + pcbStandardAndStyle->onRestore(); + plsb_ArrowSize->onRestore(); + plsb_FontSize->onRestore(); + sbAltDecimals->onRestore(); + + DrawGuiUtil::loadArrowBox(pcbArrow); + pcbArrow->setCurrentIndex(prefArrowStyle()); +} + +/** + * Sets the strings of the subwidgets using the current language. + */ +void DlgPrefsTechDrawDimensionsImp::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + saveSettings(); + retranslateUi(this); + loadSettings(); + } + else { + QWidget::changeEvent(e); + } +} + +int DlgPrefsTechDrawDimensionsImp::prefArrowStyle(void) const +{ + Base::Reference hGrp = App::GetApplication().GetUserParameter(). + GetGroup("BaseApp")->GetGroup("Preferences")-> + GetGroup("Mod/TechDraw/Dimensions"); + int style = hGrp->GetInt("ArrowStyle", 0); + return style; +} + +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.h similarity index 80% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.h index 5f54564c0a..da6f747646 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw3Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawDimensionsImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw3Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,33 +22,31 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP3_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP3_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPDIMENSIONS_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPDIMENSIONS_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw3Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw3Imp +class DlgPrefsTechDrawDimensionsImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawDimensionsImp { Q_OBJECT public: - DlgPrefsTechDraw3Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw3Imp(); + DlgPrefsTechDrawDimensionsImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawDimensionsImp(); protected: void saveSettings(); void loadSettings(); void changeEvent(QEvent *e); - - int prefBalloonArrow(void) const; - int prefArrowStyle(void) const; + int prefArrowStyle(void) const; }; } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP3_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPDIMENSIONS_H From 79f7986eb935d5a2fdc94933355dedaea90387c9 Mon Sep 17 00:00:00 2001 From: donovaly Date: Tue, 14 Apr 2020 00:11:47 +0200 Subject: [PATCH 046/142] This commits splits the "General" dialog to "General" and "Colors". --- src/Mod/TechDraw/Gui/AppTechDrawGui.cpp | 8 +- src/Mod/TechDraw/Gui/CMakeLists.txt | 15 +- .../TechDraw/Gui/DlgPrefsTechDrawColors.ui | 573 ++++++++++++++++++ .../Gui/DlgPrefsTechDrawColorsImp.cpp | 99 +++ .../TechDraw/Gui/DlgPrefsTechDrawColorsImp.h | 49 ++ ...echDraw1.ui => DlgPrefsTechDrawGeneral.ui} | 504 +-------------- ...Imp.cpp => DlgPrefsTechDrawGeneralImp.cpp} | 50 +- ...raw1Imp.h => DlgPrefsTechDrawGeneralImp.h} | 15 +- 8 files changed, 753 insertions(+), 560 deletions(-) create mode 100644 src/Mod/TechDraw/Gui/DlgPrefsTechDrawColors.ui create mode 100644 src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.cpp create mode 100644 src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.h rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw1.ui => DlgPrefsTechDrawGeneral.ui} (58%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw1Imp.cpp => DlgPrefsTechDrawGeneralImp.cpp} (71%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw1Imp.h => DlgPrefsTechDrawGeneralImp.h} (81%) diff --git a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp index 810708cfd5..555c5ac315 100644 --- a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp +++ b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp @@ -39,10 +39,11 @@ #include "Workbench.h" #include "MDIViewPage.h" -#include "DlgPrefsTechDraw1Imp.h" +#include "DlgPrefsTechDrawGeneralImp.h" #include "DlgPrefsTechDraw2Imp.h" #include "DlgPrefsTechDrawAnnotationImp.h" #include "DlgPrefsTechDrawDimensionsImp.h" +#include "DlgPrefsTechDrawColorsImp.h" #include "DlgPrefsTechDraw4Imp.h" #include "DlgPrefsTechDraw5Imp.h" #include "ViewProviderPage.h" @@ -150,10 +151,11 @@ PyMOD_INIT_FUNC(TechDrawGui) TechDrawGui::ViewProviderCosmeticExtension::init(); // register preferences pages - new Gui::PrefPageProducer ("TechDraw"); //General + new Gui::PrefPageProducer ("TechDraw"); //General new Gui::PrefPageProducer ("TechDraw"); //Scale - new Gui::PrefPageProducer ("TechDraw"); //Annotation new Gui::PrefPageProducer("TechDraw"); //Dimensions + new Gui::PrefPageProducer ("TechDraw"); //Annotation + new Gui::PrefPageProducer("TechDraw"); //Colors new Gui::PrefPageProducer ("TechDraw"); //HLR new Gui::PrefPageProducer ("TechDraw"); //Advanced diff --git a/src/Mod/TechDraw/Gui/CMakeLists.txt b/src/Mod/TechDraw/Gui/CMakeLists.txt index 35edc2ce17..edf2b77d49 100644 --- a/src/Mod/TechDraw/Gui/CMakeLists.txt +++ b/src/Mod/TechDraw/Gui/CMakeLists.txt @@ -44,10 +44,11 @@ set(TechDrawGui_MOC_HDRS QGIViewDimension.h QGIViewBalloon.h TaskProjGroup.h - DlgPrefsTechDraw1Imp.h + DlgPrefsTechDrawGeneralImp.h DlgPrefsTechDraw2Imp.h DlgPrefsTechDrawAnnotationImp.h DlgPrefsTechDrawDimensionsImp.h + DlgPrefsTechDrawColorsImp.h DlgPrefsTechDraw4Imp.h DlgPrefsTechDraw5Imp.h TaskLinkDim.h @@ -85,10 +86,11 @@ else() endif() set(TechDrawGui_UIC_SRCS - DlgPrefsTechDraw1.ui + DlgPrefsTechDrawGeneral.ui DlgPrefsTechDraw2.ui DlgPrefsTechDrawAnnotation.ui DlgPrefsTechDrawDimensions.ui + DlgPrefsTechDrawColors.ui DlgPrefsTechDraw4.ui DlgPrefsTechDraw5.ui TaskProjGroup.ui @@ -147,9 +149,9 @@ SET(TechDrawGui_SRCS TaskProjGroup.ui TaskProjGroup.cpp TaskProjGroup.h - DlgPrefsTechDraw1.ui - DlgPrefsTechDraw1Imp.cpp - DlgPrefsTechDraw1Imp.h + DlgPrefsTechDrawGeneral.ui + DlgPrefsTechDrawGeneralImp.cpp + DlgPrefsTechDrawGeneralImp.h DlgPrefsTechDraw2.ui DlgPrefsTechDraw2Imp.cpp DlgPrefsTechDraw2Imp.h @@ -159,6 +161,9 @@ SET(TechDrawGui_SRCS DlgPrefsTechDrawDimensions.ui DlgPrefsTechDrawDimensionsImp.cpp DlgPrefsTechDrawDimensionsImp.h + DlgPrefsTechDrawColors.ui + DlgPrefsTechDrawColorsImp.cpp + DlgPrefsTechDrawColorsImp.h DlgPrefsTechDraw4.ui DlgPrefsTechDraw4Imp.cpp DlgPrefsTechDraw4Imp.h diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColors.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColors.ui new file mode 100644 index 0000000000..80b91a87d6 --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColors.ui @@ -0,0 +1,573 @@ + + + TechDrawGui::DlgPrefsTechDrawColorsImp + + + + 0 + 0 + 460 + 369 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Colors + + + + + + + + + + 0 + 0 + + + + + 0 + 225 + + + + + 0 + 200 + + + + Colors + + + + + + + + + true + + + + Normal + + + + + + + Normal line color + + + + 0 + 0 + 0 + + + + NormalColor + + + Mod/TechDraw/Colors + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + true + + + + Hidden Line + + + + + + + Hidden line color + + + HiddenColor + + + Mod/TechDraw/Colors + + + + + + + + true + + + + Preselected + + + + + + + Preselection color + + + + 255 + 255 + 20 + + + + PreSelectColor + + + Mod/TechDraw/Colors + + + + + + + + true + + + + Section Face + + + + + + + Section face color + + + + 225 + 225 + 225 + + + + CutSurfaceColor + + + Mod/TechDraw/Colors + + + + + + + + true + + + + Selected + + + + + + + Selected item color + + + + 28 + 173 + 28 + + + + SelectColor + + + Mod/TechDraw/Colors + + + + + + + Section Line + + + + + + + Section line color + + + SectionColor + + + /Mod/TechDraw/Decorations + + + + + + + + true + + + + Background + + + + + + + Background color around pages + + + + 80 + 80 + 80 + + + + Background + + + /Mod/TechDraw/Colors + + + + + + + + true + + + + Hatch + + + + + + + Hatch image color + + + + 0 + 0 + 0 + + + + Hatch + + + /Mod/TechDraw/Colors + + + + + + + Dimension + + + + + + + Color of dimension lines and text. + + + + 0 + 0 + 0 + + + + Color + + + Mod/TechDraw/Dimensions + + + + + + + + true + + + + Geometric Hatch + + + + + + + Geometric hatch pattern color + + + + 0 + 0 + 0 + + + + GeomHatch + + + /Mod/TechDraw/Colors + + + + + + + Centerline + + + + + + + Centerline color + + + CenterColor + + + Mod/TechDraw/Decorations + + + + + + + Vertex + + + + + + + Color of vertices in views + + + VertexColor + + + Mod/TechDraw/Decorations + + + + + + + + 0 + 20 + + + + + true + + + + Object faces will be transparent + + + Transparent Faces + + + ClearFace + + + /Mod/TechDraw/Colors + + + + + + + Face color (if not transparent) + + + + 255 + 255 + 255 + + + + FaceColor + + + /Mod/TechDraw/Colors + + + + + + + + true + + + + Detail Highlight + + + + + + + + true + + + + Leaderline + + + + + + + Default color for leader lines + + + + 0 + 0 + 0 + + + + Color + + + Mod/TechDraw/Markups + + + + + + + + 0 + 0 + 0 + + + + HighlightColor + + + /Mod/TechDraw/Decorations + + + + + + + + + + + + + 12 + true + + + + QFrame::Box + + + Items in italics are default values for new objects. They have no effect on existing objects. + + + true + + + + + + + Qt::Vertical + + + + 20 + 19 + + + + + + + + + Gui::ColorButton + QPushButton +
    Gui/Widgets.h
    +
    + + Gui::PrefColorButton + Gui::ColorButton +
    Gui/PrefWidgets.h
    +
    + + Gui::PrefCheckBox + QCheckBox +
    Gui/PrefWidgets.h
    +
    +
    + + +
    diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.cpp new file mode 100644 index 0000000000..b3fdea7de1 --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.cpp @@ -0,0 +1,99 @@ +/*************************************************************************** + * Copyright (c) 2020 FreeCAD Developers * + * Author: Uwe Stöhr * + * Based on src/Mod/FEM/Gui/DlgSettingsFEMImp.cpp * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include "DlgPrefsTechDrawColorsImp.h" +#include + +using namespace TechDrawGui; + +DlgPrefsTechDrawColorsImp::DlgPrefsTechDrawColorsImp( QWidget* parent ) + : PreferencePage( parent ) +{ + this->setupUi(this); +} + +DlgPrefsTechDrawColorsImp::~DlgPrefsTechDrawColorsImp() +{ + // no need to delete child widgets, Qt does it all for us +} + +void DlgPrefsTechDrawColorsImp::saveSettings() +{ + pcbDimColor->onSave(); + pcb_Hatch->onSave(); + pcb_Background->onSave(); + pcb_PreSelect->onSave(); + pcb_Hidden->onSave(); + pcb_Select->onSave(); + pcb_Normal->onSave(); + pcb_Surface->onSave(); + pcb_GeomHatch->onSave(); + pcb_Face->onSave(); + pcb_PaintFaces->onSave(); + pcbSectionLine->onSave(); + pcbCenterColor->onSave(); + pcbVertexColor->onSave(); + pcbMarkup->onSave(); + pcbHighlight->onSave(); +} + +void DlgPrefsTechDrawColorsImp::loadSettings() +{ + pcbDimColor->onRestore(); + pcb_Hatch->onRestore(); + pcb_Background->onRestore(); + pcb_PreSelect->onRestore(); + pcb_Hidden->onRestore(); + pcb_Select->onRestore(); + pcb_Normal->onRestore(); + pcb_Surface->onRestore(); + pcb_GeomHatch->onRestore(); + pcb_Face->onRestore(); + pcb_PaintFaces->onRestore(); + pcbSectionLine->onRestore(); + pcbCenterColor->onRestore(); + pcbVertexColor->onRestore(); + pcbMarkup->onRestore(); + pcbHighlight->onRestore(); +} + +/** + * Sets the strings of the subwidgets using the current language. + */ +void DlgPrefsTechDrawColorsImp::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + saveSettings(); + retranslateUi(this); + loadSettings(); + } + else { + QWidget::changeEvent(e); + } +} + +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.h new file mode 100644 index 0000000000..df4422960f --- /dev/null +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawColorsImp.h @@ -0,0 +1,49 @@ + /************************************************************************** + * Copyright (c) 2020 FreeCAD Developers * + * Author: Uwe Stöhr * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPCOLORS_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPCOLORS_H + +#include +#include + +namespace TechDrawGui { + +class DlgPrefsTechDrawColorsImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawColorsImp +{ + Q_OBJECT + +public: + DlgPrefsTechDrawColorsImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawColorsImp(); + +protected: + void saveSettings(); + void loadSettings(); + void changeEvent(QEvent *e); +}; + +} // namespace TechDrawGui + +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPCOLORS_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui similarity index 58% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui index fec43f04a1..d4504a266a 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneral.ui @@ -1,13 +1,13 @@ - TechDrawGui::DlgPrefsTechDraw1Imp - + TechDrawGui::DlgPrefsTechDrawGeneralImp + 0 0 460 - 760 + 510 @@ -185,494 +185,6 @@ for ProjectionGroups - - - - - 0 - 0 - - - - - 0 - 225 - - - - - 0 - 200 - - - - Colors - - - - - - - - - true - - - - Normal - - - - - - - Normal line color - - - - 0 - 0 - 0 - - - - NormalColor - - - Mod/TechDraw/Colors - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - true - - - - Hidden Line - - - - - - - Hidden line color - - - HiddenColor - - - Mod/TechDraw/Colors - - - - - - - - true - - - - Preselected - - - - - - - Preselection color - - - - 255 - 255 - 20 - - - - PreSelectColor - - - Mod/TechDraw/Colors - - - - - - - - true - - - - Section Face - - - - - - - Section face color - - - - 225 - 225 - 225 - - - - CutSurfaceColor - - - Mod/TechDraw/Colors - - - - - - - - true - - - - Selected - - - - - - - Selected item color - - - - 28 - 173 - 28 - - - - SelectColor - - - Mod/TechDraw/Colors - - - - - - - Section Line - - - - - - - Section line color - - - SectionColor - - - /Mod/TechDraw/Decorations - - - - - - - - true - - - - Background - - - - - - - Background color around pages - - - - 80 - 80 - 80 - - - - Background - - - /Mod/TechDraw/Colors - - - - - - - - true - - - - Hatch - - - - - - - Hatch image color - - - - 0 - 0 - 0 - - - - Hatch - - - /Mod/TechDraw/Colors - - - - - - - Dimension - - - - - - - Color of dimension lines and text. - - - - 0 - 0 - 0 - - - - Color - - - Mod/TechDraw/Dimensions - - - - - - - - true - - - - Geometric Hatch - - - - - - - Geometric hatch pattern color - - - - 0 - 0 - 0 - - - - GeomHatch - - - /Mod/TechDraw/Colors - - - - - - - Centerline - - - - - - - Centerline color - - - CenterColor - - - Mod/TechDraw/Decorations - - - - - - - Vertex - - - - - - - Color of vertices in views - - - VertexColor - - - Mod/TechDraw/Decorations - - - - - - - - 0 - 20 - - - - - true - - - - Object faces will be transparent - - - Transparent Faces - - - ClearFace - - - /Mod/TechDraw/Colors - - - - - - - Face color (if not transparent) - - - - 255 - 255 - 255 - - - - FaceColor - - - /Mod/TechDraw/Colors - - - - - - - - true - - - - Detail Highlight - - - - - - - - true - - - - Leaderline - - - - - - - Default color for leader lines - - - - 0 - 0 - 0 - - - - Color - - - Mod/TechDraw/Markups - - - - - - - - 0 - 0 - 0 - - - - HighlightColor - - - /Mod/TechDraw/Decorations - - - - - - - - @@ -1162,21 +674,11 @@ for ProjectionGroups QWidget
    Gui/QuantitySpinBox.h
    - - Gui::ColorButton - QPushButton -
    Gui/Widgets.h
    -
    Gui::PrefFileChooser Gui::FileChooser
    Gui/PrefWidgets.h
    - - Gui::PrefColorButton - Gui::ColorButton -
    Gui/PrefWidgets.h
    -
    Gui::PrefCheckBox QCheckBox diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp similarity index 71% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp index 4c3c299027..52d4bc053b 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.cpp @@ -25,12 +25,12 @@ #include "PreCompiled.h" -#include "DlgPrefsTechDraw1Imp.h" +#include "DlgPrefsTechDrawGeneralImp.h" #include using namespace TechDrawGui; -DlgPrefsTechDraw1Imp::DlgPrefsTechDraw1Imp( QWidget* parent ) +DlgPrefsTechDrawGeneralImp::DlgPrefsTechDrawGeneralImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); @@ -38,12 +38,12 @@ DlgPrefsTechDraw1Imp::DlgPrefsTechDraw1Imp( QWidget* parent ) plsb_LabelSize->setMinimum(0); } -DlgPrefsTechDraw1Imp::~DlgPrefsTechDraw1Imp() +DlgPrefsTechDrawGeneralImp::~DlgPrefsTechDrawGeneralImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw1Imp::saveSettings() +void DlgPrefsTechDrawGeneralImp::saveSettings() { pfc_DefTemp->onSave(); pfc_DefDir->onSave(); @@ -60,27 +60,9 @@ void DlgPrefsTechDraw1Imp::saveSettings() cb_Override->onSave(); cb_PageUpdate->onSave(); cb_AutoDist->onSave(); - - pcbDimColor->onSave(); - pcb_Hatch->onSave(); - pcb_Background->onSave(); - pcb_PreSelect->onSave(); - pcb_Hidden->onSave(); - pcb_Select->onSave(); - pcb_Normal->onSave(); - pcb_Surface->onSave(); - pcb_GeomHatch->onSave(); - pcb_Face->onSave(); - pcb_PaintFaces->onSave(); - pcbSectionLine->onSave(); - pcbCenterColor->onSave(); - pcbVertexColor->onSave(); - - pcbMarkup->onSave(); - pcbHighlight->onSave(); } -void DlgPrefsTechDraw1Imp::loadSettings() +void DlgPrefsTechDrawGeneralImp::loadSettings() { double labelDefault = 8.0; plsb_LabelSize->setValue(labelDefault); @@ -100,30 +82,12 @@ void DlgPrefsTechDraw1Imp::loadSettings() cb_Override->onRestore(); cb_PageUpdate->onRestore(); cb_AutoDist->onRestore(); - - pcbDimColor->onRestore(); - pcb_Hatch->onRestore(); - pcb_Background->onRestore(); - pcb_PreSelect->onRestore(); - pcb_Hidden->onRestore(); - pcb_Select->onRestore(); - pcb_Normal->onRestore(); - pcb_Surface->onRestore(); - pcb_GeomHatch->onRestore(); - pcb_Face->onRestore(); - pcb_PaintFaces->onRestore(); - pcbSectionLine->onRestore(); - pcbCenterColor->onRestore(); - pcbVertexColor->onRestore(); - - pcbMarkup->onRestore(); - pcbHighlight->onRestore(); } /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw1Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawGeneralImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -135,4 +99,4 @@ void DlgPrefsTechDraw1Imp::changeEvent(QEvent *e) } } -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.h similarity index 81% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.h index e97fa37b3d..cde3671f92 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw1Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawGeneralImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw1Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,21 +22,21 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP1_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP1_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPGENERAL_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPGENERAL_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw1Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw1Imp +class DlgPrefsTechDrawGeneralImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawGeneralImp { Q_OBJECT public: - DlgPrefsTechDraw1Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw1Imp(); + DlgPrefsTechDrawGeneralImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawGeneralImp(); protected: void saveSettings(); @@ -47,4 +46,4 @@ protected: } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP1_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPGENERAL_H From ac82f6ca4a14cb910d249acb02804c2d1ec96ac0 Mon Sep 17 00:00:00 2001 From: donovaly Date: Tue, 14 Apr 2020 00:31:18 +0200 Subject: [PATCH 047/142] give all preferences dialogs a clear name The numbering was somewhat artificial. --- src/Mod/TechDraw/Gui/AppTechDrawGui.cpp | 16 +++++----- src/Mod/TechDraw/Gui/CMakeLists.txt | 30 +++++++++---------- ...chDraw4.ui => DlgPrefsTechDrawAdvanced.ui} | 4 +-- ...mp.cpp => DlgPrefsTechDrawAdvancedImp.cpp} | 14 ++++----- ...aw5Imp.h => DlgPrefsTechDrawAdvancedImp.h} | 15 +++++----- ...efsTechDraw5.ui => DlgPrefsTechDrawHLR.ui} | 6 ++-- ...raw5Imp.cpp => DlgPrefsTechDrawHLRImp.cpp} | 14 ++++----- ...echDraw4Imp.h => DlgPrefsTechDrawHLRImp.h} | 15 +++++----- ...sTechDraw2.ui => DlgPrefsTechDrawScale.ui} | 4 +-- ...w2Imp.cpp => DlgPrefsTechDrawScaleImp.cpp} | 16 +++++----- ...hDraw2Imp.h => DlgPrefsTechDrawScaleImp.h} | 15 +++++----- 11 files changed, 73 insertions(+), 76 deletions(-) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw4.ui => DlgPrefsTechDrawAdvanced.ui} (99%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw4Imp.cpp => DlgPrefsTechDrawAdvancedImp.cpp} (88%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw5Imp.h => DlgPrefsTechDrawAdvancedImp.h} (81%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw5.ui => DlgPrefsTechDrawHLR.ui} (98%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw5Imp.cpp => DlgPrefsTechDrawHLRImp.cpp} (88%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw4Imp.h => DlgPrefsTechDrawHLRImp.h} (81%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw2.ui => DlgPrefsTechDrawScale.ui} (99%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw2Imp.cpp => DlgPrefsTechDrawScaleImp.cpp} (88%) rename src/Mod/TechDraw/Gui/{DlgPrefsTechDraw2Imp.h => DlgPrefsTechDrawScaleImp.h} (82%) diff --git a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp index 555c5ac315..8b7eda437a 100644 --- a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp +++ b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp @@ -40,12 +40,12 @@ #include "MDIViewPage.h" #include "DlgPrefsTechDrawGeneralImp.h" -#include "DlgPrefsTechDraw2Imp.h" +#include "DlgPrefsTechDrawscaleImp.h" #include "DlgPrefsTechDrawAnnotationImp.h" #include "DlgPrefsTechDrawDimensionsImp.h" #include "DlgPrefsTechDrawColorsImp.h" -#include "DlgPrefsTechDraw4Imp.h" -#include "DlgPrefsTechDraw5Imp.h" +#include "DlgPrefsTechDrawAdvancedImp.h" +#include "DlgPrefsTechDrawHLRImp.h" #include "ViewProviderPage.h" #include "ViewProviderDrawingView.h" #include "ViewProviderDimension.h" @@ -151,13 +151,13 @@ PyMOD_INIT_FUNC(TechDrawGui) TechDrawGui::ViewProviderCosmeticExtension::init(); // register preferences pages - new Gui::PrefPageProducer ("TechDraw"); //General - new Gui::PrefPageProducer ("TechDraw"); //Scale + new Gui::PrefPageProducer ("TechDraw"); //General + new Gui::PrefPageProducer ("TechDraw"); //Scale new Gui::PrefPageProducer("TechDraw"); //Dimensions new Gui::PrefPageProducer ("TechDraw"); //Annotation - new Gui::PrefPageProducer("TechDraw"); //Colors - new Gui::PrefPageProducer ("TechDraw"); //HLR - new Gui::PrefPageProducer ("TechDraw"); //Advanced + new Gui::PrefPageProducer("TechDraw"); //Colors + new Gui::PrefPageProducer ("TechDraw"); //HLR + new Gui::PrefPageProducer ("TechDraw"); //Advanced // add resources and reloads the translators loadTechDrawResource(); diff --git a/src/Mod/TechDraw/Gui/CMakeLists.txt b/src/Mod/TechDraw/Gui/CMakeLists.txt index edf2b77d49..817bebdbde 100644 --- a/src/Mod/TechDraw/Gui/CMakeLists.txt +++ b/src/Mod/TechDraw/Gui/CMakeLists.txt @@ -45,12 +45,12 @@ set(TechDrawGui_MOC_HDRS QGIViewBalloon.h TaskProjGroup.h DlgPrefsTechDrawGeneralImp.h - DlgPrefsTechDraw2Imp.h + DlgPrefsTechDrawscaleImp.h DlgPrefsTechDrawAnnotationImp.h DlgPrefsTechDrawDimensionsImp.h DlgPrefsTechDrawColorsImp.h - DlgPrefsTechDraw4Imp.h - DlgPrefsTechDraw5Imp.h + DlgPrefsTechDrawAdvancedImp.h + DlgPrefsTechDrawHLRImp.h TaskLinkDim.h DlgTemplateField.h TaskSectionView.h @@ -87,12 +87,12 @@ endif() set(TechDrawGui_UIC_SRCS DlgPrefsTechDrawGeneral.ui - DlgPrefsTechDraw2.ui + DlgPrefsTechDrawscale.ui DlgPrefsTechDrawAnnotation.ui DlgPrefsTechDrawDimensions.ui DlgPrefsTechDrawColors.ui - DlgPrefsTechDraw4.ui - DlgPrefsTechDraw5.ui + DlgPrefsTechDrawAdvanced.ui + DlgPrefsTechDrawHLR.ui TaskProjGroup.ui TaskLinkDim.ui DlgTemplateField.ui @@ -152,9 +152,9 @@ SET(TechDrawGui_SRCS DlgPrefsTechDrawGeneral.ui DlgPrefsTechDrawGeneralImp.cpp DlgPrefsTechDrawGeneralImp.h - DlgPrefsTechDraw2.ui - DlgPrefsTechDraw2Imp.cpp - DlgPrefsTechDraw2Imp.h + DlgPrefsTechDrawscale.ui + DlgPrefsTechDrawscaleImp.cpp + DlgPrefsTechDrawscaleImp.h DlgPrefsTechDrawAnnotation.ui DlgPrefsTechDrawAnnotationImp.cpp DlgPrefsTechDrawAnnotationImp.h @@ -164,12 +164,12 @@ SET(TechDrawGui_SRCS DlgPrefsTechDrawColors.ui DlgPrefsTechDrawColorsImp.cpp DlgPrefsTechDrawColorsImp.h - DlgPrefsTechDraw4.ui - DlgPrefsTechDraw4Imp.cpp - DlgPrefsTechDraw4Imp.h - DlgPrefsTechDraw5.ui - DlgPrefsTechDraw5Imp.cpp - DlgPrefsTechDraw5Imp.h + DlgPrefsTechDrawAdvanced.ui + DlgPrefsTechDrawAdvancedImp.cpp + DlgPrefsTechDrawAdvancedImp.h + DlgPrefsTechDrawHLR.ui + DlgPrefsTechDrawHLRImp.cpp + DlgPrefsTechDrawHLRImp.h TaskLinkDim.ui TaskLinkDim.cpp TaskLinkDim.h diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvanced.ui similarity index 99% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvanced.ui index 60ab9bc0c4..8ab7e5c067 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvanced.ui @@ -1,7 +1,7 @@ - TechDrawGui::DlgPrefsTechDraw4Imp - + TechDrawGui::DlgPrefsTechDrawAdvancedImp + 0 diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.cpp similarity index 88% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.cpp index 9725728ee4..9f2a2c5cf7 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.cpp @@ -25,23 +25,23 @@ #include "PreCompiled.h" -#include "DlgPrefsTechDraw4Imp.h" +#include "DlgPrefsTechDrawAdvancedImp.h" #include using namespace TechDrawGui; -DlgPrefsTechDraw4Imp::DlgPrefsTechDraw4Imp( QWidget* parent ) +DlgPrefsTechDrawAdvancedImp::DlgPrefsTechDrawAdvancedImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); } -DlgPrefsTechDraw4Imp::~DlgPrefsTechDraw4Imp() +DlgPrefsTechDrawAdvancedImp::~DlgPrefsTechDrawAdvancedImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw4Imp::saveSettings() +void DlgPrefsTechDrawAdvancedImp::saveSettings() { cbEndCap->onSave(); cbCrazyEdges->onSave(); @@ -56,7 +56,7 @@ void DlgPrefsTechDraw4Imp::saveSettings() leFormatSpec->onSave(); } -void DlgPrefsTechDraw4Imp::loadSettings() +void DlgPrefsTechDrawAdvancedImp::loadSettings() { cbEndCap->onRestore(); cbCrazyEdges->onRestore(); @@ -74,7 +74,7 @@ void DlgPrefsTechDraw4Imp::loadSettings() /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw4Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawAdvancedImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -86,4 +86,4 @@ void DlgPrefsTechDraw4Imp::changeEvent(QEvent *e) } } -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.h similarity index 81% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.h index 4bc1b28bbc..4404c1ba6f 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawAdvancedImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw5Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,21 +22,21 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP5_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP5_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPADVANCED_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPADVANCED_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw5Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw5Imp +class DlgPrefsTechDrawAdvancedImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawAdvancedImp { Q_OBJECT public: - DlgPrefsTechDraw5Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw5Imp(); + DlgPrefsTechDrawAdvancedImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawAdvancedImp(); protected: void saveSettings(); @@ -47,4 +46,4 @@ protected: } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP5_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPADVANCED_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLR.ui similarity index 98% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLR.ui index 3e5df3bc9d..9b011b92b9 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLR.ui @@ -1,7 +1,7 @@ - TechDrawGui::DlgPrefsTechDraw5Imp - + TechDrawGui::DlgPrefsTechDrawHLRImp + 0 @@ -23,7 +23,7 @@ - HLR Parameters + HLR diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.cpp similarity index 88% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.cpp index 1a78fc83a1..96ec646b79 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw5Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.cpp @@ -25,23 +25,23 @@ #include "PreCompiled.h" -#include "DlgPrefsTechDraw5Imp.h" +#include "DlgPrefsTechDrawHLRImp.h" #include using namespace TechDrawGui; -DlgPrefsTechDraw5Imp::DlgPrefsTechDraw5Imp( QWidget* parent ) +DlgPrefsTechDrawHLRImp::DlgPrefsTechDrawHLRImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); } -DlgPrefsTechDraw5Imp::~DlgPrefsTechDraw5Imp() +DlgPrefsTechDrawHLRImp::~DlgPrefsTechDrawHLRImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw5Imp::saveSettings() +void DlgPrefsTechDrawHLRImp::saveSettings() { pcbSeamViz->onSave(); pcbSmoothViz->onSave(); @@ -55,7 +55,7 @@ void DlgPrefsTechDraw5Imp::saveSettings() pcbHardHid->onSave(); } -void DlgPrefsTechDraw5Imp::loadSettings() +void DlgPrefsTechDrawHLRImp::loadSettings() { pcbSeamViz->onRestore(); pcbSmoothViz->onRestore(); @@ -72,7 +72,7 @@ void DlgPrefsTechDraw5Imp::loadSettings() /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw5Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawHLRImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -84,4 +84,4 @@ void DlgPrefsTechDraw5Imp::changeEvent(QEvent *e) } } -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.h similarity index 81% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.h index 860652fdc9..1d0a86ede3 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw4Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawHLRImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw4Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,21 +22,21 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP4_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP4_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPHLR_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPHLR_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw4Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw4Imp +class DlgPrefsTechDrawHLRImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawHLRImp { Q_OBJECT public: - DlgPrefsTechDraw4Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw4Imp(); + DlgPrefsTechDrawHLRImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawHLRImp(); protected: void saveSettings(); @@ -47,4 +46,4 @@ protected: } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP4_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPHLR_H diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScale.ui similarity index 99% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawScale.ui index 49206852bd..3fd1e3e971 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2.ui +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScale.ui @@ -1,7 +1,7 @@ - TechDrawGui::DlgPrefsTechDraw2Imp - + TechDrawGui::DlgPrefsTechDrawScaleImp + 0 diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.cpp similarity index 88% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.cpp index a726891675..233bf6c7dc 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.cpp +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.cpp @@ -25,12 +25,12 @@ #include "PreCompiled.h" -#include "DlgPrefsTechDraw2Imp.h" +#include "DlgPrefsTechDrawScaleImp.h" #include using namespace TechDrawGui; -DlgPrefsTechDraw2Imp::DlgPrefsTechDraw2Imp( QWidget* parent ) +DlgPrefsTechDrawScaleImp::DlgPrefsTechDrawScaleImp( QWidget* parent ) : PreferencePage( parent ) { this->setupUi(this); @@ -42,12 +42,12 @@ DlgPrefsTechDraw2Imp::DlgPrefsTechDraw2Imp( QWidget* parent ) this, SLOT(onScaleTypeChanged(int))); } -DlgPrefsTechDraw2Imp::~DlgPrefsTechDraw2Imp() +DlgPrefsTechDrawScaleImp::~DlgPrefsTechDrawScaleImp() { // no need to delete child widgets, Qt does it all for us } -void DlgPrefsTechDraw2Imp::onScaleTypeChanged(int index) +void DlgPrefsTechDrawScaleImp::onScaleTypeChanged(int index) { // disable custom scale if the scale type is not custom @@ -57,7 +57,7 @@ void DlgPrefsTechDraw2Imp::onScaleTypeChanged(int index) this->pdsbViewScale->setEnabled(false); } -void DlgPrefsTechDraw2Imp::saveSettings() +void DlgPrefsTechDrawScaleImp::saveSettings() { pdsbToleranceScale->onSave(); pdsbTemplateMark->onSave(); @@ -72,7 +72,7 @@ void DlgPrefsTechDraw2Imp::saveSettings() pdsbSymbolScale->onSave(); } -void DlgPrefsTechDraw2Imp::loadSettings() +void DlgPrefsTechDrawScaleImp::loadSettings() { double markDefault = 3.0; pdsbTemplateMark->setValue(markDefault); @@ -92,7 +92,7 @@ void DlgPrefsTechDraw2Imp::loadSettings() /** * Sets the strings of the subwidgets using the current language. */ -void DlgPrefsTechDraw2Imp::changeEvent(QEvent *e) +void DlgPrefsTechDrawScaleImp::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { saveSettings(); @@ -104,4 +104,4 @@ void DlgPrefsTechDraw2Imp::changeEvent(QEvent *e) } } -#include +#include diff --git a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.h b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.h similarity index 82% rename from src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.h rename to src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.h index 04c8dec00b..d68e184267 100644 --- a/src/Mod/TechDraw/Gui/DlgPrefsTechDraw2Imp.h +++ b/src/Mod/TechDraw/Gui/DlgPrefsTechDrawScaleImp.h @@ -1,7 +1,6 @@ /************************************************************************** * Copyright (c) 2015 FreeCAD Developers * * Author: WandererFan * - * Based on src/Mod/FEM/Gui/DlgPrefsTechDraw2Imp.cpp * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,21 +22,21 @@ ***************************************************************************/ -#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMP2_H -#define DRAWINGGUI_DLGPREFSTECHDRAWIMP2_H +#ifndef DRAWINGGUI_DLGPREFSTECHDRAWIMPSCALE_H +#define DRAWINGGUI_DLGPREFSTECHDRAWIMPSCALE_H -#include +#include #include namespace TechDrawGui { -class DlgPrefsTechDraw2Imp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDraw2Imp +class DlgPrefsTechDrawScaleImp : public Gui::Dialog::PreferencePage, public Ui_DlgPrefsTechDrawScaleImp { Q_OBJECT public: - DlgPrefsTechDraw2Imp( QWidget* parent = 0 ); - ~DlgPrefsTechDraw2Imp(); + DlgPrefsTechDrawScaleImp( QWidget* parent = 0 ); + ~DlgPrefsTechDrawScaleImp(); protected Q_SLOTS: void onScaleTypeChanged(int index); @@ -50,4 +49,4 @@ protected: } // namespace TechDrawGui -#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMP2_H +#endif // DRAWINGGUI_DLGPREFSTECHDRAWIMPSCALE_H From 232b701b36ec51d2824c24ab2705e96e884b7716 Mon Sep 17 00:00:00 2001 From: donovaly Date: Wed, 15 Apr 2020 01:25:25 +0200 Subject: [PATCH 048/142] fix a typo --- src/Mod/TechDraw/Gui/AppTechDrawGui.cpp | 2 +- src/Mod/TechDraw/Gui/CMakeLists.txt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp index 8b7eda437a..98c264ac17 100644 --- a/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp +++ b/src/Mod/TechDraw/Gui/AppTechDrawGui.cpp @@ -40,7 +40,7 @@ #include "MDIViewPage.h" #include "DlgPrefsTechDrawGeneralImp.h" -#include "DlgPrefsTechDrawscaleImp.h" +#include "DlgPrefsTechDrawScaleImp.h" #include "DlgPrefsTechDrawAnnotationImp.h" #include "DlgPrefsTechDrawDimensionsImp.h" #include "DlgPrefsTechDrawColorsImp.h" diff --git a/src/Mod/TechDraw/Gui/CMakeLists.txt b/src/Mod/TechDraw/Gui/CMakeLists.txt index 817bebdbde..d7fc068f09 100644 --- a/src/Mod/TechDraw/Gui/CMakeLists.txt +++ b/src/Mod/TechDraw/Gui/CMakeLists.txt @@ -45,7 +45,7 @@ set(TechDrawGui_MOC_HDRS QGIViewBalloon.h TaskProjGroup.h DlgPrefsTechDrawGeneralImp.h - DlgPrefsTechDrawscaleImp.h + DlgPrefsTechDrawScaleImp.h DlgPrefsTechDrawAnnotationImp.h DlgPrefsTechDrawDimensionsImp.h DlgPrefsTechDrawColorsImp.h @@ -87,7 +87,7 @@ endif() set(TechDrawGui_UIC_SRCS DlgPrefsTechDrawGeneral.ui - DlgPrefsTechDrawscale.ui + DlgPrefsTechDrawScale.ui DlgPrefsTechDrawAnnotation.ui DlgPrefsTechDrawDimensions.ui DlgPrefsTechDrawColors.ui @@ -152,9 +152,9 @@ SET(TechDrawGui_SRCS DlgPrefsTechDrawGeneral.ui DlgPrefsTechDrawGeneralImp.cpp DlgPrefsTechDrawGeneralImp.h - DlgPrefsTechDrawscale.ui - DlgPrefsTechDrawscaleImp.cpp - DlgPrefsTechDrawscaleImp.h + DlgPrefsTechDrawScale.ui + DlgPrefsTechDrawScaleImp.cpp + DlgPrefsTechDrawScaleImp.h DlgPrefsTechDrawAnnotation.ui DlgPrefsTechDrawAnnotationImp.cpp DlgPrefsTechDrawAnnotationImp.h From 97fbda9b47cdf458751abaf54317a73ad2831f24 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Wed, 15 Apr 2020 18:01:11 +0200 Subject: [PATCH 049/142] Arch: Fixed bug in IFC export of furniture --- src/Mod/Arch/exportIFC.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Mod/Arch/exportIFC.py b/src/Mod/Arch/exportIFC.py index cafeb9fd4e..2aea1be3b6 100644 --- a/src/Mod/Arch/exportIFC.py +++ b/src/Mod/Arch/exportIFC.py @@ -69,8 +69,10 @@ translationtable = { "Stair Flight":"StairFlight", "Curtain Wall":"CurtainWall", "Pipe Segment":"PipeSegment", - "Pipe Fitting":"PipeFitting" -} + "Pipe Fitting":"PipeFitting", + "VisGroup":"Group", + "Undefined":"BuildingElementProxy", + } # the base IFC template for export @@ -1550,12 +1552,6 @@ def getIfcTypeFromObj(obj): if ifctype in translationtable.keys(): ifctype = translationtable[ifctype] - if ifctype == "VisGroup": - ifctype = "Group" - if ifctype == "Undefined": - ifctype = "BuildingElementProxy" - if ifctype == "Furniture": - ifctype = "FurnishingElement" return "Ifc" + ifctype @@ -1595,6 +1591,7 @@ def exportIFC2X3Attributes(obj, kwargs, scale=0.001): def exportIfcAttributes(obj, kwargs, scale=0.001): + ifctype = getIfcTypeFromObj(obj) for property in obj.PropertiesList: if obj.getGroupOfProperty(property) == "IFC Attributes" and obj.getPropertyByName(property): value = obj.getPropertyByName(property) @@ -1602,7 +1599,10 @@ def exportIfcAttributes(obj, kwargs, scale=0.001): value = float(value) if property in ["ElevationWithFlooring","Elevation"]: value = value*scale # some properties must be changed to meters - kwargs.update({property: value}) + if (ifctype == "IfcFurnishingElement") and (property == "PredefinedType"): + pass # IFC2x3 Furniture objects get converted to IfcFurnishingElement and have no PredefinedType anymore + else: + kwargs.update({property: value}) return kwargs From 070099daa4b0e16f16a2a3c63a9e5d1f7c7a782d Mon Sep 17 00:00:00 2001 From: Eric Trombly Date: Wed, 15 Apr 2020 12:32:48 -0500 Subject: [PATCH 050/142] fix some typos in recent lazyloader implementation --- src/Mod/Path/PathScripts/PathDressupDogbone.py | 2 +- src/Mod/Path/PathScripts/PathUtils.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathDressupDogbone.py b/src/Mod/Path/PathScripts/PathDressupDogbone.py index 8af09df11d..b168532455 100644 --- a/src/Mod/Path/PathScripts/PathDressupDogbone.py +++ b/src/Mod/Path/PathScripts/PathDressupDogbone.py @@ -35,7 +35,7 @@ from PySide import QtCore # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader -DraftDraftGeomUtils = LazyLoader('DraftDraftGeomUtils', globals(), 'DraftDraftGeomUtils') +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') Part = LazyLoader('Part', globals(), 'Part') LOG_MODULE = PathLog.thisModule() diff --git a/src/Mod/Path/PathScripts/PathUtils.py b/src/Mod/Path/PathScripts/PathUtils.py index 29dfcdb2bd..f5c8b0416f 100644 --- a/src/Mod/Path/PathScripts/PathUtils.py +++ b/src/Mod/Path/PathScripts/PathUtils.py @@ -37,7 +37,7 @@ from PySide import QtGui # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader -geomType = LazyLoader('DraftDraftGeomUtils', globals(), 'DraftDraftGeomUtils.geomType') +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') Part = LazyLoader('Part', globals(), 'Part') TechDraw = LazyLoader('TechDraw', globals(), 'TechDraw') @@ -337,13 +337,13 @@ def getEnvelope(partshape, subshape=None, depthparams=None): def reverseEdge(e): - if geomType(e) == "Circle": + if DraftGeomUtils.geomType(e) == "Circle": arcstpt = e.valueAt(e.FirstParameter) arcmid = e.valueAt((e.LastParameter - e.FirstParameter) * 0.5 + e.FirstParameter) arcendpt = e.valueAt(e.LastParameter) arcofCirc = Part.ArcOfCircle(arcendpt, arcmid, arcstpt) newedge = arcofCirc.toShape() - elif geomType(e) == "LineSegment" or geomType(e) == "Line": + elif DraftGeomUtils.geomType(e) == "LineSegment" or DraftGeomUtils.geomType(e) == "Line": stpt = e.valueAt(e.FirstParameter) endpt = e.valueAt(e.LastParameter) newedge = Part.makeLine(endpt, stpt) From 98aaad5fb1c539642bbd88d390c86a3459eaa91e Mon Sep 17 00:00:00 2001 From: wandererfan Date: Wed, 15 Apr 2020 09:53:27 -0400 Subject: [PATCH 051/142] [TD]fix KB lockout in dialogs - valueChanged signal is emitted for every keystroke causing multiple recomputes for every change of parameter. --- src/Mod/TechDraw/Gui/TaskDetail.cpp | 24 ++++++++++++++-------- src/Mod/TechDraw/Gui/TaskSectionView.cpp | 26 +++++++++++++----------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/Mod/TechDraw/Gui/TaskDetail.cpp b/src/Mod/TechDraw/Gui/TaskDetail.cpp index 1388159ff5..f7b1a6214f 100644 --- a/src/Mod/TechDraw/Gui/TaskDetail.cpp +++ b/src/Mod/TechDraw/Gui/TaskDetail.cpp @@ -116,13 +116,16 @@ TaskDetail::TaskDetail(TechDraw::DrawViewPart* baseFeat): connect(ui->pbDragger, SIGNAL(clicked(bool)), this, SLOT(onDraggerClicked(bool))); - connect(ui->qsbX, SIGNAL(valueChanged(double)), + + //use editingFinished signal instead of valueChanged to prevent keyboard lock out + //valueChanged fires every keystroke causing a recompute. + connect(ui->qsbX, SIGNAL(editingFinished()), this, SLOT(onXEdit())); - connect(ui->qsbY, SIGNAL(valueChanged(double)), + connect(ui->qsbY, SIGNAL(editingFinished()), this, SLOT(onYEdit())); - connect(ui->qsbRadius, SIGNAL(valueChanged(double)), + connect(ui->qsbRadius, SIGNAL(editingFinished()), this, SLOT(onRadiusEdit())); - connect(ui->aeReference, SIGNAL(textChanged(QString)), + connect(ui->aeReference, SIGNAL(editingFinished()), this, SLOT(onReferenceEdit())); m_ghost = new QGIGhostHighlight(); @@ -186,14 +189,17 @@ TaskDetail::TaskDetail(TechDraw::DrawViewDetail* detailFeat): connect(ui->pbDragger, SIGNAL(clicked(bool)), this, SLOT(onDraggerClicked(bool))); - connect(ui->qsbX, SIGNAL(valueChanged(double)), + + //use editingFinished signal instead of valueChanged to prevent keyboard lock out + //valueChanged fires every keystroke causing a recompute. + connect(ui->qsbX, SIGNAL(editingFinished()), this, SLOT(onXEdit())); - connect(ui->qsbY, SIGNAL(valueChanged(double)), + connect(ui->qsbY, SIGNAL(editingFinished()), this, SLOT(onYEdit())); - connect(ui->qsbRadius, SIGNAL(valueChanged(double)), + connect(ui->qsbRadius, SIGNAL(editingFinished()), this, SLOT(onRadiusEdit())); - connect(ui->aeReference, SIGNAL(textChanged(QString)), - this, SLOT(onReferenceEdit())); + connect(ui->aeReference, SIGNAL(editingFinished()), + this, SLOT(onReferenceEdit())); m_ghost = new QGIGhostHighlight(); m_scene->addItem(m_ghost); diff --git a/src/Mod/TechDraw/Gui/TaskSectionView.cpp b/src/Mod/TechDraw/Gui/TaskSectionView.cpp index 36e5dc1e93..6b571780d5 100644 --- a/src/Mod/TechDraw/Gui/TaskSectionView.cpp +++ b/src/Mod/TechDraw/Gui/TaskSectionView.cpp @@ -176,12 +176,13 @@ void TaskSectionView::setUiPrimary() this->setToolTip(QObject::tr("Select at first an orientation")); enableAll(false); - // now connect and not earlier to avoid premature apply() calls - connect(ui->leSymbol, SIGNAL(textChanged(QString)), this, SLOT(onIdentifierChanged())); - connect(ui->sbScale, SIGNAL(valueChanged(double)), this, SLOT(onScaleChanged())); - connect(ui->sbOrgX, SIGNAL(valueChanged(double)), this, SLOT(onXChanged())); - connect(ui->sbOrgY, SIGNAL(valueChanged(double)), this, SLOT(onYChanged())); - connect(ui->sbOrgZ, SIGNAL(valueChanged(double)), this, SLOT(onZChanged())); + //use editingFinished signal instead of valueChanged to prevent keyboard lock out + //valueChanged fires every keystroke causing a recompute. + connect(ui->leSymbol, SIGNAL(editingFinished()), this, SLOT(onIdentifierChanged())); + connect(ui->sbScale, SIGNAL(editingFinished()), this, SLOT(onScaleChanged())); + connect(ui->sbOrgX, SIGNAL(editingFinished()), this, SLOT(onXChanged())); + connect(ui->sbOrgY, SIGNAL(editingFinished()), this, SLOT(onYChanged())); + connect(ui->sbOrgZ, SIGNAL(editingFinished()), this, SLOT(onZChanged())); } void TaskSectionView::setUiEdit() @@ -206,12 +207,13 @@ void TaskSectionView::setUiEdit() ui->sbOrgZ->setUnit(Base::Unit::Length); ui->sbOrgZ->setValue(origin.z); - // connect affter initializing the object values - connect(ui->leSymbol, SIGNAL(textChanged(QString)), this, SLOT(onIdentifierChanged())); - connect(ui->sbScale, SIGNAL(valueChanged(double)), this, SLOT(onScaleChanged())); - connect(ui->sbOrgX, SIGNAL(valueChanged(double)), this, SLOT(onXChanged())); - connect(ui->sbOrgY, SIGNAL(valueChanged(double)), this, SLOT(onYChanged())); - connect(ui->sbOrgZ, SIGNAL(valueChanged(double)), this, SLOT(onZChanged())); + //use editingFinished signal instead of valueChanged to prevent keyboard lock out + //valueChanged fires every keystroke causing a recompute. + connect(ui->leSymbol, SIGNAL(editingFinished()), this, SLOT(onIdentifierChanged())); + connect(ui->sbScale, SIGNAL(editingFinished()), this, SLOT(onScaleChanged())); + connect(ui->sbOrgX, SIGNAL(editingFinished()), this, SLOT(onXChanged())); + connect(ui->sbOrgY, SIGNAL(editingFinished()), this, SLOT(onYChanged())); + connect(ui->sbOrgZ, SIGNAL(editingFinished()), this, SLOT(onZChanged())); } //save the start conditions From c6b9adec25a13f769013df5bd57326bba69e7632 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 9 Apr 2020 15:53:30 -0500 Subject: [PATCH 052/142] Path: GUI improvement - swap setEnabled() for show() and hide() Improve GUI usability by using hide() and show(). Apply hide() and show() to labels as well. Fix visibility update issue on loading task window for editing of existing operation. --- src/Mod/Path/PathScripts/PathSurfaceGui.py | 34 +++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py index 41f11f6007..7ff1342360 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceGui.py +++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py @@ -41,7 +41,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): def initPage(self, obj): self.setTitle("3D Surface") - self.updateVisibility() + # self.updateVisibility() def getForm(self): '''getForm() ... returns UI''' @@ -118,6 +118,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): else: self.form.optimizeStepOverTransitions.setCheckState(QtCore.Qt.Unchecked) + self.updateVisibility() + def getSignalsForUpdate(self, obj): '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' signals = [] @@ -140,16 +142,26 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): return signals def updateVisibility(self): - if self.form.scanType.currentText() == "Planar": - self.form.cutPattern.setEnabled(True) - self.form.boundBoxExtraOffsetX.setEnabled(False) - self.form.boundBoxExtraOffsetY.setEnabled(False) - self.form.dropCutterDirSelect.setEnabled(False) - else: - self.form.cutPattern.setEnabled(False) - self.form.boundBoxExtraOffsetX.setEnabled(True) - self.form.boundBoxExtraOffsetY.setEnabled(True) - self.form.dropCutterDirSelect.setEnabled(True) + if self.form.scanType.currentText() == 'Planar': + self.form.cutPattern.show() + self.form.cutPattern_label.show() + self.form.optimizeStepOverTransitions.show() + + self.form.boundBoxExtraOffsetX.hide() + self.form.boundBoxExtraOffsetY.hide() + self.form.boundBoxExtraOffset_label.hide() + self.form.dropCutterDirSelect.hide() + self.form.dropCutterDirSelect_label.hide() + elif self.form.scanType.currentText() == 'Rotational': + self.form.cutPattern.hide() + self.form.cutPattern_label.hide() + self.form.optimizeStepOverTransitions.hide() + + self.form.boundBoxExtraOffsetX.show() + self.form.boundBoxExtraOffsetY.show() + self.form.boundBoxExtraOffset_label.show() + self.form.dropCutterDirSelect.show() + self.form.dropCutterDirSelect_label.show() def registerSignalHandlers(self, obj): self.form.scanType.currentIndexChanged.connect(self.updateVisibility) From 15e1fa7bb9e1441e1db034c1c72a0cbcf4cf1e83 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 00:08:40 -0500 Subject: [PATCH 053/142] Path: Added missing tooltips and added new Cut Pattern options Add `Spiral' option to Cut Pattern list Add `Offset' option to Cut Pattern list --- .../Gui/Resources/panels/PageOpSurfaceEdit.ui | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui index e4aaf19e5e..bb14461cc4 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui @@ -6,7 +6,7 @@ 0 0 - 350 + 368 400 @@ -59,6 +59,9 @@ + + <html><head/><body><p>Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.</p></body></html> + Planar @@ -73,6 +76,9 @@ + + <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html> + Single-pass @@ -127,6 +133,9 @@ + + <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html> + Optimize Linear Paths @@ -148,6 +157,9 @@ + + <html><head/><body><p>Make True, if specifying a Start Point</p></body></html> + Use Start Point @@ -169,6 +181,9 @@ + + <html><head/><body><p>Set the Z-axis depth offset from the target surface.</p></body></html> + mm @@ -184,6 +199,9 @@ 0 + + <html><head/><body><p>Additional offset to the selected bounding box along the X axis."</p></body></html> + mm @@ -191,6 +209,9 @@
    + + <html><head/><body><p>Additional offset to the selected bounding box along the Y axis."</p></body></html> + mm @@ -200,6 +221,9 @@ + + <html><head/><body><p>Set the sampling resolution. Smaller values quickly increase processing time.</p></body></html> + mm @@ -214,6 +238,9 @@ + + <html><head/><body><p>Dropcutter lines are created parallel to this axis.</p></body></html> + X @@ -228,6 +255,9 @@ + + <html><head/><body><p>Select the overall boundary for the operation.</p></body></html> + Stock @@ -242,6 +272,9 @@ + + <html><head/><body><p>Enable separate optimization of transitions between, and breaks within, each step over path.</p></body></html> + Optimize StepOver Transitions @@ -256,16 +289,9 @@ - - - Line - - - - - ZigZag - - + + <html><head/><body><p>Set the geometric clearing pattern to use for the operation.</p></body></html> + Circular @@ -276,6 +302,26 @@ CircularZigZag + + + Line + + + + + Offset + + + + + Spiral + + + + + ZigZag + + @@ -299,8 +345,8 @@ Gui::InputField - QWidget -
    gui::inputfield.h
    + QLineEdit +
    Gui/InputField.h
    From b5048f4f021b97ebed1e49579c8ada0321439e6e Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Sat, 11 Apr 2020 14:08:32 -0500 Subject: [PATCH 054/142] Path: Multiple fixes; code optimizations and cleanup Fix _processRotationalOp() arguments and erroneous call thereto. Fix isOnLineSegment() usage. Fix transition between profile edge and clearing. Fix _planarSinglepassProcess() method. Fix `ProfileEdges` feature. Fix Y-axis rotation error in G-code and display. Fix error handling of `import ocl` test. Raise `import ocl` test in code sequence. Convert ocl.Point scan results and handling to FreeCAD.Vector() type throughout `Rotational` scan code. Change properties group from `Rotational` to `Rotation`. Make operation property details accessible through class. Relocate Draft import to dependent function. Improve property visibility in Data tab. Issue warning for Rotational scans if faces are selected. Compact setup() function. LGTM cleanup throughout. Delete unnecessary comments. --- src/Mod/Path/PathScripts/PathSurface.py | 7360 +++++++++++------------ 1 file changed, 3616 insertions(+), 3744 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 40cd771398..f7351662b6 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -1,3744 +1,3616 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2016 sliptonic * -# * * -# * 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. * -# * * -# * This program 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 this program; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** -# * * -# * Additional modifications and contributions beginning 2019 * -# * by Russell Johnson 2020-03-18 12:29 CST * -# * * -# *************************************************************************** - -from __future__ import print_function - -import FreeCAD -import Path -import PathScripts.PathLog as PathLog -import PathScripts.PathUtils as PathUtils -import PathScripts.PathOp as PathOp - -from PySide import QtCore -import time -import math - -# lazily loaded modules -from lazy_loader.lazy_loader import LazyLoader -MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') -Draft = LazyLoader('Draft', globals(), 'Draft') -Part = LazyLoader('Part', globals(), 'Part') - -if FreeCAD.GuiUp: - import FreeCADGui - -__title__ = "Path Surface Operation" -__author__ = "sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Class and implementation of Mill Facing operation." - -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -# PathLog.trackModule(PathLog.thisModule()) - - -# Qt translation handling -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -# OCL must be installed -try: - import ocl -except ImportError: - FreeCAD.Console.PrintError( - translate("Path_Surface", "This operation requires OpenCamLib to be installed.") + "\n") - import sys - sys.exit(translate("Path_Surface", "This operation requires OpenCamLib to be installed.")) - - -class ObjectSurface(PathOp.ObjectOp): - '''Proxy object for Surfacing operation.''' - - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - - def opFeatures(self, obj): - '''opFeatures(obj) ... return all standard features and edges based geomtries''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces - - def initOperation(self, obj): - '''initPocketOp(obj) ... create operation specific properties''' - self.initOpProperties(obj) - - # For debugging - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - - if not hasattr(obj, 'DoNotSetDefaultValues'): - self.setEditorProperties(obj) - - def initOpProperties(self, obj): - '''initOpProperties(obj) ... create operation specific properties''' - - PROPS = [ - ("App::PropertyBool", "ShowTempObjects", "Debug", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), - - ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.")), - ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values do not increase processing time much.")), - - ("App::PropertyFloat", "CutterTilt", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), - ("App::PropertyEnumeration", "DropCutterDir", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")), - ("App::PropertyVectorDistance", "DropCutterExtraOffset", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")), - ("App::PropertyEnumeration", "RotationAxis", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")), - ("App::PropertyFloat", "StartIndex", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")), - ("App::PropertyFloat", "StopIndex", "Rotational", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), - - ("App::PropertyEnumeration", "ScanType", "Surface", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")), - - ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), - ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), - ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), - ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), - ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), - ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), - ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), - - ("App::PropertyEnumeration", "BoundBox", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")), - ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), - ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")), - ("App::PropertyEnumeration", "CutMode", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), - ("App::PropertyEnumeration", "CutPattern", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), - ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), - ("App::PropertyBool", "CutPatternReversed", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), - ("App::PropertyDistance", "DepthOffset", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), - ("App::PropertyEnumeration", "LayerMode", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), - ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), - ("App::PropertyDistance", "SampleInterval", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), - ("App::PropertyPercent", "StepOver", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), - - ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), - ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), - ("App::PropertyBool", "CircularUseG2G3", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")), - ("App::PropertyDistance", "GapThreshold", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), - ("App::PropertyString", "GapSizes", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), - - ("App::PropertyVectorDistance", "StartPoint", "Start Point", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), - ("App::PropertyBool", "UseStartPoint", "Start Point", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) - ] - - missing = list() - for (prtyp, nm, grp, tt) in PROPS: - if not hasattr(obj, nm): - obj.addProperty(prtyp, nm, grp, tt) - missing.append(nm) - - # Set enumeration lists for enumeration properties - if len(missing) > 0: - ENUMS = self._propertyEnumerations() - for n in ENUMS: - if n in missing: - cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) - exec(cmdStr) - - self.addedAllProperties = True - - def _propertyEnumerations(self): - # Enumeration lists for App::PropertyEnumeration properties - return { - 'BoundBox': ['BaseBoundBox', 'Stock'], - 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], - 'CutMode': ['Conventional', 'Climb'], - 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] - 'DropCutterDir': ['X', 'Y'], - 'HandleMultipleFeatures': ['Collectively', 'Individually'], - 'LayerMode': ['Single-pass', 'Multi-pass'], - 'ProfileEdges': ['None', 'Only', 'First', 'Last'], - 'RotationAxis': ['X', 'Y'], - 'ScanType': ['Planar', 'Rotational'] - } - - def setEditorProperties(self, obj): - # Used to hide inputs in properties list - - mode = 2 # 2=hidden - if obj.ScanType == 'Planar': - show = 0 - hide = 2 - # if obj.CutPattern in ['Line', 'ZigZag']: - if obj.CutPattern in ['Circular', 'CircularZigZag']: - show = 2 # hide - hide = 0 # show - obj.setEditorMode('CutPatternAngle', show) - obj.setEditorMode('CircularCenterAt', hide) - obj.setEditorMode('CircularCenterCustom', hide) - elif obj.ScanType == 'Rotational': - mode = 0 # show and editable - obj.setEditorMode('DropCutterDir', mode) - obj.setEditorMode('DropCutterExtraOffset', mode) - obj.setEditorMode('RotationAxis', mode) - obj.setEditorMode('StartIndex', mode) - obj.setEditorMode('StopIndex', mode) - obj.setEditorMode('CutterTilt', mode) - - def onChanged(self, obj, prop): - if hasattr(self, 'addedAllProperties'): - if self.addedAllProperties is True: - if prop == 'ScanType': - self.setEditorProperties(obj) - if prop == 'CutPattern': - self.setEditorProperties(obj) - - def opOnDocumentRestored(self, obj): - self.initOpProperties(obj) - - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - else: - obj.setEditorMode('ShowTempObjects', 0) # show - - self.setEditorProperties(obj) - - def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj, job) ... initialize defaults''' - job = PathUtils.findParentJob(obj) - - obj.OptimizeLinearPaths = True - obj.InternalFeaturesCut = True - obj.OptimizeStepOverTransitions = False - obj.CircularUseG2G3 = False - obj.BoundaryEnforcement = True - obj.UseStartPoint = False - obj.AvoidLastX_InternalFeatures = True - obj.CutPatternReversed = False - obj.StartPoint.x = 0.0 - obj.StartPoint.y = 0.0 - obj.StartPoint.z = obj.ClearanceHeight.Value - obj.ProfileEdges = 'None' - obj.LayerMode = 'Single-pass' - obj.ScanType = 'Planar' - obj.RotationAxis = 'X' - obj.CutMode = 'Conventional' - obj.CutPattern = 'Line' - obj.HandleMultipleFeatures = 'Collectively' # 'Individually' - obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' - obj.GapSizes = 'No gaps identified.' - obj.StepOver = 100 - obj.CutPatternAngle = 0.0 - obj.CutterTilt = 0.0 - obj.StartIndex = 0.0 - obj.StopIndex = 360.0 - obj.SampleInterval.Value = 1.0 - obj.BoundaryAdjustment.Value = 0.0 - obj.InternalFeaturesAdjustment.Value = 0.0 - obj.AvoidLastX_Faces = 0 - obj.CircularCenterCustom.x = 0.0 - obj.CircularCenterCustom.y = 0.0 - obj.CircularCenterCustom.z = 0.0 - obj.GapThreshold.Value = 0.005 - obj.AngularDeflection.Value = 0.25 - obj.LinearDeflection.Value = job.GeometryTolerance - # For debugging - obj.ShowTempObjects = False - - # need to overwrite the default depth calculations for facing - d = None - if job: - if job.Stock: - d = PathUtils.guessDepths(job.Stock.Shape, None) - PathLog.debug("job.Stock exists") - else: - PathLog.debug("job.Stock NOT exist") - else: - PathLog.debug("job NOT exist") - - if d is not None: - obj.OpFinalDepth.Value = d.final_depth - obj.OpStartDepth.Value = d.start_depth - else: - obj.OpFinalDepth.Value = -10 - obj.OpStartDepth.Value = 10 - - PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) - PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) - - def opApplyPropertyLimits(self, obj): - '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' - # Limit start index - if obj.StartIndex < 0.0: - obj.StartIndex = 0.0 - if obj.StartIndex > 360.0: - obj.StartIndex = 360.0 - - # Limit stop index - if obj.StopIndex > 360.0: - obj.StopIndex = 360.0 - if obj.StopIndex < 0.0: - obj.StopIndex = 0.0 - - # Limit cutter tilt - if obj.CutterTilt < -90.0: - obj.CutterTilt = -90.0 - if obj.CutterTilt > 90.0: - obj.CutterTilt = 90.0 - - # Limit sample interval - if obj.SampleInterval.Value < 0.0001: - obj.SampleInterval.Value = 0.0001 - PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) - if obj.SampleInterval.Value > 25.4: - obj.SampleInterval.Value = 25.4 - PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) - - # Limit cut pattern angle - if obj.CutPatternAngle < -360.0: - obj.CutPatternAngle = 0.0 - PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +-360 degrees.')) - if obj.CutPatternAngle >= 360.0: - obj.CutPatternAngle = 0.0 - PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.')) - - # Limit StepOver to natural number percentage - if obj.StepOver > 100: - obj.StepOver = 100 - if obj.StepOver < 1: - obj.StepOver = 1 - - # Limit AvoidLastX_Faces to zero and positive values - if obj.AvoidLastX_Faces < 0: - obj.AvoidLastX_Faces = 0 - PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Only zero or positive values permitted.')) - if obj.AvoidLastX_Faces > 100: - obj.AvoidLastX_Faces = 100 - PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) - - def opExecute(self, obj): - '''opExecute(obj) ... process surface operation''' - PathLog.track() - - self.modelSTLs = list() - self.safeSTLs = list() - self.modelTypes = list() - self.boundBoxes = list() - self.profileShapes = list() - self.collectiveShapes = list() - self.individualShapes = list() - self.avoidShapes = list() - self.deflection = None - self.tempGroup = None - self.CutClimb = False - self.closedGap = False - self.gaps = [0.1, 0.2, 0.3] - CMDS = list() - modelVisibility = list() - FCAD = FreeCAD.ActiveDocument - - # Set debugging behavior - self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects - self.showDebugObjects = obj.ShowTempObjects - deleteTempsFlag = True # Set to False for debugging - if PathLog.getLevel(PathLog.thisModule()) == 4: - deleteTempsFlag = False - else: - self.showDebugObjects = False - - # mark beginning of operation and identify parent Job - PathLog.info('\nBegin 3D Surface operation...') - startTime = time.time() - - # Identify parent Job - JOB = PathUtils.findParentJob(obj) - if JOB is None: - PathLog.error(translate('PathSurface', "No JOB")) - return - self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin - - # set cut mode; reverse as needed - if obj.CutMode == 'Climb': - self.CutClimb = True - if obj.CutPatternReversed is True: - if self.CutClimb is True: - self.CutClimb = False - else: - self.CutClimb = True - - # Begin GCode for operation with basic information - # ... and move cutter to clearance height and startpoint - output = '' - if obj.Comment != '': - self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) - self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) - self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) - self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) - self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) - self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) - self.commandlist.append(Path.Command('N ({})'.format(output), {})) - self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - if obj.UseStartPoint is True: - self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) - - # Instantiate additional class operation variables - self.resetOpVariables() - - # Impose property limits - self.opApplyPropertyLimits(obj) - - # Create temporary group for temporary objects, removing existing - # if self.showDebugObjects is True: - tempGroupName = 'tempPathSurfaceGroup' - if FCAD.getObject(tempGroupName): - for to in FCAD.getObject(tempGroupName).Group: - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName) # remove temp directory if already exists - if FCAD.getObject(tempGroupName + '001'): - for to in FCAD.getObject(tempGroupName + '001').Group: - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists - tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) - tempGroupName = tempGroup.Name - self.tempGroup = tempGroup - tempGroup.purgeTouched() - # Add temp object to temp group folder with following code: - # ... self.tempGroup.addObject(OBJ) - - # Setup cutter for OCL and cutout value for operation - based on tool controller properties - self.cutter = self.setOclCutter(obj) - self.safeCutter = self.setOclCutter(obj, safe=True) - if self.cutter is False or self.safeCutter is False: - PathLog.error(translate('PathSurface', "Canceling 3D Surface operation. Error creating OCL cutter.")) - return - toolDiam = self.cutter.getDiameter() - self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) - self.radius = toolDiam / 2.0 - self.gaps = [toolDiam, toolDiam, toolDiam] - - # Get height offset values for later use - self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value - self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value - - # Calculate default depthparams for operation - self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) - self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 - - # make circle for workplane - self.wpc = Part.makeCircle(2.0) - - # Set deflection values for mesh generation - try: # try/except is for Path Jobs created before GeometryTolerance - self.deflection = JOB.GeometryTolerance.Value - except AttributeError as ee: - PathLog.warning('Error setting Mesh deflection: {}. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) - import PathScripts.PathPreferences as PathPreferences - self.deflection = PathPreferences.defaultGeometryTolerance() - - # Save model visibilities for restoration - if FreeCAD.GuiUp: - for m in range(0, len(JOB.Model.Group)): - mNm = JOB.Model.Group[m].Name - modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) - - # Setup STL, model type, and bound box containers for each model in Job - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - self.modelSTLs.append(False) - self.safeSTLs.append(False) - self.profileShapes.append(False) - # Set bound box - if obj.BoundBox == 'BaseBoundBox': - if M.TypeId.startswith('Mesh'): - self.modelTypes.append('M') # Mesh - self.boundBoxes.append(M.Mesh.BoundBox) - else: - self.modelTypes.append('S') # Solid - self.boundBoxes.append(M.Shape.BoundBox) - elif obj.BoundBox == 'Stock': - self.modelTypes.append('S') # Solid - self.boundBoxes.append(JOB.Stock.Shape.BoundBox) - - # ###### MAIN COMMANDS FOR OPERATION ###### - - # Begin processing obj.Base data and creating GCode - # Process selected faces, if available - pPM = self._preProcessModel(JOB, obj) - if pPM is False: - PathLog.error('Unable to pre-process obj.Base.') - else: - (FACES, VOIDS) = pPM - - # Create OCL.stl model objects - self._prepareModelSTLs(JOB, obj) - - for m in range(0, len(JOB.Model.Group)): - Mdl = JOB.Model.Group[m] - if FACES[m] is False: - PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) - else: - if m > 0: - # Raise to clearance between models - CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) - CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) - # make stock-model-voidShapes STL model for avoidance detection on transitions - self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) - #time.sleep(0.2) - # Process model/faces - OCL objects must be ready - CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m])) - - # Save gcode produced - self.commandlist.extend(CMDS) - - # ###### CLOSING COMMANDS FOR OPERATION ###### - - # Delete temporary objects - # Restore model visibilities for restoration - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - M.Visibility = modelVisibility[m] - - if deleteTempsFlag is True: - for to in tempGroup.Group: - if hasattr(to, 'Group'): - for go in to.Group: - FCAD.removeObject(go.Name) - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName) - else: - if len(tempGroup.Group) == 0: - FCAD.removeObject(tempGroupName) - else: - tempGroup.purgeTouched() - - # Provide user feedback for gap sizes - gaps = list() - for g in self.gaps: - if g != toolDiam: - gaps.append(g) - if len(gaps) > 0: - obj.GapSizes = '{} mm'.format(gaps) - else: - if self.closedGap is True: - obj.GapSizes = 'Closed gaps < Gap Threshold.' - else: - obj.GapSizes = 'No gaps identified.' - - # clean up class variables - self.resetOpVariables() - self.deleteOpVariables() - - self.modelSTLs = None - self.safeSTLs = None - self.modelTypes = None - self.boundBoxes = None - self.gaps = None - self.closedGap = None - self.SafeHeightOffset = None - self.ClearHeightOffset = None - self.depthParams = None - self.midDep = None - self.wpc = None - self.deflection = None - del self.modelSTLs - del self.safeSTLs - del self.modelTypes - del self.boundBoxes - del self.gaps - del self.closedGap - del self.SafeHeightOffset - del self.ClearHeightOffset - del self.depthParams - del self.midDep - del self.wpc - del self.deflection - - execTime = time.time() - startTime - PathLog.info('Operation time: {} sec.'.format(execTime)) - - return True - - # Methods for constructing the cut area - def _preProcessModel(self, JOB, obj): - PathLog.debug('_preProcessModel()') - - FACES = list() - VOIDS = list() - fShapes = list() - vShapes = list() - preProcEr = translate('PathSurface', 'Error pre-processing Face') - warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face') - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Crete place holders for each base model in Job - for m in range(0, lenGRP): - FACES.append(False) - VOIDS.append(False) - fShapes.append(False) - vShapes.append(False) - - # The user has selected subobjects from the base. Pre-Process each. - if obj.Base and len(obj.Base) > 0: - PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') - - (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) - - # Cycle through each base model, processing faces for each - for m in range(0, lenGRP): - base = GRP[m] - (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) - fShapes[m] = mFS - vShapes[m] = mVS - self.profileShapes[m] = mPS - else: - PathLog.debug(' -No obj.Base data.') - for m in range(0, lenGRP): - self.modelSTLs[m] = True - - # Process each model base, as a whole, as needed - # PathLog.debug(' -Pre-processing all models in Job.') - for m in range(0, lenGRP): - if fShapes[m] is False: - PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) - if obj.BoundBox == 'BaseBoundBox': - base = GRP[m] - elif obj.BoundBox == 'Stock': - base = JOB.Stock - - pPEB = self._preProcessEntireBase(obj, base, m) - if pPEB is False: - PathLog.error(' -Failed to pre-process base as a whole.') - else: - (fcShp, prflShp) = pPEB - if fcShp is not False: - if fcShp is True: - PathLog.debug(' -fcShp is True.') - fShapes[m] = True - else: - fShapes[m] = [fcShp] - if prflShp is not False: - if fcShp is not False: - PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) - if vShapes[m] is not False: - PathLog.debug(' -Cutting void from base profile shape.') - adjPS = prflShp.cut(vShapes[m][0]) - self.profileShapes[m] = [adjPS] - else: - PathLog.debug(' -vShapes[m] is False.') - self.profileShapes[m] = [prflShp] - else: - PathLog.debug(' -Saving base profile shape.') - self.profileShapes[m] = [prflShp] - PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) - # Efor - - return (fShapes, vShapes) - - def _identifyFacesAndVoids(self, JOB, obj, F, V): - TUPS = list() - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Separate selected faces into (base, face) tuples and flag model(s) for STL creation - for (bs, SBS) in obj.Base: - for sb in SBS: - # Flag model for STL creation - mdlIdx = None - for m in range(0, lenGRP): - if bs is GRP[m]: - self.modelSTLs[m] = True - mdlIdx = m - break - TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) - - # Apply `AvoidXFaces` value - faceCnt = len(TUPS) - add = faceCnt - obj.AvoidLastX_Faces - for bst in range(0, faceCnt): - (m, base, sub) = TUPS[bst] - shape = getattr(base.Shape, sub) - if isinstance(shape, Part.Face): - faceIdx = int(sub[4:]) - 1 - if bst < add: - if F[m] is False: - F[m] = list() - F[m].append((shape, faceIdx)) - else: - if V[m] is False: - V[m] = list() - V[m].append((shape, faceIdx)) - return (F, V) - - def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): - mFS = False - mVS = False - mPS = False - mIFS = list() - BB = base.Shape.BoundBox - - if FACES[m] is not False: - isHole = False - if obj.HandleMultipleFeatures == 'Collectively': - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - outFCS = list() - - # Get collective envelope slice of selected faces - for (fcshp, fcIdx) in FACES[m]: - fNum = fcIdx + 1 - fsL.append(fcshp) - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - PathLog.debug('Attempting to get cross-section of collective faces.') - if len(outFCS) == 0: - PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) - cont = False - else: - cfsL = Part.makeCompound(outFCS) - - # Handle profile edges request - if cont is True and obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(cfsL, ofstVal) - if psOfst is not False: - mPS = [psOfst] - if obj.ProfileEdges == 'Only': - mFS = True - cont = False - else: - PathLog.error(' -Failed to create profile geometry for selected faces.') - cont = False - - if cont: - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') - T.Shape = cfsL - T.purgeTouched() - self.tempGroup.addObject(T) - - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(cfsL, ofstVal) - if faceOfstShp is False: - PathLog.error(' -Failed to create offset face.') - cont = False - - if cont: - lenIfL = len(ifL) - if obj.InternalFeaturesCut is False: - if lenIfL == 0: - PathLog.debug(' -No internal features saved.') - else: - if lenIfL == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - if self.showDebugObjects is True: - C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') - C.Shape = casL - C.purgeTouched() - self.tempGroup.addObject(C) - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - mFS = [faceOfstShp] - # Eif - - elif obj.HandleMultipleFeatures == 'Individually': - for (fcshp, fcIdx) in FACES[m]: - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - fNum = fcIdx + 1 - outerFace = False - - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - cont = False - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - cont = False - outerFace = False - else: - ((otrFace, raised), intWires) = gFW - outerFace = otrFace - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - if outerFace is not False: - PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) - - if obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(outerFace, ofstVal) - if psOfst is not False: - if mPS is False: - mPS = list() - mPS.append(psOfst) - if obj.ProfileEdges == 'Only': - if mFS is False: - mFS = list() - mFS.append(True) - cont = False - else: - PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) - cont = False - - if cont: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(outerFace, ofstVal) - - lenIfl = len(ifL) - if obj.InternalFeaturesCut is False and lenIfl > 0: - if lenIfl == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - if mFS is False: - mFS = list() - mFS.append(faceOfstShp) - # Eif - # Efor - # Eif - # Eif - - if len(mIFS) > 0: - if mVS is False: - mVS = list() - for ifs in mIFS: - mVS.append(ifs) - - if VOIDS[m] is not False: - PathLog.debug('Processing avoid faces.') - cont = True - isHole = False - outFCS = list() - intFEAT = list() - - for (fcshp, fcIdx) in VOIDS[m]: - fNum = fcIdx + 1 - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) - cont = False - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.AvoidLastX_InternalFeatures is False: - if intWires is not False: - for (iFace, rsd) in intWires: - intFEAT.append(iFace) - - lenOtFcs = len(outFCS) - if lenOtFcs == 0: - cont = False - else: - if lenOtFcs == 1: - avoid = outFCS[0] - else: - avoid = Part.makeCompound(outFCS) - - if self.showDebugObjects is True: - PathLog.debug('*** tmpAvoidArea') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') - P.Shape = avoid - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - - if cont: - if self.showDebugObjects is True: - PathLog.debug('*** tmpVoidCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') - P.Shape = avoid - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) - avdOfstShp = self._extractFaceOffset(avoid, ofstVal) - if avdOfstShp is False: - PathLog.error('Failed to create collective offset avoid face.') - cont = False - - if cont: - avdShp = avdOfstShp - - if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: - if len(intFEAT) > 1: - ifc = Part.makeCompound(intFEAT) - else: - ifc = intFEAT[0] - ofstVal = self._calculateOffsetValue(obj, isHole=True) - ifOfstShp = self._extractFaceOffset(ifc, ofstVal) - if ifOfstShp is False: - PathLog.error('Failed to create collective offset avoid internal features.') - else: - avdShp = avdOfstShp.cut(ifOfstShp) - - if mVS is False: - mVS = list() - mVS.append(avdShp) - - - return (mFS, mVS, mPS) - - def _getFaceWires(self, base, fcshp, fcIdx): - outFace = False - INTFCS = list() - fNum = fcIdx + 1 - # preProcEr = translate('PathSurface', 'Error pre-processing Face') - warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face') - - PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) - WIRES = self._extractWiresFromFace(base, fcshp) - if WIRES is False: - PathLog.error('Failed to extract wires from Face{}'.format(fNum)) - return False - - # Process remaining internal features, adding to FCS list - lenW = len(WIRES) - for w in range(0, lenW): - (wire, rsd) = WIRES[w] - PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) - if wire.isClosed() is False: - PathLog.debug(' -wire is not closed.') - else: - slc = self._flattenWireToFace(wire) - if slc is False: - PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) - else: - if w == 0: - outFace = (slc, rsd) - else: - # add to VOIDS so cutter avoids area. - PathLog.warning(warnFinDep + str(fNum) + '.') - INTFCS.append((slc, rsd)) - if len(INTFCS) == 0: - return (outFace, False) - else: - return (outFace, INTFCS) - - def _preProcessEntireBase(self, obj, base, m): - cont = True - isHole = False - prflShp = False - # Create envelope, extract cross-section and make offset co-planar shape - # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) - - try: - baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as ee: - PathLog.error(str(ee)) - shell = base.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as eee: - PathLog.error(str(eee)) - cont = False - #time.sleep(0.2) - - if cont: - csFaceShape = self._getShapeSlice(baseEnv) - if csFaceShape is False: - PathLog.debug('_getShapeSlice(baseEnv) failed') - csFaceShape = self._getCrossSection(baseEnv) - if csFaceShape is False: - PathLog.debug('_getCrossSection(baseEnv) failed') - csFaceShape = self._getSliceFromEnvelope(baseEnv) - if csFaceShape is False: - PathLog.error('Failed to slice baseEnv shape.') - cont = False - - if cont is True and obj.ProfileEdges != 'None': - PathLog.debug(' -Attempting profile geometry for model base.') - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(csFaceShape, ofstVal) - if psOfst is not False: - if obj.ProfileEdges == 'Only': - return (True, psOfst) - prflShp = psOfst - else: - PathLog.error(' -Failed to create profile geometry.') - cont = False - - if cont: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOffsetShape = self._extractFaceOffset(csFaceShape, ofstVal) - if faceOffsetShape is False: - PathLog.error('_extractFaceOffset() failed.') - else: - faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) - return (faceOffsetShape, prflShp) - return False - - def _extractWiresFromFace(self, base, fc): - '''_extractWiresFromFace(base, fc) ... - Attempts to return all closed wires within a parent face, including the outer most wire of the parent. - The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). - ''' - PathLog.debug('_extractWiresFromFace()') - - WIRES = list() - lenWrs = len(fc.Wires) - PathLog.debug(' -Wire count: {}'.format(lenWrs)) - - def index0(tup): - return tup[0] - - # Cycle through wires in face - for w in range(0, lenWrs): - PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) - wire = fc.Wires[w] - checkEdges = False - cont = True - - # Check for closed edges (circles, ellipses, etc...) - for E in wire.Edges: - if E.isClosed() is True: - checkEdges = True - break - - if checkEdges is True: - PathLog.debug(' -checkEdges is True') - for e in range(0, len(wire.Edges)): - edge = wire.Edges[e] - if edge.isClosed() is True and edge.Mass > 0.01: - PathLog.debug(' -Found closed edge') - raised = False - ip = self._isPocket(base, fc, edge) - if ip is False: - raised = True - ebb = edge.BoundBox - eArea = ebb.XLength * ebb.YLength - F = Part.Face(Part.Wire([edge])) - WIRES.append((eArea, F.Wires[0], raised)) - cont = False - - if cont: - PathLog.debug(' -cont is True') - # If only one wire and not checkEdges, return first wire - if lenWrs == 1: - return [(wire, False)] - - raised = False - wbb = wire.BoundBox - wArea = wbb.XLength * wbb.YLength - if w > 0: - ip = self._isPocket(base, fc, wire) - if ip is False: - raised = True - WIRES.append((wArea, Part.Wire(wire.Edges), raised)) - - nf = len(WIRES) - if nf > 0: - PathLog.debug(' -number of wires found is {}'.format(nf)) - if nf == 1: - (area, W, raised) = WIRES[0] - return [(W, raised)] - else: - sortedWIRES = sorted(WIRES, key=index0, reverse=True) - return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size - - return False - - def _calculateOffsetValue(self, obj, isHole, isVoid=False): - '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. - Calculate the offset for the Path.Area() function.''' - JOB = PathUtils.findParentJob(obj) - tolrnc = JOB.GeometryTolerance.Value - - if isVoid is False: - if isHole is True: - offset = -1 * obj.InternalFeaturesAdjustment.Value - offset += self.radius + (tolrnc / 10.0) - else: - offset = -1 * obj.BoundaryAdjustment.Value - if obj.BoundaryEnforcement is True: - offset += self.radius + (tolrnc / 10.0) - else: - offset -= self.radius + (tolrnc / 10.0) - offset = 0.0 - offset - else: - offset = -1 * obj.BoundaryAdjustment.Value - offset += self.radius + (tolrnc / 10.0) - - return offset - - def _extractFaceOffset(self, fcShape, offset): - '''_extractFaceOffset(fcShape, offset) ... internal function. - Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. - Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' - PathLog.debug('_extractFaceOffset()') - - if fcShape.BoundBox.ZMin != 0.0: - fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) - - areaParams = {} - areaParams['Offset'] = offset - areaParams['Fill'] = 1 - areaParams['Coplanar'] = 0 - areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections - areaParams['Reorient'] = True - areaParams['OpenMode'] = 0 - areaParams['MaxArcPoints'] = 400 # 400 - areaParams['Project'] = True - - area = Path.Area() # Create instance of Area() class object - # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane - area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 - area.add(fcShape) - area.setParams(**areaParams) # set parameters - - offsetShape = area.getShape() - wCnt = len(offsetShape.Wires) - if wCnt == 0: - return False - elif wCnt == 1: - ofstFace = Part.Face(offsetShape.Wires[0]) - else: - W = list() - for wr in offsetShape.Wires: - W.append(Part.Face(wr)) - ofstFace = Part.makeCompound(W) - - return ofstFace # offsetShape - - def _isPocket(self, b, f, w): - '''_isPocket(b, f, w)... - Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. - Returns True if pocket, False if raised protrusion.''' - e = w.Edges[0] - for fi in range(0, len(b.Shape.Faces)): - face = b.Shape.Faces[fi] - for ei in range(0, len(face.Edges)): - edge = face.Edges[ei] - if e.isSame(edge) is True: - if f is face: - # Alternative: run loop to see if all edges are same - pass # same source face, look for another - else: - if face.CenterOfMass.z < f.CenterOfMass.z: - return True - return False - - def _flattenWireToFace(self, wire): - PathLog.debug('_flattenWireToFace()') - if wire.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - - # If wire is planar horizontal, convert to a face and return - if wire.BoundBox.ZLength == 0.0: - slc = Part.Face(wire) - return slc - - # Attempt to create a new wire for manipulation, if not, use original - newWire = Part.Wire(wire.Edges) - if newWire.isClosed() is True: - nWire = newWire - else: - PathLog.debug(' -newWire.isClosed() is False') - nWire = wire - - # Attempt extrusion, and then try a manual slice and then cross-section - ext = self._getExtrudedShape(nWire) - if ext is False: - PathLog.debug('_getExtrudedShape() failed') - else: - slc = self._getShapeSlice(ext) - if slc is not False: - return slc - cs = self._getCrossSection(ext, True) - if cs is not False: - return cs - - # Attempt creating an envelope, and then try a manual slice and then cross-section - env = self._getShapeEnvelope(nWire) - if env is False: - PathLog.debug('_getShapeEnvelope() failed') - else: - slc = self._getShapeSlice(env) - if slc is not False: - return slc - cs = self._getCrossSection(env, True) - if cs is not False: - return cs - - # Attempt creating a projection - slc = self._getProjectedFace(nWire) - if slc is False: - PathLog.debug('_getProjectedFace() failed') - else: - return slc - - return False - - def _getExtrudedShape(self, wire): - PathLog.debug('_getExtrudedShape()') - wBB = wire.BoundBox - extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 - - try: - # slower, but renders collective faces correctly. Method 5 in TESTING - shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) - except Exception as ee: - PathLog.error(' -extrude wire failed: \n{}'.format(ee)) - return False - - SHP = Part.makeSolid(shell) - return SHP - - def _getShapeSlice(self, shape): - PathLog.debug('_getShapeSlice()') - - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - xmin = bb.XMin - 1.0 - xmax = bb.XMax + 1.0 - ymin = bb.YMin - 1.0 - ymax = bb.YMax + 1.0 - p1 = FreeCAD.Vector(xmin, ymin, mid) - p2 = FreeCAD.Vector(xmax, ymin, mid) - p3 = FreeCAD.Vector(xmax, ymax, mid) - p4 = FreeCAD.Vector(xmin, ymax, mid) - - e1 = Part.makeLine(p1, p2) - e2 = Part.makeLine(p2, p3) - e3 = Part.makeLine(p3, p4) - e4 = Part.makeLine(p4, p1) - face = Part.Face(Part.Wire([e1, e2, e3, e4])) - fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area - sArea = shape.BoundBox.XLength * shape.BoundBox.YLength - midArea = (fArea + sArea) / 2.0 - - slcShp = shape.common(face) - slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength - - if slcArea < midArea: - for W in slcShp.Wires: - if W.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - if len(slcShp.Wires) == 1: - wire = slcShp.Wires[0] - slc = Part.Face(wire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - else: - fL = list() - for W in slcShp.Wires: - slc = Part.Face(W) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - fL.append(slc) - comp = Part.makeCompound(fL) - if self.showDebugObjects is True: - PathLog.debug('*** tmpSliceCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') - P.Shape = comp - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - return comp - - PathLog.debug(' -slcArea !< midArea') - PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) - return False - - def _getProjectedFace(self, wire): - PathLog.debug('_getProjectedFace()') - F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') - F.Shape = wire - F.purgeTouched() - self.tempGroup.addObject(F) - try: - prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) - prj.recompute() - prj.purgeTouched() - self.tempGroup.addObject(prj) - except Exception as ee: - PathLog.error(str(ee)) - return False - else: - pWire = Part.Wire(prj.Shape.Edges) - if pWire.isClosed() is False: - # PathLog.debug(' -pWire.isClosed() is False') - return False - slc = Part.Face(pWire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - return False - - def _getCrossSection(self, shape, withExtrude=False): - PathLog.debug('_getCrossSection()') - wires = list() - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - - for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): - wires.append(i) - - if len(wires) > 0: - comp = Part.Compound(wires) # produces correct cross-section wire ! - comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) - csWire = comp.Wires[0] - if csWire.isClosed() is False: - PathLog.debug(' -comp.Wires[0] is not closed') - return False - if withExtrude is True: - ext = self._getExtrudedShape(csWire) - CS = self._getShapeSlice(ext) - if CS is False: - return False - else: - CS = Part.Face(csWire) - CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) - return CS - else: - PathLog.debug(' -No wires from .slice() method') - - return False - - def _getShapeEnvelope(self, shape): - PathLog.debug('_getShapeEnvelope()') - - wBB = shape.BoundBox - extFwd = wBB.ZLength + 10.0 - minz = wBB.ZMin - maxz = wBB.ZMin + extFwd - stpDwn = (maxz - minz) / 4.0 - dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) - - try: - env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape - except Exception as ee: - PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) - return False - else: - return env - - return False - - def _getSliceFromEnvelope(self, env): - PathLog.debug('_getSliceFromEnvelope()') - eBB = env.BoundBox - extFwd = eBB.ZLength + 10.0 - maxz = eBB.ZMin + extFwd - - maxMax = env.Edges[0].BoundBox.ZMin - emax = math.floor(maxz - 1.0) - E = list() - for e in range(0, len(env.Edges)): - emin = env.Edges[e].BoundBox.ZMin - if emin > emax: - E.append(env.Edges[e]) - tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) - tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) - - return tf - - def _prepareModelSTLs(self, JOB, obj): - PathLog.debug('_prepareModelSTLs()') - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - - # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") - if self.modelTypes[m] == 'M': - #TODO: test if this works - facets = M.Mesh.Facets.Points - else: - facets = Part.getFacets(M.Shape) - - if self.modelSTLs[m] is True: - stl = ocl.STLSurf() - - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - self.modelSTLs[m] = stl - return - - def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): - '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... - Creates and OCL.stl object with combined data with waste stock, - model, and avoided faces. Travel lines can be checked against this - STL object to determine minimum travel height to clear stock and model.''' - PathLog.debug('_makeSafeSTL()') - - fuseShapes = list() - Mdl = JOB.Model.Group[mdlIdx] - FCAD = FreeCAD.ActiveDocument - mBB = Mdl.Shape.BoundBox - sBB = JOB.Stock.Shape.BoundBox - - # add Model shape to safeSTL shape - fuseShapes.append(Mdl.Shape) - - if obj.BoundBox == 'BaseBoundBox': - cont = False - extFwd = (sBB.ZLength) - zmin = mBB.ZMin - zmax = mBB.ZMin + extFwd - stpDwn = (zmax - zmin) / 4.0 - dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) - - try: - envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as ee: - PathLog.error(str(ee)) - shell = Mdl.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as eee: - PathLog.error(str(eee)) - - if cont: - stckWst = JOB.Stock.Shape.cut(envBB) - if obj.BoundaryAdjustment > 0.0: - cmpndFS = Part.makeCompound(faceShapes) - baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape - adjStckWst = stckWst.cut(baBB) - else: - adjStckWst = stckWst - fuseShapes.append(adjStckWst) - else: - PathLog.warning('Path transitions might not avoid the model. Verify paths.') - #time.sleep(0.3) - else: - # If boundbox is Job.Stock, add hidden pad under stock as base plate - toolDiam = self.cutter.getDiameter() - zMin = JOB.Stock.Shape.BoundBox.ZMin - xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam - yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam - bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) - bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) - bH = 1.0 - crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) - B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) - fuseShapes.append(B) - - if voidShapes is not False: - voidComp = Part.makeCompound(voidShapes) - voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape - fuseShapes.append(voidEnv) - - fused = Part.makeCompound(fuseShapes) - - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') - T.Shape = fused - T.purgeTouched() - self.tempGroup.addObject(T) - - facets = Part.getFacets(fused) - - stl = ocl.STLSurf() - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - - self.safeSTLs[mdlIdx] = stl - - def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): - '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... - This method applies any avoided faces or regions to the selected faces. - It then calls the correct scan method depending on the ScanType property.''' - PathLog.debug('_processCutAreas()') - - final = list() - base = JOB.Model.Group[mdlIdx] - - # Process faces Collectively or Individually - if obj.HandleMultipleFeatures == 'Collectively': - if FCS is True: - COMP = False - else: - ADD = Part.makeCompound(FCS) - if VDS is not False: - DEL = Part.makeCompound(VDS) - COMP = ADD.cut(DEL) - else: - COMP = ADD - - if obj.ScanType == 'Planar': - final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0)) - elif obj.ScanType == 'Rotational': - final.extend(self._processRotationalOp(obj, base, COMP)) - - elif obj.HandleMultipleFeatures == 'Individually': - for fsi in range(0, len(FCS)): - fShp = FCS[fsi] - # self.deleteOpVariables(all=False) - self.resetOpVariables(all=False) - - if fShp is True: - COMP = False - else: - ADD = Part.makeCompound([fShp]) - if VDS is not False: - DEL = Part.makeCompound(VDS) - COMP = ADD.cut(DEL) - else: - COMP = ADD - - if obj.ScanType == 'Planar': - final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi)) - elif obj.ScanType == 'Rotational': - final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP)) - COMP = None - # Eif - - return final - - # Methods for creating path geometry - def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi): - '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)... - This method compiles the main components for the procedural portion of a planar operation (non-rotational). - It creates the OCL PathDropCutter objects: model and safeTravel. - It makes the necessary facial geometries for the actual cut area. - It calls the correct Single or Multi-pass method as needed. - It returns the gcode for the operation. ''' - PathLog.debug('_processPlanarOp()') - final = list() - SCANDATA = list() - # base = JOB.Model.Group[mdlIdx] - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == 'Single-pass': - depthparams = [obj.FinalDepth.Value] - elif obj.LayerMode == 'Multi-pass': - depthparams = [i for i in self.depthParams] - lenDP = len(depthparams) - - # Prepare PathDropCutter objects with STL data - pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value) - safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], - depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) - - profScan = list() - if obj.ProfileEdges != 'None': - prflShp = self.profileShapes[mdlIdx][fsi] - if prflShp is False: - PathLog.error('No profile shape is False.') - return list() - if self.showDebugObjects is True: - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNewProfileShape') - P.Shape = prflShp - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - # get offset path geometry and perform OCL scan with that geometry - pathOffsetGeom = self._planarMakeProfileGeom(obj, prflShp) - if pathOffsetGeom is False: - PathLog.error('No profile geometry returned.') - return list() - profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, offsetPoints=True)] - - geoScan = list() - if obj.ProfileEdges != 'Only': - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutArea') - F.Shape = cmpdShp - # F.recompute() - F.purgeTouched() - self.tempGroup.addObject(F) - # get internal path geometry and perform OCL scan with that geometry - pathGeom = self._planarMakePathGeom(obj, cmpdShp) - if pathGeom is False: - PathLog.error('No path geometry returned.') - return list() - geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False) - - if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last'] - SCANDATA.extend(profScan) - if obj.ProfileEdges == 'None': - SCANDATA.extend(geoScan) - if obj.ProfileEdges == 'First': - SCANDATA.extend(profScan) - SCANDATA.extend(geoScan) - if obj.ProfileEdges == 'Last': - SCANDATA.extend(geoScan) - SCANDATA.extend(profScan) - - # Apply depth offset - if obj.DepthOffset.Value != 0.0: - self._planarApplyDepthOffset(SCANDATA, obj.DepthOffset.Value) - - if len(SCANDATA) == 0: - PathLog.error('No scan data to convert to Gcode.') - return list() - - # If cut pattern is `Circular`, there are zero(almost zero) straight lines to optimize - # Store initial `OptimizeLinearPaths` value for later restoration - self.preOLP = obj.OptimizeLinearPaths - if obj.CutPattern in ['Circular', 'CircularZigZag']: - obj.OptimizeLinearPaths = False - - # Process OCL scan data - if obj.LayerMode == 'Single-pass': - final.extend(self._planarDropCutSingle(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) - elif obj.LayerMode == 'Multi-pass': - final.extend(self._planarDropCutMulti(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) - - # If cut pattern is `Circular`, restore initial OLP value - if obj.CutPattern in ['Circular', 'CircularZigZag']: - obj.OptimizeLinearPaths = self.preOLP - - # Raise to safe height between individual faces. - if obj.HandleMultipleFeatures == 'Individually': - final.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - - return final - - def _planarMakePathGeom(self, obj, faceShp): - '''_planarMakePathGeom(obj, faceShp)... - Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. - The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' - PathLog.debug('_planarMakePathGeom()') - GeoSet = list() - - # Apply drop cutter extra offset and set the max and min XY area of the operation - xmin = faceShp.BoundBox.XMin - xmax = faceShp.BoundBox.XMax - ymin = faceShp.BoundBox.YMin - ymax = faceShp.BoundBox.YMax - zmin = faceShp.BoundBox.ZMin - zmax = faceShp.BoundBox.ZMax - - # Compute weighted center of mass of all faces combined - fCnt = 0 - totArea = 0.0 - zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) - for F in faceShp.Faces: - comF = F.CenterOfMass - areaF = F.Area - totArea += areaF - fCnt += 1 - zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) - if fCnt == 0: - PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) - zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) - else: - avgArea = totArea / fCnt - zeroCOM.multiply(1 / fCnt) - zeroCOM.multiply(1 / avgArea) - COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) - - # get X, Y, Z spans; Compute center of rotation - deltaX = abs(xmax-xmin) - deltaY = abs(ymax-ymin) - deltaZ = abs(zmax-zmin) - deltaC = math.sqrt(deltaX**2 + deltaY**2) - lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - halfLL = math.ceil(lineLen / 2.0) - cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - halfPasses = math.ceil(cutPasses / 2.0) - bbC = faceShp.BoundBox.Center - - # Generate the Draft line/circle sets to be intersected with the cut-face-area - if obj.CutPattern in ['ZigZag', 'Line']: - MaxLC = -1 - centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model - cAng = math.atan(deltaX / deltaY) # BoundaryBox angle - - # Determine end points and create top lines - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - diag = None - if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: - MaxLC = math.floor(deltaY / self.cutOut) - diag = deltaY - elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: - MaxLC = math.floor(deltaX / self.cutOut) - diag = deltaX - else: - perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC - MaxLC = math.floor(perpDist / self.cutOut) - diag = perpDist - y1 = centRot.y + diag - # y2 = y1 - - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - topLineTuple = (p1, p2) - ny1 = centRot.y - diag - n1 = FreeCAD.Vector(x1, ny1, 0.0) - n2 = FreeCAD.Vector(x2, ny1, 0.0) - negTopLineTuple = (n1, n2) - - # Create end points for set of lines to intersect with cross-section face - pntTuples = list() - for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): - # if lc == (cutPasses - MaxLC - 1): - # pntTuples.append(negTopLineTuple) - # if lc == (MaxLC + 1): - # pntTuples.append(topLineTuple) - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - y1 = centRot.y + (lc * self.cutOut) - # y2 = y1 - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - pntTuples.append( (p1, p2) ) - - # Convert end points to lines - for (p1, p2) in pntTuples: - line = Part.makeLine(p1, p2) - GeoSet.append(line) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - zTgt = faceShp.BoundBox.ZMin - axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) - cntr = FreeCAD.Placement() - cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) - - if obj.CircularCenterAt == 'CenterOfMass': - cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass - elif obj.CircularCenterAt == 'CenterOfBoundBox': - cent = faceShp.BoundBox.Center - cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) - elif obj.CircularCenterAt == 'XminYmin': - cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) - elif obj.CircularCenterAt == 'Custom': - newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) - cntr.Base = newCent - - # recalculate cutPasses value, if need be - radialPasses = halfPasses - if obj.CircularCenterAt != 'CenterOfBoundBox': - # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center - EBB = faceShp.BoundBox - CORNERS = [ - FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), - FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), - ] - dMax = 0.0 - for c in range(0, 4): - dist = CORNERS[c].sub(cntr.Base).Length - if dist > dMax: - dMax = dist - lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - - # Update COM point and current CircularCenter - if obj.CircularCenterAt != 'Custom': - obj.CircularCenterCustom = cntr.Base - - minRad = self.cutter.getDiameter() * 0.45 - siX3 = 3 * obj.SampleInterval.Value - minRadSI = (siX3 / 2.0) / math.pi - if minRad < minRadSI: - minRad = minRadSI - - # Make small center circle to start pattern - if obj.StepOver > 50: - circle = Part.makeCircle(minRad, cntr.Base) - GeoSet.append(circle) - - for lc in range(1, radialPasses + 1): - rad = (lc * self.cutOut) - if rad >= minRad: - circle = Part.makeCircle(rad, cntr.Base) - GeoSet.append(circle) - # Efor - COM = cntr.Base - # Eif - - if obj.CutPatternReversed is True: - GeoSet.reverse() - - if faceShp.BoundBox.ZMin != 0.0: - faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) - - # Create compound object to bind all lines in Lineset - geomShape = Part.makeCompound(GeoSet) - - # Position and rotate the Line and ZigZag geometry - if obj.CutPattern in ['Line', 'ZigZag']: - if obj.CutPatternAngle != 0.0: - geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) - geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') - F.Shape = geomShape - F.purgeTouched() - self.tempGroup.addObject(F) - - # Identify intersection of cross-section face and lineset - cmnShape = faceShp.common(geomShape) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') - F.Shape = cmnShape - F.purgeTouched() - self.tempGroup.addObject(F) - - self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) - return cmnShape - - def _planarMakeProfileGeom(self, obj, subShp): - PathLog.debug('_planarMakeProfileGeom()') - - offsetLists = list() - dist = obj.SampleInterval.Value / 5.0 - defl = obj.SampleInterval.Value / 5.0 - - # Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939 - for fc in subShp.Faces: - # Reverse order of wires in each face - inside to outside - for w in range(len(fc.Wires) - 1, -1, -1): - W = fc.Wires[w] - PNTS = W.discretize(Distance=dist) - # PNTS = W.discretize(Deflection=defl) - if self.CutClimb is True: - PNTS.reverse() - offsetLists.append(PNTS) - - return offsetLists - - def _planarPerformOclScan(self, obj, pdc, pathGeom, offsetPoints=False): - '''_planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)... - Switching function for calling the appropriate path-geometry to OCL points conversion function - for the various cut patterns.''' - PathLog.debug('_planarPerformOclScan()') - SCANS = list() - - if offsetPoints is True: - PNTSET = self._pathGeomToOffsetPointSet(obj, pathGeom) - for D in PNTSET: - stpOvr = list() - ofst = list() - for I in D: - if I == 'BRK': - stpOvr.append(ofst) - stpOvr.append(I) - ofst = list() - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = I - ofst.extend(self._planarDropCutScan(pdc, A, B)) - if len(ofst) > 0: - stpOvr.append(ofst) - SCANS.extend(stpOvr) - elif obj.CutPattern == 'Line': - stpOvr = list() - PNTSET = self._pathGeomToLinesPointSet(obj, pathGeom) - for D in PNTSET: - for I in D: - if I == 'BRK': - stpOvr.append(I) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = I - stpOvr.append(self._planarDropCutScan(pdc, A, B)) - SCANS.append(stpOvr) - stpOvr = list() - elif obj.CutPattern == 'ZigZag': - stpOvr = list() - PNTSET = self._pathGeomToZigzagPointSet(obj, pathGeom) - for (dirFlg, LNS) in PNTSET: - for SEG in LNS: - if SEG == 'BRK': - stpOvr.append(SEG) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = SEG - stpOvr.append(self._planarDropCutScan(pdc, A, B)) - SCANS.append(stpOvr) - stpOvr = list() - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - # PNTSET is list, by stepover. - # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) - PNTSET = self._pathGeomToArcPointSet(obj, pathGeom) - - for so in range(0, len(PNTSET)): - stpOvr = list() - erFlg = False - (aTyp, dirFlg, ARCS) = PNTSET[so] - - if dirFlg == 1: # 1 - cMode = True - else: - cMode = False - - for a in range(0, len(ARCS)): - Arc = ARCS[a] - if Arc == 'BRK': - stpOvr.append('BRK') - else: - scan = self._planarCircularDropCutScan(pdc, Arc, cMode) - if scan is False: - erFlg = True - else: - if aTyp == 'L': - scan.append(FreeCAD.Vector(scan[0].x, scan[0].y, scan[0].z)) - stpOvr.append(scan) - if erFlg is False: - SCANS.append(stpOvr) - - return SCANS - - def _pathGeomToOffsetPointSet(self, obj, compGeoShp): - '''_pathGeomToOffsetPointSet(obj, compGeoShp)... - Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.''' - PathLog.debug('_pathGeomToOffsetPointSet()') - - LINES = list() - optimize = obj.OptimizeLinearPaths - ofstCnt = len(compGeoShp) - - # Cycle through offeset loops - for ei in range(0, ofstCnt): - OS = compGeoShp[ei] - lenOS = len(OS) - - if ei > 0: - LINES.append('BRK') - - fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z) - OS.append(fp) - - # Cycle through points in each loop - prev = OS[0] - pnt = OS[1] - for v in range(1, lenOS): - nxt = OS[v + 1] - if optimize is True: - iPOL = prev.isOnLineSegment(nxt, pnt) - if iPOL is True: - pnt = nxt - else: - tup = ((prev.x, prev.y), (pnt.x, pnt.y)) - LINES.append(tup) - prev = pnt - pnt = nxt - else: - tup = ((prev.x, prev.y), (pnt.x, pnt.y)) - LINES.append(tup) - prev = pnt - pnt = nxt - if iPOL is True: - tup = ((prev.x, prev.y), (pnt.x, pnt.y)) - LINES.append(tup) - # Efor - - return [LINES] - - def _pathGeomToLinesPointSet(self, obj, compGeoShp): - '''_pathGeomToLinesPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' - PathLog.debug('_pathGeomToLinesPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - chkGap = False - lnCnt = 0 - ec = len(compGeoShp.Edges) - cutClimb = self.CutClimb - toolDiam = 2.0 * self.radius - cpa = obj.CutPatternAngle - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if cutClimb is True: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - else: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - inLine.append(tup) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - - for ei in range(1, ec): - chkGap = False - edg = compGeoShp.Edges[ei] # Get edge for vertexes - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 - - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) - iC = sp.isOnLineSegment(ep, cp) - if iC is True: - inLine.append('BRK') - chkGap = True - else: - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - lnCnt += 1 - inLine = list() # reset collinear container - if cutClimb is True: - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - else: - sp = ep - - if cutClimb is True: - tup = (v2, v1) - if chkGap is True: - gap = abs(toolDiam - lst.sub(ep).Length) - lst = cp - else: - tup = (v1, v2) - if chkGap is True: - gap = abs(toolDiam - lst.sub(cp).Length) - lst = ep - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - tup = (vA, tup[1]) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - - # Handle last inLine set, reversing it. - if obj.CutPatternReversed is True: - if cpa != 0.0 and cpa % 90.0 == 0.0: - F = LINES.pop(0) - rev = list() - for iL in F: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - rev.reverse() - LINES.insert(0, rev) - - isEven = lnCnt % 2 - if isEven == 0: - PathLog.debug('Line count is ODD.') - else: - PathLog.debug('Line count is even.') - - return LINES - - def _pathGeomToZigzagPointSet(self, obj, compGeoShp): - '''_pathGeomToZigzagPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings - with a ZigZag directional indicator included for each collinear group.''' - PathLog.debug('_pathGeomToZigzagPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - lnCnt = 0 - chkGap = False - ec = len(compGeoShp.Edges) - toolDiam = 2.0 * self.radius - - if self.CutClimb is True: - dirFlg = -1 - else: - dirFlg = 1 - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if dirFlg == 1: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - else: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point - inLine.append(tup) - otr = lst - - for ei in range(1, ec): - edg = compGeoShp.Edges[ei] - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) - - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - iC = sp.isOnLineSegment(ep, cp) - if iC is True: - inLine.append('BRK') - chkGap = True - gap = abs(toolDiam - lst.sub(cp).Length) - else: - chkGap = False - if dirFlg == -1: - inLine.reverse() - LINES.append((dirFlg, inLine)) - lnCnt += 1 - dirFlg = -1 * dirFlg # Change zig to zag - inLine = list() # reset collinear container - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - otr = ep - - lst = ep - if dirFlg == 1: - tup = (v1, v2) - else: - tup = (v2, v1) - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - if dirFlg == 1: - tup = (vA, tup[1]) - else: - #tup = (vA, tup[1]) - #tup = (tup[1], vA) - tup = (tup[0], vB) - self.closedGap = True - else: - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - - # Fix directional issue with LAST line when line count is even - isEven = lnCnt % 2 - if isEven == 0: # Changed to != with 90 degree CutPatternAngle - PathLog.debug('Line count is even.') - else: - PathLog.debug('Line count is ODD.') - dirFlg = -1 * dirFlg - if obj.CutPatternReversed is False: - if self.CutClimb is True: - dirFlg = -1 * dirFlg - - if obj.CutPatternReversed is True: - dirFlg = -1 * dirFlg - - # Handle last inLine list - if dirFlg == 1: - rev = list() - for iL in inLine: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - - if obj.CutPatternReversed is False: - rev.reverse() - else: - rev2 = list() - for iL in rev: - if iL == 'BRK': - rev2.append(iL) - else: - (p1, p2) = iL - rev2.append((p2, p1)) - rev2.reverse() - rev = rev2 - - LINES.append((dirFlg, rev)) - else: - LINES.append((dirFlg, inLine)) - - return LINES - - def _pathGeomToArcPointSet(self, obj, compGeoShp): - '''_pathGeomToArcPointSet(obj, compGeoShp)... - Convert a compound set of arcs/circles to a set of directionally-oriented arc end points - and the corresponding center point.''' - # Extract intersection line segments for return value as list() - PathLog.debug('_pathGeomToArcPointSet()') - ARCS = list() - stpOvrEI = list() - segEI = list() - isSame = False - sameRad = None - COM = self.tmpCOM - toolDiam = 2.0 * self.radius - ec = len(compGeoShp.Edges) - - def gapDist(sp, ep): - X = (ep[0] - sp[0])**2 - Y = (ep[1] - sp[1])**2 - Z = (ep[2] - sp[2])**2 - # return math.sqrt(X + Y + Z) - return math.sqrt(X + Y) # the 'z' value is zero in both points - - # Separate arc data into Loops and Arcs - for ei in range(0, ec): - edg = compGeoShp.Edges[ei] - if edg.Closed is True: - stpOvrEI.append(('L', ei, False)) - else: - if isSame is False: - segEI.append(ei) - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - else: - # Check if arc is co-radial to current SEGS - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - if abs(sameRad - pnt.sub(COM).Length) > 0.00001: - isSame = False - - if isSame is True: - segEI.append(ei) - else: - # Move co-radial arc segments - stpOvrEI.append(['A', segEI, False]) - # Start new list of arc segments - segEI = [ei] - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - # Process trailing `segEI` data, if available - if isSame is True: - stpOvrEI.append(['A', segEI, False]) - - # Identify adjacent arcs with y=0 start/end points that connect - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'A': - startOnAxis = list() - endOnAxis = list() - EI = SO[1] # list of corresponding compGeoShp.Edges indexes - - # Identify startOnAxis and endOnAxis arcs - for i in range(0, len(EI)): - ei = EI[i] # edge index - E = compGeoShp.Edges[ei] # edge object - if abs(COM.y - E.Vertexes[0].Y) < 0.00001: - startOnAxis.append((i, ei, E.Vertexes[0])) - elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: - endOnAxis.append((i, ei, E.Vertexes[1])) - - # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected - lenSOA = len(startOnAxis) - lenEOA = len(endOnAxis) - if lenSOA > 0 and lenEOA > 0: - delIdxs = list() - lstFindIdx = 0 - for soa in range(0, lenSOA): - (iS, eiS, vS) = startOnAxis[soa] - for eoa in range(0, len(endOnAxis)): - (iE, eiE, vE) = endOnAxis[eoa] - dist = vE.X - vS.X - if abs(dist) < 0.00001: # They connect on axis at same radius - SO[2] = (eiE, eiS) - break - elif dist > 0: - break # stop searching - # Eif - # Eif - # Efor - - # Construct arc data tuples for OCL - dirFlg = 1 - # cutPat = obj.CutPattern - if self.CutClimb is False: # True yields Climb when set to Conventional - dirFlg = -1 - - # Cycle through stepOver data - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'L': # L = Loop/Ring/Circle - lei = SO[1] # loop Edges index - v1 = compGeoShp.Edges[lei].Vertexes[0] - - space = obj.SampleInterval.Value / 2.0 - - p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) - sp = (v1.X, v1.Y, 0.0) - rad = p1.sub(COM).Length - spcRadRatio = space/rad - if spcRadRatio < 1.0: - tolrncAng = math.asin(spcRadRatio) - else: - tolrncAng = 0.999998 * math.pi - X = COM.x + (rad * math.cos(tolrncAng)) - Y = v1.Y - space # rad * math.sin(tolrncAng) - - sp = (v1.X, v1.Y, 0.0) - ep = (X, Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - ARCS.append(('L', dirFlg, [arc])) - else: # SO[0] == 'A' A = Arc - PRTS = list() - EI = SO[1] # list of corresponding Edges indexes - CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) - chkGap = False - lst = None - - if CONN is not False: - (iE, iS) = CONN - v1 = compGeoShp.Edges[iE].Vertexes[0] - v2 = compGeoShp.Edges[iS].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - lst = sp - PRTS.append(arc) - # Pop connected edge index values from arc segments index list - iEi = EI.index(iE) - iSi = EI.index(iS) - if iEi > iSi: - EI.pop(iEi) - EI.pop(iSi) - else: - EI.pop(iSi) - EI.pop(iEi) - if len(EI) > 0: - PRTS.append('BRK') - chkGap = True - cnt = 0 - for ei in EI: - if cnt > 0: - PRTS.append('BRK') - chkGap = True - v1 = compGeoShp.Edges[ei].Vertexes[0] - v2 = compGeoShp.Edges[ei].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) - lst = sp - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = PRTS.pop() # pop off 'BRK' marker - (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current - arc = (vA, arc[1], vC) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - PRTS.append(arc) - cnt += 1 - - if dirFlg == -1: - PRTS.reverse() - - ARCS.append(('A', dirFlg, PRTS)) - # Eif - if obj.CutPattern == 'CircularZigZag': - dirFlg = -1 * dirFlg - # Efor - - return ARCS - - def _planarDropCutScan(self, pdc, A, B): - #PNTS = list() - (x1, y1) = A - (x2, y2) = B - path = ocl.Path() # create an empty path object - p1 = ocl.Point(x1, y1, 0) # start-point of line - p2 = ocl.Point(x2, y2, 0) # end-point of line - lo = ocl.Line(p1, p2) # line-object - path.append(lo) # add the line to the path - pdc.setPath(path) - pdc.run() # run dropcutter algorithm on path - CLP = pdc.getCLPoints() - PNTS = [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP] - return PNTS # pdc.getCLPoints() - - def _planarCircularDropCutScan(self, pdc, Arc, cMode): - PNTS = list() - path = ocl.Path() # create an empty path object - (sp, ep, cp) = Arc - - # process list of segment tuples (vect, vect) - path = ocl.Path() # create an empty path object - p1 = ocl.Point(sp[0], sp[1], 0) # start point of arc - p2 = ocl.Point(ep[0], ep[1], 0) # end point of arc - C = ocl.Point(cp[0], cp[1], 0) # center point of arc - ao = ocl.Arc(p1, p2, C, cMode) # arc object - path.append(ao) # add the arc to the path - pdc.setPath(path) - pdc.run() # run dropcutter algorithm on path - CLP = pdc.getCLPoints() - - # Convert OCL object data to FreeCAD vectors - for p in CLP: - PNTS.append(FreeCAD.Vector(p.x, p.y, p.z)) - - return PNTS - - # Main planar scan functions - def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): - PathLog.debug('_planarDropCutSingle()') - - GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] - tolrnc = JOB.GeometryTolerance.Value - prevDepth = obj.SafeHeight.Value - lenDP = len(depthparams) - lenSCANDATA = len(SCANDATA) - gDIR = ['G3', 'G2'] - - if self.CutClimb is True: - gDIR = ['G2', 'G3'] - - # Set `ProfileEdges` specific trigger indexes - peIdx = lenSCANDATA # off by default - if obj.ProfileEdges == 'Only': - peIdx = -1 - elif obj.ProfileEdges == 'First': - peIdx = 0 - elif obj.ProfileEdges == 'Last': - peIdx = lenSCANDATA - 1 - - # Send cutter to x,y position of first point on first line - first = SCANDATA[0][0][0] # [step][item][point] - GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) - - # Cycle through step-over sections (line segments or arcs) - odd = True - lstStpEnd = None - prevDepth = obj.SafeHeight.Value # Not used for Single-pass - for so in range(0, lenSCANDATA): - cmds = list() - PRTS = SCANDATA[so] - lenPRTS = len(PRTS) - first = PRTS[0][0] # first point of arc/line stepover group - start = PRTS[0][0] # will change with each line/arc segment - last = None - cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) - - if so > 0: - if obj.CutPattern == 'CircularZigZag': - if odd is True: - odd = False - else: - odd = True - minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL - # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) - cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) - - # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization - if so == peIdx or peIdx == -1: - obj.OptimizeLinearPaths = self.preOLP - - # Cycle through current step-over parts - for i in range(0, lenPRTS): - prt = PRTS[i] - lenPrt = len(prt) - if prt == 'BRK': - nxtStart = PRTS[i + 1][0] - minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL - cmds.append(Path.Command('N (Break)', {})) - cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) - else: - cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) - start = prt[0] - last = prt[lenPrt - 1] - if so == peIdx or peIdx == -1: - cmds.extend(self._planarSinglepassProcess(obj, prt)) - elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2: - (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) - if rtnVal is True: - cmds.extend(gcode) - else: - cmds.extend(self._planarSinglepassProcess(obj, prt)) - else: - cmds.extend(self._planarSinglepassProcess(obj, prt)) - cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) - GCODE.extend(cmds) # save line commands - lstStpEnd = last - - # Return `OptimizeLinearPaths` to disabled - if so == peIdx or peIdx == -1: - if obj.CutPattern in ['Circular', 'CircularZigZag']: - obj.OptimizeLinearPaths = False - # Efor - - return GCODE - - def _planarSinglepassProcess(self, obj, PNTS): - if obj.OptimizeLinearPaths: - # first item will be compared to the last point, but I think that should work - output = [Path.Command('G1', {'X': PNTS[i].x, 'Y': PNTS[i].y, 'Z': PNTS[i].z, 'F': self.horizFeed}) - for i in range(0, len(PNTS) - 1) - if not PNTS[i].isOnLineSegment(PNTS[i -1],PNTS[i + 1])] - output.append(Path.Command('G1', {'X': PNTS[-1].x, 'Y': PNTS[-1].y, 'Z': PNTS[-1].z, 'F': self.horizFeed})) - else: - output = [Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed}) for pnt in PNTS] - - return output - - def _planarDropCutMulti(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): - GCODE = [Path.Command('N (Beginning of Multi-pass layers.)', {})] - tolrnc = JOB.GeometryTolerance.Value - lenDP = len(depthparams) - prevDepth = depthparams[0] - lenSCANDATA = len(SCANDATA) - gDIR = ['G3', 'G2'] - - if self.CutClimb is True: - gDIR = ['G2', 'G3'] - - # Set `ProfileEdges` specific trigger indexes - peIdx = lenSCANDATA # off by default - if obj.ProfileEdges == 'Only': - peIdx = -1 - elif obj.ProfileEdges == 'First': - peIdx = 0 - elif obj.ProfileEdges == 'Last': - peIdx = lenSCANDATA - 1 - - # Process each layer in depthparams - prvLyrFirst = None - prvLyrLast = None - lastPrvStpLast = None - actvLyrs = 0 - for lyr in range(0, lenDP): - odd = True # ZigZag directional switch - lyrHasCmds = False - lstStpEnd = None - actvSteps = 0 - LYR = list() - prvStpFirst = None - if lyr > 0: - if prvStpLast is not None: - lastPrvStpLast = prvStpLast - prvStpLast = None - lyrDep = depthparams[lyr] - PathLog.debug('Multi-pass lyrDep: {}'.format(round(lyrDep, 4))) - - # Cycle through step-over sections (line segments or arcs) - for so in range(0, len(SCANDATA)): - SO = SCANDATA[so] - lenSO = len(SO) - - # Pre-process step-over parts for layer depth and holds - ADJPRTS = list() - LMAX = list() - soHasPnts = False - brkFlg = False - for i in range(0, lenSO): - prt = SO[i] - lenPrt = len(prt) - if prt == 'BRK': - if brkFlg is True: - ADJPRTS.append(prt) - LMAX.append(prt) - brkFlg = False - else: - (PTS, lMax) = self._planarMultipassPreProcess(obj, prt, prevDepth, lyrDep) - if len(PTS) > 0: - ADJPRTS.append(PTS) - soHasPnts = True - brkFlg = True - LMAX.append(lMax) - # Efor - lenAdjPrts = len(ADJPRTS) - - # Process existing parts within current step over - prtsHasCmds = False - stepHasCmds = False - prtsCmds = list() - stpOvrCmds = list() - transCmds = list() - if soHasPnts is True: - first = ADJPRTS[0][0] # first point of arc/line stepover group - - # Manage step over transition and CircularZigZag direction - if so > 0: - # PathLog.debug(' stepover index: {}'.format(so)) - # Control ZigZag direction - if obj.CutPattern == 'CircularZigZag': - if odd is True: - odd = False - else: - odd = True - # Control step over transition - if prvStpLast is None: - prvStpLast = lastPrvStpLast - minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL - transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {})) - transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc)) - - # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization - if so == peIdx or peIdx == -1: - obj.OptimizeLinearPaths = self.preOLP - - # Cycle through current step-over parts - for i in range(0, lenAdjPrts): - prt = ADJPRTS[i] - lenPrt = len(prt) - # PathLog.debug(' adj parts index - lenPrt: {} - {}'.format(i, lenPrt)) - if prt == 'BRK' and prtsHasCmds is True: - nxtStart = ADJPRTS[i + 1][0] - minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL - prtsCmds.append(Path.Command('N (--Break)', {})) - prtsCmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) - else: - segCmds = False - prtsCmds.append(Path.Command('N (part {})'.format(i + 1), {})) - last = prt[lenPrt - 1] - if so == peIdx or peIdx == -1: - segCmds = self._planarSinglepassProcess(obj, prt) - elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2: - (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) - if rtnVal is True: - segCmds = gcode - else: - segCmds = self._planarSinglepassProcess(obj, prt) - else: - segCmds = self._planarSinglepassProcess(obj, prt) - - if segCmds is not False: - prtsCmds.extend(segCmds) - prtsHasCmds = True - prvStpLast = last - # Eif - # Efor - # Eif - - # Return `OptimizeLinearPaths` to disabled - if so == peIdx or peIdx == -1: - if obj.CutPattern in ['Circular', 'CircularZigZag']: - obj.OptimizeLinearPaths = False - - # Compile step over(prts) commands - if prtsHasCmds is True: - stepHasCmds = True - actvSteps += 1 - prvStpFirst = first - stpOvrCmds.extend(transCmds) - stpOvrCmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) - stpOvrCmds.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) - stpOvrCmds.extend(prtsCmds) - stpOvrCmds.append(Path.Command('N (End of step {}.)'.format(so), {})) - - # Layer transition at first active step over in current layer - if actvSteps == 1: - prvLyrFirst = first - LYR.append(Path.Command('N (Layer {} begins)'.format(lyr), {})) - if lyr > 0: - LYR.append(Path.Command('N (Layer transition)', {})) - LYR.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - LYR.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) - - if stepHasCmds is True: - lyrHasCmds = True - LYR.extend(stpOvrCmds) - # Eif - - # Close layer, saving commands, if any - if lyrHasCmds is True: - prvLyrLast = last - GCODE.extend(LYR) # save line commands - GCODE.append(Path.Command('N (End of layer {})'.format(lyr), {})) - - # Set previous depth - prevDepth = lyrDep - # Efor - - PathLog.debug('Multi-pass op has {} layers (step downs).'.format(lyr + 1)) - - return GCODE - - def _planarMultipassPreProcess(self, obj, LN, prvDep, layDep): - ALL = list() - PTS = list() - brkFlg = False - optLinTrans = obj.OptimizeStepOverTransitions - safe = math.ceil(obj.SafeHeight.Value) - - if optLinTrans is True: - for P in LN: - ALL.append(P) - # Handle layer depth AND hold points - if P.z <= layDep: - PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) - elif P.z > prvDep: - PTS.append(FreeCAD.Vector(P.x, P.y, safe)) - else: - PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) - # Efor - else: - for P in LN: - ALL.append(P) - # Handle layer depth only - if P.z <= layDep: - PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) - else: - PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) - # Efor - - if optLinTrans is True: - # Remove leading and trailing Hold Points - popList = list() - for i in range(0, len(PTS)): # identify leading string - if PTS[i].z == safe: - popList.append(i) - else: - break - popList.sort(reverse=True) - for p in popList: # Remove hold points - PTS.pop(p) - ALL.pop(p) - popList = list() - for i in range(len(PTS) - 1, -1, -1): # identify trailing string - if PTS[i].z == safe: - popList.append(i) - else: - break - popList.sort(reverse=True) - for p in popList: # Remove hold points - PTS.pop(p) - ALL.pop(p) - - # Determine max Z height for remaining points on line - lMax = obj.FinalDepth.Value - if len(ALL) > 0: - lMax = ALL[0].z - for P in ALL: - if P.z > lMax: - lMax = P.z - - return (PTS, lMax) - - def _planarMultipassProcess(self, obj, PNTS, lMax): - output = list() - optimize = obj.OptimizeLinearPaths - safe = math.ceil(obj.SafeHeight.Value) - lenPNTS = len(PNTS) - lastPNTS = lenPNTS - 1 - prcs = True - onHold = False - onLine = False - clrScnLn = lMax + 2.0 - - # Initialize first three points - nxt = None - pnt = PNTS[0] - prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847) - - # Add temp end point - PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425)) - - # Begin processing ocl points list into gcode - for i in range(0, lenPNTS): - prcs = True - nxt = PNTS[i + 1] - - if pnt.z == safe: - prcs = False - if onHold is False: - onHold = True - output.append( Path.Command('N (Start hold)', {}) ) - output.append( Path.Command('G0', {'Z': clrScnLn, 'F': self.vertRapid}) ) - else: - if onHold is True: - onHold = False - output.append( Path.Command('N (End hold)', {}) ) - output.append( Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}) ) - - # Process point - if prcs is True: - if optimize is True: - iPOL = prev.isOnLineSegment(nxt, pnt) - if iPOL is True: - onLine = True - else: - onLine = False - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - else: - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - - # Rotate point data - if onLine is False: - prev = pnt - pnt = nxt - # Efor - - temp = PNTS.pop() # Remove temp end point - - return output - - def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): - cmds = list() - rtpd = False - horizGC = 'G0' - hSpeed = self.horizRapid - height = obj.SafeHeight.Value - - if obj.CutPattern in ['Line', 'Circular']: - if obj.OptimizeStepOverTransitions is True: - height = minSTH + 2.0 - # if obj.LayerMode == 'Multi-pass': - # rtpd = minSTH - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: - if obj.OptimizeStepOverTransitions is True: - zChng = first.z - lstPnt.z - # PathLog.debug('first.z: {}'.format(first.z)) - # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) - # PathLog.debug('zChng: {}'.format(zChng)) - # PathLog.debug('minSTH: {}'.format(minSTH)) - if abs(zChng) < tolrnc: # transitions to same Z height - PathLog.debug('abs(zChng) < tolrnc') - if (minSTH - first.z) > tolrnc: - PathLog.debug('(minSTH - first.z) > tolrnc') - height = minSTH + 2.0 - else: - PathLog.debug('ELSE (minSTH - first.z) > tolrnc') - horizGC = 'G1' - height = first.z - elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): - height = False # allow end of Zig to cut to beginning of Zag - - - # Create raise, shift, and optional lower commands - if height is not False: - cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) - cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) - if rtpd is not False: # ReturnToPreviousDepth - cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) - - return cmds - - def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): - cmds = list() - rtpd = False - horizGC = 'G0' - hSpeed = self.horizRapid - height = obj.SafeHeight.Value - - if obj.CutPattern in ['Line', 'Circular']: - if obj.OptimizeStepOverTransitions is True: - height = minSTH + 2.0 - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: - if obj.OptimizeStepOverTransitions is True: - zChng = first.z - lstPnt.z - if abs(zChng) < tolrnc: # transitions to same Z height - if (minSTH - first.z) > tolrnc: - height = minSTH + 2.0 - else: - height = first.z + 2.0 # first.z - - cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) - cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) - if rtpd is not False: # ReturnToPreviousDepth - cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) - - return cmds - - def _arcsToG2G3(self, LN, numPts, odd, gDIR, tolrnc): - cmds = list() - strtPnt = LN[0] - endPnt = LN[numPts - 1] - strtHght = strtPnt.z - coPlanar = True - isCircle = False - inrPnt = None - gdi = 0 - if odd is True: - gdi = 1 - - # Test if pnt set is circle - if abs(strtPnt.x - endPnt.x) < tolrnc: - if abs(strtPnt.y - endPnt.y) < tolrnc: - if abs(strtPnt.z - endPnt.z) < tolrnc: - isCircle = True - isCircle = False - - if isCircle is True: - # convert LN to G2/G3 arc, consolidating GCode - # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc - # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ - # Dividing circle into two arcs allows for G2/G3 on inclined surfaces - - # ijk = self.tmpCOM - strtPnt # vector from start to center - ijk = self.tmpCOM - strtPnt # vector from start to center - xyz = self.tmpCOM.add(ijk) # end point - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) - ijk = self.tmpCOM - xyz # vector from start to center - rst = strtPnt # end point - cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - else: - for pt in LN: - if abs(pt.z - strtHght) > tolrnc: # test for horizontal coplanar - coPlanar = False - break - if coPlanar is True: - # ijk = self.tmpCOM - strtPnt - ijk = self.tmpCOM.sub(strtPnt) # vector from start to center - xyz = endPnt - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) - - return (coPlanar, cmds) - - def _planarApplyDepthOffset(self, SCANDATA, DepthOffset): - PathLog.debug('Applying DepthOffset value: {}'.format(DepthOffset)) - lenScans = len(SCANDATA) - for s in range(0, lenScans): - SO = SCANDATA[s] # StepOver - numParts = len(SO) - for prt in range(0, numParts): - PRT = SO[prt] - if PRT != 'BRK': - numPts = len(PRT) - for pt in range(0, numPts): - SCANDATA[s][prt][pt].z += DepthOffset - - def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): - pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object - pdc.setSTL(stl) # add stl model - if useSafeCutter is True: - pdc.setCutter(self.safeCutter) # add safeCutter - else: - pdc.setCutter(self.cutter) # add cutter - pdc.setZ(finalDep) # set minimumZ (final / target depth value) - pdc.setSampling(SampleInterval) # set sampling size - return pdc - - # Main rotational scan functions - def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None): - PathLog.debug('_processRotationalOp(self, obj, mdlIdx, compoundFaces=None)') - initIdx = 0.0 - final = list() - - JOB = PathUtils.findParentJob(obj) - base = JOB.Model.Group[mdlIdx] - bb = self.boundBoxes[mdlIdx] - stl = self.modelSTLs[mdlIdx] - - # Rotate model to initial index - initIdx = obj.CutterTilt + obj.StartIndex - if initIdx != 0.0: - self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement - if obj.RotationAxis == 'X': - base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(1.0, 0.0, 0.0), initIdx)) - else: - base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(0.0, 1.0, 0.0), initIdx)) - - # Prepare global holdpoint container - if self.holdPoint is None: - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) - if self.layerEndPnt is None: - self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Avoid division by zero in rotational scan calculations - if obj.FinalDepth.Value <= 0.0: - zero = obj.SampleInterval.Value # 0.00001 - self.FinalDepth = zero - obj.FinalDepth.Value = 0.0 - else: - self.FinalDepth = obj.FinalDepth.Value - - # Determine boundbox radius based upon xzy limits data - if math.fabs(bb.ZMin) > math.fabs(bb.ZMax): - vlim = bb.ZMin - else: - vlim = bb.ZMax - if obj.RotationAxis == 'X': - # Rotation is around X-axis, cutter moves along same axis - if math.fabs(bb.YMin) > math.fabs(bb.YMax): - hlim = bb.YMin - else: - hlim = bb.YMax - else: - # Rotation is around Y-axis, cutter moves along same axis - if math.fabs(bb.XMin) > math.fabs(bb.XMax): - hlim = bb.XMin - else: - hlim = bb.XMax - - # Compute max radius of stock, as it rotates, and rotational clearance & safe heights - self.bbRadius = math.sqrt(hlim**2 + vlim**2) - self.clearHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value - self.safeHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value - - final = self._rotationalDropCutterOp(obj, stl, bb) - - return final - - def _rotationalDropCutterOp(self, obj, stl, bb): - self.resetTolerance = 0.0000001 # degrees - self.layerEndzMax = 0.0 - commands = [] - scanLines = [] - advances = [] - iSTG = [] - rSTG = [] - rings = [] - lCnt = 0 - rNum = 0 - # stepDeg = 1.1 - # layCircum = 1.1 - # begIdx = 0.0 - # endIdx = 0.0 - # arc = 0.0 - # sumAdv = 0.0 - bbRad = self.bbRadius - - def invertAdvances(advances): - idxs = [1.1] - for adv in advances: - idxs.append(-1 * adv) - idxs.pop(0) - return idxs - - def linesToPointRings(scanLines): - rngs = [] - numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval.Value spacing - for line in scanLines: # extract circular set(ring) of points from scan lines - if len(line) != numPnts: - PathLog.debug('Error: line lengths not equal') - return rngs - - for num in range(0, numPnts): - rngs.append([1.1]) # Initiate new ring - for line in scanLines: # extract circular set(ring) of points from scan lines - rngs[num].append(line[num]) - rngs[num].pop(0) - return rngs - - def indexAdvances(arc, stepDeg): - indexes = [0.0] - numSteps = int(math.floor(arc / stepDeg)) - for ns in range(0, numSteps): - indexes.append(stepDeg) - - travel = sum(indexes) - if arc == 360.0: - indexes.insert(0, 0.0) - else: - indexes.append(arc - travel) - - return indexes - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == 'Single-pass': - depthparams = [self.FinalDepth] - else: - dep_par = PathUtils.depth_params(self.clearHeight, self.safeHeight, self.bbRadius, obj.StepDown.Value, 0.0, self.FinalDepth) - depthparams = [i for i in dep_par] - prevDepth = depthparams[0] - lenDP = len(depthparams) - - # Set drop cutter extra offset - cdeoX = obj.DropCutterExtraOffset.x - cdeoY = obj.DropCutterExtraOffset.y - - # Set updated bound box values and redefine the new min/mas XY area of the operation based on greatest point radius of model - bb.ZMin = -1 * bbRad - bb.ZMax = bbRad - if obj.RotationAxis == 'X': - bb.YMin = -1 * bbRad - bb.YMax = bbRad - ymin = 0.0 - ymax = 0.0 - xmin = bb.XMin - cdeoX - xmax = bb.XMax + cdeoX - else: - bb.XMin = -1 * bbRad - bb.XMax = bbRad - ymin = bb.YMin - cdeoY - ymax = bb.YMax + cdeoY - xmin = 0.0 - xmax = 0.0 - - # Calculate arc - begIdx = obj.StartIndex - endIdx = obj.StopIndex - if endIdx < begIdx: - begIdx -= 360.0 - arc = endIdx - begIdx - - # Begin gcode operation with raising cutter to safe height - commands.append(Path.Command('G0', {'Z': self.safeHeight, 'F': self.vertRapid})) - - # Complete rotational scans at layer and translate into gcode - for layDep in depthparams: - t_before = time.time() - - # Compute circumference and step angles for current layer - layCircum = 2 * math.pi * layDep - if lenDP == 1: - layCircum = 2 * math.pi * bbRad - - # Set axial feed rates - self.axialFeed = 360 / layCircum * self.horizFeed - self.axialRapid = 360 / layCircum * self.horizRapid - - # Determine step angle. - if obj.RotationAxis == obj.DropCutterDir: # Same == indexed - stepDeg = (self.cutOut / layCircum) * 360.0 - else: - stepDeg = (obj.SampleInterval.Value / layCircum) * 360.0 - - # Limit step angle and determine rotational index angles [indexes]. - if stepDeg > 120.0: - stepDeg = 120.0 - advances = indexAdvances(arc, stepDeg) # Reset for each step down layer - - # Perform rotational indexed scans to layer depth - if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel - sample = obj.SampleInterval.Value - else: - sample = self.cutOut - scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample) - - # Complete rotation if necessary - if arc == 360.0: - advances.append(360.0 - sum(advances)) - advances.pop(0) - zero = scanLines.pop(0) - scanLines.append(zero) - - # Translate OCL scans into gcode - if obj.RotationAxis == obj.DropCutterDir: # Same == indexed (cutter runs parallel to axis) - # Invert advances if RotationAxis == Y - if obj.RotationAxis == 'Y': - advances = invertAdvances(advances) - - # Translate scan to gcode - # sumAdv = 0.0 - sumAdv = begIdx - for sl in range(0, len(scanLines)): - sumAdv += advances[sl] - # Translate scan to gcode - iSTG = self._indexedScanToGcode(obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP) - commands.extend(iSTG) - - # Add rise to clear height before beginning next index in CutPattern: Line - # if obj.CutPattern == 'Line': - # commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - - # Raise cutter to safe height after each index cut - commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - # Eol - else: - if self.CutClimb is False: - advances = invertAdvances(advances) - advances.reverse() - scanLines.reverse() - - # Invert advances if RotationAxis == Y - if obj.RotationAxis == 'Y': - advances = invertAdvances(advances) - - # Begin gcode operation with raising cutter to safe height - commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - - # Convert rotational scans into gcode - rings = linesToPointRings(scanLines) - rNum = 0 - for rng in rings: - rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances) - commands.extend(rSTG) - if arc != 360.0: - clrZ = self.layerEndzMax + self.SafeHeightOffset - commands.append(Path.Command('G0', {'Z': clrZ, 'F': self.vertRapid})) - rNum += 1 - # Eol - - # Add rise to clear height before beginning next index in CutPattern: Line - # if obj.CutPattern == 'Line': - # commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - - prevDepth = layDep - lCnt += 1 # increment layer count - PathLog.debug("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s") - #time.sleep(0.2) - # Eol - return commands - - def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample): - cutterOfst = 0.0 - # radsRot = 0.0 - # reset = 0.0 - iCnt = 0 - Lines = [] - result = None - - pdc = ocl.PathDropCutter() # create a pdc - pdc.setCutter(self.cutter) - pdc.setZ(layDep) # set minimumZ (final / ta9rget depth value) - pdc.setSampling(sample) - - # if self.useTiltCutter == True: - if obj.CutterTilt != 0.0: - cutterOfst = layDep * math.sin(math.radians(obj.CutterTilt)) - PathLog.debug("CutterTilt: cutterOfst is " + str(cutterOfst)) - - sumAdv = 0.0 - for adv in advances: - sumAdv += adv - if adv > 0.0: - # Rotate STL object using OCL method - radsRot = math.radians(adv) - if obj.RotationAxis == 'X': - stl.rotate(radsRot, 0.0, 0.0) - else: - stl.rotate(0.0, radsRot, 0.0) - - # Set STL after rotation is made - pdc.setSTL(stl) - - # add Line objects to the path in this loop - if obj.RotationAxis == 'X': - p1 = ocl.Point(xmin, cutterOfst, 0.0) # start-point of line - p2 = ocl.Point(xmax, cutterOfst, 0.0) # end-point of line - else: - p1 = ocl.Point(cutterOfst, ymin, 0.0) # start-point of line - p2 = ocl.Point(cutterOfst, ymax, 0.0) # end-point of line - - # Create line object - if obj.RotationAxis == obj.DropCutterDir: # parallel cut - if obj.CutPattern == 'ZigZag': - if (iCnt % 2 == 0.0): # even - lo = ocl.Line(p1, p2) - else: # odd - lo = ocl.Line(p2, p1) - elif obj.CutPattern == 'Line': - if self.CutClimb is True: - lo = ocl.Line(p2, p1) - else: - lo = ocl.Line(p1, p2) - else: - lo = ocl.Line(p1, p2) # line-object - - path = ocl.Path() # create an empty path object - path.append(lo) # add the line to the path - pdc.setPath(path) # set path - pdc.run() # run drop-cutter on the path - result = pdc.getCLPoints() - Lines.append(result) # request the list of points - - iCnt += 1 - # End loop - # Rotate STL object back to original position using OCL method - reset = -1 * math.radians(sumAdv - self.resetTolerance) - if obj.RotationAxis == 'X': - stl.rotate(reset, 0.0, 0.0) - else: - stl.rotate(0.0, reset, 0.0) - self.resetTolerance = 0.0 - - return Lines - - def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps): - # generate the path commands - output = [] - optimize = obj.OptimizeLinearPaths - holdCount = 0 - holdStart = False - holdStop = False - zMax = prvDep - lenCLP = len(CLP) - lastCLP = lenCLP - 1 - prev = ocl.Point(float("inf"), float("inf"), float("inf")) - nxt = ocl.Point(float("inf"), float("inf"), float("inf")) - pnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Create first point - pnt.x = CLP[0].x - pnt.y = CLP[0].y - pnt.z = CLP[0].z + float(obj.DepthOffset.Value) - - # Rotate to correct index location - if obj.RotationAxis == 'X': - output.append(Path.Command('G0', {'A': idxAng, 'F': self.axialFeed})) - else: - output.append(Path.Command('G0', {'B': idxAng, 'F': self.axialFeed})) - - if li > 0: - if pnt.z > self.layerEndPnt.z: - clrZ = pnt.z + 2.0 - output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) - else: - output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) - - output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) - output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) - - for i in range(0, lenCLP): - if i < lastCLP: - nxt.x = CLP[i + 1].x - nxt.y = CLP[i + 1].y - nxt.z = CLP[i + 1].z + float(obj.DepthOffset.Value) - else: - optimize = False - - # Update zMax values - if pnt.z > zMax: - zMax = pnt.z - - if obj.LayerMode == 'Multi-pass': - # if z travels above previous layer, start/continue hold high cycle - if pnt.z > prvDep and optimize is True: - if self.onHold is False: - holdStart = True - self.onHold = True - - if self.onHold is True: - if holdStart is True: - # go to current coordinate - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - # Save holdStart coordinate and prvDep values - self.holdPoint.x = pnt.x - self.holdPoint.y = pnt.y - self.holdPoint.z = pnt.z - holdCount += 1 # Increment hold count - holdStart = False # cancel holdStart - - # hold cutter high until Z value drops below prvDep - if pnt.z <= prvDep: - holdStop = True - - if holdStop is True: - # Send hold and current points to - zMax += 2.0 - for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"): - output.append(cmd) - # reset necessary hold related settings - zMax = prvDep - holdStop = False - self.onHold = False - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) - - if self.onHold is False: - if not optimize or not FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - # elif i == lastCLP: - # output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) - - # Rotate point data - prev.x = pnt.x - prev.y = pnt.y - prev.z = pnt.z - pnt.x = nxt.x - pnt.y = nxt.y - pnt.z = nxt.z - output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {})) - - # Save layer end point for use in transitioning to next layer - self.layerEndPnt.x = pnt.x - self.layerEndPnt.y = pnt.y - self.layerEndPnt.z = pnt.z - - return output - - def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances): - '''_rotationalScanToGcode(obj, RNG, rN, prvDep, layDep, advances) ... - Convert rotational scan data to gcode path commands.''' - output = [] - nxtAng = 0 - zMax = 0.0 - # prev = ocl.Point(float("inf"), float("inf"), float("inf")) - nxt = ocl.Point(float("inf"), float("inf"), float("inf")) - pnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - begIdx = obj.StartIndex - endIdx = obj.StopIndex - if endIdx < begIdx: - begIdx -= 360.0 - - # Rotate to correct index location - axisOfRot = 'A' - if obj.RotationAxis == 'Y': - axisOfRot = 'B' - - # Create first point - ang = 0.0 + obj.CutterTilt - pnt.x = RNG[0].x - pnt.y = RNG[0].y - pnt.z = RNG[0].z + float(obj.DepthOffset.Value) - - # Adjust feed rate based on radius/circumference of cutter. - # Original feed rate based on travel at circumference. - if rN > 0: - # if pnt.z > self.layerEndPnt.z: - if pnt.z >= self.layerEndzMax: - clrZ = pnt.z + 5.0 - output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) - else: - output.append(Path.Command('G1', {'Z': self.clearHeight, 'F': self.vertRapid})) - - output.append(Path.Command('G0', {axisOfRot: ang, 'F': self.axialFeed})) - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.axialFeed})) - output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.axialFeed})) - - lenRNG = len(RNG) - lastIdx = lenRNG - 1 - for i in range(0, lenRNG): - if i < lastIdx: - nxtAng = ang + advances[i + 1] - nxt.x = RNG[i + 1].x - nxt.y = RNG[i + 1].y - nxt.z = RNG[i + 1].z + float(obj.DepthOffset.Value) - - if pnt.z > zMax: - zMax = pnt.z - - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, axisOfRot: ang, 'F': self.axialFeed})) - pnt.x = nxt.x - pnt.y = nxt.y - pnt.z = nxt.z - ang = nxtAng - - # Save layer end point for use in transitioning to next layer - self.layerEndPnt.x = RNG[0].x - self.layerEndPnt.y = RNG[0].y - self.layerEndPnt.z = RNG[0].z - self.layerEndzMax = zMax - - # Move cutter to final point - # output.append(Path.Command('G1', {'X': self.layerEndPnt.x, 'Y': self.layerEndPnt.y, 'Z': self.layerEndPnt.z, axisOfRot: endang, 'F': self.axialFeed})) - - return output - - - '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.''' - # generate the path commands - output = [] - optimize = obj.OptimizeLinearPaths - - prev = ocl.Point(float("inf"), float("inf"), float("inf")) - nxt = ocl.Point(float("inf"), float("inf"), float("inf")) - pnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Create first point - pnt.x = loop[0].x - pnt.y = loop[0].y - pnt.z = layDep - - # Position cutter to begin loop - output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) - output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) - - lenCLP = len(loop) - lastIdx = lenCLP - 1 - # Cycle through each point on loop - for i in range(0, lenCLP): - if i < lastIdx: - nxt.x = loop[i + 1].x - nxt.y = loop[i + 1].y - nxt.z = layDep - else: - optimize = False - - if not optimize or not FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed})) - - # Rotate point data - prev.x = pnt.x - prev.y = pnt.y - prev.z = pnt.z - pnt.x = nxt.x - pnt.y = nxt.y - pnt.z = nxt.z - - # Save layer end point for use in transitioning to next layer - self.layerEndPnt.x = pnt.x - self.layerEndPnt.y = pnt.y - self.layerEndPnt.z = pnt.z - - return output - - def holdStopCmds(self, obj, zMax, pd, p2, txt): - '''holdStopCmds(obj, zMax, pd, p2, txt) ... Gcode commands to be executed at beginning of hold.''' - cmds = [] - msg = 'N (' + txt + ')' - cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel - cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel - cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate - if zMax != pd: - cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth - cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed - return cmds - - def resetOpVariables(self, all=True): - '''resetOpVariables() ... Reset class variables used for instance of operation.''' - self.holdPoint = None - self.layerEndPnt = None - self.onHold = False - self.SafeHeightOffset = 2.0 - self.ClearHeightOffset = 4.0 - self.layerEndzMax = 0.0 - self.resetTolerance = 0.0 - self.holdPntCnt = 0 - self.bbRadius = 0.0 - self.axialFeed = 0.0 - self.axialRapid = 0.0 - self.FinalDepth = 0.0 - self.clearHeight = 0.0 - self.safeHeight = 0.0 - self.faceZMax = -999999999999.0 - if all is True: - self.cutter = None - self.stl = None - self.fullSTL = None - self.cutOut = 0.0 - self.radius = 0.0 - self.useTiltCutter = False - return True - - def deleteOpVariables(self, all=True): - '''deleteOpVariables() ... Reset class variables used for instance of operation.''' - del self.holdPoint - del self.layerEndPnt - del self.onHold - del self.SafeHeightOffset - del self.ClearHeightOffset - del self.layerEndzMax - del self.resetTolerance - del self.holdPntCnt - del self.bbRadius - del self.axialFeed - del self.axialRapid - del self.FinalDepth - del self.clearHeight - del self.safeHeight - del self.faceZMax - if all is True: - del self.cutter - del self.stl - del self.fullSTL - del self.cutOut - del self.radius - del self.useTiltCutter - return True - - def setOclCutter(self, obj, safe=False): - ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' - # Set cutter details - # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details - diam_1 = float(obj.ToolController.Tool.Diameter) - lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 - FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 - CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 - CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 - - # Make safeCutter with 2 mm buffer around physical cutter - if safe is True: - diam_1 += 4.0 - if FR != 0.0: - FR += 2.0 - - PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) - if obj.ToolController.Tool.ToolType == 'EndMill': - # Standard End Mill - return ocl.CylCutter(diam_1, (CEH + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: - # Standard Ball End Mill - # OCL -> BallCutter::BallCutter(diameter, length) - self.useTiltCutter = True - return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> BallCutter::BallCutter(diameter, length) - return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) - - elif obj.ToolController.Tool.ToolType == 'ChamferMill': - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) - else: - # Default to standard end mill - PathLog.warning("Defaulting cutter to standard end mill.") - return ocl.CylCutter(diam_1, (CEH + lenOfst)) - - # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html - ''' - # Available FreeCAD cutter types - some still need translation to available OCL cutter classes. - Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap, - EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver - ''' - # Adittional problem is with new ToolBit user-defined cutter shapes. - # Some sort of translation/conversion will have to be defined to make compatible with OCL. - PathLog.error('Unable to set OCL cutter.') - return False - - def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None): - A = (p1.x, p1.y) - B = (p2.x, p2.y) - LINE = self._planarDropCutScan(pdc, A, B) - zMax = max([obj.z for obj in LINE]) - if minDep is not None: - if zMax < minDep: - zMax = minDep - return zMax - - -def SetupProperties(): - ''' SetupProperties() ... Return list of properties required for operation.''' - setup = [] - setup.append('AvoidLastX_Faces') - setup.append('AvoidLastX_InternalFeatures') - setup.append('BoundBox') - setup.append('BoundaryAdjustment') - setup.append('CircularCenterAt') - setup.append('CircularCenterCustom') - setup.append('CircularUseG2G3') - setup.append('InternalFeaturesCut') - setup.append('InternalFeaturesAdjustment') - setup.append('CutMode') - setup.append('CutPattern') - setup.append('CutPatternAngle') - setup.append('CutPatternReversed') - setup.append('CutterTilt') - setup.append('DepthOffset') - setup.append('DropCutterDir') - setup.append('GapSizes') - setup.append('GapThreshold') - setup.append('HandleMultipleFeatures') - setup.append('LayerMode') - setup.append('OptimizeStepOverTransitions') - setup.append('ProfileEdges') - setup.append('BoundaryEnforcement') - setup.append('RotationAxis') - setup.append('SampleInterval') - setup.append('ScanType') - setup.append('StartIndex') - setup.append('StartPoint') - setup.append('StepOver') - setup.append('StopIndex') - setup.append('UseStartPoint') - setup.append('AngularDeflection') - setup.append('LinearDeflection') - # For debugging - setup.append('ShowTempObjects') - return setup - - -def Create(name, obj=None): - '''Create(name) ... Creates and returns a Surface operation.''' - if obj is None: - obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) - obj.Proxy = ObjectSurface(obj, name) - return obj +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2016 sliptonic * +# * * +# * 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. * +# * * +# * This program 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 this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** +# * * +# * Additional modifications and contributions beginning 2019 * +# * by Russell Johnson 2020-04-10 11:46 CST * +# * * +# *************************************************************************** + +from __future__ import print_function + +__title__ = "Path Surface Operation" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Class and implementation of Mill Facing operation." +__contributors__ = "russ4262 (Russell Johnson)" + +import FreeCAD +from PySide import QtCore + +# OCL must be installed +try: + import ocl +except ImportError: + msg = QtCore.QCoreApplication.translate("PathSurface", "This operation requires OpenCamLib to be installed.") + FreeCAD.Console.PrintError(msg + "\n") + raise + # import sys + # sys.exit(msg) + +import MeshPart +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils +import PathScripts.PathOp as PathOp +import time +import math +import Part + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') + +if FreeCAD.GuiUp: + import FreeCADGui + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class ObjectSurface(PathOp.ObjectOp): + '''Proxy object for Surfacing operation.''' + + def baseObject(self): + '''baseObject() ... returns super of receiver + Used to call base implementation in overwritten functions.''' + return super(self.__class__, self) + + def opFeatures(self, obj): + '''opFeatures(obj) ... return all standard features and edges based geometries''' + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces + + def initOperation(self, obj): + '''initPocketOp(obj) ... create operation specific properties''' + self.initOpProperties(obj) + + # For debugging + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + + if not hasattr(obj, 'DoNotSetDefaultValues'): + self.setEditorProperties(obj) + + def initOpProperties(self, obj): + '''initOpProperties(obj) ... create operation specific properties''' + missing = list() + + for (prtyp, nm, grp, tt) in self.opProperties(): + if not hasattr(obj, nm): + obj.addProperty(prtyp, nm, grp, tt) + missing.append(nm) + + # Set enumeration lists for enumeration properties + if len(missing) > 0: + ENUMS = self.propertyEnumerations() + for n in ENUMS: + if n in missing: + cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) + exec(cmdStr) + + self.addedAllProperties = True + + def opProperties(self): + '''opProperties(obj) ... Store operation specific properties''' + + return [ + ("App::PropertyBool", "ShowTempObjects", "Debug", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), + + ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values increase processing time a lot.")), + ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate mesh. Smaller values do not increase processing time much.")), + + ("App::PropertyFloat", "CutterTilt", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), + ("App::PropertyEnumeration", "DropCutterDir", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")), + ("App::PropertyVectorDistance", "DropCutterExtraOffset", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")), + ("App::PropertyEnumeration", "RotationAxis", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The model will be rotated around this axis.")), + ("App::PropertyFloat", "StartIndex", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Start index(angle) for rotational scan")), + ("App::PropertyFloat", "StopIndex", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), + + ("App::PropertyEnumeration", "ScanType", "Surface", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.")), + + ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), + ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), + ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), + ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), + ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), + ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), + ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), + + ("App::PropertyEnumeration", "BoundBox", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")), + ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), + ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")), + ("App::PropertyEnumeration", "CutMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), + ("App::PropertyEnumeration", "CutPattern", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), + ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), + ("App::PropertyBool", "CutPatternReversed", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), + ("App::PropertyDistance", "DepthOffset", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), + ("App::PropertyEnumeration", "LayerMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), + ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), + ("App::PropertyDistance", "SampleInterval", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), + ("App::PropertyPercent", "StepOver", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), + + ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), + ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), + ("App::PropertyBool", "CircularUseG2G3", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Convert co-planar arcs to G2/G3 gcode commands for `Circular` and `CircularZigZag` cut patterns.")), + ("App::PropertyDistance", "GapThreshold", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), + ("App::PropertyString", "GapSizes", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), + + ("App::PropertyVectorDistance", "StartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), + ("App::PropertyBool", "UseStartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) + ] + + def propertyEnumerations(self): + # Enumeration lists for App::PropertyEnumeration properties + return { + 'BoundBox': ['BaseBoundBox', 'Stock'], + 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], + 'CutMode': ['Conventional', 'Climb'], + 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] + 'DropCutterDir': ['X', 'Y'], + 'HandleMultipleFeatures': ['Collectively', 'Individually'], + 'LayerMode': ['Single-pass', 'Multi-pass'], + 'ProfileEdges': ['None', 'Only', 'First', 'Last'], + 'RotationAxis': ['X', 'Y'], + 'ScanType': ['Planar', 'Rotational'] + } + + def setEditorProperties(self, obj): + # Used to hide inputs in properties list + + P0 = R2 = 0 # 0 = show + P2 = R0 = 2 # 2 = hide + if obj.ScanType == 'Planar': + # if obj.CutPattern in ['Line', 'ZigZag']: + if obj.CutPattern in ['Circular', 'CircularZigZag']: + P0 = 2 + P2 = 0 + R0 = 0 + elif obj.ScanType == 'Rotational': + R2 = P0 = P2 = 2 + R0 = 0 + obj.setEditorMode('DropCutterDir', R0) + obj.setEditorMode('DropCutterExtraOffset', R0) + obj.setEditorMode('RotationAxis', R0) + obj.setEditorMode('StartIndex', R0) + obj.setEditorMode('StopIndex', R0) + obj.setEditorMode('CutterTilt', R0) + obj.setEditorMode('CutPattern', R2) + obj.setEditorMode('CutPatternAngle', P0) + obj.setEditorMode('CircularCenterAt', P2) + obj.setEditorMode('CircularCenterCustom', P2) + + def onChanged(self, obj, prop): + if hasattr(self, 'addedAllProperties'): + if self.addedAllProperties is True: + if prop == 'ScanType': + self.setEditorProperties(obj) + if prop == 'CutPattern': + self.setEditorProperties(obj) + + def opOnDocumentRestored(self, obj): + self.initOpProperties(obj) + + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + else: + obj.setEditorMode('ShowTempObjects', 0) # show + + self.setEditorProperties(obj) + + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj, job) ... initialize defaults''' + job = PathUtils.findParentJob(obj) + + obj.OptimizeLinearPaths = True + obj.InternalFeaturesCut = True + obj.OptimizeStepOverTransitions = False + obj.CircularUseG2G3 = False + obj.BoundaryEnforcement = True + obj.UseStartPoint = False + obj.AvoidLastX_InternalFeatures = True + obj.CutPatternReversed = False + obj.StartPoint.x = 0.0 + obj.StartPoint.y = 0.0 + obj.StartPoint.z = obj.ClearanceHeight.Value + obj.ProfileEdges = 'None' + obj.LayerMode = 'Single-pass' + obj.ScanType = 'Planar' + obj.RotationAxis = 'X' + obj.CutMode = 'Conventional' + obj.CutPattern = 'Line' + obj.HandleMultipleFeatures = 'Collectively' # 'Individually' + obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' + obj.GapSizes = 'No gaps identified.' + obj.StepOver = 100 + obj.CutPatternAngle = 0.0 + obj.CutterTilt = 0.0 + obj.StartIndex = 0.0 + obj.StopIndex = 360.0 + obj.SampleInterval.Value = 1.0 + obj.BoundaryAdjustment.Value = 0.0 + obj.InternalFeaturesAdjustment.Value = 0.0 + obj.AvoidLastX_Faces = 0 + obj.CircularCenterCustom.x = 0.0 + obj.CircularCenterCustom.y = 0.0 + obj.CircularCenterCustom.z = 0.0 + obj.GapThreshold.Value = 0.005 + obj.AngularDeflection.Value = 0.25 + obj.LinearDeflection.Value = job.GeometryTolerance.Value + # For debugging + obj.ShowTempObjects = False + + if job.GeometryTolerance.Value == 0.0: + PathLog.warning(translate('PathSurface', 'The GeometryTolerance for this Job is 0.0. Initializing LinearDeflection to 0.0001 mm.')) + obj.LinearDeflection.Value = 0.0001 + + # need to overwrite the default depth calculations for facing + d = None + if job: + if job.Stock: + d = PathUtils.guessDepths(job.Stock.Shape, None) + PathLog.debug("job.Stock exists") + else: + PathLog.debug("job.Stock NOT exist") + else: + PathLog.debug("job NOT exist") + + if d is not None: + obj.OpFinalDepth.Value = d.final_depth + obj.OpStartDepth.Value = d.start_depth + else: + obj.OpFinalDepth.Value = -10 + obj.OpStartDepth.Value = 10 + + PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) + PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) + + def opApplyPropertyLimits(self, obj): + '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' + # Limit start index + if obj.StartIndex < 0.0: + obj.StartIndex = 0.0 + if obj.StartIndex > 360.0: + obj.StartIndex = 360.0 + + # Limit stop index + if obj.StopIndex > 360.0: + obj.StopIndex = 360.0 + if obj.StopIndex < 0.0: + obj.StopIndex = 0.0 + + # Limit cutter tilt + if obj.CutterTilt < -90.0: + obj.CutterTilt = -90.0 + if obj.CutterTilt > 90.0: + obj.CutterTilt = 90.0 + + # Limit sample interval + if obj.SampleInterval.Value < 0.0001: + obj.SampleInterval.Value = 0.0001 + PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) + if obj.SampleInterval.Value > 25.4: + obj.SampleInterval.Value = 25.4 + PathLog.error(translate('PathSurface', 'Sample interval limits are 0.001 to 25.4 millimeters.')) + + # Limit cut pattern angle + if obj.CutPatternAngle < -360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +-360 degrees.')) + if obj.CutPatternAngle >= 360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.')) + + # Limit StepOver to natural number percentage + if obj.StepOver > 100: + obj.StepOver = 100 + if obj.StepOver < 1: + obj.StepOver = 1 + + # Limit AvoidLastX_Faces to zero and positive values + if obj.AvoidLastX_Faces < 0: + obj.AvoidLastX_Faces = 0 + PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Only zero or positive values permitted.')) + if obj.AvoidLastX_Faces > 100: + obj.AvoidLastX_Faces = 100 + PathLog.error(translate('PathSurface', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) + + def opExecute(self, obj): + '''opExecute(obj) ... process surface operation''' + PathLog.track() + + self.modelSTLs = list() + self.safeSTLs = list() + self.modelTypes = list() + self.boundBoxes = list() + self.profileShapes = list() + self.collectiveShapes = list() + self.individualShapes = list() + self.avoidShapes = list() + self.deflection = None + self.tempGroup = None + self.CutClimb = False + self.closedGap = False + self.gaps = [0.1, 0.2, 0.3] + CMDS = list() + modelVisibility = list() + FCAD = FreeCAD.ActiveDocument + + # Set debugging behavior + self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects + self.showDebugObjects = obj.ShowTempObjects + deleteTempsFlag = True # Set to False for debugging + if PathLog.getLevel(PathLog.thisModule()) == 4: + deleteTempsFlag = False + else: + self.showDebugObjects = False + + # mark beginning of operation and identify parent Job + PathLog.info('\nBegin 3D Surface operation...') + startTime = time.time() + + # Identify parent Job + JOB = PathUtils.findParentJob(obj) + if JOB is None: + PathLog.error(translate('PathSurface', "No JOB")) + return + self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin + + # set cut mode; reverse as needed + if obj.CutMode == 'Climb': + self.CutClimb = True + if obj.CutPatternReversed is True: + if self.CutClimb is True: + self.CutClimb = False + else: + self.CutClimb = True + + # Begin GCode for operation with basic information + # ... and move cutter to clearance height and startpoint + output = '' + if obj.Comment != '': + self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) + self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) + self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) + self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) + self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) + self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) + self.commandlist.append(Path.Command('N ({})'.format(output), {})) + self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + if obj.UseStartPoint is True: + self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) + + # Instantiate additional class operation variables + self.resetOpVariables() + + # Impose property limits + self.opApplyPropertyLimits(obj) + + # Create temporary group for temporary objects, removing existing + tempGroupName = 'tempPathSurfaceGroup' + if FCAD.getObject(tempGroupName): + for to in FCAD.getObject(tempGroupName).Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) # remove temp directory if already exists + if FCAD.getObject(tempGroupName + '001'): + for to in FCAD.getObject(tempGroupName + '001').Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists + tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) + tempGroupName = tempGroup.Name + self.tempGroup = tempGroup + tempGroup.purgeTouched() + # Add temp object to temp group folder with following code: + # ... self.tempGroup.addObject(OBJ) + + # Setup cutter for OCL and cutout value for operation - based on tool controller properties + self.cutter = self.setOclCutter(obj) + self.safeCutter = self.setOclCutter(obj, safe=True) + if self.cutter is False or self.safeCutter is False: + PathLog.error(translate('PathSurface', "Canceling 3D Surface operation. Error creating OCL cutter.")) + return + toolDiam = self.cutter.getDiameter() + self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) + self.radius = toolDiam / 2.0 + self.gaps = [toolDiam, toolDiam, toolDiam] + + # Get height offset values for later use + self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value + self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value + + # Calculate default depthparams for operation + self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) + self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 + + # make circle for workplane + self.wpc = Part.makeCircle(2.0) + + # Set deflection values for mesh generation + try: # try/except is for Path Jobs created before GeometryTolerance + self.deflection = JOB.GeometryTolerance.Value + except AttributeError as ee: + PathLog.warning('Error setting Mesh deflection: {}. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) + import PathScripts.PathPreferences as PathPreferences + self.deflection = PathPreferences.defaultGeometryTolerance() + + # Save model visibilities for restoration + if FreeCAD.GuiUp: + for m in range(0, len(JOB.Model.Group)): + mNm = JOB.Model.Group[m].Name + modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) + + # Setup STL, model type, and bound box containers for each model in Job + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + self.modelSTLs.append(False) + self.safeSTLs.append(False) + self.profileShapes.append(False) + # Set bound box + if obj.BoundBox == 'BaseBoundBox': + if M.TypeId.startswith('Mesh'): + self.modelTypes.append('M') # Mesh + self.boundBoxes.append(M.Mesh.BoundBox) + else: + self.modelTypes.append('S') # Solid + self.boundBoxes.append(M.Shape.BoundBox) + elif obj.BoundBox == 'Stock': + self.modelTypes.append('S') # Solid + self.boundBoxes.append(JOB.Stock.Shape.BoundBox) + + # ###### MAIN COMMANDS FOR OPERATION ###### + + # Begin processing obj.Base data and creating GCode + # Process selected faces, if available + pPM = self._preProcessModel(JOB, obj) + if pPM is False: + PathLog.error('Unable to pre-process obj.Base.') + else: + (FACES, VOIDS) = pPM + + # Create OCL.stl model objects + self._prepareModelSTLs(JOB, obj) + + for m in range(0, len(JOB.Model.Group)): + Mdl = JOB.Model.Group[m] + if FACES[m] is False: + PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) + else: + if m > 0: + # Raise to clearance between models + CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) + CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) + # make stock-model-voidShapes STL model for avoidance detection on transitions + self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) + # Process model/faces - OCL objects must be ready + CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m])) + + # Save gcode produced + self.commandlist.extend(CMDS) + + # ###### CLOSING COMMANDS FOR OPERATION ###### + + # Delete temporary objects + # Restore model visibilities for restoration + if FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + M.Visibility = modelVisibility[m] + + if deleteTempsFlag is True: + for to in tempGroup.Group: + if hasattr(to, 'Group'): + for go in to.Group: + FCAD.removeObject(go.Name) + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) + else: + if len(tempGroup.Group) == 0: + FCAD.removeObject(tempGroupName) + else: + tempGroup.purgeTouched() + + # Provide user feedback for gap sizes + gaps = list() + for g in self.gaps: + if g != toolDiam: + gaps.append(g) + if len(gaps) > 0: + obj.GapSizes = '{} mm'.format(gaps) + else: + if self.closedGap is True: + obj.GapSizes = 'Closed gaps < Gap Threshold.' + else: + obj.GapSizes = 'No gaps identified.' + + # clean up class variables + self.resetOpVariables() + self.deleteOpVariables() + + self.modelSTLs = None + self.safeSTLs = None + self.modelTypes = None + self.boundBoxes = None + self.gaps = None + self.closedGap = None + self.SafeHeightOffset = None + self.ClearHeightOffset = None + self.depthParams = None + self.midDep = None + self.wpc = None + self.deflection = None + del self.modelSTLs + del self.safeSTLs + del self.modelTypes + del self.boundBoxes + del self.gaps + del self.closedGap + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.depthParams + del self.midDep + del self.wpc + del self.deflection + + execTime = time.time() - startTime + PathLog.info('Operation time: {} sec.'.format(execTime)) + + return True + + # Methods for constructing the cut area + def _preProcessModel(self, JOB, obj): + PathLog.debug('_preProcessModel()') + + FACES = list() + VOIDS = list() + fShapes = list() + vShapes = list() + GRP = JOB.Model.Group + lenGRP = len(GRP) + + # Crete place holders for each base model in Job + for m in range(0, lenGRP): + FACES.append(False) + VOIDS.append(False) + fShapes.append(False) + vShapes.append(False) + + checkBase = False + if obj.Base: + if len(obj.Base) > 0: + checkBase = True + if obj.ScanType == 'Rotational': + checkBase = False + PathLog.warning(translate('PathSurface', + 'Face selection is unavailable for Rotational scans. Ignoring selected faces.')) + + # The user has selected subobjects from the base. Pre-Process each. + if checkBase: + PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') + + (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) + + # Cycle through each base model, processing faces for each + for m in range(0, lenGRP): + base = GRP[m] + (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) + fShapes[m] = mFS + vShapes[m] = mVS + self.profileShapes[m] = mPS + else: + PathLog.debug(' -No obj.Base data.') + for m in range(0, lenGRP): + self.modelSTLs[m] = True + + # Process each model base, as a whole, as needed + # PathLog.debug(' -Pre-processing all models in Job.') + for m in range(0, lenGRP): + if fShapes[m] is False: + PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) + if obj.BoundBox == 'BaseBoundBox': + base = GRP[m] + elif obj.BoundBox == 'Stock': + base = JOB.Stock + + pPEB = self._preProcessEntireBase(obj, base, m) + if pPEB is False: + PathLog.error(' -Failed to pre-process base as a whole.') + else: + (fcShp, prflShp) = pPEB + if fcShp is not False: + if fcShp is True: + PathLog.debug(' -fcShp is True.') + fShapes[m] = True + else: + fShapes[m] = [fcShp] + if prflShp is not False: + if fcShp is not False: + PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) + if vShapes[m] is not False: + PathLog.debug(' -Cutting void from base profile shape.') + adjPS = prflShp.cut(vShapes[m][0]) + self.profileShapes[m] = [adjPS] + else: + PathLog.debug(' -vShapes[m] is False.') + self.profileShapes[m] = [prflShp] + else: + PathLog.debug(' -Saving base profile shape.') + self.profileShapes[m] = [prflShp] + PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) + # Efor + + return (fShapes, vShapes) + + def _identifyFacesAndVoids(self, JOB, obj, F, V): + TUPS = list() + GRP = JOB.Model.Group + lenGRP = len(GRP) + + # Separate selected faces into (base, face) tuples and flag model(s) for STL creation + for (bs, SBS) in obj.Base: + for sb in SBS: + # Flag model for STL creation + mdlIdx = None + for m in range(0, lenGRP): + if bs is GRP[m]: + self.modelSTLs[m] = True + mdlIdx = m + break + TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) + + # Apply `AvoidXFaces` value + faceCnt = len(TUPS) + add = faceCnt - obj.AvoidLastX_Faces + for bst in range(0, faceCnt): + (m, base, sub) = TUPS[bst] + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + faceIdx = int(sub[4:]) - 1 + if bst < add: + if F[m] is False: + F[m] = list() + F[m].append((shape, faceIdx)) + else: + if V[m] is False: + V[m] = list() + V[m].append((shape, faceIdx)) + return (F, V) + + def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): + mFS = False + mVS = False + mPS = False + mIFS = list() + + if FACES[m] is not False: + isHole = False + if obj.HandleMultipleFeatures == 'Collectively': + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + outFCS = list() + + # Get collective envelope slice of selected faces + for (fcshp, fcIdx) in FACES[m]: + fNum = fcIdx + 1 + fsL.append(fcshp) + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + PathLog.debug('Attempting to get cross-section of collective faces.') + if len(outFCS) == 0: + PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) + cont = False + else: + cfsL = Part.makeCompound(outFCS) + + # Handle profile edges request + if cont is True and obj.ProfileEdges != 'None': + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(cfsL, ofstVal) + if psOfst is not False: + mPS = [psOfst] + if obj.ProfileEdges == 'Only': + mFS = True + cont = False + else: + PathLog.error(' -Failed to create profile geometry for selected faces.') + cont = False + + if cont: + if self.showDebugObjects is True: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') + T.Shape = cfsL + T.purgeTouched() + self.tempGroup.addObject(T) + + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOfstShp = self._extractFaceOffset(cfsL, ofstVal) + if faceOfstShp is False: + PathLog.error(' -Failed to create offset face.') + cont = False + + if cont: + lenIfL = len(ifL) + if obj.InternalFeaturesCut is False: + if lenIfL == 0: + PathLog.debug(' -No internal features saved.') + else: + if lenIfL == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + if self.showDebugObjects is True: + C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') + C.Shape = casL + C.purgeTouched() + self.tempGroup.addObject(C) + ofstVal = self._calculateOffsetValue(obj, isHole=True) + intOfstShp = self._extractFaceOffset(casL, ofstVal) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + mFS = [faceOfstShp] + # Eif + + elif obj.HandleMultipleFeatures == 'Individually': + for (fcshp, fcIdx) in FACES[m]: + cont = True + ifL = list() # avoid shape list + fNum = fcIdx + 1 + outerFace = False + + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + cont = False + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + cont = False + outerFace = False + else: + ((otrFace, raised), intWires) = gFW + outerFace = otrFace + if obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + if outerFace is not False: + PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) + + if obj.ProfileEdges != 'None': + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(outerFace, ofstVal) + if psOfst is not False: + if mPS is False: + mPS = list() + mPS.append(psOfst) + if obj.ProfileEdges == 'Only': + if mFS is False: + mFS = list() + mFS.append(True) + cont = False + else: + PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOfstShp = self._extractFaceOffset(outerFace, ofstVal) + + lenIfl = len(ifL) + if obj.InternalFeaturesCut is False and lenIfl > 0: + if lenIfl == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + + ofstVal = self._calculateOffsetValue(obj, isHole=True) + intOfstShp = self._extractFaceOffset(casL, ofstVal) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + if mFS is False: + mFS = list() + mFS.append(faceOfstShp) + # Eif + # Efor + # Eif + # Eif + + if len(mIFS) > 0: + if mVS is False: + mVS = list() + for ifs in mIFS: + mVS.append(ifs) + + if VOIDS[m] is not False: + PathLog.debug('Processing avoid faces.') + cont = True + isHole = False + outFCS = list() + intFEAT = list() + + for (fcshp, fcIdx) in VOIDS[m]: + fNum = fcIdx + 1 + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) + cont = False + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if obj.AvoidLastX_InternalFeatures is False: + if intWires is not False: + for (iFace, rsd) in intWires: + intFEAT.append(iFace) + + lenOtFcs = len(outFCS) + if lenOtFcs == 0: + cont = False + else: + if lenOtFcs == 1: + avoid = outFCS[0] + else: + avoid = Part.makeCompound(outFCS) + + if self.showDebugObjects is True: + PathLog.debug('*** tmpAvoidArea') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + + if cont: + if self.showDebugObjects is True: + PathLog.debug('*** tmpVoidCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) + avdOfstShp = self._extractFaceOffset(avoid, ofstVal) + if avdOfstShp is False: + PathLog.error('Failed to create collective offset avoid face.') + cont = False + + if cont: + avdShp = avdOfstShp + + if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: + if len(intFEAT) > 1: + ifc = Part.makeCompound(intFEAT) + else: + ifc = intFEAT[0] + ofstVal = self._calculateOffsetValue(obj, isHole=True) + ifOfstShp = self._extractFaceOffset(ifc, ofstVal) + if ifOfstShp is False: + PathLog.error('Failed to create collective offset avoid internal features.') + else: + avdShp = avdOfstShp.cut(ifOfstShp) + + if mVS is False: + mVS = list() + mVS.append(avdShp) + + + return (mFS, mVS, mPS) + + def _getFaceWires(self, base, fcshp, fcIdx): + outFace = False + INTFCS = list() + fNum = fcIdx + 1 + warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face') + + PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) + WIRES = self._extractWiresFromFace(base, fcshp) + if WIRES is False: + PathLog.error('Failed to extract wires from Face{}'.format(fNum)) + return False + + # Process remaining internal features, adding to FCS list + lenW = len(WIRES) + for w in range(0, lenW): + (wire, rsd) = WIRES[w] + PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) + if wire.isClosed() is False: + PathLog.debug(' -wire is not closed.') + else: + slc = self._flattenWireToFace(wire) + if slc is False: + PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) + else: + if w == 0: + outFace = (slc, rsd) + else: + # add to VOIDS so cutter avoids area. + PathLog.warning(warnFinDep + str(fNum) + '.') + INTFCS.append((slc, rsd)) + if len(INTFCS) == 0: + return (outFace, False) + else: + return (outFace, INTFCS) + + def _preProcessEntireBase(self, obj, base, m): + cont = True + isHole = False + prflShp = False + # Create envelope, extract cross-section and make offset co-planar shape + # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) + + try: + baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as ee: + PathLog.error(str(ee)) + shell = base.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as eee: + PathLog.error(str(eee)) + cont = False + + if cont: + csFaceShape = self._getShapeSlice(baseEnv) + if csFaceShape is False: + PathLog.debug('_getShapeSlice(baseEnv) failed') + csFaceShape = self._getCrossSection(baseEnv) + if csFaceShape is False: + PathLog.debug('_getCrossSection(baseEnv) failed') + csFaceShape = self._getSliceFromEnvelope(baseEnv) + if csFaceShape is False: + PathLog.error('Failed to slice baseEnv shape.') + cont = False + + if cont is True and obj.ProfileEdges != 'None': + PathLog.debug(' -Attempting profile geometry for model base.') + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(csFaceShape, ofstVal) + if psOfst is not False: + if obj.ProfileEdges == 'Only': + return (True, psOfst) + prflShp = psOfst + else: + PathLog.error(' -Failed to create profile geometry.') + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOffsetShape = self._extractFaceOffset(csFaceShape, ofstVal) + if faceOffsetShape is False: + PathLog.error('_extractFaceOffset() failed.') + else: + faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) + return (faceOffsetShape, prflShp) + return False + + def _extractWiresFromFace(self, base, fc): + '''_extractWiresFromFace(base, fc) ... + Attempts to return all closed wires within a parent face, including the outer most wire of the parent. + The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). + ''' + PathLog.debug('_extractWiresFromFace()') + + WIRES = list() + lenWrs = len(fc.Wires) + PathLog.debug(' -Wire count: {}'.format(lenWrs)) + + def index0(tup): + return tup[0] + + # Cycle through wires in face + for w in range(0, lenWrs): + PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) + wire = fc.Wires[w] + checkEdges = False + cont = True + + # Check for closed edges (circles, ellipses, etc...) + for E in wire.Edges: + if E.isClosed() is True: + checkEdges = True + break + + if checkEdges is True: + PathLog.debug(' -checkEdges is True') + for e in range(0, len(wire.Edges)): + edge = wire.Edges[e] + if edge.isClosed() is True and edge.Mass > 0.01: + PathLog.debug(' -Found closed edge') + raised = False + ip = self._isPocket(base, fc, edge) + if ip is False: + raised = True + ebb = edge.BoundBox + eArea = ebb.XLength * ebb.YLength + F = Part.Face(Part.Wire([edge])) + WIRES.append((eArea, F.Wires[0], raised)) + cont = False + + if cont: + PathLog.debug(' -cont is True') + # If only one wire and not checkEdges, return first wire + if lenWrs == 1: + return [(wire, False)] + + raised = False + wbb = wire.BoundBox + wArea = wbb.XLength * wbb.YLength + if w > 0: + ip = self._isPocket(base, fc, wire) + if ip is False: + raised = True + WIRES.append((wArea, Part.Wire(wire.Edges), raised)) + + nf = len(WIRES) + if nf > 0: + PathLog.debug(' -number of wires found is {}'.format(nf)) + if nf == 1: + (area, W, raised) = WIRES[0] + return [(W, raised)] + else: + sortedWIRES = sorted(WIRES, key=index0, reverse=True) + return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + + return False + + def _calculateOffsetValue(self, obj, isHole, isVoid=False): + '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. + Calculate the offset for the Path.Area() function.''' + JOB = PathUtils.findParentJob(obj) + tolrnc = JOB.GeometryTolerance.Value + + if isVoid is False: + if isHole is True: + offset = -1 * obj.InternalFeaturesAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + else: + offset = -1 * obj.BoundaryAdjustment.Value + if obj.BoundaryEnforcement is True: + offset += self.radius + (tolrnc / 10.0) + else: + offset -= self.radius + (tolrnc / 10.0) + offset = 0.0 - offset + else: + offset = -1 * obj.BoundaryAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + + return offset + + def _extractFaceOffset(self, fcShape, offset): + '''_extractFaceOffset(fcShape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('_extractFaceOffset()') + + if fcShape.BoundBox.ZMin != 0.0: + fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 + area.add(fcShape) + area.setParams(**areaParams) # set parameters + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + ofstFace = Part.makeCompound(W) + + return ofstFace # offsetShape + + def _isPocket(self, b, f, w): + '''_isPocket(b, f, w)... + Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. + Returns True if pocket, False if raised protrusion.''' + e = w.Edges[0] + for fi in range(0, len(b.Shape.Faces)): + face = b.Shape.Faces[fi] + for ei in range(0, len(face.Edges)): + edge = face.Edges[ei] + if e.isSame(edge) is True: + if f is face: + # Alternative: run loop to see if all edges are same + pass # same source face, look for another + else: + if face.CenterOfMass.z < f.CenterOfMass.z: + return True + return False + + def _flattenWireToFace(self, wire): + PathLog.debug('_flattenWireToFace()') + if wire.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + + # If wire is planar horizontal, convert to a face and return + if wire.BoundBox.ZLength == 0.0: + slc = Part.Face(wire) + return slc + + # Attempt to create a new wire for manipulation, if not, use original + newWire = Part.Wire(wire.Edges) + if newWire.isClosed() is True: + nWire = newWire + else: + PathLog.debug(' -newWire.isClosed() is False') + nWire = wire + + # Attempt extrusion, and then try a manual slice and then cross-section + ext = self._getExtrudedShape(nWire) + if ext is False: + PathLog.debug('_getExtrudedShape() failed') + else: + slc = self._getShapeSlice(ext) + if slc is not False: + return slc + cs = self._getCrossSection(ext, True) + if cs is not False: + return cs + + # Attempt creating an envelope, and then try a manual slice and then cross-section + env = self._getShapeEnvelope(nWire) + if env is False: + PathLog.debug('_getShapeEnvelope() failed') + else: + slc = self._getShapeSlice(env) + if slc is not False: + return slc + cs = self._getCrossSection(env, True) + if cs is not False: + return cs + + # Attempt creating a projection + slc = self._getProjectedFace(nWire) + if slc is False: + PathLog.debug('_getProjectedFace() failed') + else: + return slc + + return False + + def _getExtrudedShape(self, wire): + PathLog.debug('_getExtrudedShape()') + wBB = wire.BoundBox + extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 + + try: + # slower, but renders collective faces correctly. Method 5 in TESTING + shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + except Exception as ee: + PathLog.error(' -extrude wire failed: \n{}'.format(ee)) + return False + + SHP = Part.makeSolid(shell) + return SHP + + def _getShapeSlice(self, shape): + PathLog.debug('_getShapeSlice()') + + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + xmin = bb.XMin - 1.0 + xmax = bb.XMax + 1.0 + ymin = bb.YMin - 1.0 + ymax = bb.YMax + 1.0 + p1 = FreeCAD.Vector(xmin, ymin, mid) + p2 = FreeCAD.Vector(xmax, ymin, mid) + p3 = FreeCAD.Vector(xmax, ymax, mid) + p4 = FreeCAD.Vector(xmin, ymax, mid) + + e1 = Part.makeLine(p1, p2) + e2 = Part.makeLine(p2, p3) + e3 = Part.makeLine(p3, p4) + e4 = Part.makeLine(p4, p1) + face = Part.Face(Part.Wire([e1, e2, e3, e4])) + fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area + sArea = shape.BoundBox.XLength * shape.BoundBox.YLength + midArea = (fArea + sArea) / 2.0 + + slcShp = shape.common(face) + slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength + + if slcArea < midArea: + for W in slcShp.Wires: + if W.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + if len(slcShp.Wires) == 1: + wire = slcShp.Wires[0] + slc = Part.Face(wire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + else: + fL = list() + for W in slcShp.Wires: + slc = Part.Face(W) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + fL.append(slc) + comp = Part.makeCompound(fL) + if self.showDebugObjects is True: + PathLog.debug('*** tmpSliceCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') + P.Shape = comp + P.purgeTouched() + self.tempGroup.addObject(P) + return comp + + PathLog.debug(' -slcArea !< midArea') + PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) + return False + + def _getProjectedFace(self, wire): + import Draft + PathLog.debug('_getProjectedFace()') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') + F.Shape = wire + F.purgeTouched() + self.tempGroup.addObject(F) + try: + prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) + prj.recompute() + prj.purgeTouched() + self.tempGroup.addObject(prj) + except Exception as ee: + PathLog.error(str(ee)) + return False + else: + pWire = Part.Wire(prj.Shape.Edges) + if pWire.isClosed() is False: + # PathLog.debug(' -pWire.isClosed() is False') + return False + slc = Part.Face(pWire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + + def _getCrossSection(self, shape, withExtrude=False): + PathLog.debug('_getCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) + csWire = comp.Wires[0] + if csWire.isClosed() is False: + PathLog.debug(' -comp.Wires[0] is not closed') + return False + if withExtrude is True: + ext = self._getExtrudedShape(csWire) + CS = self._getShapeSlice(ext) + if CS is False: + return False + else: + CS = Part.Face(csWire) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + + def _getShapeEnvelope(self, shape): + PathLog.debug('_getShapeEnvelope()') + + wBB = shape.BoundBox + extFwd = wBB.ZLength + 10.0 + minz = wBB.ZMin + maxz = wBB.ZMin + extFwd + stpDwn = (maxz - minz) / 4.0 + dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) + + try: + env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape + except Exception as ee: + PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) + return False + else: + return env + + def _getSliceFromEnvelope(self, env): + PathLog.debug('_getSliceFromEnvelope()') + eBB = env.BoundBox + extFwd = eBB.ZLength + 10.0 + maxz = eBB.ZMin + extFwd + + emax = math.floor(maxz - 1.0) + E = list() + for e in range(0, len(env.Edges)): + emin = env.Edges[e].BoundBox.ZMin + if emin > emax: + E.append(env.Edges[e]) + tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) + tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) + + return tf + + def _prepareModelSTLs(self, JOB, obj): + PathLog.debug('_prepareModelSTLs()') + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + + # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") + if self.modelTypes[m] == 'M': + #TODO: test if this works + facets = M.Mesh.Facets.Points + else: + facets = Part.getFacets(M.Shape) + + if self.modelSTLs[m] is True: + stl = ocl.STLSurf() + + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + self.modelSTLs[m] = stl + return + + def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): + '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... + Creates and OCL.stl object with combined data with waste stock, + model, and avoided faces. Travel lines can be checked against this + STL object to determine minimum travel height to clear stock and model.''' + PathLog.debug('_makeSafeSTL()') + + fuseShapes = list() + Mdl = JOB.Model.Group[mdlIdx] + mBB = Mdl.Shape.BoundBox + sBB = JOB.Stock.Shape.BoundBox + + # add Model shape to safeSTL shape + fuseShapes.append(Mdl.Shape) + + if obj.BoundBox == 'BaseBoundBox': + cont = False + extFwd = (sBB.ZLength) + zmin = mBB.ZMin + zmax = mBB.ZMin + extFwd + stpDwn = (zmax - zmin) / 4.0 + dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) + + try: + envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as ee: + PathLog.error(str(ee)) + shell = Mdl.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as eee: + PathLog.error(str(eee)) + + if cont: + stckWst = JOB.Stock.Shape.cut(envBB) + if obj.BoundaryAdjustment > 0.0: + cmpndFS = Part.makeCompound(faceShapes) + baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape + adjStckWst = stckWst.cut(baBB) + else: + adjStckWst = stckWst + fuseShapes.append(adjStckWst) + else: + PathLog.warning('Path transitions might not avoid the model. Verify paths.') + else: + # If boundbox is Job.Stock, add hidden pad under stock as base plate + toolDiam = self.cutter.getDiameter() + zMin = JOB.Stock.Shape.BoundBox.ZMin + xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam + yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam + bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) + bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) + bH = 1.0 + crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) + B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) + fuseShapes.append(B) + + if voidShapes is not False: + voidComp = Part.makeCompound(voidShapes) + voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape + fuseShapes.append(voidEnv) + + fused = Part.makeCompound(fuseShapes) + + if self.showDebugObjects is True: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') + T.Shape = fused + T.purgeTouched() + self.tempGroup.addObject(T) + + facets = Part.getFacets(fused) + + stl = ocl.STLSurf() + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + + self.safeSTLs[mdlIdx] = stl + + def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): + '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... + This method applies any avoided faces or regions to the selected faces. + It then calls the correct scan method depending on the ScanType property.''' + PathLog.debug('_processCutAreas()') + + final = list() + + # Process faces Collectively or Individually + if obj.HandleMultipleFeatures == 'Collectively': + if FCS is True: + COMP = False + else: + ADD = Part.makeCompound(FCS) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + if obj.ScanType == 'Planar': + final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, 0)) + elif obj.ScanType == 'Rotational': + final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP)) + + elif obj.HandleMultipleFeatures == 'Individually': + for fsi in range(0, len(FCS)): + fShp = FCS[fsi] + # self.deleteOpVariables(all=False) + self.resetOpVariables(all=False) + + if fShp is True: + COMP = False + else: + ADD = Part.makeCompound([fShp]) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + if obj.ScanType == 'Planar': + final.extend(self._processPlanarOp(JOB, obj, mdlIdx, COMP, fsi)) + elif obj.ScanType == 'Rotational': + final.extend(self._processRotationalOp(JOB, obj, mdlIdx, COMP)) + COMP = None + # Eif + + return final + + # Methods for creating path geometry + def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi): + '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)... + This method compiles the main components for the procedural portion of a planar operation (non-rotational). + It creates the OCL PathDropCutter objects: model and safeTravel. + It makes the necessary facial geometries for the actual cut area. + It calls the correct Single or Multi-pass method as needed. + It returns the gcode for the operation. ''' + PathLog.debug('_processPlanarOp()') + final = list() + SCANDATA = list() + + def getTransition(two): + first = two[0][0][0] # [step][item][point] + safe = obj.SafeHeight.Value + 0.1 + trans = [[FreeCAD.Vector(first.x, first.y, safe)]] + return trans + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [obj.FinalDepth.Value] + elif obj.LayerMode == 'Multi-pass': + depthparams = [i for i in self.depthParams] + lenDP = len(depthparams) + + # Prepare PathDropCutter objects with STL data + pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value) + safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], + depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) + + profScan = list() + if obj.ProfileEdges != 'None': + prflShp = self.profileShapes[mdlIdx][fsi] + if prflShp is False: + PathLog.error('No profile shape is False.') + return list() + if self.showDebugObjects is True: + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNewProfileShape') + P.Shape = prflShp + P.purgeTouched() + self.tempGroup.addObject(P) + # get offset path geometry and perform OCL scan with that geometry + pathOffsetGeom = self._planarMakeProfileGeom(obj, prflShp) + if pathOffsetGeom is False: + PathLog.error('No profile geometry returned.') + return list() + profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, offsetPoints=True)] + + geoScan = list() + if obj.ProfileEdges != 'Only': + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutArea') + F.Shape = cmpdShp + F.purgeTouched() + self.tempGroup.addObject(F) + # get internal path geometry and perform OCL scan with that geometry + pathGeom = self._planarMakePathGeom(obj, cmpdShp) + if pathGeom is False: + PathLog.error('No path geometry returned.') + return list() + geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False) + + if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last'] + SCANDATA.extend(profScan) + if obj.ProfileEdges == 'None': + SCANDATA.extend(geoScan) + if obj.ProfileEdges == 'First': + profScan.append(getTransition(geoScan)) + SCANDATA.extend(profScan) + SCANDATA.extend(geoScan) + if obj.ProfileEdges == 'Last': + SCANDATA.extend(geoScan) + SCANDATA.extend(profScan) + + if len(SCANDATA) == 0: + PathLog.error('No scan data to convert to Gcode.') + return list() + + # Apply depth offset + if obj.DepthOffset.Value != 0.0: + self._planarApplyDepthOffset(SCANDATA, obj.DepthOffset.Value) + + # If cut pattern is `Circular`, there are zero(almost zero) straight lines to optimize + # Store initial `OptimizeLinearPaths` value for later restoration + self.preOLP = obj.OptimizeLinearPaths + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = False + + # Process OCL scan data + if obj.LayerMode == 'Single-pass': + final.extend(self._planarDropCutSingle(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) + elif obj.LayerMode == 'Multi-pass': + final.extend(self._planarDropCutMulti(JOB, obj, pdc, safePDC, depthparams, SCANDATA)) + + # If cut pattern is `Circular`, restore initial OLP value + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = self.preOLP + + # Raise to safe height between individual faces. + if obj.HandleMultipleFeatures == 'Individually': + final.insert(0, Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + + return final + + def _planarMakePathGeom(self, obj, faceShp): + '''_planarMakePathGeom(obj, faceShp)... + Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. + The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' + PathLog.debug('_planarMakePathGeom()') + GeoSet = list() + + # Apply drop cutter extra offset and set the max and min XY area of the operation + xmin = faceShp.BoundBox.XMin + xmax = faceShp.BoundBox.XMax + ymin = faceShp.BoundBox.YMin + ymax = faceShp.BoundBox.YMax + zmin = faceShp.BoundBox.ZMin + zmax = faceShp.BoundBox.ZMax + + # Compute weighted center of mass of all faces combined + fCnt = 0 + totArea = 0.0 + zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) + for F in faceShp.Faces: + comF = F.CenterOfMass + areaF = F.Area + totArea += areaF + fCnt += 1 + zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) + if fCnt == 0: + PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) + zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) + else: + avgArea = totArea / fCnt + zeroCOM.multiply(1 / fCnt) + zeroCOM.multiply(1 / avgArea) + COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + + # get X, Y, Z spans; Compute center of rotation + deltaX = abs(xmax-xmin) + deltaY = abs(ymax-ymin) + deltaC = math.sqrt(deltaX**2 + deltaY**2) + lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end + halfLL = math.ceil(lineLen / 2.0) + cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen + halfPasses = math.ceil(cutPasses / 2.0) + bbC = faceShp.BoundBox.Center + + # Generate the line/circle sets to be intersected with the cut-face-area + if obj.CutPattern in ['ZigZag', 'Line']: + centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model + cAng = math.atan(deltaX / deltaY) # BoundaryBox angle + + # Determine end points and create top lines + x1 = centRot.x - halfLL + x2 = centRot.x + halfLL + diag = None + if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: + diag = deltaY + elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: + diag = deltaX + else: + perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC + diag = perpDist + y1 = centRot.y + diag + # y2 = y1 + + # Create end points for set of lines to intersect with cross-section face + pntTuples = list() + for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): + x1 = centRot.x - halfLL + x2 = centRot.x + halfLL + y1 = centRot.y + (lc * self.cutOut) + # y2 = y1 + p1 = FreeCAD.Vector(x1, y1, 0.0) + p2 = FreeCAD.Vector(x2, y1, 0.0) + pntTuples.append( (p1, p2) ) + + # Convert end points to lines + for (p1, p2) in pntTuples: + line = Part.makeLine(p1, p2) + GeoSet.append(line) + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + zTgt = faceShp.BoundBox.ZMin + axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) + cntr = FreeCAD.Placement() + cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) + + if obj.CircularCenterAt == 'CenterOfMass': + cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass + elif obj.CircularCenterAt == 'CenterOfBoundBox': + cent = faceShp.BoundBox.Center + cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) + elif obj.CircularCenterAt == 'XminYmin': + cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) + elif obj.CircularCenterAt == 'Custom': + newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) + cntr.Base = newCent + + # recalculate cutPasses value, if need be + radialPasses = halfPasses + if obj.CircularCenterAt != 'CenterOfBoundBox': + # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center + EBB = faceShp.BoundBox + CORNERS = [ + FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), + FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), + ] + dMax = 0.0 + for c in range(0, 4): + dist = CORNERS[c].sub(cntr.Base).Length + if dist > dMax: + dMax = dist + lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end + radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen + + # Update COM point and current CircularCenter + if obj.CircularCenterAt != 'Custom': + obj.CircularCenterCustom = cntr.Base + + minRad = self.cutter.getDiameter() * 0.45 + siX3 = 3 * obj.SampleInterval.Value + minRadSI = (siX3 / 2.0) / math.pi + if minRad < minRadSI: + minRad = minRadSI + + # Make small center circle to start pattern + if obj.StepOver > 50: + circle = Part.makeCircle(minRad, cntr.Base) + GeoSet.append(circle) + + for lc in range(1, radialPasses + 1): + rad = (lc * self.cutOut) + if rad >= minRad: + circle = Part.makeCircle(rad, cntr.Base) + GeoSet.append(circle) + # Efor + COM = cntr.Base + # Eif + + if obj.CutPatternReversed is True: + GeoSet.reverse() + + if faceShp.BoundBox.ZMin != 0.0: + faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) + + # Create compound object to bind all lines in Lineset + geomShape = Part.makeCompound(GeoSet) + + # Position and rotate the Line and ZigZag geometry + if obj.CutPattern in ['Line', 'ZigZag']: + if obj.CutPatternAngle != 0.0: + geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) + geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) + + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') + F.Shape = geomShape + F.purgeTouched() + self.tempGroup.addObject(F) + + # Identify intersection of cross-section face and lineset + cmnShape = faceShp.common(geomShape) + + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') + F.Shape = cmnShape + F.purgeTouched() + self.tempGroup.addObject(F) + + self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) + return cmnShape + + def _planarMakeProfileGeom(self, obj, subShp): + PathLog.debug('_planarMakeProfileGeom()') + + offsetLists = list() + dist = obj.SampleInterval.Value / 5.0 + # defl = obj.SampleInterval.Value / 5.0 + + # Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939 + for fc in subShp.Faces: + # Reverse order of wires in each face - inside to outside + for w in range(len(fc.Wires) - 1, -1, -1): + W = fc.Wires[w] + PNTS = W.discretize(Distance=dist) + # PNTS = W.discretize(Deflection=defl) + if self.CutClimb is True: + PNTS.reverse() + offsetLists.append(PNTS) + + return offsetLists + + def _planarPerformOclScan(self, obj, pdc, pathGeom, offsetPoints=False): + '''_planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False)... + Switching function for calling the appropriate path-geometry to OCL points conversion function + for the various cut patterns.''' + PathLog.debug('_planarPerformOclScan()') + SCANS = list() + + if offsetPoints is True: + PNTSET = self._pathGeomToOffsetPointSet(obj, pathGeom) + for D in PNTSET: + stpOvr = list() + ofst = list() + for I in D: + if I == 'BRK': + stpOvr.append(ofst) + stpOvr.append(I) + ofst = list() + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = I + ofst.extend(self._planarDropCutScan(pdc, A, B)) + if len(ofst) > 0: + stpOvr.append(ofst) + SCANS.extend(stpOvr) + elif obj.CutPattern == 'Line': + stpOvr = list() + PNTSET = self._pathGeomToLinesPointSet(obj, pathGeom) + for D in PNTSET: + for I in D: + if I == 'BRK': + stpOvr.append(I) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = I + stpOvr.append(self._planarDropCutScan(pdc, A, B)) + SCANS.append(stpOvr) + stpOvr = list() + elif obj.CutPattern == 'ZigZag': + stpOvr = list() + PNTSET = self._pathGeomToZigzagPointSet(obj, pathGeom) + for (dirFlg, LNS) in PNTSET: + for SEG in LNS: + if SEG == 'BRK': + stpOvr.append(SEG) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = SEG + stpOvr.append(self._planarDropCutScan(pdc, A, B)) + SCANS.append(stpOvr) + stpOvr = list() + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + # PNTSET is list, by stepover. + # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) + PNTSET = self._pathGeomToArcPointSet(obj, pathGeom) + + for so in range(0, len(PNTSET)): + stpOvr = list() + erFlg = False + (aTyp, dirFlg, ARCS) = PNTSET[so] + + if dirFlg == 1: # 1 + cMode = True + else: + cMode = False + + for a in range(0, len(ARCS)): + Arc = ARCS[a] + if Arc == 'BRK': + stpOvr.append('BRK') + else: + scan = self._planarCircularDropCutScan(pdc, Arc, cMode) + if scan is False: + erFlg = True + else: + if aTyp == 'L': + scan.append(FreeCAD.Vector(scan[0].x, scan[0].y, scan[0].z)) + stpOvr.append(scan) + if erFlg is False: + SCANS.append(stpOvr) + + return SCANS + + def _pathGeomToOffsetPointSet(self, obj, compGeoShp): + '''_pathGeomToOffsetPointSet(obj, compGeoShp)... + Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.''' + PathLog.debug('_pathGeomToOffsetPointSet()') + + LINES = list() + optimize = obj.OptimizeLinearPaths + ofstCnt = len(compGeoShp) + + # Cycle through offeset loops + for ei in range(0, ofstCnt): + OS = compGeoShp[ei] + lenOS = len(OS) + + if ei > 0: + LINES.append('BRK') + + fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z) + OS.append(fp) + + # Cycle through points in each loop + prev = OS[0] + pnt = OS[1] + for v in range(1, lenOS): + nxt = OS[v + 1] + if optimize is True: + # iPOL = prev.isOnLineSegment(nxt, pnt) + iPOL = pnt.isOnLineSegment(prev, nxt) + if iPOL is True: + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + if iPOL is True: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + # Efor + + return [LINES] + + def _pathGeomToLinesPointSet(self, obj, compGeoShp): + '''_pathGeomToLinesPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' + PathLog.debug('_pathGeomToLinesPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + chkGap = False + lnCnt = 0 + ec = len(compGeoShp.Edges) + cutClimb = self.CutClimb + toolDiam = 2.0 * self.radius + cpa = obj.CutPatternAngle + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if cutClimb is True: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + else: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + inLine.append(tup) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + + for ei in range(1, ec): + chkGap = False + edg = compGeoShp.Edges[ei] # Get edge for vertexes + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 + + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + else: + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset collinear container + if cutClimb is True: + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + else: + sp = ep + + if cutClimb is True: + tup = (v2, v1) + if chkGap is True: + gap = abs(toolDiam - lst.sub(ep).Length) + lst = cp + else: + tup = (v1, v2) + if chkGap is True: + gap = abs(toolDiam - lst.sub(cp).Length) + lst = ep + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + tup = (vA, tup[1]) + self.closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + + # Handle last inLine set, reversing it. + if obj.CutPatternReversed is True: + if cpa != 0.0 and cpa % 90.0 == 0.0: + F = LINES.pop(0) + rev = list() + for iL in F: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + rev.reverse() + LINES.insert(0, rev) + + isEven = lnCnt % 2 + if isEven == 0: + PathLog.debug('Line count is ODD.') + else: + PathLog.debug('Line count is even.') + + return LINES + + def _pathGeomToZigzagPointSet(self, obj, compGeoShp): + '''_pathGeomToZigzagPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings + with a ZigZag directional indicator included for each collinear group.''' + PathLog.debug('_pathGeomToZigzagPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + chkGap = False + ec = len(compGeoShp.Edges) + toolDiam = 2.0 * self.radius + + if self.CutClimb is True: + dirFlg = -1 + else: + dirFlg = 1 + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if dirFlg == 1: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + else: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point + inLine.append(tup) + + for ei in range(1, ec): + edg = compGeoShp.Edges[ei] + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) + + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + gap = abs(toolDiam - lst.sub(cp).Length) + else: + chkGap = False + if dirFlg == -1: + inLine.reverse() + LINES.append((dirFlg, inLine)) + lnCnt += 1 + dirFlg = -1 * dirFlg # Change zig to zag + inLine = list() # reset collinear container + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + + lst = ep + if dirFlg == 1: + tup = (v1, v2) + else: + tup = (v2, v1) + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + if dirFlg == 1: + tup = (vA, tup[1]) + else: + #tup = (vA, tup[1]) + #tup = (tup[1], vA) + tup = (tup[0], vB) + self.closedGap = True + else: + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + + # Fix directional issue with LAST line when line count is even + isEven = lnCnt % 2 + if isEven == 0: # Changed to != with 90 degree CutPatternAngle + PathLog.debug('Line count is even.') + else: + PathLog.debug('Line count is ODD.') + dirFlg = -1 * dirFlg + if obj.CutPatternReversed is False: + if self.CutClimb is True: + dirFlg = -1 * dirFlg + + if obj.CutPatternReversed is True: + dirFlg = -1 * dirFlg + + # Handle last inLine list + if dirFlg == 1: + rev = list() + for iL in inLine: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + + if obj.CutPatternReversed is False: + rev.reverse() + else: + rev2 = list() + for iL in rev: + if iL == 'BRK': + rev2.append(iL) + else: + (p1, p2) = iL + rev2.append((p2, p1)) + rev2.reverse() + rev = rev2 + + LINES.append((dirFlg, rev)) + else: + LINES.append((dirFlg, inLine)) + + return LINES + + def _pathGeomToArcPointSet(self, obj, compGeoShp): + '''_pathGeomToArcPointSet(obj, compGeoShp)... + Convert a compound set of arcs/circles to a set of directionally-oriented arc end points + and the corresponding center point.''' + # Extract intersection line segments for return value as list() + PathLog.debug('_pathGeomToArcPointSet()') + ARCS = list() + stpOvrEI = list() + segEI = list() + isSame = False + sameRad = None + COM = self.tmpCOM + toolDiam = 2.0 * self.radius + ec = len(compGeoShp.Edges) + + def gapDist(sp, ep): + X = (ep[0] - sp[0])**2 + Y = (ep[1] - sp[1])**2 + # Z = (ep[2] - sp[2])**2 + # return math.sqrt(X + Y + Z) + return math.sqrt(X + Y) # the 'z' value is zero in both points + + # Separate arc data into Loops and Arcs + for ei in range(0, ec): + edg = compGeoShp.Edges[ei] + if edg.Closed is True: + stpOvrEI.append(('L', ei, False)) + else: + if isSame is False: + segEI.append(ei) + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + else: + # Check if arc is co-radial to current SEGS + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + if abs(sameRad - pnt.sub(COM).Length) > 0.00001: + isSame = False + + if isSame is True: + segEI.append(ei) + else: + # Move co-radial arc segments + stpOvrEI.append(['A', segEI, False]) + # Start new list of arc segments + segEI = [ei] + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + # Process trailing `segEI` data, if available + if isSame is True: + stpOvrEI.append(['A', segEI, False]) + + # Identify adjacent arcs with y=0 start/end points that connect + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'A': + startOnAxis = list() + endOnAxis = list() + EI = SO[1] # list of corresponding compGeoShp.Edges indexes + + # Identify startOnAxis and endOnAxis arcs + for i in range(0, len(EI)): + ei = EI[i] # edge index + E = compGeoShp.Edges[ei] # edge object + if abs(COM.y - E.Vertexes[0].Y) < 0.00001: + startOnAxis.append((i, ei, E.Vertexes[0])) + elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: + endOnAxis.append((i, ei, E.Vertexes[1])) + + # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected + lenSOA = len(startOnAxis) + lenEOA = len(endOnAxis) + if lenSOA > 0 and lenEOA > 0: + for soa in range(0, lenSOA): + (iS, eiS, vS) = startOnAxis[soa] + for eoa in range(0, len(endOnAxis)): + (iE, eiE, vE) = endOnAxis[eoa] + dist = vE.X - vS.X + if abs(dist) < 0.00001: # They connect on axis at same radius + SO[2] = (eiE, eiS) + break + elif dist > 0: + break # stop searching + # Eif + # Eif + # Efor + + # Construct arc data tuples for OCL + dirFlg = 1 + # cutPat = obj.CutPattern + if self.CutClimb is False: # True yields Climb when set to Conventional + dirFlg = -1 + + # Cycle through stepOver data + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'L': # L = Loop/Ring/Circle + lei = SO[1] # loop Edges index + v1 = compGeoShp.Edges[lei].Vertexes[0] + + space = obj.SampleInterval.Value / 2.0 + + p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) + sp = (v1.X, v1.Y, 0.0) + rad = p1.sub(COM).Length + spcRadRatio = space/rad + if spcRadRatio < 1.0: + tolrncAng = math.asin(spcRadRatio) + else: + tolrncAng = 0.999998 * math.pi + X = COM.x + (rad * math.cos(tolrncAng)) + Y = v1.Y - space # rad * math.sin(tolrncAng) + + sp = (v1.X, v1.Y, 0.0) + ep = (X, Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + ARCS.append(('L', dirFlg, [arc])) + else: # SO[0] == 'A' A = Arc + PRTS = list() + EI = SO[1] # list of corresponding Edges indexes + CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) + chkGap = False + lst = None + + if CONN is not False: + (iE, iS) = CONN + v1 = compGeoShp.Edges[iE].Vertexes[0] + v2 = compGeoShp.Edges[iS].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + lst = sp + PRTS.append(arc) + # Pop connected edge index values from arc segments index list + iEi = EI.index(iE) + iSi = EI.index(iS) + if iEi > iSi: + EI.pop(iEi) + EI.pop(iSi) + else: + EI.pop(iSi) + EI.pop(iEi) + if len(EI) > 0: + PRTS.append('BRK') + chkGap = True + cnt = 0 + for ei in EI: + if cnt > 0: + PRTS.append('BRK') + chkGap = True + v1 = compGeoShp.Edges[ei].Vertexes[0] + v2 = compGeoShp.Edges[ei].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) + lst = sp + if chkGap is True: + if gap < obj.GapThreshold.Value: + PRTS.pop() # pop off 'BRK' marker + (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current + arc = (vA, arc[1], vC) + self.closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + PRTS.append(arc) + cnt += 1 + + if dirFlg == -1: + PRTS.reverse() + + ARCS.append(('A', dirFlg, PRTS)) + # Eif + if obj.CutPattern == 'CircularZigZag': + dirFlg = -1 * dirFlg + # Efor + + return ARCS + + def _planarDropCutScan(self, pdc, A, B): + #PNTS = list() + (x1, y1) = A + (x2, y2) = B + path = ocl.Path() # create an empty path object + p1 = ocl.Point(x1, y1, 0) # start-point of line + p2 = ocl.Point(x2, y2, 0) # end-point of line + lo = ocl.Line(p1, p2) # line-object + path.append(lo) # add the line to the path + pdc.setPath(path) + pdc.run() # run dropcutter algorithm on path + CLP = pdc.getCLPoints() + PNTS = [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP] + return PNTS # pdc.getCLPoints() + + def _planarCircularDropCutScan(self, pdc, Arc, cMode): + PNTS = list() + path = ocl.Path() # create an empty path object + (sp, ep, cp) = Arc + + # process list of segment tuples (vect, vect) + p1 = ocl.Point(sp[0], sp[1], 0) # start point of arc + p2 = ocl.Point(ep[0], ep[1], 0) # end point of arc + C = ocl.Point(cp[0], cp[1], 0) # center point of arc + ao = ocl.Arc(p1, p2, C, cMode) # arc object + path.append(ao) # add the arc to the path + pdc.setPath(path) + pdc.run() # run dropcutter algorithm on path + CLP = pdc.getCLPoints() + + # Convert OCL object data to FreeCAD vectors + for p in CLP: + PNTS.append(FreeCAD.Vector(p.x, p.y, p.z)) + + return PNTS + + # Main planar scan functions + def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): + PathLog.debug('_planarDropCutSingle()') + + GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] + tolrnc = JOB.GeometryTolerance.Value + lenSCANDATA = len(SCANDATA) + gDIR = ['G3', 'G2'] + + if self.CutClimb is True: + gDIR = ['G2', 'G3'] + + # Set `ProfileEdges` specific trigger indexes + peIdx = lenSCANDATA # off by default + if obj.ProfileEdges == 'Only': + peIdx = -1 + elif obj.ProfileEdges == 'First': + peIdx = 0 + elif obj.ProfileEdges == 'Last': + peIdx = lenSCANDATA - 1 + + # Send cutter to x,y position of first point on first line + first = SCANDATA[0][0][0] # [step][item][point] + GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + + # Cycle through step-over sections (line segments or arcs) + odd = True + lstStpEnd = None + for so in range(0, lenSCANDATA): + cmds = list() + PRTS = SCANDATA[so] + lenPRTS = len(PRTS) + first = PRTS[0][0] # first point of arc/line stepover group + start = PRTS[0][0] # will change with each line/arc segment + last = None + cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) + + if so > 0: + if obj.CutPattern == 'CircularZigZag': + if odd is True: + odd = False + else: + odd = True + minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL + # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) + cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) + + # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization + if so == peIdx or peIdx == -1: + obj.OptimizeLinearPaths = self.preOLP + + # Cycle through current step-over parts + for i in range(0, lenPRTS): + prt = PRTS[i] + lenPrt = len(prt) + if prt == 'BRK': + nxtStart = PRTS[i + 1][0] + minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL + cmds.append(Path.Command('N (Break)', {})) + cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) + else: + cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) + start = prt[0] + last = prt[lenPrt - 1] + if so == peIdx or peIdx == -1: + cmds.extend(self._planarSinglepassProcess(obj, prt)) + elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2: + (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) + if rtnVal is True: + cmds.extend(gcode) + else: + cmds.extend(self._planarSinglepassProcess(obj, prt)) + else: + cmds.extend(self._planarSinglepassProcess(obj, prt)) + cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) + GCODE.extend(cmds) # save line commands + lstStpEnd = last + + # Return `OptimizeLinearPaths` to disabled + if so == peIdx or peIdx == -1: + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = False + # Efor + + return GCODE + + def _planarSinglepassProcess(self, obj, PNTS): + output = [] + optimize = obj.OptimizeLinearPaths + lenPNTS = len(PNTS) + lop = None + onLine = False + + # Initialize first three points + nxt = None + pnt = PNTS[0] + prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847) + + # Add temp end point + PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425)) + + # Begin processing ocl points list into gcode + for i in range(0, lenPNTS): + # Calculate next point for consideration with current point + nxt = PNTS[i + 1] + + # Process point + if optimize: + if pnt.isOnLineSegment(prev, nxt): + onLine = True + else: + onLine = False + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + else: + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + + # Rotate point data + if onLine is False: + prev = pnt + pnt = nxt + # Efor + + PNTS.pop() # Remove temp end point + + return output + + def _planarDropCutMulti(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): + GCODE = [Path.Command('N (Beginning of Multi-pass layers.)', {})] + tolrnc = JOB.GeometryTolerance.Value + lenDP = len(depthparams) + prevDepth = depthparams[0] + lenSCANDATA = len(SCANDATA) + gDIR = ['G3', 'G2'] + + if self.CutClimb is True: + gDIR = ['G2', 'G3'] + + # Set `ProfileEdges` specific trigger indexes + peIdx = lenSCANDATA # off by default + if obj.ProfileEdges == 'Only': + peIdx = -1 + elif obj.ProfileEdges == 'First': + peIdx = 0 + elif obj.ProfileEdges == 'Last': + peIdx = lenSCANDATA - 1 + + # Process each layer in depthparams + prvLyrFirst = None + prvLyrLast = None + lastPrvStpLast = None + for lyr in range(0, lenDP): + odd = True # ZigZag directional switch + lyrHasCmds = False + actvSteps = 0 + LYR = list() + prvStpFirst = None + if lyr > 0: + if prvStpLast is not None: + lastPrvStpLast = prvStpLast + prvStpLast = None + lyrDep = depthparams[lyr] + PathLog.debug('Multi-pass lyrDep: {}'.format(round(lyrDep, 4))) + + # Cycle through step-over sections (line segments or arcs) + for so in range(0, len(SCANDATA)): + SO = SCANDATA[so] + lenSO = len(SO) + + # Pre-process step-over parts for layer depth and holds + ADJPRTS = list() + LMAX = list() + soHasPnts = False + brkFlg = False + for i in range(0, lenSO): + prt = SO[i] + lenPrt = len(prt) + if prt == 'BRK': + if brkFlg is True: + ADJPRTS.append(prt) + LMAX.append(prt) + brkFlg = False + else: + (PTS, lMax) = self._planarMultipassPreProcess(obj, prt, prevDepth, lyrDep) + if len(PTS) > 0: + ADJPRTS.append(PTS) + soHasPnts = True + brkFlg = True + LMAX.append(lMax) + # Efor + lenAdjPrts = len(ADJPRTS) + + # Process existing parts within current step over + prtsHasCmds = False + stepHasCmds = False + prtsCmds = list() + stpOvrCmds = list() + transCmds = list() + if soHasPnts is True: + first = ADJPRTS[0][0] # first point of arc/line stepover group + + # Manage step over transition and CircularZigZag direction + if so > 0: + # PathLog.debug(' stepover index: {}'.format(so)) + # Control ZigZag direction + if obj.CutPattern == 'CircularZigZag': + if odd is True: + odd = False + else: + odd = True + # Control step over transition + if prvStpLast is None: + prvStpLast = lastPrvStpLast + minTrnsHght = self._getMinSafeTravelHeight(safePDC, prvStpLast, first, minDep=None) # Check safe travel height against fullSTL + transCmds.append(Path.Command('N (--Step {} transition)'.format(so), {})) + transCmds.extend(self._stepTransitionCmds(obj, prvStpLast, first, minTrnsHght, tolrnc)) + + # Override default `OptimizeLinearPaths` behavior to allow `ProfileEdges` optimization + if so == peIdx or peIdx == -1: + obj.OptimizeLinearPaths = self.preOLP + + # Cycle through current step-over parts + for i in range(0, lenAdjPrts): + prt = ADJPRTS[i] + lenPrt = len(prt) + # PathLog.debug(' adj parts index - lenPrt: {} - {}'.format(i, lenPrt)) + if prt == 'BRK' and prtsHasCmds is True: + nxtStart = ADJPRTS[i + 1][0] + minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart, minDep=None) # Check safe travel height against fullSTL + prtsCmds.append(Path.Command('N (--Break)', {})) + prtsCmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) + else: + segCmds = False + prtsCmds.append(Path.Command('N (part {})'.format(i + 1), {})) + last = prt[lenPrt - 1] + if so == peIdx or peIdx == -1: + segCmds = self._planarSinglepassProcess(obj, prt) + elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2: + (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) + if rtnVal is True: + segCmds = gcode + else: + segCmds = self._planarSinglepassProcess(obj, prt) + else: + segCmds = self._planarSinglepassProcess(obj, prt) + + if segCmds is not False: + prtsCmds.extend(segCmds) + prtsHasCmds = True + prvStpLast = last + # Eif + # Efor + # Eif + + # Return `OptimizeLinearPaths` to disabled + if so == peIdx or peIdx == -1: + if obj.CutPattern in ['Circular', 'CircularZigZag']: + obj.OptimizeLinearPaths = False + + # Compile step over(prts) commands + if prtsHasCmds is True: + stepHasCmds = True + actvSteps += 1 + prvStpFirst = first + stpOvrCmds.extend(transCmds) + stpOvrCmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) + stpOvrCmds.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + stpOvrCmds.extend(prtsCmds) + stpOvrCmds.append(Path.Command('N (End of step {}.)'.format(so), {})) + + # Layer transition at first active step over in current layer + if actvSteps == 1: + prvLyrFirst = first + LYR.append(Path.Command('N (Layer {} begins)'.format(lyr), {})) + if lyr > 0: + LYR.append(Path.Command('N (Layer transition)', {})) + LYR.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + LYR.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + + if stepHasCmds is True: + lyrHasCmds = True + LYR.extend(stpOvrCmds) + # Eif + + # Close layer, saving commands, if any + if lyrHasCmds is True: + prvLyrLast = last + GCODE.extend(LYR) # save line commands + GCODE.append(Path.Command('N (End of layer {})'.format(lyr), {})) + + # Set previous depth + prevDepth = lyrDep + # Efor + + PathLog.debug('Multi-pass op has {} layers (step downs).'.format(lyr + 1)) + + return GCODE + + def _planarMultipassPreProcess(self, obj, LN, prvDep, layDep): + ALL = list() + PTS = list() + optLinTrans = obj.OptimizeStepOverTransitions + safe = math.ceil(obj.SafeHeight.Value) + + if optLinTrans is True: + for P in LN: + ALL.append(P) + # Handle layer depth AND hold points + if P.z <= layDep: + PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) + elif P.z > prvDep: + PTS.append(FreeCAD.Vector(P.x, P.y, safe)) + else: + PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) + # Efor + else: + for P in LN: + ALL.append(P) + # Handle layer depth only + if P.z <= layDep: + PTS.append(FreeCAD.Vector(P.x, P.y, layDep)) + else: + PTS.append(FreeCAD.Vector(P.x, P.y, P.z)) + # Efor + + if optLinTrans is True: + # Remove leading and trailing Hold Points + popList = list() + for i in range(0, len(PTS)): # identify leading string + if PTS[i].z == safe: + popList.append(i) + else: + break + popList.sort(reverse=True) + for p in popList: # Remove hold points + PTS.pop(p) + ALL.pop(p) + popList = list() + for i in range(len(PTS) - 1, -1, -1): # identify trailing string + if PTS[i].z == safe: + popList.append(i) + else: + break + popList.sort(reverse=True) + for p in popList: # Remove hold points + PTS.pop(p) + ALL.pop(p) + + # Determine max Z height for remaining points on line + lMax = obj.FinalDepth.Value + if len(ALL) > 0: + lMax = ALL[0].z + for P in ALL: + if P.z > lMax: + lMax = P.z + + return (PTS, lMax) + + def _planarMultipassProcess(self, obj, PNTS, lMax): + output = list() + optimize = obj.OptimizeLinearPaths + safe = math.ceil(obj.SafeHeight.Value) + lenPNTS = len(PNTS) + prcs = True + onHold = False + onLine = False + clrScnLn = lMax + 2.0 + + # Initialize first three points + nxt = None + pnt = PNTS[0] + prev = FreeCAD.Vector(-442064564.6, 258539656553.27, 3538553425.847) + + # Add temp end point + PNTS.append(FreeCAD.Vector(-4895747464.6, -25855763553.2, 35865763425)) + + # Begin processing ocl points list into gcode + for i in range(0, lenPNTS): + prcs = True + nxt = PNTS[i + 1] + + if pnt.z == safe: + prcs = False + if onHold is False: + onHold = True + output.append( Path.Command('N (Start hold)', {}) ) + output.append( Path.Command('G0', {'Z': clrScnLn, 'F': self.vertRapid}) ) + else: + if onHold is True: + onHold = False + output.append( Path.Command('N (End hold)', {}) ) + output.append( Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid}) ) + + # Process point + if prcs is True: + if optimize is True: + # iPOL = prev.isOnLineSegment(nxt, pnt) + iPOL = pnt.isOnLineSegment(prev, nxt) + if iPOL is True: + onLine = True + else: + onLine = False + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + else: + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + + # Rotate point data + if onLine is False: + prev = pnt + pnt = nxt + # Efor + + PNTS.pop() # Remove temp end point + + return output + + def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + # if obj.LayerMode == 'Multi-pass': + # rtpd = minSTH + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + # PathLog.debug('first.z: {}'.format(first.z)) + # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) + # PathLog.debug('zChng: {}'.format(zChng)) + # PathLog.debug('minSTH: {}'.format(minSTH)) + if abs(zChng) < tolrnc: # transitions to same Z height + PathLog.debug('abs(zChng) < tolrnc') + if (minSTH - first.z) > tolrnc: + PathLog.debug('(minSTH - first.z) > tolrnc') + height = minSTH + 2.0 + else: + PathLog.debug('ELSE (minSTH - first.z) > tolrnc') + horizGC = 'G1' + height = first.z + elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): + height = False # allow end of Zig to cut to beginning of Zag + + + # Create raise, shift, and optional lower commands + if height is not False: + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + if abs(zChng) < tolrnc: # transitions to same Z height + if (minSTH - first.z) > tolrnc: + height = minSTH + 2.0 + else: + height = first.z + 2.0 # first.z + + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _arcsToG2G3(self, LN, numPts, odd, gDIR, tolrnc): + cmds = list() + strtPnt = LN[0] + endPnt = LN[numPts - 1] + strtHght = strtPnt.z + coPlanar = True + isCircle = False + gdi = 0 + if odd is True: + gdi = 1 + + # Test if pnt set is circle + if abs(strtPnt.x - endPnt.x) < tolrnc: + if abs(strtPnt.y - endPnt.y) < tolrnc: + if abs(strtPnt.z - endPnt.z) < tolrnc: + isCircle = True + isCircle = False + + if isCircle is True: + # convert LN to G2/G3 arc, consolidating GCode + # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc + # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ + # Dividing circle into two arcs allows for G2/G3 on inclined surfaces + + # ijk = self.tmpCOM - strtPnt # vector from start to center + ijk = self.tmpCOM - strtPnt # vector from start to center + xyz = self.tmpCOM.add(ijk) # end point + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) + ijk = self.tmpCOM - xyz # vector from start to center + rst = strtPnt # end point + cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + else: + for pt in LN: + if abs(pt.z - strtHght) > tolrnc: # test for horizontal coplanar + coPlanar = False + break + if coPlanar is True: + # ijk = self.tmpCOM - strtPnt + ijk = self.tmpCOM.sub(strtPnt) # vector from start to center + xyz = endPnt + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) + + return (coPlanar, cmds) + + def _planarApplyDepthOffset(self, SCANDATA, DepthOffset): + PathLog.debug('Applying DepthOffset value: {}'.format(DepthOffset)) + lenScans = len(SCANDATA) + for s in range(0, lenScans): + SO = SCANDATA[s] # StepOver + numParts = len(SO) + for prt in range(0, numParts): + PRT = SO[prt] + if PRT != 'BRK': + numPts = len(PRT) + for pt in range(0, numPts): + SCANDATA[s][prt][pt].z += DepthOffset + + def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): + pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object + pdc.setSTL(stl) # add stl model + if useSafeCutter is True: + pdc.setCutter(self.safeCutter) # add safeCutter + else: + pdc.setCutter(self.cutter) # add cutter + pdc.setZ(finalDep) # set minimumZ (final / target depth value) + pdc.setSampling(SampleInterval) # set sampling size + return pdc + + # Main rotational scan functions + def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None): + PathLog.debug('_processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None)') + + base = JOB.Model.Group[mdlIdx] + bb = self.boundBoxes[mdlIdx] + stl = self.modelSTLs[mdlIdx] + + # Rotate model to initial index + initIdx = obj.CutterTilt + obj.StartIndex + if initIdx != 0.0: + self.basePlacement = FreeCAD.ActiveDocument.getObject(base.Name).Placement + if obj.RotationAxis == 'X': + base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(1.0, 0.0, 0.0), initIdx)) + else: + base.Placement = FreeCAD.Placement(FreeCAD.Vector(0.0, 0.0, 0.0), FreeCAD.Rotation(FreeCAD.Vector(0.0, 1.0, 0.0), initIdx)) + + # Prepare global holdpoint container + if self.holdPoint is None: + self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + if self.layerEndPnt is None: + self.layerEndPnt = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Avoid division by zero in rotational scan calculations + if obj.FinalDepth.Value == 0.0: + zero = obj.SampleInterval.Value # 0.00001 + self.FinalDepth = zero + # obj.FinalDepth.Value = 0.0 + else: + self.FinalDepth = obj.FinalDepth.Value + + # Determine boundbox radius based upon xzy limits data + if math.fabs(bb.ZMin) > math.fabs(bb.ZMax): + vlim = bb.ZMin + else: + vlim = bb.ZMax + if obj.RotationAxis == 'X': + # Rotation is around X-axis, cutter moves along same axis + if math.fabs(bb.YMin) > math.fabs(bb.YMax): + hlim = bb.YMin + else: + hlim = bb.YMax + else: + # Rotation is around Y-axis, cutter moves along same axis + if math.fabs(bb.XMin) > math.fabs(bb.XMax): + hlim = bb.XMin + else: + hlim = bb.XMax + + # Compute max radius of stock, as it rotates, and rotational clearance & safe heights + self.bbRadius = math.sqrt(hlim**2 + vlim**2) + self.clearHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value + self.safeHeight = self.bbRadius + JOB.SetupSheet.ClearanceHeightOffset.Value + + return self._rotationalDropCutterOp(obj, stl, bb) + + def _rotationalDropCutterOp(self, obj, stl, bb): + self.resetTolerance = 0.0000001 # degrees + self.layerEndzMax = 0.0 + commands = [] + scanLines = [] + advances = [] + iSTG = [] + rSTG = [] + rings = [] + lCnt = 0 + rNum = 0 + bbRad = self.bbRadius + + def invertAdvances(advances): + idxs = [1.1] + for adv in advances: + idxs.append(-1 * adv) + idxs.pop(0) + return idxs + + def linesToPointRings(scanLines): + rngs = [] + numPnts = len(scanLines[0]) # Number of points per line along axis, at obj.SampleInterval.Value spacing + for line in scanLines: # extract circular set(ring) of points from scan lines + if len(line) != numPnts: + PathLog.debug('Error: line lengths not equal') + return rngs + + for num in range(0, numPnts): + rngs.append([1.1]) # Initiate new ring + for line in scanLines: # extract circular set(ring) of points from scan lines + rngs[num].append(line[num]) + rngs[num].pop(0) + return rngs + + def indexAdvances(arc, stepDeg): + indexes = [0.0] + numSteps = int(math.floor(arc / stepDeg)) + for ns in range(0, numSteps): + indexes.append(stepDeg) + + travel = sum(indexes) + if arc == 360.0: + indexes.insert(0, 0.0) + else: + indexes.append(arc - travel) + + return indexes + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [self.FinalDepth] + else: + dep_par = PathUtils.depth_params(self.clearHeight, self.safeHeight, self.bbRadius, obj.StepDown.Value, 0.0, self.FinalDepth) + depthparams = [i for i in dep_par] + prevDepth = depthparams[0] + lenDP = len(depthparams) + + # Set drop cutter extra offset + cdeoX = obj.DropCutterExtraOffset.x + cdeoY = obj.DropCutterExtraOffset.y + + # Set updated bound box values and redefine the new min/mas XY area of the operation based on greatest point radius of model + bb.ZMin = -1 * bbRad + bb.ZMax = bbRad + if obj.RotationAxis == 'X': + bb.YMin = -1 * bbRad + bb.YMax = bbRad + ymin = 0.0 + ymax = 0.0 + xmin = bb.XMin - cdeoX + xmax = bb.XMax + cdeoX + else: + bb.XMin = -1 * bbRad + bb.XMax = bbRad + ymin = bb.YMin - cdeoY + ymax = bb.YMax + cdeoY + xmin = 0.0 + xmax = 0.0 + + # Calculate arc + begIdx = obj.StartIndex + endIdx = obj.StopIndex + if endIdx < begIdx: + begIdx -= 360.0 + arc = endIdx - begIdx + + # Begin gcode operation with raising cutter to safe height + commands.append(Path.Command('G0', {'Z': self.safeHeight, 'F': self.vertRapid})) + + # Complete rotational scans at layer and translate into gcode + for layDep in depthparams: + t_before = time.time() + + # Compute circumference and step angles for current layer + layCircum = 2 * math.pi * layDep + if lenDP == 1: + layCircum = 2 * math.pi * bbRad + + # Set axial feed rates + self.axialFeed = 360 / layCircum * self.horizFeed + self.axialRapid = 360 / layCircum * self.horizRapid + + # Determine step angle. + if obj.RotationAxis == obj.DropCutterDir: # Same == indexed + stepDeg = (self.cutOut / layCircum) * 360.0 + else: + stepDeg = (obj.SampleInterval.Value / layCircum) * 360.0 + + # Limit step angle and determine rotational index angles [indexes]. + if stepDeg > 120.0: + stepDeg = 120.0 + advances = indexAdvances(arc, stepDeg) # Reset for each step down layer + + # Perform rotational indexed scans to layer depth + if obj.RotationAxis == obj.DropCutterDir: # Same == indexed OR parallel + sample = obj.SampleInterval.Value + else: + sample = self.cutOut + scanLines = self._indexedDropCutScan(obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample) + + # Complete rotation if necessary + if arc == 360.0: + advances.append(360.0 - sum(advances)) + advances.pop(0) + zero = scanLines.pop(0) + scanLines.append(zero) + + # Translate OCL scans into gcode + if obj.RotationAxis == obj.DropCutterDir: # Same == indexed (cutter runs parallel to axis) + + # Translate scan to gcode + sumAdv = begIdx + for sl in range(0, len(scanLines)): + sumAdv += advances[sl] + # Translate scan to gcode + iSTG = self._indexedScanToGcode(obj, sl, scanLines[sl], sumAdv, prevDepth, layDep, lenDP) + commands.extend(iSTG) + + # Raise cutter to safe height after each index cut + commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) + # Eol + else: + if self.CutClimb is False: + advances = invertAdvances(advances) + advances.reverse() + scanLines.reverse() + + # Begin gcode operation with raising cutter to safe height + commands.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) + + # Convert rotational scans into gcode + rings = linesToPointRings(scanLines) + rNum = 0 + for rng in rings: + rSTG = self._rotationalScanToGcode(obj, rng, rNum, prevDepth, layDep, advances) + commands.extend(rSTG) + if arc != 360.0: + clrZ = self.layerEndzMax + self.SafeHeightOffset + commands.append(Path.Command('G0', {'Z': clrZ, 'F': self.vertRapid})) + rNum += 1 + # Eol + + prevDepth = layDep + lCnt += 1 # increment layer count + PathLog.debug("--Layer " + str(lCnt) + ": " + str(len(advances)) + " OCL scans and gcode in " + str(time.time() - t_before) + " s") + # Eol + + return commands + + def _indexedDropCutScan(self, obj, stl, advances, xmin, ymin, xmax, ymax, layDep, sample): + cutterOfst = 0.0 + iCnt = 0 + Lines = [] + result = None + + pdc = ocl.PathDropCutter() # create a pdc + pdc.setCutter(self.cutter) + pdc.setZ(layDep) # set minimumZ (final / ta9rget depth value) + pdc.setSampling(sample) + + # if self.useTiltCutter == True: + if obj.CutterTilt != 0.0: + cutterOfst = layDep * math.sin(math.radians(obj.CutterTilt)) + PathLog.debug("CutterTilt: cutterOfst is " + str(cutterOfst)) + + sumAdv = 0.0 + for adv in advances: + sumAdv += adv + if adv > 0.0: + # Rotate STL object using OCL method + radsRot = math.radians(adv) + if obj.RotationAxis == 'X': + stl.rotate(radsRot, 0.0, 0.0) + else: + stl.rotate(0.0, radsRot, 0.0) + + # Set STL after rotation is made + pdc.setSTL(stl) + + # add Line objects to the path in this loop + if obj.RotationAxis == 'X': + p1 = ocl.Point(xmin, cutterOfst, 0.0) # start-point of line + p2 = ocl.Point(xmax, cutterOfst, 0.0) # end-point of line + else: + p1 = ocl.Point(cutterOfst, ymin, 0.0) # start-point of line + p2 = ocl.Point(cutterOfst, ymax, 0.0) # end-point of line + + # Create line object + if obj.RotationAxis == obj.DropCutterDir: # parallel cut + if obj.CutPattern == 'ZigZag': + if (iCnt % 2 == 0.0): # even + lo = ocl.Line(p1, p2) + else: # odd + lo = ocl.Line(p2, p1) + elif obj.CutPattern == 'Line': + if self.CutClimb is True: + lo = ocl.Line(p2, p1) + else: + lo = ocl.Line(p1, p2) + else: + lo = ocl.Line(p1, p2) # line-object + + path = ocl.Path() # create an empty path object + path.append(lo) # add the line to the path + pdc.setPath(path) # set path + pdc.run() # run drop-cutter on the path + result = pdc.getCLPoints() # request the list of points + + # Convert list of OCL objects to list of Vectors for faster access and Apply depth offset + if obj.DepthOffset.Value != 0.0: + Lines.append([FreeCAD.Vector(p.x, p.y, p.z + obj.DepthOffset.Value) for p in result]) + else: + Lines.append([FreeCAD.Vector(p.x, p.y, p.z) for p in result]) + + iCnt += 1 + # End loop + + # Rotate STL object back to original position using OCL method + reset = -1 * math.radians(sumAdv - self.resetTolerance) + if obj.RotationAxis == 'X': + stl.rotate(reset, 0.0, 0.0) + else: + stl.rotate(0.0, reset, 0.0) + self.resetTolerance = 0.0 + + return Lines + + def _indexedScanToGcode(self, obj, li, CLP, idxAng, prvDep, layerDepth, numDeps): + # generate the path commands + output = [] + optimize = obj.OptimizeLinearPaths + holdCount = 0 + holdStart = False + holdStop = False + zMax = prvDep + lenCLP = len(CLP) + lastCLP = lenCLP - 1 + prev = FreeCAD.Vector(0.0, 0.0, 0.0) + nxt = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Create first point + pnt = CLP[0] + + # Rotate to correct index location + if obj.RotationAxis == 'X': + output.append(Path.Command('G0', {'A': idxAng, 'F': self.axialFeed})) + else: + output.append(Path.Command('G0', {'B': idxAng, 'F': self.axialFeed})) + + if li > 0: + if pnt.z > self.layerEndPnt.z: + clrZ = pnt.z + 2.0 + output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) + else: + output.append(Path.Command('G0', {'Z': self.clearHeight, 'F': self.vertRapid})) + + output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) + output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) + + for i in range(0, lenCLP): + if i < lastCLP: + nxt = CLP[i + 1] + else: + optimize = False + + # Update zMax values + if pnt.z > zMax: + zMax = pnt.z + + if obj.LayerMode == 'Multi-pass': + # if z travels above previous layer, start/continue hold high cycle + if pnt.z > prvDep and optimize is True: + if self.onHold is False: + holdStart = True + self.onHold = True + + if self.onHold is True: + if holdStart is True: + # go to current coordinate + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + # Save holdStart coordinate and prvDep values + self.holdPoint = pnt + holdCount += 1 # Increment hold count + holdStart = False # cancel holdStart + + # hold cutter high until Z value drops below prvDep + if pnt.z <= prvDep: + holdStop = True + + if holdStop is True: + # Send hold and current points to + zMax += 2.0 + for cmd in self.holdStopCmds(obj, zMax, prvDep, pnt, "Hold Stop: in-line"): + output.append(cmd) + # reset necessary hold related settings + zMax = prvDep + holdStop = False + self.onHold = False + self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + + if self.onHold is False: + if not optimize or not pnt.isOnLineSegment(prev, nxt): + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, 'F': self.horizFeed})) + + # Rotate point data + prev = pnt + pnt = nxt + output.append(Path.Command('N (End index angle ' + str(round(idxAng, 4)) + ')', {})) + + # Save layer end point for use in transitioning to next layer + self.layerEndPnt = pnt + + return output + + def _rotationalScanToGcode(self, obj, RNG, rN, prvDep, layDep, advances): + '''_rotationalScanToGcode(obj, RNG, rN, prvDep, layDep, advances) ... + Convert rotational scan data to gcode path commands.''' + output = [] + nxtAng = 0 + zMax = 0.0 + nxt = FreeCAD.Vector(0.0, 0.0, 0.0) + + begIdx = obj.StartIndex + endIdx = obj.StopIndex + if endIdx < begIdx: + begIdx -= 360.0 + + # Rotate to correct index location + axisOfRot = 'A' + if obj.RotationAxis == 'Y': + axisOfRot = 'B' + + # Create first point + ang = 0.0 + obj.CutterTilt + pnt = RNG[0] + + # Adjust feed rate based on radius/circumference of cutter. + # Original feed rate based on travel at circumference. + if rN > 0: + if pnt.z >= self.layerEndzMax: + clrZ = pnt.z + 5.0 + output.append(Path.Command('G1', {'Z': clrZ, 'F': self.vertRapid})) + else: + output.append(Path.Command('G1', {'Z': self.clearHeight, 'F': self.vertRapid})) + + output.append(Path.Command('G0', {axisOfRot: ang, 'F': self.axialFeed})) + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.axialFeed})) + output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.axialFeed})) + + lenRNG = len(RNG) + lastIdx = lenRNG - 1 + for i in range(0, lenRNG): + if i < lastIdx: + nxtAng = ang + advances[i + 1] + nxt = RNG[i + 1] + + if pnt.z > zMax: + zMax = pnt.z + + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'Z': pnt.z, axisOfRot: ang, 'F': self.axialFeed})) + pnt = nxt + ang = nxtAng + + # Save layer end point for use in transitioning to next layer + self.layerEndPnt = RNG[0] + self.layerEndzMax = zMax + + return output + + def holdStopCmds(self, obj, zMax, pd, p2, txt): + '''holdStopCmds(obj, zMax, pd, p2, txt) ... Gcode commands to be executed at beginning of hold.''' + cmds = [] + msg = 'N (' + txt + ')' + cmds.append(Path.Command(msg, {})) # Raise cutter rapid to zMax in line of travel + cmds.append(Path.Command('G0', {'Z': zMax, 'F': self.vertRapid})) # Raise cutter rapid to zMax in line of travel + cmds.append(Path.Command('G0', {'X': p2.x, 'Y': p2.y, 'F': self.horizRapid})) # horizontal rapid to current XY coordinate + if zMax != pd: + cmds.append(Path.Command('G0', {'Z': pd, 'F': self.vertRapid})) # drop cutter down rapidly to prevDepth depth + cmds.append(Path.Command('G0', {'Z': p2.z, 'F': self.vertFeed})) # drop cutter down to current Z depth, returning to normal cut path and speed + return cmds + + # Additional support methods + def resetOpVariables(self, all=True): + '''resetOpVariables() ... Reset class variables used for instance of operation.''' + self.holdPoint = None + self.layerEndPnt = None + self.onHold = False + self.SafeHeightOffset = 2.0 + self.ClearHeightOffset = 4.0 + self.layerEndzMax = 0.0 + self.resetTolerance = 0.0 + self.holdPntCnt = 0 + self.bbRadius = 0.0 + self.axialFeed = 0.0 + self.axialRapid = 0.0 + self.FinalDepth = 0.0 + self.clearHeight = 0.0 + self.safeHeight = 0.0 + self.faceZMax = -999999999999.0 + if all is True: + self.cutter = None + self.stl = None + self.fullSTL = None + self.cutOut = 0.0 + self.radius = 0.0 + self.useTiltCutter = False + return True + + def deleteOpVariables(self, all=True): + '''deleteOpVariables() ... Reset class variables used for instance of operation.''' + del self.holdPoint + del self.layerEndPnt + del self.onHold + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.layerEndzMax + del self.resetTolerance + del self.holdPntCnt + del self.bbRadius + del self.axialFeed + del self.axialRapid + del self.FinalDepth + del self.clearHeight + del self.safeHeight + del self.faceZMax + if all is True: + del self.cutter + del self.stl + del self.fullSTL + del self.cutOut + del self.radius + del self.useTiltCutter + return True + + def setOclCutter(self, obj, safe=False): + ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' + # Set cutter details + # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details + diam_1 = float(obj.ToolController.Tool.Diameter) + lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 + FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 + CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 + CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 + + # Make safeCutter with 2 mm buffer around physical cutter + if safe is True: + diam_1 += 4.0 + if FR != 0.0: + FR += 2.0 + + PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) + if obj.ToolController.Tool.ToolType == 'EndMill': + # Standard End Mill + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: + # Standard Ball End Mill + # OCL -> BallCutter::BallCutter(diameter, length) + self.useTiltCutter = True + return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> BallCutter::BallCutter(diameter, length) + return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + + elif obj.ToolController.Tool.ToolType == 'ChamferMill': + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + else: + # Default to standard end mill + PathLog.warning("Defaulting cutter to standard end mill.") + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + def _getMinSafeTravelHeight(self, pdc, p1, p2, minDep=None): + A = (p1.x, p1.y) + B = (p2.x, p2.y) + LINE = self._planarDropCutScan(pdc, A, B) + zMax = max([obj.z for obj in LINE]) + if minDep is not None: + if zMax < minDep: + zMax = minDep + return zMax + + +def SetupProperties(): + ''' SetupProperties() ... Return list of properties required for operation.''' + setup = ['AvoidLastX_Faces', 'AvoidLastX_InternalFeatures', 'BoundBox'] + setup.extend(['BoundaryAdjustment', 'CircularCenterAt', 'CircularCenterCustom']) + setup.extend(['CircularUseG2G3', 'InternalFeaturesCut', 'InternalFeaturesAdjustment']) + setup.extend(['CutMode', 'CutPattern', 'CutPatternAngle', 'CutPatternReversed']) + setup.extend(['CutterTilt', 'DepthOffset', 'DropCutterDir', 'GapSizes', 'GapThreshold']) + setup.extend(['HandleMultipleFeatures', 'LayerMode', 'OptimizeStepOverTransitions']) + setup.extend(['ProfileEdges', 'BoundaryEnforcement', 'RotationAxis', 'SampleInterval']) + setup.extend(['ScanType', 'StartIndex', 'StartPoint', 'StepOver', 'StopIndex']) + setup.extend(['UseStartPoint', 'AngularDeflection', 'LinearDeflection', 'ShowTempObjects']) + return setup + + +def Create(name, obj=None): + '''Create(name) ... Creates and returns a Surface operation.''' + if obj is None: + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + obj.Proxy = ObjectSurface(obj, name) + return obj From f41ec732e1c96080e21996973db259ef56da3a2d Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Mon, 13 Apr 2020 03:47:53 -0500 Subject: [PATCH 055/142] Path: Added `Spiral` cut pattern; Adjusted tooltip language --- src/Mod/Path/PathScripts/PathSurface.py | 185 ++++++++++++++++++++++-- 1 file changed, 175 insertions(+), 10 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index f7351662b6..076b53a839 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -21,11 +21,7 @@ # * USA * # * * # *************************************************************************** -# * * -# * Additional modifications and contributions beginning 2019 * -# * by Russell Johnson 2020-04-10 11:46 CST * -# * * -# *************************************************************************** + from __future__ import print_function @@ -44,7 +40,7 @@ try: except ImportError: msg = QtCore.QCoreApplication.translate("PathSurface", "This operation requires OpenCamLib to be installed.") FreeCAD.Console.PrintError(msg + "\n") - raise + raise ImportError # import sys # sys.exit(msg) @@ -132,7 +128,7 @@ class ObjectSurface(PathOp.ObjectOp): ("App::PropertyFloat", "CutterTilt", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Stop index(angle) for rotational scan")), ("App::PropertyEnumeration", "DropCutterDir", "Rotation", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction along which dropcutter lines are created")), + QtCore.QT_TRANSLATE_NOOP("App::Property", "Dropcutter lines are created parallel to this axis.")), ("App::PropertyVectorDistance", "DropCutterExtraOffset", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Additional offset to the selected bounding box")), ("App::PropertyEnumeration", "RotationAxis", "Rotation", @@ -161,7 +157,7 @@ class ObjectSurface(PathOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), ("App::PropertyEnumeration", "BoundBox", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")), + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation.")), ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", @@ -208,7 +204,7 @@ class ObjectSurface(PathOp.ObjectOp): 'BoundBox': ['BaseBoundBox', 'Stock'], 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], 'CutMode': ['Conventional', 'Climb'], - 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] + 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'ZigZagOffset', 'Grid', 'Triangle'] 'DropCutterDir': ['X', 'Y'], 'HandleMultipleFeatures': ['Collectively', 'Individually'], 'LayerMode': ['Single-pass', 'Multi-pass'], @@ -227,7 +223,6 @@ class ObjectSurface(PathOp.ObjectOp): if obj.CutPattern in ['Circular', 'CircularZigZag']: P0 = 2 P2 = 0 - R0 = 0 elif obj.ScanType == 'Rotational': R2 = P0 = P2 = 2 R0 = 0 @@ -1687,6 +1682,16 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.debug('_planarMakePathGeom()') GeoSet = list() + def getSpiralPoint(move, b, radAng): + x = b * radAng * math.cos(radAng) + y = b * radAng * math.sin(radAng) + return FreeCAD.Vector(x, y, 0.0).add(move) + + def getOppositeSpiralPoint(move, b, radAng): + x = b * radAng * math.cos(radAng) + y = b * radAng * math.sin(radAng) + return FreeCAD.Vector(-1 * x, y, 0.0).add(move) + # Apply drop cutter extra offset and set the max and min XY area of the operation xmin = faceShp.BoundBox.XMin xmax = faceShp.BoundBox.XMax @@ -1816,6 +1821,107 @@ class ObjectSurface(PathOp.ObjectOp): GeoSet.append(circle) # Efor COM = cntr.Base + elif obj.CutPattern in ['Spiral']: + SEGS = list() + loopRadians = 0.0 # Used to keep track of complete loops/cycles + sumRadians = 0.0 + loopCnt = 0 + segCnt = 0 + twoPi = 2.0 * math.pi + maxDist = halfLL + move = COM # FreeCAD.Vector(0.0, 0.0, 0.0) # Use to translate the center of the spiral + + # Set tool properties and calculate cutout + effectiveCut = self.cutter.getDiameter() * float(obj.StepOver) / 100.0 + cutOut = effectiveCut / twoPi + + segLen = obj.SampleInterval.Value # CutterDiameter / 10.0 # SampleInterval.Value + stepAng = segLen / ((loopCnt + 1) * effectiveCut) # math.pi / 18.0 # 10 degrees + stopRadians = maxDist / cutOut + + draw = True + lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + if obj.CutPatternReversed: + if obj.CutMode == 'Conventional': + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getOppositeSpiralPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * effectiveCut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p2, p1) + SEGS.append(lineSeg) + else: + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getSpiralPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * effectiveCut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p2, p1) + SEGS.append(lineSeg) + # Eif + SEGS.reverse() + else: + if obj.CutMode == 'Climb': + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getOppositeSpiralPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * effectiveCut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p1, p2) + SEGS.append(lineSeg) + else: + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getSpiralPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * effectiveCut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p1, p2) + SEGS.append(lineSeg) + # Eif + spiral = Part.Wire([ls.Edges[0] for ls in SEGS]) + GeoSet.append(spiral) + elif obj.CutPattern in ['Offset']: + pass # Eif if obj.CutPatternReversed is True: @@ -1950,6 +2056,20 @@ class ObjectSurface(PathOp.ObjectOp): stpOvr.append(scan) if erFlg is False: SCANS.append(stpOvr) + elif obj.CutPattern == 'Spiral': + stpOvr = list() + PNTSET = self._pathGeomToSpiralPointSet(obj, pathGeom) + for D in PNTSET: + for I in D: + if I == 'BRK': + stpOvr.append(I) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = I + stpOvr.append(self._planarDropCutScan(pdc, A, B)) + SCANS.append(stpOvr) + stpOvr = list() + # Eif return SCANS @@ -2426,6 +2546,51 @@ class ObjectSurface(PathOp.ObjectOp): return ARCS + def _pathGeomToSpiralPointSet(self, obj, compGeoShp): + '''_pathGeomToSpiralPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directional, connected groupings.''' + PathLog.debug('_pathGeomToSpiralPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + ec = len(compGeoShp.Edges) + start = 2 + + if obj.CutPatternReversed: + edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail + ec -= 1 + start = 1 + else: + edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail + p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0) + p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0) + tup = ((p1.x, p1.y), (p2.x, p2.y)) + inLine.append(tup) + lst = p2 + + for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1 + edg = compGeoShp.Edges[ei] # Get edge for vertexes + sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point) + ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point + tup = ((sp.x, sp.y), (ep.x, ep.y)) + + if sp.sub(p2).Length < 0.000001: + inLine.append(tup) + else: + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset container + inLine.append(tup) + p1 = sp + p2 = ep + # Efor + + lnCnt += 1 + LINES.append(inLine) # Save inLine segments + + return LINES + def _planarDropCutScan(self, pdc, A, B): #PNTS = list() (x1, y1) = A From a6cb530d57615951e77cd431262eeb95fc5fe7a3 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Mon, 13 Apr 2020 14:55:24 -0500 Subject: [PATCH 056/142] Path: Fix weakness in face analysis for unique OuterWire cases --- src/Mod/Path/PathScripts/PathSurface.py | 30 ++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 076b53a839..413058000a 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -384,7 +384,6 @@ class ObjectSurface(PathOp.ObjectOp): self.collectiveShapes = list() self.individualShapes = list() self.avoidShapes = list() - self.deflection = None self.tempGroup = None self.CutClimb = False self.closedGap = False @@ -408,6 +407,7 @@ class ObjectSurface(PathOp.ObjectOp): # Identify parent Job JOB = PathUtils.findParentJob(obj) + self.JOB = JOB if JOB is None: PathLog.error(translate('PathSurface', "No JOB")) return @@ -482,14 +482,6 @@ class ObjectSurface(PathOp.ObjectOp): # make circle for workplane self.wpc = Part.makeCircle(2.0) - # Set deflection values for mesh generation - try: # try/except is for Path Jobs created before GeometryTolerance - self.deflection = JOB.GeometryTolerance.Value - except AttributeError as ee: - PathLog.warning('Error setting Mesh deflection: {}. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) - import PathScripts.PathPreferences as PathPreferences - self.deflection = PathPreferences.defaultGeometryTolerance() - # Save model visibilities for restoration if FreeCAD.GuiUp: for m in range(0, len(JOB.Model.Group)): @@ -596,7 +588,6 @@ class ObjectSurface(PathOp.ObjectOp): self.depthParams = None self.midDep = None self.wpc = None - self.deflection = None del self.modelSTLs del self.safeSTLs del self.modelTypes @@ -608,7 +599,6 @@ class ObjectSurface(PathOp.ObjectOp): del self.depthParams del self.midDep del self.wpc - del self.deflection execTime = time.time() - startTime PathLog.info('Operation time: {} sec.'.format(execTime)) @@ -1116,10 +1106,24 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.debug(' -number of wires found is {}'.format(nf)) if nf == 1: (area, W, raised) = WIRES[0] - return [(W, raised)] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + return [(OW, False), (W, raised)] + else: + return [(W, raised)] else: sortedWIRES = sorted(WIRES, key=index0, reverse=True) - return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + # Check if OuterWire is larger than largest in WRS list + (W, raised) = WRS[0] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + WRS.insert(0, (OW, False)) + return WRS return False From fd34891c999f20ee07c2871c93699874730d5fa6 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 00:25:36 -0500 Subject: [PATCH 057/142] Path: Improve compatibility between file versions Verify enumerations on document reload, maintaining the existing value for the property. --- src/Mod/Path/PathScripts/PathSurface.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 413058000a..c11ada918f 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -102,6 +102,9 @@ class ObjectSurface(PathOp.ObjectOp): if not hasattr(obj, nm): obj.addProperty(prtyp, nm, grp, tt) missing.append(nm) + newPropMsg = translate('PathSurface', 'New property added: ') + nm + '. ' + newPropMsg += translate('PathSurface', 'Check its default value.') + PathLog.warning(newPropMsg) # Set enumeration lists for enumeration properties if len(missing) > 0: @@ -253,6 +256,20 @@ class ObjectSurface(PathOp.ObjectOp): else: obj.setEditorMode('ShowTempObjects', 0) # show + # Repopulate enumerations in case of changes + ENUMS = self.propertyEnumerations() + for n in ENUMS: + restore = False + if hasattr(obj, n): + val = obj.getPropertyByName(n) + restore = True + cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) + exec(cmdStr) + if restore: + cmdStr = 'obj.{}={}'.format(n, "'" + val + "'") + exec(cmdStr) + + self.setEditorProperties(obj) def opSetDefaultValues(self, obj, job): From f6ad7101c678f48144d5a8c3759ffb581c8cb889 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 22:53:08 -0500 Subject: [PATCH 058/142] Path: Create shared support module for 3D Surface and Waterline --- src/Mod/Path/CMakeLists.txt | 1 + .../Path/PathScripts/PathSurfaceSupport.py | 441 ++++++++++++++++++ 2 files changed, 442 insertions(+) create mode 100644 src/Mod/Path/PathScripts/PathSurfaceSupport.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index ded7c91a93..78415e1e32 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -107,6 +107,7 @@ SET(PathScripts_SRCS PathScripts/PathStop.py PathScripts/PathSurface.py PathScripts/PathSurfaceGui.py + PathScripts/PathSurfaceSupport.py PathScripts/PathToolBit.py PathScripts/PathToolBitCmd.py PathScripts/PathToolBitEdit.py diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py new file mode 100644 index 0000000000..68abd2abc5 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py @@ -0,0 +1,441 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2020 russ4262 * +# * * +# * 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. * +# * * +# * This program 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 this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +__title__ = "Path Surface Support Module" +__author__ = "russ4262 (Russell Johnson)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Support functions and classes for 3D Surface and Waterline operations." +__contributors__ = "" + +import FreeCAD +from PySide import QtCore +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils +import math +import Part + + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class PathGeometryGenerator: + '''Creates a path geometry shape from an assigned pattern for conversion to tool paths. + PathGeometryGenerator(obj, shape, pattern) + `obj` is the operation object, `shape` is the horizontal planar shape object, + and `pattern` is the name of the geometric pattern to apply. + Frist, call the getCenterOfMass() method for the CenterOfMass for patterns allowing a custom center. + Next, call the getPathGeometryGenerator() method to request the path geometry shape.''' + + # Register valid patterns here by name + # Create a corresponding processing method below. Precede the name with an underscore(_) + patterns = ('Circular', 'CircularZigZag', 'Line', 'Offset', 'Spiral', 'ZigZag') + + def __init__(self, obj, shape, pattern): + '''__init__(obj, shape, pattern)... Instantiate PathGeometryGenerator class. + Required arguments are the operation object, horizontal planar shape, and pattern name.''' + self.debugObjectsGroup = False + self.pattern = None + self.shape = None + self.pathGeometry = None + self.rawGeoList = None + self.centerOfMass = None + self.deltaX = None + self.deltaY = None + self.deltaC = None + self.halfDiag = None + self.halfPasses = None + self.obj = obj + self.toolDiam = float(obj.ToolController.Tool.Diameter) + self.cutOut = self.toolDiam * (float(obj.StepOver) / 100.0) + self.wpc = Part.makeCircle(2.0) # make circle for workplane + + # validate requested pattern + if pattern in self.patterns: + if hasattr(self, '_' + pattern): + self.pattern = pattern + + if shape.BoundBox.ZMin != 0.0: + shape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - shape.BoundBox.ZMin)) + if shape.BoundBox.ZMax == 0.0: + self.shape = shape + else: + PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax)) + + self._prepareConstants() + + def _prepareConstants(self): + # Apply drop cutter extra offset and set the max and min XY area of the operation + xmin = self.shape.BoundBox.XMin + xmax = self.shape.BoundBox.XMax + ymin = self.shape.BoundBox.YMin + ymax = self.shape.BoundBox.YMax + + # Compute weighted center of mass of all faces combined + fCnt = 0 + totArea = 0.0 + zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) + for F in self.shape.Faces: + comF = F.CenterOfMass + areaF = F.Area + totArea += areaF + fCnt += 1 + zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) + if fCnt == 0: + PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) + zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) + else: + avgArea = totArea / fCnt + zeroCOM.multiply(1 / fCnt) + zeroCOM.multiply(1 / avgArea) + self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + + # get X, Y, Z spans; Compute center of rotation + self.deltaX = self.shape.BoundBox.XLength + self.deltaY = self.shape.BoundBox.YLength + self.deltaC = self.shape.BoundBox.DiagonalLength # math.sqrt(self.deltaX**2 + self.deltaY**2) + lineLen = self.deltaC + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end + self.halfDiag = math.ceil(lineLen / 2.0) + cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal + self.halfPasses = math.ceil(cutPasses / 2.0) + + # Public methods + def setDebugObjectsGroup(self, tmpGrpObject): + '''setDebugObjectsGroup(tmpGrpObject)... + Pass the temporary object group to show temporary construction objects''' + self.debugObjectsGroup = tmpGrpObject + + def getCenterOfMass(self): + '''getCenterOfMass()... + Returns the Center Of Mass for the current class instance.''' + return self.centerOfMass + + def getPathGeometryGenerator(self): + '''getPathGeometryGenerator()... + Call this function to obtain the path geometry shape, generated by this class.''' + if self.pattern is None: + PathLog.warning('PGG: No pattern set.') + return False + + if self.shape is None: + PathLog.warning('PGG: No shape set.') + return False + + cmd = 'self._' + self.pattern + '()' + exec(cmd) + + if self.obj.CutPatternReversed is True: + self.rawGeoList.reverse() + + # Create compound object to bind all lines in Lineset + geomShape = Part.makeCompound(self.rawGeoList) + + # Position and rotate the Line and ZigZag geometry + if self.pattern in ['Line', 'ZigZag']: + if self.obj.CutPatternAngle != 0.0: + geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), self.obj.CutPatternAngle) + bbC = self.shape.BoundBox.Center + geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) + + if self.debugObjectsGroup: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') + F.Shape = geomShape + F.purgeTouched() + self.debugObjectsGroup.addObject(F) + + if self.pattern == 'Offset': + return geomShape + + # Identify intersection of cross-section face and lineset + cmnShape = self.shape.common(geomShape) + + if self.debugObjectsGroup: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') + F.Shape = cmnShape + F.purgeTouched() + self.debugObjectsGroup.addObject(F) + + self.tmpCOM = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0) + return cmnShape + + # Cut pattern methods + def _Circular(self): + GeoSet = list() + zTgt = 0.0 # self.shape.BoundBox.ZMin + centerAt = self.obj.CircularCenterAt + cntr = FreeCAD.Placement() + + if centerAt == 'CenterOfMass': + cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, zTgt) # self.centerOfMass # Use center of Mass + elif centerAt == 'CenterOfBoundBox': + cent = self.shape.BoundBox.Center + cntrPnt = FreeCAD.Vector(cent.x, cent.y, zTgt) + elif centerAt == 'XminYmin': + cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, zTgt) + elif centerAt == 'Custom': + newCent = FreeCAD.Vector(self.obj.CircularCenterCustom.x, self.obj.CircularCenterCustom.y, zTgt) + cntrPnt = newCent + + # recalculate number of passes, if need be + radialPasses = self.halfPasses + if centerAt != 'CenterOfBoundBox': + # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center + EBB = self.shape.BoundBox + CORNERS = [ + FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), + FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), + ] + dMax = 0.0 + for c in range(0, 4): + dist = CORNERS[c].sub(cntrPnt).Length + if dist > dMax: + dMax = dist + diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end + radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal + + # Update self.centerOfMass point and current CircularCenter + if centerAt != 'Custom': + self.obj.CircularCenterCustom = cntrPnt + + minRad = self.toolDiam * 0.45 + siX3 = 3 * self.obj.SampleInterval.Value + minRadSI = (siX3 / 2.0) / math.pi + if minRad < minRadSI: + minRad = minRadSI + + # Make small center circle to start pattern + if self.obj.StepOver > 50: + circle = Part.makeCircle(minRad, cntrPnt) + GeoSet.append(circle) + + for lc in range(1, radialPasses + 1): + rad = (lc * self.cutOut) + if rad >= minRad: + circle = Part.makeCircle(rad, cntrPnt) + GeoSet.append(circle) + # Efor + self.centerOfMass = cntrPnt + self.rawGeoList = GeoSet + + def _CircularZigZag(self): + self._Circular() # Use _Circular generator + + def _Line(self): + GeoSet = list() + centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model + cAng = math.atan(self.deltaX / self.deltaY) # BoundaryBox angle + + # Determine end points and create top lines + x1 = centRot.x - self.halfDiag + x2 = centRot.x + self.halfDiag + diag = None + if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180: + diag = self.deltaY + elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270: + diag = self.deltaX + else: + perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC + diag = perpDist + y1 = centRot.y + diag + # y2 = y1 + + # Create end points for set of lines to intersect with cross-section face + pntTuples = list() + for lc in range((-1 * (self.halfPasses - 1)), self.halfPasses + 1): + x1 = centRot.x - self.halfDiag + x2 = centRot.x + self.halfDiag + y1 = centRot.y + (lc * self.cutOut) + # y2 = y1 + p1 = FreeCAD.Vector(x1, y1, 0.0) + p2 = FreeCAD.Vector(x2, y1, 0.0) + pntTuples.append( (p1, p2) ) + + # Convert end points to lines + for (p1, p2) in pntTuples: + line = Part.makeLine(p1, p2) + GeoSet.append(line) + + self.rawGeoList = GeoSet + + def _Offset(self): + self.rawGeoList = self._extractOffsetFaces() + + def _Spiral(self): + GeoSet = list() + SEGS = list() + draw = True + loopRadians = 0.0 # Used to keep track of complete loops/cycles + sumRadians = 0.0 + loopCnt = 0 + segCnt = 0 + twoPi = 2.0 * math.pi + maxDist = self.halfDiag + move = self.centerOfMass # FreeCAD.Vector(0.0, 0.0, 0.0) # Use to translate the center of the spiral + lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Set tool properties and calculate cutout + cutOut = self.cutOut / twoPi + segLen = self.obj.SampleInterval.Value # CutterDiameter / 10.0 # SampleInterval.Value + stepAng = segLen / ((loopCnt + 1) * self.cutOut) # math.pi / 18.0 # 10 degrees + stopRadians = maxDist / cutOut + + if self.obj.CutPatternReversed: + if self.obj.CutMode == 'Conventional': + getPoint = self._makeOppSpiralPnt + else: + getPoint = self._makeRegSpiralPnt + + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p2, p1) + SEGS.append(lineSeg) + # Ewhile + SEGS.reverse() + else: + if self.obj.CutMode == 'Climb': + getPoint = self._makeOppSpiralPnt + else: + getPoint = self._makeRegSpiralPnt + + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p1, p2) + SEGS.append(lineSeg) + # Ewhile + # Eif + spiral = Part.Wire([ls.Edges[0] for ls in SEGS]) + GeoSet.append(spiral) + + self.rawGeoList = GeoSet + + def _ZigZag(self): + self._Line() # Use _Line generator + + # Support methods + def _makeRegSpiralPnt(self, move, b, radAng): + x = b * radAng * math.cos(radAng) + y = b * radAng * math.sin(radAng) + return FreeCAD.Vector(x, y, 0.0).add(move) + + def _makeOppSpiralPnt(self, move, b, radAng): + x = b * radAng * math.cos(radAng) + y = b * radAng * math.sin(radAng) + return FreeCAD.Vector(-1 * x, y, 0.0).add(move) + + def _extractOffsetFaces(self): + PathLog.debug('_extractOffsetFaces()') + wires = list() + faces = list() + ofst = 0.0 # - self.cutOut + shape = self.shape + cont = True + cnt = 0 + while cont: + ofstArea = self._getFaceOffset(shape, ofst) + if not ofstArea: + PathLog.warning('PGG: No offset clearing area returned.') + cont = False + break + for F in ofstArea.Faces: + faces.append(F) + for w in F.Wires: + wires.append(w) + shape = ofstArea + if cnt == 0: + ofst = 0.0 - self.cutOut + cnt += 1 + return wires + + def _getFaceOffset(self, shape, offset): + '''_getFaceOffset(shape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('_getFaceOffset()') + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 # 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(shape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 + area.add(shape) + area.setParams(**areaParams) # set parameters + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + ofstFace = Part.makeCompound(W) + + return ofstFace +# Eclass From ccfd52477f01ccddba28dbf02108f341f04aeb94 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Wed, 15 Apr 2020 08:17:37 -0500 Subject: [PATCH 059/142] Path: New class - PathGeometryGenerator; new CutPattern - Offset Converted _planarMakePathGeom() into independent class, PathGeometryGenerator, as preparation to share common code with Waterline. Implementation of new class within existing code. Added new cut pattern: Offset. It is ported from Waterline. --- src/Mod/Path/PathScripts/PathSurface.py | 340 +++--------------------- 1 file changed, 43 insertions(+), 297 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index c11ada918f..49d22d66ce 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -49,6 +49,7 @@ import Path import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils import PathScripts.PathOp as PathOp +import PathScripts.PathSurfaceSupport as PathSurfaceSupport import time import math import Part @@ -207,7 +208,7 @@ class ObjectSurface(PathOp.ObjectOp): 'BoundBox': ['BaseBoundBox', 'Stock'], 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], 'CutMode': ['Conventional', 'Climb'], - 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'ZigZagOffset', 'Grid', 'Triangle'] + 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'ZigZagOffset', 'Grid', 'Triangle'] 'DropCutterDir': ['X', 'Y'], 'HandleMultipleFeatures': ['Collectively', 'Individually'], 'LayerMode': ['Single-pass', 'Multi-pass'], @@ -226,6 +227,8 @@ class ObjectSurface(PathOp.ObjectOp): if obj.CutPattern in ['Circular', 'CircularZigZag']: P0 = 2 P2 = 0 + elif obj.CutPattern == 'Offset': + P0 = 2 elif obj.ScanType == 'Rotational': R2 = P0 = P2 = 2 R0 = 0 @@ -404,6 +407,7 @@ class ObjectSurface(PathOp.ObjectOp): self.tempGroup = None self.CutClimb = False self.closedGap = False + self.tmpCOM = None self.gaps = [0.1, 0.2, 0.3] CMDS = list() modelVisibility = list() @@ -1623,36 +1627,50 @@ class ObjectSurface(PathOp.ObjectOp): depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) profScan = list() + offsetPoints = False if obj.ProfileEdges != 'None': + offsetPoints = True prflShp = self.profileShapes[mdlIdx][fsi] if prflShp is False: PathLog.error('No profile shape is False.') return list() - if self.showDebugObjects is True: + if self.showDebugObjects: P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNewProfileShape') P.Shape = prflShp P.purgeTouched() self.tempGroup.addObject(P) # get offset path geometry and perform OCL scan with that geometry - pathOffsetGeom = self._planarMakeProfileGeom(obj, prflShp) + pathOffsetGeom = self._offsetFacesToPointData(obj, prflShp) if pathOffsetGeom is False: PathLog.error('No profile geometry returned.') return list() - profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, offsetPoints=True)] + profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, offsetPoints)] geoScan = list() if obj.ProfileEdges != 'Only': - if self.showDebugObjects is True: + if self.showDebugObjects: F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutArea') F.Shape = cmpdShp F.purgeTouched() self.tempGroup.addObject(F) # get internal path geometry and perform OCL scan with that geometry - pathGeom = self._planarMakePathGeom(obj, cmpdShp) + PGG = PathSurfaceSupport.PathGeometryGenerator(obj, cmpdShp, obj.CutPattern) + if self.showDebugObjects: + PGG.setDebugObjectsGroup(self.tempGroup) + self.tmpCOM = PGG.getCenterOfMass() + pathGeom = PGG.getPathGeometryGenerator() if pathGeom is False: PathLog.error('No path geometry returned.') return list() - geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, offsetPoints=False) + if obj.CutPattern == 'Offset': + offsetPoints = True + useGeom = self._offsetFacesToPointData(obj, pathGeom, profile=False) + if useGeom is False: + PathLog.error('No profile geometry returned.') + return list() + geoScan = [self._planarPerformOclScan(obj, pdc, useGeom, offsetPoints)] + else: + geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, offsetPoints) if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last'] SCANDATA.extend(profScan) @@ -1696,305 +1714,33 @@ class ObjectSurface(PathOp.ObjectOp): return final - def _planarMakePathGeom(self, obj, faceShp): - '''_planarMakePathGeom(obj, faceShp)... - Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. - The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' - PathLog.debug('_planarMakePathGeom()') - GeoSet = list() - - def getSpiralPoint(move, b, radAng): - x = b * radAng * math.cos(radAng) - y = b * radAng * math.sin(radAng) - return FreeCAD.Vector(x, y, 0.0).add(move) - - def getOppositeSpiralPoint(move, b, radAng): - x = b * radAng * math.cos(radAng) - y = b * radAng * math.sin(radAng) - return FreeCAD.Vector(-1 * x, y, 0.0).add(move) - - # Apply drop cutter extra offset and set the max and min XY area of the operation - xmin = faceShp.BoundBox.XMin - xmax = faceShp.BoundBox.XMax - ymin = faceShp.BoundBox.YMin - ymax = faceShp.BoundBox.YMax - zmin = faceShp.BoundBox.ZMin - zmax = faceShp.BoundBox.ZMax - - # Compute weighted center of mass of all faces combined - fCnt = 0 - totArea = 0.0 - zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) - for F in faceShp.Faces: - comF = F.CenterOfMass - areaF = F.Area - totArea += areaF - fCnt += 1 - zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) - if fCnt == 0: - PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) - zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) - else: - avgArea = totArea / fCnt - zeroCOM.multiply(1 / fCnt) - zeroCOM.multiply(1 / avgArea) - COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) - - # get X, Y, Z spans; Compute center of rotation - deltaX = abs(xmax-xmin) - deltaY = abs(ymax-ymin) - deltaC = math.sqrt(deltaX**2 + deltaY**2) - lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - halfLL = math.ceil(lineLen / 2.0) - cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - halfPasses = math.ceil(cutPasses / 2.0) - bbC = faceShp.BoundBox.Center - - # Generate the line/circle sets to be intersected with the cut-face-area - if obj.CutPattern in ['ZigZag', 'Line']: - centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model - cAng = math.atan(deltaX / deltaY) # BoundaryBox angle - - # Determine end points and create top lines - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - diag = None - if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: - diag = deltaY - elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: - diag = deltaX - else: - perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC - diag = perpDist - y1 = centRot.y + diag - # y2 = y1 - - # Create end points for set of lines to intersect with cross-section face - pntTuples = list() - for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - y1 = centRot.y + (lc * self.cutOut) - # y2 = y1 - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - pntTuples.append( (p1, p2) ) - - # Convert end points to lines - for (p1, p2) in pntTuples: - line = Part.makeLine(p1, p2) - GeoSet.append(line) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - zTgt = faceShp.BoundBox.ZMin - axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) - cntr = FreeCAD.Placement() - cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) - - if obj.CircularCenterAt == 'CenterOfMass': - cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass - elif obj.CircularCenterAt == 'CenterOfBoundBox': - cent = faceShp.BoundBox.Center - cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) - elif obj.CircularCenterAt == 'XminYmin': - cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) - elif obj.CircularCenterAt == 'Custom': - newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) - cntr.Base = newCent - - # recalculate cutPasses value, if need be - radialPasses = halfPasses - if obj.CircularCenterAt != 'CenterOfBoundBox': - # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center - EBB = faceShp.BoundBox - CORNERS = [ - FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), - FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), - ] - dMax = 0.0 - for c in range(0, 4): - dist = CORNERS[c].sub(cntr.Base).Length - if dist > dMax: - dMax = dist - lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - - # Update COM point and current CircularCenter - if obj.CircularCenterAt != 'Custom': - obj.CircularCenterCustom = cntr.Base - - minRad = self.cutter.getDiameter() * 0.45 - siX3 = 3 * obj.SampleInterval.Value - minRadSI = (siX3 / 2.0) / math.pi - if minRad < minRadSI: - minRad = minRadSI - - # Make small center circle to start pattern - if obj.StepOver > 50: - circle = Part.makeCircle(minRad, cntr.Base) - GeoSet.append(circle) - - for lc in range(1, radialPasses + 1): - rad = (lc * self.cutOut) - if rad >= minRad: - circle = Part.makeCircle(rad, cntr.Base) - GeoSet.append(circle) - # Efor - COM = cntr.Base - elif obj.CutPattern in ['Spiral']: - SEGS = list() - loopRadians = 0.0 # Used to keep track of complete loops/cycles - sumRadians = 0.0 - loopCnt = 0 - segCnt = 0 - twoPi = 2.0 * math.pi - maxDist = halfLL - move = COM # FreeCAD.Vector(0.0, 0.0, 0.0) # Use to translate the center of the spiral - - # Set tool properties and calculate cutout - effectiveCut = self.cutter.getDiameter() * float(obj.StepOver) / 100.0 - cutOut = effectiveCut / twoPi - - segLen = obj.SampleInterval.Value # CutterDiameter / 10.0 # SampleInterval.Value - stepAng = segLen / ((loopCnt + 1) * effectiveCut) # math.pi / 18.0 # 10 degrees - stopRadians = maxDist / cutOut - - draw = True - lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0) - if obj.CutPatternReversed: - if obj.CutMode == 'Conventional': - while draw: - radAng = sumRadians + stepAng - p1 = lastPoint - p2 = getOppositeSpiralPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng - sumRadians += stepAng # Increment sumRadians - loopRadians += stepAng # Increment loopRadians - if loopRadians > twoPi: - loopCnt += 1 - loopRadians -= twoPi - stepAng = segLen / ((loopCnt + 1) * effectiveCut) # adjust stepAng with each loop/cycle - segCnt += 1 - lastPoint = p2 - if sumRadians > stopRadians: - draw = False - # Create line and show in Object tree - lineSeg = Part.makeLine(p2, p1) - SEGS.append(lineSeg) - else: - while draw: - radAng = sumRadians + stepAng - p1 = lastPoint - p2 = getSpiralPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng - sumRadians += stepAng # Increment sumRadians - loopRadians += stepAng # Increment loopRadians - if loopRadians > twoPi: - loopCnt += 1 - loopRadians -= twoPi - stepAng = segLen / ((loopCnt + 1) * effectiveCut) # adjust stepAng with each loop/cycle - segCnt += 1 - lastPoint = p2 - if sumRadians > stopRadians: - draw = False - # Create line and show in Object tree - lineSeg = Part.makeLine(p2, p1) - SEGS.append(lineSeg) - # Eif - SEGS.reverse() - else: - if obj.CutMode == 'Climb': - while draw: - radAng = sumRadians + stepAng - p1 = lastPoint - p2 = getOppositeSpiralPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng - sumRadians += stepAng # Increment sumRadians - loopRadians += stepAng # Increment loopRadians - if loopRadians > twoPi: - loopCnt += 1 - loopRadians -= twoPi - stepAng = segLen / ((loopCnt + 1) * effectiveCut) # adjust stepAng with each loop/cycle - segCnt += 1 - lastPoint = p2 - if sumRadians > stopRadians: - draw = False - # Create line and show in Object tree - lineSeg = Part.makeLine(p1, p2) - SEGS.append(lineSeg) - else: - while draw: - radAng = sumRadians + stepAng - p1 = lastPoint - p2 = getSpiralPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng - sumRadians += stepAng # Increment sumRadians - loopRadians += stepAng # Increment loopRadians - if loopRadians > twoPi: - loopCnt += 1 - loopRadians -= twoPi - stepAng = segLen / ((loopCnt + 1) * effectiveCut) # adjust stepAng with each loop/cycle - segCnt += 1 - lastPoint = p2 - if sumRadians > stopRadians: - draw = False - # Create line and show in Object tree - lineSeg = Part.makeLine(p1, p2) - SEGS.append(lineSeg) - # Eif - spiral = Part.Wire([ls.Edges[0] for ls in SEGS]) - GeoSet.append(spiral) - elif obj.CutPattern in ['Offset']: - pass - # Eif - - if obj.CutPatternReversed is True: - GeoSet.reverse() - - if faceShp.BoundBox.ZMin != 0.0: - faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) - - # Create compound object to bind all lines in Lineset - geomShape = Part.makeCompound(GeoSet) - - # Position and rotate the Line and ZigZag geometry - if obj.CutPattern in ['Line', 'ZigZag']: - if obj.CutPatternAngle != 0.0: - geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) - geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') - F.Shape = geomShape - F.purgeTouched() - self.tempGroup.addObject(F) - - # Identify intersection of cross-section face and lineset - cmnShape = faceShp.common(geomShape) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') - F.Shape = cmnShape - F.purgeTouched() - self.tempGroup.addObject(F) - - self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) - return cmnShape - - def _planarMakeProfileGeom(self, obj, subShp): - PathLog.debug('_planarMakeProfileGeom()') + def _offsetFacesToPointData(self, obj, subShp, profile=True): + PathLog.debug('_offsetFacesToPointData()') offsetLists = list() dist = obj.SampleInterval.Value / 5.0 # defl = obj.SampleInterval.Value / 5.0 - # Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939 - for fc in subShp.Faces: + if not profile: # Reverse order of wires in each face - inside to outside - for w in range(len(fc.Wires) - 1, -1, -1): - W = fc.Wires[w] + for w in range(len(subShp.Wires) - 1, -1, -1): + W = subShp.Wires[w] PNTS = W.discretize(Distance=dist) # PNTS = W.discretize(Deflection=defl) - if self.CutClimb is True: + if self.CutClimb: PNTS.reverse() offsetLists.append(PNTS) + else: + # Reference https://forum.freecadweb.org/viewtopic.php?t=28861#p234939 + for fc in subShp.Faces: + # Reverse order of wires in each face - inside to outside + for w in range(len(fc.Wires) - 1, -1, -1): + W = fc.Wires[w] + PNTS = W.discretize(Distance=dist) + # PNTS = W.discretize(Deflection=defl) + if self.CutClimb: + PNTS.reverse() + offsetLists.append(PNTS) return offsetLists @@ -2005,7 +1751,7 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.debug('_planarPerformOclScan()') SCANS = list() - if offsetPoints is True: + if offsetPoints or obj.CutPattern == 'Offset': PNTSET = self._pathGeomToOffsetPointSet(obj, pathGeom) for D in PNTSET: stpOvr = list() From 0bd0d52bafca26340aaff9a43871f900605ec2a1 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 09:59:08 -0500 Subject: [PATCH 060/142] Path: Simplify `if ... is True:` statements. --- src/Mod/Path/PathScripts/PathSurface.py | 72 ++++++++++++------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 49d22d66ce..5c741c7029 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -28,7 +28,7 @@ from __future__ import print_function __title__ = "Path Surface Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" -__doc__ = "Class and implementation of Mill Facing operation." +__doc__ = "Class and implementation of 3D Surface operation." __contributors__ = "russ4262 (Russell Johnson)" import FreeCAD @@ -797,7 +797,7 @@ class ObjectSurface(PathOp.ObjectOp): cont = False if cont: - if self.showDebugObjects is True: + if self.showDebugObjects: T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') T.Shape = cfsL T.purgeTouched() @@ -819,7 +819,7 @@ class ObjectSurface(PathOp.ObjectOp): casL = ifL[0] else: casL = Part.makeCompound(ifL) - if self.showDebugObjects is True: + if self.showDebugObjects: C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') C.Shape = casL C.purgeTouched() @@ -934,7 +934,7 @@ class ObjectSurface(PathOp.ObjectOp): else: avoid = Part.makeCompound(outFCS) - if self.showDebugObjects is True: + if self.showDebugObjects: PathLog.debug('*** tmpAvoidArea') P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') P.Shape = avoid @@ -942,7 +942,7 @@ class ObjectSurface(PathOp.ObjectOp): self.tempGroup.addObject(P) if cont: - if self.showDebugObjects is True: + if self.showDebugObjects: PathLog.debug('*** tmpVoidCompound') P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') P.Shape = avoid @@ -1338,7 +1338,7 @@ class ObjectSurface(PathOp.ObjectOp): slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) fL.append(slc) comp = Part.makeCompound(fL) - if self.showDebugObjects is True: + if self.showDebugObjects: PathLog.debug('*** tmpSliceCompound') P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') P.Shape = comp @@ -1529,7 +1529,7 @@ class ObjectSurface(PathOp.ObjectOp): fused = Part.makeCompound(fuseShapes) - if self.showDebugObjects is True: + if self.showDebugObjects: T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') T.Shape = fused T.purgeTouched() @@ -1865,10 +1865,10 @@ class ObjectSurface(PathOp.ObjectOp): pnt = OS[1] for v in range(1, lenOS): nxt = OS[v + 1] - if optimize is True: + if optimize: # iPOL = prev.isOnLineSegment(nxt, pnt) iPOL = pnt.isOnLineSegment(prev, nxt) - if iPOL is True: + if iPOL: pnt = nxt else: tup = ((prev.x, prev.y), (pnt.x, pnt.y)) @@ -1880,7 +1880,7 @@ class ObjectSurface(PathOp.ObjectOp): LINES.append(tup) prev = pnt pnt = nxt - if iPOL is True: + if iPOL: tup = ((prev.x, prev.y), (pnt.x, pnt.y)) LINES.append(tup) # Efor @@ -1904,7 +1904,7 @@ class ObjectSurface(PathOp.ObjectOp): edg0 = compGeoShp.Edges[0] p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if cutClimb is True: + if cutClimb: tup = (p2, p1) lst = FreeCAD.Vector(p1[0], p1[1], 0.0) else: @@ -1923,32 +1923,32 @@ class ObjectSurface(PathOp.ObjectOp): cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) # iC = sp.isOnLineSegment(ep, cp) iC = cp.isOnLineSegment(sp, ep) - if iC is True: + if iC: inLine.append('BRK') chkGap = True else: - if cutClimb is True: + if cutClimb: inLine.reverse() LINES.append(inLine) # Save inLine segments lnCnt += 1 inLine = list() # reset collinear container - if cutClimb is True: + if cutClimb: sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) else: sp = ep - if cutClimb is True: + if cutClimb: tup = (v2, v1) - if chkGap is True: + if chkGap: gap = abs(toolDiam - lst.sub(ep).Length) lst = cp else: tup = (v1, v2) - if chkGap is True: + if chkGap: gap = abs(toolDiam - lst.sub(cp).Length) lst = ep - if chkGap is True: + if chkGap: if gap < obj.GapThreshold.Value: b = inLine.pop() # pop off 'BRK' marker (vA, vB) = inLine.pop() # pop off previous line segment for combining with current @@ -1963,12 +1963,12 @@ class ObjectSurface(PathOp.ObjectOp): inLine.append(tup) # Efor lnCnt += 1 - if cutClimb is True: + if cutClimb: inLine.reverse() LINES.append(inLine) # Save inLine segments # Handle last inLine set, reversing it. - if obj.CutPatternReversed is True: + if obj.CutPatternReversed: if cpa != 0.0 and cpa % 90.0 == 0.0: F = LINES.pop(0) rev = list() @@ -2002,7 +2002,7 @@ class ObjectSurface(PathOp.ObjectOp): ec = len(compGeoShp.Edges) toolDiam = 2.0 * self.radius - if self.CutClimb is True: + if self.CutClimb: dirFlg = -1 else: dirFlg = 1 @@ -2029,7 +2029,7 @@ class ObjectSurface(PathOp.ObjectOp): ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point # iC = sp.isOnLineSegment(ep, cp) iC = cp.isOnLineSegment(sp, ep) - if iC is True: + if iC: inLine.append('BRK') chkGap = True gap = abs(toolDiam - lst.sub(cp).Length) @@ -2049,7 +2049,7 @@ class ObjectSurface(PathOp.ObjectOp): else: tup = (v2, v1) - if chkGap is True: + if chkGap: if gap < obj.GapThreshold.Value: b = inLine.pop() # pop off 'BRK' marker (vA, vB) = inLine.pop() # pop off previous line segment for combining with current @@ -2077,10 +2077,10 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.debug('Line count is ODD.') dirFlg = -1 * dirFlg if obj.CutPatternReversed is False: - if self.CutClimb is True: + if self.CutClimb: dirFlg = -1 * dirFlg - if obj.CutPatternReversed is True: + if obj.CutPatternReversed: dirFlg = -1 * dirFlg # Handle last inLine list @@ -2137,7 +2137,7 @@ class ObjectSurface(PathOp.ObjectOp): # Separate arc data into Loops and Arcs for ei in range(0, ec): edg = compGeoShp.Edges[ei] - if edg.Closed is True: + if edg.Closed: stpOvrEI.append(('L', ei, False)) else: if isSame is False: @@ -2151,7 +2151,7 @@ class ObjectSurface(PathOp.ObjectOp): if abs(sameRad - pnt.sub(COM).Length) > 0.00001: isSame = False - if isSame is True: + if isSame: segEI.append(ei) else: # Move co-radial arc segments @@ -2162,7 +2162,7 @@ class ObjectSurface(PathOp.ObjectOp): pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) sameRad = pnt.sub(COM).Length # Process trailing `segEI` data, if available - if isSame is True: + if isSame: stpOvrEI.append(['A', segEI, False]) # Identify adjacent arcs with y=0 start/end points that connect @@ -2279,15 +2279,15 @@ class ObjectSurface(PathOp.ObjectOp): cp = (COM.x, COM.y, 0.0) if dirFlg == 1: arc = (sp, ep, cp) - if chkGap is True: + if chkGap: gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) lst = ep else: arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - if chkGap is True: + if chkGap: gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) lst = sp - if chkGap is True: + if chkGap: if gap < obj.GapThreshold.Value: PRTS.pop() # pop off 'BRK' marker (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current @@ -2403,7 +2403,7 @@ class ObjectSurface(PathOp.ObjectOp): lenSCANDATA = len(SCANDATA) gDIR = ['G3', 'G2'] - if self.CutClimb is True: + if self.CutClimb: gDIR = ['G2', 'G3'] # Set `ProfileEdges` specific trigger indexes @@ -2433,7 +2433,7 @@ class ObjectSurface(PathOp.ObjectOp): if so > 0: if obj.CutPattern == 'CircularZigZag': - if odd is True: + if odd: odd = False else: odd = True @@ -2462,7 +2462,7 @@ class ObjectSurface(PathOp.ObjectOp): cmds.extend(self._planarSinglepassProcess(obj, prt)) elif obj.CutPattern in ['Circular', 'CircularZigZag'] and obj.CircularUseG2G3 is True and lenPrt > 2: (rtnVal, gcode) = self._arcsToG2G3(prt, lenPrt, odd, gDIR, tolrnc) - if rtnVal is True: + if rtnVal: cmds.extend(gcode) else: cmds.extend(self._planarSinglepassProcess(obj, prt)) @@ -2528,7 +2528,7 @@ class ObjectSurface(PathOp.ObjectOp): lenSCANDATA = len(SCANDATA) gDIR = ['G3', 'G2'] - if self.CutClimb is True: + if self.CutClimb: gDIR = ['G2', 'G3'] # Set `ProfileEdges` specific trigger indexes @@ -2571,7 +2571,7 @@ class ObjectSurface(PathOp.ObjectOp): prt = SO[i] lenPrt = len(prt) if prt == 'BRK': - if brkFlg is True: + if brkFlg: ADJPRTS.append(prt) LMAX.append(prt) brkFlg = False From 341da1092d08da1246bedb236fce1e74690e349a Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 16 Apr 2020 00:20:41 -0500 Subject: [PATCH 061/142] Path: Move more common methods to PathSurfaceSupport module --- src/Mod/Path/PathScripts/PathSurface.py | 1513 +--------------- .../Path/PathScripts/PathSurfaceSupport.py | 1560 ++++++++++++++++- 2 files changed, 1564 insertions(+), 1509 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 5c741c7029..9c6f2d3c0c 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -44,7 +44,6 @@ except ImportError: # import sys # sys.exit(msg) -import MeshPart import Path import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils @@ -52,12 +51,10 @@ import PathScripts.PathOp as PathOp import PathScripts.PathSurfaceSupport as PathSurfaceSupport import time import math -import Part # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') -Draft = LazyLoader('Draft', globals(), 'Draft') Part = LazyLoader('Part', globals(), 'Part') if FreeCAD.GuiUp: @@ -95,7 +92,7 @@ class ObjectSurface(PathOp.ObjectOp): if not hasattr(obj, 'DoNotSetDefaultValues'): self.setEditorProperties(obj) - def initOpProperties(self, obj): + def initOpProperties(self, obj, warn=False): '''initOpProperties(obj) ... create operation specific properties''' missing = list() @@ -103,17 +100,17 @@ class ObjectSurface(PathOp.ObjectOp): if not hasattr(obj, nm): obj.addProperty(prtyp, nm, grp, tt) missing.append(nm) - newPropMsg = translate('PathSurface', 'New property added: ') + nm + '. ' - newPropMsg += translate('PathSurface', 'Check its default value.') - PathLog.warning(newPropMsg) + if warn: + newPropMsg = translate('PathSurface', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. ' + newPropMsg += translate('PathSurface', 'Check its default value.') + PathLog.warning(newPropMsg) # Set enumeration lists for enumeration properties if len(missing) > 0: ENUMS = self.propertyEnumerations() for n in ENUMS: if n in missing: - cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) - exec(cmdStr) + setattr(obj, n, ENUMS[n]) self.addedAllProperties = True @@ -162,10 +159,6 @@ class ObjectSurface(PathOp.ObjectOp): ("App::PropertyEnumeration", "BoundBox", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation.")), - ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), - ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")), ("App::PropertyEnumeration", "CutMode", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), ("App::PropertyEnumeration", "CutPattern", "Clearing Options", @@ -178,6 +171,10 @@ class ObjectSurface(PathOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), ("App::PropertyEnumeration", "LayerMode", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), + ("App::PropertyVectorDistance", "PatternCenterCustom", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for the cut pattern.")), + ("App::PropertyEnumeration", "PatternCenterAt", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the cut pattern.")), ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), ("App::PropertyDistance", "SampleInterval", "Clearing Options", @@ -206,7 +203,7 @@ class ObjectSurface(PathOp.ObjectOp): # Enumeration lists for App::PropertyEnumeration properties return { 'BoundBox': ['BaseBoundBox', 'Stock'], - 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], + 'PatternCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], 'CutMode': ['Conventional', 'Climb'], 'CutPattern': ['Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'ZigZagOffset', 'Grid', 'Triangle'] 'DropCutterDir': ['X', 'Y'], @@ -224,7 +221,7 @@ class ObjectSurface(PathOp.ObjectOp): P2 = R0 = 2 # 2 = hide if obj.ScanType == 'Planar': # if obj.CutPattern in ['Line', 'ZigZag']: - if obj.CutPattern in ['Circular', 'CircularZigZag']: + if obj.CutPattern in ['Circular', 'CircularZigZag', 'Spiral']: P0 = 2 P2 = 0 elif obj.CutPattern == 'Offset': @@ -240,8 +237,8 @@ class ObjectSurface(PathOp.ObjectOp): obj.setEditorMode('CutterTilt', R0) obj.setEditorMode('CutPattern', R2) obj.setEditorMode('CutPatternAngle', P0) - obj.setEditorMode('CircularCenterAt', P2) - obj.setEditorMode('CircularCenterCustom', P2) + obj.setEditorMode('PatternCenterAt', P2) + obj.setEditorMode('PatternCenterCustom', P2) def onChanged(self, obj, prop): if hasattr(self, 'addedAllProperties'): @@ -252,7 +249,7 @@ class ObjectSurface(PathOp.ObjectOp): self.setEditorProperties(obj) def opOnDocumentRestored(self, obj): - self.initOpProperties(obj) + self.initOpProperties(obj, warn=True) if PathLog.getLevel(PathLog.thisModule()) != 4: obj.setEditorMode('ShowTempObjects', 2) # hide @@ -266,12 +263,9 @@ class ObjectSurface(PathOp.ObjectOp): if hasattr(obj, n): val = obj.getPropertyByName(n) restore = True - cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) - exec(cmdStr) + setattr(obj, n, ENUMS[n]) if restore: - cmdStr = 'obj.{}={}'.format(n, "'" + val + "'") - exec(cmdStr) - + setattr(obj, n, val) self.setEditorProperties(obj) @@ -297,7 +291,7 @@ class ObjectSurface(PathOp.ObjectOp): obj.CutMode = 'Conventional' obj.CutPattern = 'Line' obj.HandleMultipleFeatures = 'Collectively' # 'Individually' - obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' + obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' obj.GapSizes = 'No gaps identified.' obj.StepOver = 100 obj.CutPatternAngle = 0.0 @@ -308,9 +302,9 @@ class ObjectSurface(PathOp.ObjectOp): obj.BoundaryAdjustment.Value = 0.0 obj.InternalFeaturesAdjustment.Value = 0.0 obj.AvoidLastX_Faces = 0 - obj.CircularCenterCustom.x = 0.0 - obj.CircularCenterCustom.y = 0.0 - obj.CircularCenterCustom.z = 0.0 + obj.PatternCenterCustom.x = 0.0 + obj.PatternCenterCustom.y = 0.0 + obj.PatternCenterCustom.z = 0.0 obj.GapThreshold.Value = 0.005 obj.AngularDeflection.Value = 0.25 obj.LinearDeflection.Value = job.GeometryTolerance.Value @@ -413,6 +407,12 @@ class ObjectSurface(PathOp.ObjectOp): modelVisibility = list() FCAD = FreeCAD.ActiveDocument + try: + dotIdx = __name__.index('.') + 1 + except Exception: + dotIdx = 0 + self.module = __name__[dotIdx:] + # Set debugging behavior self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects self.showDebugObjects = obj.ShowTempObjects @@ -483,14 +483,13 @@ class ObjectSurface(PathOp.ObjectOp): # Setup cutter for OCL and cutout value for operation - based on tool controller properties self.cutter = self.setOclCutter(obj) - self.safeCutter = self.setOclCutter(obj, safe=True) - if self.cutter is False or self.safeCutter is False: + if self.cutter is False: PathLog.error(translate('PathSurface', "Canceling 3D Surface operation. Error creating OCL cutter.")) return - toolDiam = self.cutter.getDiameter() - self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) - self.radius = toolDiam / 2.0 - self.gaps = [toolDiam, toolDiam, toolDiam] + self.toolDiam = self.cutter.getDiameter() + self.radius = self.toolDiam / 2.0 + self.cutOut = (self.toolDiam * (float(obj.StepOver) / 100.0)) + self.gaps = [self.toolDiam, self.toolDiam, self.toolDiam] # Get height offset values for later use self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value @@ -500,9 +499,6 @@ class ObjectSurface(PathOp.ObjectOp): self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 - # make circle for workplane - self.wpc = Part.makeCircle(2.0) - # Save model visibilities for restoration if FreeCAD.GuiUp: for m in range(0, len(JOB.Model.Group)): @@ -530,12 +526,18 @@ class ObjectSurface(PathOp.ObjectOp): # ###### MAIN COMMANDS FOR OPERATION ###### # Begin processing obj.Base data and creating GCode + PSF = PathSurfaceSupport.ProcessSelectedFaces(JOB, obj) + PSF.setShowDebugObjects(tempGroup, self.showDebugObjects) + PSF.radius = self.radius + PSF.depthParams = self.depthParams + pPM = PSF.preProcessModel(self.module) # Process selected faces, if available - pPM = self._preProcessModel(JOB, obj) if pPM is False: PathLog.error('Unable to pre-process obj.Base.') else: (FACES, VOIDS) = pPM + self.modelSTLs = PSF.modelSTLs + self.profileShapes = PSF.profileShapes # Create OCL.stl model objects self._prepareModelSTLs(JOB, obj) @@ -584,7 +586,7 @@ class ObjectSurface(PathOp.ObjectOp): # Provide user feedback for gap sizes gaps = list() for g in self.gaps: - if g != toolDiam: + if g != self.toolDiam: gaps.append(g) if len(gaps) > 0: obj.GapSizes = '{} mm'.format(gaps) @@ -608,7 +610,6 @@ class ObjectSurface(PathOp.ObjectOp): self.ClearHeightOffset = None self.depthParams = None self.midDep = None - self.wpc = None del self.modelSTLs del self.safeSTLs del self.modelTypes @@ -619,7 +620,6 @@ class ObjectSurface(PathOp.ObjectOp): del self.ClearHeightOffset del self.depthParams del self.midDep - del self.wpc execTime = time.time() - startTime PathLog.info('Operation time: {} sec.'.format(execTime)) @@ -627,818 +627,6 @@ class ObjectSurface(PathOp.ObjectOp): return True # Methods for constructing the cut area - def _preProcessModel(self, JOB, obj): - PathLog.debug('_preProcessModel()') - - FACES = list() - VOIDS = list() - fShapes = list() - vShapes = list() - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Crete place holders for each base model in Job - for m in range(0, lenGRP): - FACES.append(False) - VOIDS.append(False) - fShapes.append(False) - vShapes.append(False) - - checkBase = False - if obj.Base: - if len(obj.Base) > 0: - checkBase = True - if obj.ScanType == 'Rotational': - checkBase = False - PathLog.warning(translate('PathSurface', - 'Face selection is unavailable for Rotational scans. Ignoring selected faces.')) - - # The user has selected subobjects from the base. Pre-Process each. - if checkBase: - PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') - - (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) - - # Cycle through each base model, processing faces for each - for m in range(0, lenGRP): - base = GRP[m] - (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) - fShapes[m] = mFS - vShapes[m] = mVS - self.profileShapes[m] = mPS - else: - PathLog.debug(' -No obj.Base data.') - for m in range(0, lenGRP): - self.modelSTLs[m] = True - - # Process each model base, as a whole, as needed - # PathLog.debug(' -Pre-processing all models in Job.') - for m in range(0, lenGRP): - if fShapes[m] is False: - PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) - if obj.BoundBox == 'BaseBoundBox': - base = GRP[m] - elif obj.BoundBox == 'Stock': - base = JOB.Stock - - pPEB = self._preProcessEntireBase(obj, base, m) - if pPEB is False: - PathLog.error(' -Failed to pre-process base as a whole.') - else: - (fcShp, prflShp) = pPEB - if fcShp is not False: - if fcShp is True: - PathLog.debug(' -fcShp is True.') - fShapes[m] = True - else: - fShapes[m] = [fcShp] - if prflShp is not False: - if fcShp is not False: - PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) - if vShapes[m] is not False: - PathLog.debug(' -Cutting void from base profile shape.') - adjPS = prflShp.cut(vShapes[m][0]) - self.profileShapes[m] = [adjPS] - else: - PathLog.debug(' -vShapes[m] is False.') - self.profileShapes[m] = [prflShp] - else: - PathLog.debug(' -Saving base profile shape.') - self.profileShapes[m] = [prflShp] - PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) - # Efor - - return (fShapes, vShapes) - - def _identifyFacesAndVoids(self, JOB, obj, F, V): - TUPS = list() - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Separate selected faces into (base, face) tuples and flag model(s) for STL creation - for (bs, SBS) in obj.Base: - for sb in SBS: - # Flag model for STL creation - mdlIdx = None - for m in range(0, lenGRP): - if bs is GRP[m]: - self.modelSTLs[m] = True - mdlIdx = m - break - TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) - - # Apply `AvoidXFaces` value - faceCnt = len(TUPS) - add = faceCnt - obj.AvoidLastX_Faces - for bst in range(0, faceCnt): - (m, base, sub) = TUPS[bst] - shape = getattr(base.Shape, sub) - if isinstance(shape, Part.Face): - faceIdx = int(sub[4:]) - 1 - if bst < add: - if F[m] is False: - F[m] = list() - F[m].append((shape, faceIdx)) - else: - if V[m] is False: - V[m] = list() - V[m].append((shape, faceIdx)) - return (F, V) - - def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): - mFS = False - mVS = False - mPS = False - mIFS = list() - - if FACES[m] is not False: - isHole = False - if obj.HandleMultipleFeatures == 'Collectively': - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - outFCS = list() - - # Get collective envelope slice of selected faces - for (fcshp, fcIdx) in FACES[m]: - fNum = fcIdx + 1 - fsL.append(fcshp) - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - PathLog.debug('Attempting to get cross-section of collective faces.') - if len(outFCS) == 0: - PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) - cont = False - else: - cfsL = Part.makeCompound(outFCS) - - # Handle profile edges request - if cont is True and obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(cfsL, ofstVal) - if psOfst is not False: - mPS = [psOfst] - if obj.ProfileEdges == 'Only': - mFS = True - cont = False - else: - PathLog.error(' -Failed to create profile geometry for selected faces.') - cont = False - - if cont: - if self.showDebugObjects: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') - T.Shape = cfsL - T.purgeTouched() - self.tempGroup.addObject(T) - - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(cfsL, ofstVal) - if faceOfstShp is False: - PathLog.error(' -Failed to create offset face.') - cont = False - - if cont: - lenIfL = len(ifL) - if obj.InternalFeaturesCut is False: - if lenIfL == 0: - PathLog.debug(' -No internal features saved.') - else: - if lenIfL == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - if self.showDebugObjects: - C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') - C.Shape = casL - C.purgeTouched() - self.tempGroup.addObject(C) - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - mFS = [faceOfstShp] - # Eif - - elif obj.HandleMultipleFeatures == 'Individually': - for (fcshp, fcIdx) in FACES[m]: - cont = True - ifL = list() # avoid shape list - fNum = fcIdx + 1 - outerFace = False - - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - cont = False - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - cont = False - outerFace = False - else: - ((otrFace, raised), intWires) = gFW - outerFace = otrFace - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - if outerFace is not False: - PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) - - if obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(outerFace, ofstVal) - if psOfst is not False: - if mPS is False: - mPS = list() - mPS.append(psOfst) - if obj.ProfileEdges == 'Only': - if mFS is False: - mFS = list() - mFS.append(True) - cont = False - else: - PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) - cont = False - - if cont: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(outerFace, ofstVal) - - lenIfl = len(ifL) - if obj.InternalFeaturesCut is False and lenIfl > 0: - if lenIfl == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - if mFS is False: - mFS = list() - mFS.append(faceOfstShp) - # Eif - # Efor - # Eif - # Eif - - if len(mIFS) > 0: - if mVS is False: - mVS = list() - for ifs in mIFS: - mVS.append(ifs) - - if VOIDS[m] is not False: - PathLog.debug('Processing avoid faces.') - cont = True - isHole = False - outFCS = list() - intFEAT = list() - - for (fcshp, fcIdx) in VOIDS[m]: - fNum = fcIdx + 1 - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) - cont = False - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.AvoidLastX_InternalFeatures is False: - if intWires is not False: - for (iFace, rsd) in intWires: - intFEAT.append(iFace) - - lenOtFcs = len(outFCS) - if lenOtFcs == 0: - cont = False - else: - if lenOtFcs == 1: - avoid = outFCS[0] - else: - avoid = Part.makeCompound(outFCS) - - if self.showDebugObjects: - PathLog.debug('*** tmpAvoidArea') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') - P.Shape = avoid - P.purgeTouched() - self.tempGroup.addObject(P) - - if cont: - if self.showDebugObjects: - PathLog.debug('*** tmpVoidCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') - P.Shape = avoid - P.purgeTouched() - self.tempGroup.addObject(P) - ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) - avdOfstShp = self._extractFaceOffset(avoid, ofstVal) - if avdOfstShp is False: - PathLog.error('Failed to create collective offset avoid face.') - cont = False - - if cont: - avdShp = avdOfstShp - - if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: - if len(intFEAT) > 1: - ifc = Part.makeCompound(intFEAT) - else: - ifc = intFEAT[0] - ofstVal = self._calculateOffsetValue(obj, isHole=True) - ifOfstShp = self._extractFaceOffset(ifc, ofstVal) - if ifOfstShp is False: - PathLog.error('Failed to create collective offset avoid internal features.') - else: - avdShp = avdOfstShp.cut(ifOfstShp) - - if mVS is False: - mVS = list() - mVS.append(avdShp) - - - return (mFS, mVS, mPS) - - def _getFaceWires(self, base, fcshp, fcIdx): - outFace = False - INTFCS = list() - fNum = fcIdx + 1 - warnFinDep = translate('PathSurface', 'Final Depth might need to be lower. Internal features detected in Face') - - PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) - WIRES = self._extractWiresFromFace(base, fcshp) - if WIRES is False: - PathLog.error('Failed to extract wires from Face{}'.format(fNum)) - return False - - # Process remaining internal features, adding to FCS list - lenW = len(WIRES) - for w in range(0, lenW): - (wire, rsd) = WIRES[w] - PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) - if wire.isClosed() is False: - PathLog.debug(' -wire is not closed.') - else: - slc = self._flattenWireToFace(wire) - if slc is False: - PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) - else: - if w == 0: - outFace = (slc, rsd) - else: - # add to VOIDS so cutter avoids area. - PathLog.warning(warnFinDep + str(fNum) + '.') - INTFCS.append((slc, rsd)) - if len(INTFCS) == 0: - return (outFace, False) - else: - return (outFace, INTFCS) - - def _preProcessEntireBase(self, obj, base, m): - cont = True - isHole = False - prflShp = False - # Create envelope, extract cross-section and make offset co-planar shape - # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) - - try: - baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as ee: - PathLog.error(str(ee)) - shell = base.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as eee: - PathLog.error(str(eee)) - cont = False - - if cont: - csFaceShape = self._getShapeSlice(baseEnv) - if csFaceShape is False: - PathLog.debug('_getShapeSlice(baseEnv) failed') - csFaceShape = self._getCrossSection(baseEnv) - if csFaceShape is False: - PathLog.debug('_getCrossSection(baseEnv) failed') - csFaceShape = self._getSliceFromEnvelope(baseEnv) - if csFaceShape is False: - PathLog.error('Failed to slice baseEnv shape.') - cont = False - - if cont is True and obj.ProfileEdges != 'None': - PathLog.debug(' -Attempting profile geometry for model base.') - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(csFaceShape, ofstVal) - if psOfst is not False: - if obj.ProfileEdges == 'Only': - return (True, psOfst) - prflShp = psOfst - else: - PathLog.error(' -Failed to create profile geometry.') - cont = False - - if cont: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOffsetShape = self._extractFaceOffset(csFaceShape, ofstVal) - if faceOffsetShape is False: - PathLog.error('_extractFaceOffset() failed.') - else: - faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) - return (faceOffsetShape, prflShp) - return False - - def _extractWiresFromFace(self, base, fc): - '''_extractWiresFromFace(base, fc) ... - Attempts to return all closed wires within a parent face, including the outer most wire of the parent. - The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). - ''' - PathLog.debug('_extractWiresFromFace()') - - WIRES = list() - lenWrs = len(fc.Wires) - PathLog.debug(' -Wire count: {}'.format(lenWrs)) - - def index0(tup): - return tup[0] - - # Cycle through wires in face - for w in range(0, lenWrs): - PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) - wire = fc.Wires[w] - checkEdges = False - cont = True - - # Check for closed edges (circles, ellipses, etc...) - for E in wire.Edges: - if E.isClosed() is True: - checkEdges = True - break - - if checkEdges is True: - PathLog.debug(' -checkEdges is True') - for e in range(0, len(wire.Edges)): - edge = wire.Edges[e] - if edge.isClosed() is True and edge.Mass > 0.01: - PathLog.debug(' -Found closed edge') - raised = False - ip = self._isPocket(base, fc, edge) - if ip is False: - raised = True - ebb = edge.BoundBox - eArea = ebb.XLength * ebb.YLength - F = Part.Face(Part.Wire([edge])) - WIRES.append((eArea, F.Wires[0], raised)) - cont = False - - if cont: - PathLog.debug(' -cont is True') - # If only one wire and not checkEdges, return first wire - if lenWrs == 1: - return [(wire, False)] - - raised = False - wbb = wire.BoundBox - wArea = wbb.XLength * wbb.YLength - if w > 0: - ip = self._isPocket(base, fc, wire) - if ip is False: - raised = True - WIRES.append((wArea, Part.Wire(wire.Edges), raised)) - - nf = len(WIRES) - if nf > 0: - PathLog.debug(' -number of wires found is {}'.format(nf)) - if nf == 1: - (area, W, raised) = WIRES[0] - owLen = fc.OuterWire.Length - wLen = W.Length - if abs(owLen - wLen) > 0.0000001: - OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) - return [(OW, False), (W, raised)] - else: - return [(W, raised)] - else: - sortedWIRES = sorted(WIRES, key=index0, reverse=True) - WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size - # Check if OuterWire is larger than largest in WRS list - (W, raised) = WRS[0] - owLen = fc.OuterWire.Length - wLen = W.Length - if abs(owLen - wLen) > 0.0000001: - OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) - WRS.insert(0, (OW, False)) - return WRS - - return False - - def _calculateOffsetValue(self, obj, isHole, isVoid=False): - '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. - Calculate the offset for the Path.Area() function.''' - JOB = PathUtils.findParentJob(obj) - tolrnc = JOB.GeometryTolerance.Value - - if isVoid is False: - if isHole is True: - offset = -1 * obj.InternalFeaturesAdjustment.Value - offset += self.radius + (tolrnc / 10.0) - else: - offset = -1 * obj.BoundaryAdjustment.Value - if obj.BoundaryEnforcement is True: - offset += self.radius + (tolrnc / 10.0) - else: - offset -= self.radius + (tolrnc / 10.0) - offset = 0.0 - offset - else: - offset = -1 * obj.BoundaryAdjustment.Value - offset += self.radius + (tolrnc / 10.0) - - return offset - - def _extractFaceOffset(self, fcShape, offset): - '''_extractFaceOffset(fcShape, offset) ... internal function. - Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. - Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' - PathLog.debug('_extractFaceOffset()') - - if fcShape.BoundBox.ZMin != 0.0: - fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) - - areaParams = {} - areaParams['Offset'] = offset - areaParams['Fill'] = 1 - areaParams['Coplanar'] = 0 - areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections - areaParams['Reorient'] = True - areaParams['OpenMode'] = 0 - areaParams['MaxArcPoints'] = 400 # 400 - areaParams['Project'] = True - - area = Path.Area() # Create instance of Area() class object - # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane - area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 - area.add(fcShape) - area.setParams(**areaParams) # set parameters - - offsetShape = area.getShape() - wCnt = len(offsetShape.Wires) - if wCnt == 0: - return False - elif wCnt == 1: - ofstFace = Part.Face(offsetShape.Wires[0]) - else: - W = list() - for wr in offsetShape.Wires: - W.append(Part.Face(wr)) - ofstFace = Part.makeCompound(W) - - return ofstFace # offsetShape - - def _isPocket(self, b, f, w): - '''_isPocket(b, f, w)... - Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. - Returns True if pocket, False if raised protrusion.''' - e = w.Edges[0] - for fi in range(0, len(b.Shape.Faces)): - face = b.Shape.Faces[fi] - for ei in range(0, len(face.Edges)): - edge = face.Edges[ei] - if e.isSame(edge) is True: - if f is face: - # Alternative: run loop to see if all edges are same - pass # same source face, look for another - else: - if face.CenterOfMass.z < f.CenterOfMass.z: - return True - return False - - def _flattenWireToFace(self, wire): - PathLog.debug('_flattenWireToFace()') - if wire.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - - # If wire is planar horizontal, convert to a face and return - if wire.BoundBox.ZLength == 0.0: - slc = Part.Face(wire) - return slc - - # Attempt to create a new wire for manipulation, if not, use original - newWire = Part.Wire(wire.Edges) - if newWire.isClosed() is True: - nWire = newWire - else: - PathLog.debug(' -newWire.isClosed() is False') - nWire = wire - - # Attempt extrusion, and then try a manual slice and then cross-section - ext = self._getExtrudedShape(nWire) - if ext is False: - PathLog.debug('_getExtrudedShape() failed') - else: - slc = self._getShapeSlice(ext) - if slc is not False: - return slc - cs = self._getCrossSection(ext, True) - if cs is not False: - return cs - - # Attempt creating an envelope, and then try a manual slice and then cross-section - env = self._getShapeEnvelope(nWire) - if env is False: - PathLog.debug('_getShapeEnvelope() failed') - else: - slc = self._getShapeSlice(env) - if slc is not False: - return slc - cs = self._getCrossSection(env, True) - if cs is not False: - return cs - - # Attempt creating a projection - slc = self._getProjectedFace(nWire) - if slc is False: - PathLog.debug('_getProjectedFace() failed') - else: - return slc - - return False - - def _getExtrudedShape(self, wire): - PathLog.debug('_getExtrudedShape()') - wBB = wire.BoundBox - extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 - - try: - # slower, but renders collective faces correctly. Method 5 in TESTING - shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) - except Exception as ee: - PathLog.error(' -extrude wire failed: \n{}'.format(ee)) - return False - - SHP = Part.makeSolid(shell) - return SHP - - def _getShapeSlice(self, shape): - PathLog.debug('_getShapeSlice()') - - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - xmin = bb.XMin - 1.0 - xmax = bb.XMax + 1.0 - ymin = bb.YMin - 1.0 - ymax = bb.YMax + 1.0 - p1 = FreeCAD.Vector(xmin, ymin, mid) - p2 = FreeCAD.Vector(xmax, ymin, mid) - p3 = FreeCAD.Vector(xmax, ymax, mid) - p4 = FreeCAD.Vector(xmin, ymax, mid) - - e1 = Part.makeLine(p1, p2) - e2 = Part.makeLine(p2, p3) - e3 = Part.makeLine(p3, p4) - e4 = Part.makeLine(p4, p1) - face = Part.Face(Part.Wire([e1, e2, e3, e4])) - fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area - sArea = shape.BoundBox.XLength * shape.BoundBox.YLength - midArea = (fArea + sArea) / 2.0 - - slcShp = shape.common(face) - slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength - - if slcArea < midArea: - for W in slcShp.Wires: - if W.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - if len(slcShp.Wires) == 1: - wire = slcShp.Wires[0] - slc = Part.Face(wire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - else: - fL = list() - for W in slcShp.Wires: - slc = Part.Face(W) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - fL.append(slc) - comp = Part.makeCompound(fL) - if self.showDebugObjects: - PathLog.debug('*** tmpSliceCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') - P.Shape = comp - P.purgeTouched() - self.tempGroup.addObject(P) - return comp - - PathLog.debug(' -slcArea !< midArea') - PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) - return False - - def _getProjectedFace(self, wire): - import Draft - PathLog.debug('_getProjectedFace()') - F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') - F.Shape = wire - F.purgeTouched() - self.tempGroup.addObject(F) - try: - prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) - prj.recompute() - prj.purgeTouched() - self.tempGroup.addObject(prj) - except Exception as ee: - PathLog.error(str(ee)) - return False - else: - pWire = Part.Wire(prj.Shape.Edges) - if pWire.isClosed() is False: - # PathLog.debug(' -pWire.isClosed() is False') - return False - slc = Part.Face(pWire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - - def _getCrossSection(self, shape, withExtrude=False): - PathLog.debug('_getCrossSection()') - wires = list() - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - - for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): - wires.append(i) - - if len(wires) > 0: - comp = Part.Compound(wires) # produces correct cross-section wire ! - comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) - csWire = comp.Wires[0] - if csWire.isClosed() is False: - PathLog.debug(' -comp.Wires[0] is not closed') - return False - if withExtrude is True: - ext = self._getExtrudedShape(csWire) - CS = self._getShapeSlice(ext) - if CS is False: - return False - else: - CS = Part.Face(csWire) - CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) - return CS - else: - PathLog.debug(' -No wires from .slice() method') - - return False - - def _getShapeEnvelope(self, shape): - PathLog.debug('_getShapeEnvelope()') - - wBB = shape.BoundBox - extFwd = wBB.ZLength + 10.0 - minz = wBB.ZMin - maxz = wBB.ZMin + extFwd - stpDwn = (maxz - minz) / 4.0 - dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) - - try: - env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape - except Exception as ee: - PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) - return False - else: - return env - - def _getSliceFromEnvelope(self, env): - PathLog.debug('_getSliceFromEnvelope()') - eBB = env.BoundBox - extFwd = eBB.ZLength + 10.0 - maxz = eBB.ZMin + extFwd - - emax = math.floor(maxz - 1.0) - E = list() - for e in range(0, len(env.Edges)): - emin = env.Edges[e].BoundBox.ZMin - if emin > emax: - E.append(env.Edges[e]) - tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) - tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) - - return tf - def _prepareModelSTLs(self, JOB, obj): PathLog.debug('_prepareModelSTLs()') for m in range(0, len(JOB.Model.Group)): @@ -1446,26 +634,26 @@ class ObjectSurface(PathOp.ObjectOp): # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") if self.modelTypes[m] == 'M': - #TODO: test if this works + # TODO: test if this works facets = M.Mesh.Facets.Points else: - facets = Part.getFacets(M.Shape) + facets = Part.getFacets(M.Shape) if self.modelSTLs[m] is True: stl = ocl.STLSurf() - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - self.modelSTLs[m] = stl + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + self.modelSTLs[m] = stl return def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... Creates and OCL.stl object with combined data with waste stock, - model, and avoided faces. Travel lines can be checked against this + model, and avoided faces. Travel lines can be checked against this STL object to determine minimum travel height to clear stock and model.''' PathLog.debug('_makeSafeSTL()') @@ -1484,7 +672,7 @@ class ObjectSurface(PathOp.ObjectOp): zmax = mBB.ZMin + extFwd stpDwn = (zmax - zmin) / 4.0 dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) - + try: envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape cont = True @@ -1622,14 +810,11 @@ class ObjectSurface(PathOp.ObjectOp): lenDP = len(depthparams) # Prepare PathDropCutter objects with STL data - pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value) - safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], - depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) + pdc = self._planarGetPDC(self.modelSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter) + safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter) profScan = list() - offsetPoints = False if obj.ProfileEdges != 'None': - offsetPoints = True prflShp = self.profileShapes[mdlIdx][fsi] if prflShp is False: PathLog.error('No profile shape is False.') @@ -1644,7 +829,7 @@ class ObjectSurface(PathOp.ObjectOp): if pathOffsetGeom is False: PathLog.error('No profile geometry returned.') return list() - profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, offsetPoints)] + profScan = [self._planarPerformOclScan(obj, pdc, pathOffsetGeom, True)] geoScan = list() if obj.ProfileEdges != 'Only': @@ -1657,20 +842,19 @@ class ObjectSurface(PathOp.ObjectOp): PGG = PathSurfaceSupport.PathGeometryGenerator(obj, cmpdShp, obj.CutPattern) if self.showDebugObjects: PGG.setDebugObjectsGroup(self.tempGroup) - self.tmpCOM = PGG.getCenterOfMass() - pathGeom = PGG.getPathGeometryGenerator() + self.tmpCOM = PGG.getCenterOfPattern() + pathGeom = PGG.generatePathGeometry() if pathGeom is False: PathLog.error('No path geometry returned.') return list() if obj.CutPattern == 'Offset': - offsetPoints = True useGeom = self._offsetFacesToPointData(obj, pathGeom, profile=False) if useGeom is False: PathLog.error('No profile geometry returned.') return list() - geoScan = [self._planarPerformOclScan(obj, pdc, useGeom, offsetPoints)] + geoScan = [self._planarPerformOclScan(obj, pdc, useGeom, True)] else: - geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, offsetPoints) + geoScan = self._planarPerformOclScan(obj, pdc, pathGeom, False) if obj.ProfileEdges == 'Only': # ['None', 'Only', 'First', 'Last'] SCANDATA.extend(profScan) @@ -1752,7 +936,7 @@ class ObjectSurface(PathOp.ObjectOp): SCANS = list() if offsetPoints or obj.CutPattern == 'Offset': - PNTSET = self._pathGeomToOffsetPointSet(obj, pathGeom) + PNTSET = PathSurfaceSupport.pathGeomToOffsetPointSet(obj, pathGeom) for D in PNTSET: stpOvr = list() ofst = list() @@ -1768,36 +952,29 @@ class ObjectSurface(PathOp.ObjectOp): if len(ofst) > 0: stpOvr.append(ofst) SCANS.extend(stpOvr) - elif obj.CutPattern == 'Line': + elif obj.CutPattern in ['Line', 'Spiral', 'ZigZag']: stpOvr = list() - PNTSET = self._pathGeomToLinesPointSet(obj, pathGeom) - for D in PNTSET: - for I in D: - if I == 'BRK': - stpOvr.append(I) + if obj.CutPattern == 'Line': + PNTSET = PathSurfaceSupport.pathGeomToLinesPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps) + elif obj.CutPattern == 'ZigZag': + PNTSET = PathSurfaceSupport.pathGeomToZigzagPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps) + elif obj.CutPattern == 'Spiral': + PNTSET = PathSurfaceSupport.pathGeomToSpiralPointSet(obj, pathGeom) + + for STEP in PNTSET: + for LN in STEP: + if LN == 'BRK': + stpOvr.append(LN) else: # D format is ((p1, p2), (p3, p4)) - (A, B) = I - stpOvr.append(self._planarDropCutScan(pdc, A, B)) - SCANS.append(stpOvr) - stpOvr = list() - elif obj.CutPattern == 'ZigZag': - stpOvr = list() - PNTSET = self._pathGeomToZigzagPointSet(obj, pathGeom) - for (dirFlg, LNS) in PNTSET: - for SEG in LNS: - if SEG == 'BRK': - stpOvr.append(SEG) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = SEG + (A, B) = LN stpOvr.append(self._planarDropCutScan(pdc, A, B)) SCANS.append(stpOvr) stpOvr = list() elif obj.CutPattern in ['Circular', 'CircularZigZag']: # PNTSET is list, by stepover. # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) - PNTSET = self._pathGeomToArcPointSet(obj, pathGeom) + PNTSET = PathSurfaceSupport.pathGeomToCircularPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps, self.tmpCOM) for so in range(0, len(PNTSET)): stpOvr = list() @@ -1823,541 +1000,10 @@ class ObjectSurface(PathOp.ObjectOp): stpOvr.append(scan) if erFlg is False: SCANS.append(stpOvr) - elif obj.CutPattern == 'Spiral': - stpOvr = list() - PNTSET = self._pathGeomToSpiralPointSet(obj, pathGeom) - for D in PNTSET: - for I in D: - if I == 'BRK': - stpOvr.append(I) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = I - stpOvr.append(self._planarDropCutScan(pdc, A, B)) - SCANS.append(stpOvr) - stpOvr = list() # Eif return SCANS - def _pathGeomToOffsetPointSet(self, obj, compGeoShp): - '''_pathGeomToOffsetPointSet(obj, compGeoShp)... - Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.''' - PathLog.debug('_pathGeomToOffsetPointSet()') - - LINES = list() - optimize = obj.OptimizeLinearPaths - ofstCnt = len(compGeoShp) - - # Cycle through offeset loops - for ei in range(0, ofstCnt): - OS = compGeoShp[ei] - lenOS = len(OS) - - if ei > 0: - LINES.append('BRK') - - fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z) - OS.append(fp) - - # Cycle through points in each loop - prev = OS[0] - pnt = OS[1] - for v in range(1, lenOS): - nxt = OS[v + 1] - if optimize: - # iPOL = prev.isOnLineSegment(nxt, pnt) - iPOL = pnt.isOnLineSegment(prev, nxt) - if iPOL: - pnt = nxt - else: - tup = ((prev.x, prev.y), (pnt.x, pnt.y)) - LINES.append(tup) - prev = pnt - pnt = nxt - else: - tup = ((prev.x, prev.y), (pnt.x, pnt.y)) - LINES.append(tup) - prev = pnt - pnt = nxt - if iPOL: - tup = ((prev.x, prev.y), (pnt.x, pnt.y)) - LINES.append(tup) - # Efor - - return [LINES] - - def _pathGeomToLinesPointSet(self, obj, compGeoShp): - '''_pathGeomToLinesPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' - PathLog.debug('_pathGeomToLinesPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - chkGap = False - lnCnt = 0 - ec = len(compGeoShp.Edges) - cutClimb = self.CutClimb - toolDiam = 2.0 * self.radius - cpa = obj.CutPatternAngle - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if cutClimb: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - else: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - inLine.append(tup) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - - for ei in range(1, ec): - chkGap = False - edg = compGeoShp.Edges[ei] # Get edge for vertexes - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 - - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) - # iC = sp.isOnLineSegment(ep, cp) - iC = cp.isOnLineSegment(sp, ep) - if iC: - inLine.append('BRK') - chkGap = True - else: - if cutClimb: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - lnCnt += 1 - inLine = list() # reset collinear container - if cutClimb: - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - else: - sp = ep - - if cutClimb: - tup = (v2, v1) - if chkGap: - gap = abs(toolDiam - lst.sub(ep).Length) - lst = cp - else: - tup = (v1, v2) - if chkGap: - gap = abs(toolDiam - lst.sub(cp).Length) - lst = ep - - if chkGap: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - tup = (vA, tup[1]) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - if cutClimb: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - - # Handle last inLine set, reversing it. - if obj.CutPatternReversed: - if cpa != 0.0 and cpa % 90.0 == 0.0: - F = LINES.pop(0) - rev = list() - for iL in F: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - rev.reverse() - LINES.insert(0, rev) - - isEven = lnCnt % 2 - if isEven == 0: - PathLog.debug('Line count is ODD.') - else: - PathLog.debug('Line count is even.') - - return LINES - - def _pathGeomToZigzagPointSet(self, obj, compGeoShp): - '''_pathGeomToZigzagPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings - with a ZigZag directional indicator included for each collinear group.''' - PathLog.debug('_pathGeomToZigzagPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - lnCnt = 0 - chkGap = False - ec = len(compGeoShp.Edges) - toolDiam = 2.0 * self.radius - - if self.CutClimb: - dirFlg = -1 - else: - dirFlg = 1 - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if dirFlg == 1: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - else: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point - inLine.append(tup) - - for ei in range(1, ec): - edg = compGeoShp.Edges[ei] - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) - - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - # iC = sp.isOnLineSegment(ep, cp) - iC = cp.isOnLineSegment(sp, ep) - if iC: - inLine.append('BRK') - chkGap = True - gap = abs(toolDiam - lst.sub(cp).Length) - else: - chkGap = False - if dirFlg == -1: - inLine.reverse() - LINES.append((dirFlg, inLine)) - lnCnt += 1 - dirFlg = -1 * dirFlg # Change zig to zag - inLine = list() # reset collinear container - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - - lst = ep - if dirFlg == 1: - tup = (v1, v2) - else: - tup = (v2, v1) - - if chkGap: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - if dirFlg == 1: - tup = (vA, tup[1]) - else: - #tup = (vA, tup[1]) - #tup = (tup[1], vA) - tup = (tup[0], vB) - self.closedGap = True - else: - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - - # Fix directional issue with LAST line when line count is even - isEven = lnCnt % 2 - if isEven == 0: # Changed to != with 90 degree CutPatternAngle - PathLog.debug('Line count is even.') - else: - PathLog.debug('Line count is ODD.') - dirFlg = -1 * dirFlg - if obj.CutPatternReversed is False: - if self.CutClimb: - dirFlg = -1 * dirFlg - - if obj.CutPatternReversed: - dirFlg = -1 * dirFlg - - # Handle last inLine list - if dirFlg == 1: - rev = list() - for iL in inLine: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - - if obj.CutPatternReversed is False: - rev.reverse() - else: - rev2 = list() - for iL in rev: - if iL == 'BRK': - rev2.append(iL) - else: - (p1, p2) = iL - rev2.append((p2, p1)) - rev2.reverse() - rev = rev2 - - LINES.append((dirFlg, rev)) - else: - LINES.append((dirFlg, inLine)) - - return LINES - - def _pathGeomToArcPointSet(self, obj, compGeoShp): - '''_pathGeomToArcPointSet(obj, compGeoShp)... - Convert a compound set of arcs/circles to a set of directionally-oriented arc end points - and the corresponding center point.''' - # Extract intersection line segments for return value as list() - PathLog.debug('_pathGeomToArcPointSet()') - ARCS = list() - stpOvrEI = list() - segEI = list() - isSame = False - sameRad = None - COM = self.tmpCOM - toolDiam = 2.0 * self.radius - ec = len(compGeoShp.Edges) - - def gapDist(sp, ep): - X = (ep[0] - sp[0])**2 - Y = (ep[1] - sp[1])**2 - # Z = (ep[2] - sp[2])**2 - # return math.sqrt(X + Y + Z) - return math.sqrt(X + Y) # the 'z' value is zero in both points - - # Separate arc data into Loops and Arcs - for ei in range(0, ec): - edg = compGeoShp.Edges[ei] - if edg.Closed: - stpOvrEI.append(('L', ei, False)) - else: - if isSame is False: - segEI.append(ei) - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - else: - # Check if arc is co-radial to current SEGS - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - if abs(sameRad - pnt.sub(COM).Length) > 0.00001: - isSame = False - - if isSame: - segEI.append(ei) - else: - # Move co-radial arc segments - stpOvrEI.append(['A', segEI, False]) - # Start new list of arc segments - segEI = [ei] - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - # Process trailing `segEI` data, if available - if isSame: - stpOvrEI.append(['A', segEI, False]) - - # Identify adjacent arcs with y=0 start/end points that connect - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'A': - startOnAxis = list() - endOnAxis = list() - EI = SO[1] # list of corresponding compGeoShp.Edges indexes - - # Identify startOnAxis and endOnAxis arcs - for i in range(0, len(EI)): - ei = EI[i] # edge index - E = compGeoShp.Edges[ei] # edge object - if abs(COM.y - E.Vertexes[0].Y) < 0.00001: - startOnAxis.append((i, ei, E.Vertexes[0])) - elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: - endOnAxis.append((i, ei, E.Vertexes[1])) - - # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected - lenSOA = len(startOnAxis) - lenEOA = len(endOnAxis) - if lenSOA > 0 and lenEOA > 0: - for soa in range(0, lenSOA): - (iS, eiS, vS) = startOnAxis[soa] - for eoa in range(0, len(endOnAxis)): - (iE, eiE, vE) = endOnAxis[eoa] - dist = vE.X - vS.X - if abs(dist) < 0.00001: # They connect on axis at same radius - SO[2] = (eiE, eiS) - break - elif dist > 0: - break # stop searching - # Eif - # Eif - # Efor - - # Construct arc data tuples for OCL - dirFlg = 1 - # cutPat = obj.CutPattern - if self.CutClimb is False: # True yields Climb when set to Conventional - dirFlg = -1 - - # Cycle through stepOver data - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'L': # L = Loop/Ring/Circle - lei = SO[1] # loop Edges index - v1 = compGeoShp.Edges[lei].Vertexes[0] - - space = obj.SampleInterval.Value / 2.0 - - p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) - sp = (v1.X, v1.Y, 0.0) - rad = p1.sub(COM).Length - spcRadRatio = space/rad - if spcRadRatio < 1.0: - tolrncAng = math.asin(spcRadRatio) - else: - tolrncAng = 0.999998 * math.pi - X = COM.x + (rad * math.cos(tolrncAng)) - Y = v1.Y - space # rad * math.sin(tolrncAng) - - sp = (v1.X, v1.Y, 0.0) - ep = (X, Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - ARCS.append(('L', dirFlg, [arc])) - else: # SO[0] == 'A' A = Arc - PRTS = list() - EI = SO[1] # list of corresponding Edges indexes - CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) - chkGap = False - lst = None - - if CONN is not False: - (iE, iS) = CONN - v1 = compGeoShp.Edges[iE].Vertexes[0] - v2 = compGeoShp.Edges[iS].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - lst = sp - PRTS.append(arc) - # Pop connected edge index values from arc segments index list - iEi = EI.index(iE) - iSi = EI.index(iS) - if iEi > iSi: - EI.pop(iEi) - EI.pop(iSi) - else: - EI.pop(iSi) - EI.pop(iEi) - if len(EI) > 0: - PRTS.append('BRK') - chkGap = True - cnt = 0 - for ei in EI: - if cnt > 0: - PRTS.append('BRK') - chkGap = True - v1 = compGeoShp.Edges[ei].Vertexes[0] - v2 = compGeoShp.Edges[ei].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - if chkGap: - gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - if chkGap: - gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) - lst = sp - if chkGap: - if gap < obj.GapThreshold.Value: - PRTS.pop() # pop off 'BRK' marker - (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current - arc = (vA, arc[1], vC) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - PRTS.append(arc) - cnt += 1 - - if dirFlg == -1: - PRTS.reverse() - - ARCS.append(('A', dirFlg, PRTS)) - # Eif - if obj.CutPattern == 'CircularZigZag': - dirFlg = -1 * dirFlg - # Efor - - return ARCS - - def _pathGeomToSpiralPointSet(self, obj, compGeoShp): - '''_pathGeomToSpiralPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directional, connected groupings.''' - PathLog.debug('_pathGeomToSpiralPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - lnCnt = 0 - ec = len(compGeoShp.Edges) - start = 2 - - if obj.CutPatternReversed: - edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail - ec -= 1 - start = 1 - else: - edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail - p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0) - p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0) - tup = ((p1.x, p1.y), (p2.x, p2.y)) - inLine.append(tup) - lst = p2 - - for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1 - edg = compGeoShp.Edges[ei] # Get edge for vertexes - sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point) - ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point - tup = ((sp.x, sp.y), (ep.x, ep.y)) - - if sp.sub(p2).Length < 0.000001: - inLine.append(tup) - else: - LINES.append(inLine) # Save inLine segments - lnCnt += 1 - inLine = list() # reset container - inLine.append(tup) - p1 = sp - p2 = ep - # Efor - - lnCnt += 1 - LINES.append(inLine) # Save inLine segments - - return LINES - def _planarDropCutScan(self, pdc, A, B): #PNTS = list() (x1, y1) = A @@ -2389,10 +1035,7 @@ class ObjectSurface(PathOp.ObjectOp): CLP = pdc.getCLPoints() # Convert OCL object data to FreeCAD vectors - for p in CLP: - PNTS.append(FreeCAD.Vector(p.x, p.y, p.z)) - - return PNTS + return [FreeCAD.Vector(p.x, p.y, p.z) for p in CLP] # Main planar scan functions def _planarDropCutSingle(self, JOB, obj, pdc, safePDC, depthparams, SCANDATA): @@ -2944,17 +1587,15 @@ class ObjectSurface(PathOp.ObjectOp): for pt in range(0, numPts): SCANDATA[s][prt][pt].z += DepthOffset - def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): + def _planarGetPDC(self, stl, finalDep, SampleInterval, cutter): pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object pdc.setSTL(stl) # add stl model - if useSafeCutter is True: - pdc.setCutter(self.safeCutter) # add safeCutter - else: - pdc.setCutter(self.cutter) # add cutter + pdc.setCutter(cutter) # add cutter pdc.setZ(finalDep) # set minimumZ (final / target depth value) pdc.setSampling(SampleInterval) # set sampling size return pdc + # Main rotational scan functions def _processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None): PathLog.debug('_processRotationalOp(self, JOB, obj, mdlIdx, compoundFaces=None)') @@ -3529,7 +2170,7 @@ class ObjectSurface(PathOp.ObjectOp): def SetupProperties(): ''' SetupProperties() ... Return list of properties required for operation.''' setup = ['AvoidLastX_Faces', 'AvoidLastX_InternalFeatures', 'BoundBox'] - setup.extend(['BoundaryAdjustment', 'CircularCenterAt', 'CircularCenterCustom']) + setup.extend(['BoundaryAdjustment', 'PatternCenterAt', 'PatternCenterCustom']) setup.extend(['CircularUseG2G3', 'InternalFeaturesCut', 'InternalFeaturesAdjustment']) setup.extend(['CutMode', 'CutPattern', 'CutPatternAngle', 'CutPatternReversed']) setup.extend(['CutterTilt', 'DepthOffset', 'DropCutterDir', 'GapSizes', 'GapThreshold']) diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py index 68abd2abc5..e991b28163 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceSupport.py +++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py @@ -28,6 +28,7 @@ __title__ = "Path Surface Support Module" __author__ = "russ4262 (Russell Johnson)" __url__ = "http://www.freecadweb.org" __doc__ = "Support functions and classes for 3D Surface and Waterline operations." +# __name__ = "PathSurfaceSupport" __contributors__ = "" import FreeCAD @@ -53,8 +54,8 @@ class PathGeometryGenerator: PathGeometryGenerator(obj, shape, pattern) `obj` is the operation object, `shape` is the horizontal planar shape object, and `pattern` is the name of the geometric pattern to apply. - Frist, call the getCenterOfMass() method for the CenterOfMass for patterns allowing a custom center. - Next, call the getPathGeometryGenerator() method to request the path geometry shape.''' + Frist, call the getCenterOfPattern() method for the CenterOfMass for patterns allowing a custom center. + Next, call the generatePathGeometry() method to request the path geometry shape.''' # Register valid patterns here by name # Create a corresponding processing method below. Precede the name with an underscore(_) @@ -64,11 +65,12 @@ class PathGeometryGenerator: '''__init__(obj, shape, pattern)... Instantiate PathGeometryGenerator class. Required arguments are the operation object, horizontal planar shape, and pattern name.''' self.debugObjectsGroup = False - self.pattern = None + self.pattern = 'None' self.shape = None self.pathGeometry = None self.rawGeoList = None self.centerOfMass = None + self.centerofPattern = None self.deltaX = None self.deltaY = None self.deltaC = None @@ -86,7 +88,7 @@ class PathGeometryGenerator: if shape.BoundBox.ZMin != 0.0: shape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - shape.BoundBox.ZMin)) - if shape.BoundBox.ZMax == 0.0: + if shape.BoundBox.ZLength == 0.0: self.shape = shape else: PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax)) @@ -101,23 +103,30 @@ class PathGeometryGenerator: ymax = self.shape.BoundBox.YMax # Compute weighted center of mass of all faces combined - fCnt = 0 - totArea = 0.0 - zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) - for F in self.shape.Faces: - comF = F.CenterOfMass - areaF = F.Area - totArea += areaF - fCnt += 1 - zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) - if fCnt == 0: - PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) - zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) + if self.pattern in ['Circular', 'CircularZigZag', 'Spiral']: + if self.obj.PatternCenterAt == 'CenterOfMass': + fCnt = 0 + totArea = 0.0 + zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) + for F in self.shape.Faces: + comF = F.CenterOfMass + areaF = F.Area + totArea += areaF + fCnt += 1 + zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) + if fCnt == 0: + PathLog.error(translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.')) + bbC = self.shape.BoundBox.Center + zeroCOM = FreeCAD.Vector(bbC.x, bbC.y, 0.0) + else: + avgArea = totArea / fCnt + zeroCOM.multiply(1 / fCnt) + zeroCOM.multiply(1 / avgArea) + self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + self.centerOfPattern = self._getPatternCenter() else: - avgArea = totArea / fCnt - zeroCOM.multiply(1 / fCnt) - zeroCOM.multiply(1 / avgArea) - self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + bbC = self.shape.BoundBox.Center + self.centerOfPattern = FreeCAD.Vector(bbC.x, bbC.y, 0.0) # get X, Y, Z spans; Compute center of rotation self.deltaX = self.shape.BoundBox.XLength @@ -134,15 +143,15 @@ class PathGeometryGenerator: Pass the temporary object group to show temporary construction objects''' self.debugObjectsGroup = tmpGrpObject - def getCenterOfMass(self): - '''getCenterOfMass()... + def getCenterOfPattern(self): + '''getCenterOfPattern()... Returns the Center Of Mass for the current class instance.''' - return self.centerOfMass + return self.centerOfPattern - def getPathGeometryGenerator(self): - '''getPathGeometryGenerator()... + def generatePathGeometry(self): + '''generatePathGeometry()... Call this function to obtain the path geometry shape, generated by this class.''' - if self.pattern is None: + if self.pattern == 'None': PathLog.warning('PGG: No pattern set.') return False @@ -167,7 +176,7 @@ class PathGeometryGenerator: geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) if self.debugObjectsGroup: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpGeometrySet') F.Shape = geomShape F.purgeTouched() self.debugObjectsGroup.addObject(F) @@ -179,73 +188,36 @@ class PathGeometryGenerator: cmnShape = self.shape.common(geomShape) if self.debugObjectsGroup: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpPathGeometry') F.Shape = cmnShape F.purgeTouched() self.debugObjectsGroup.addObject(F) - self.tmpCOM = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0) return cmnShape # Cut pattern methods def _Circular(self): GeoSet = list() - zTgt = 0.0 # self.shape.BoundBox.ZMin - centerAt = self.obj.CircularCenterAt - cntr = FreeCAD.Placement() - - if centerAt == 'CenterOfMass': - cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, zTgt) # self.centerOfMass # Use center of Mass - elif centerAt == 'CenterOfBoundBox': - cent = self.shape.BoundBox.Center - cntrPnt = FreeCAD.Vector(cent.x, cent.y, zTgt) - elif centerAt == 'XminYmin': - cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, zTgt) - elif centerAt == 'Custom': - newCent = FreeCAD.Vector(self.obj.CircularCenterCustom.x, self.obj.CircularCenterCustom.y, zTgt) - cntrPnt = newCent - - # recalculate number of passes, if need be - radialPasses = self.halfPasses - if centerAt != 'CenterOfBoundBox': - # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center - EBB = self.shape.BoundBox - CORNERS = [ - FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), - FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), - ] - dMax = 0.0 - for c in range(0, 4): - dist = CORNERS[c].sub(cntrPnt).Length - if dist > dMax: - dMax = dist - diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end - radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal - - # Update self.centerOfMass point and current CircularCenter - if centerAt != 'Custom': - self.obj.CircularCenterCustom = cntrPnt - + radialPasses = self._getRadialPasses() minRad = self.toolDiam * 0.45 siX3 = 3 * self.obj.SampleInterval.Value minRadSI = (siX3 / 2.0) / math.pi + if minRad < minRadSI: minRad = minRadSI + PathLog.debug(' -centerOfPattern: {}'.format(self.centerOfPattern)) # Make small center circle to start pattern if self.obj.StepOver > 50: - circle = Part.makeCircle(minRad, cntrPnt) + circle = Part.makeCircle(minRad, self.centerOfPattern) GeoSet.append(circle) for lc in range(1, radialPasses + 1): rad = (lc * self.cutOut) if rad >= minRad: - circle = Part.makeCircle(rad, cntrPnt) + circle = Part.makeCircle(rad, self.centerOfPattern) GeoSet.append(circle) # Efor - self.centerOfMass = cntrPnt self.rawGeoList = GeoSet def _CircularZigZag(self): @@ -279,7 +251,7 @@ class PathGeometryGenerator: # y2 = y1 p1 = FreeCAD.Vector(x1, y1, 0.0) p2 = FreeCAD.Vector(x2, y1, 0.0) - pntTuples.append( (p1, p2) ) + pntTuples.append((p1, p2)) # Convert end points to lines for (p1, p2) in pntTuples: @@ -300,8 +272,8 @@ class PathGeometryGenerator: loopCnt = 0 segCnt = 0 twoPi = 2.0 * math.pi - maxDist = self.halfDiag - move = self.centerOfMass # FreeCAD.Vector(0.0, 0.0, 0.0) # Use to translate the center of the spiral + maxDist = math.ceil(self.cutOut * self._getRadialPasses()) # self.halfDiag + move = self.centerOfPattern # Use to translate the center of the spiral lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0) # Set tool properties and calculate cutout @@ -369,6 +341,48 @@ class PathGeometryGenerator: self._Line() # Use _Line generator # Support methods + def _getPatternCenter(self): + centerAt = self.obj.PatternCenterAt + + if centerAt == 'CenterOfMass': + cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0) + elif centerAt == 'CenterOfBoundBox': + cent = self.shape.BoundBox.Center + cntrPnt = FreeCAD.Vector(cent.x, cent.y, 0.0) + elif centerAt == 'XminYmin': + cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, 0.0) + elif centerAt == 'Custom': + cntrPnt = FreeCAD.Vector(self.obj.PatternCenterCustom.x, self.obj.PatternCenterCustom.y, 0.0) + + # Update centerOfPattern point + if centerAt != 'Custom': + self.obj.PatternCenterCustom = cntrPnt + self.centerOfPattern = cntrPnt + + return cntrPnt + + def _getRadialPasses(self): + # recalculate number of passes, if need be + radialPasses = self.halfPasses + if self.obj.PatternCenterAt != 'CenterOfBoundBox': + # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center + EBB = self.shape.BoundBox + CORNERS = [ + FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), + FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), + ] + dMax = 0.0 + for c in range(0, 4): + dist = CORNERS[c].sub(self.centerOfPattern).Length + if dist > dMax: + dMax = dist + diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end + radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal + + return radialPasses + def _makeRegSpiralPnt(self, move, b, radAng): x = b * radAng * math.cos(radAng) y = b * radAng * math.sin(radAng) @@ -439,3 +453,1403 @@ class PathGeometryGenerator: return ofstFace # Eclass + + +class ProcessSelectedFaces: + """ProcessSelectedFaces(JOB, obj) class. + This class processes the `obj.Base` object for selected geometery. + Calling the preProcessModel(module) method returns + two compound objects as a tuple: (FACES, VOIDS) or False.""" + + def __init__(self, JOB, obj): + self.modelSTLs = list() + self.profileShapes = list() + self.tempGroup = False + self.showDebugObjects = False + self.checkBase = False + self.module = None + self.radius = None + self.depthParams = None + self.msgNoFaces = translate(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.') + self.JOB = JOB + self.obj = obj + self.profileEdges = 'None' + + if hasattr(obj, 'ProfileEdges'): + self.profileEdges = obj.ProfileEdges + + # Setup STL, model type, and bound box containers for each model in Job + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + self.modelSTLs.append(False) + self.profileShapes.append(False) + + # make circle for workplane + self.wpc = Part.makeCircle(2.0) + + def PathSurface(self): + if self.obj.Base: + if len(self.obj.Base) > 0: + self.checkBase = True + if self.obj.ScanType == 'Rotational': + self.checkBase = False + PathLog.warning(self.msgNoFaces) + + def PathWaterline(self): + if self.obj.Base: + if len(self.obj.Base) > 0: + self.checkBase = True + if self.obj.Algorithm in ['OCL Dropcutter', 'Experimental']: + self.checkBase = False + PathLog.warning(self.msgNoFaces) + + # public class methods + def setShowDebugObjects(self, grpObj, val): + self.tempGroup = grpObj + self.showDebugObjects = val + + def preProcessModel(self, module): + PathLog.debug('preProcessModel()') + + if not self._isReady(module): + return False + + FACES = list() + VOIDS = list() + fShapes = list() + vShapes = list() + GRP = self.JOB.Model.Group + lenGRP = len(GRP) + + # Crete place holders for each base model in Job + for m in range(0, lenGRP): + FACES.append(False) + VOIDS.append(False) + fShapes.append(False) + vShapes.append(False) + + # The user has selected subobjects from the base. Pre-Process each. + if self.checkBase: + PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') + + # (FACES, VOIDS) = self._identifyFacesAndVoids(FACES, VOIDS) + (F, V) = self._identifyFacesAndVoids(FACES, VOIDS) + + # Cycle through each base model, processing faces for each + for m in range(0, lenGRP): + base = GRP[m] + (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, m, FACES, VOIDS) + fShapes[m] = mFS + vShapes[m] = mVS + self.profileShapes[m] = mPS + else: + PathLog.debug(' -No obj.Base data.') + for m in range(0, lenGRP): + self.modelSTLs[m] = True + + # Process each model base, as a whole, as needed + # PathLog.debug(' -Pre-processing all models in Job.') + for m in range(0, lenGRP): + if fShapes[m] is False: + PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) + if self.obj.BoundBox == 'BaseBoundBox': + base = GRP[m] + elif self.obj.BoundBox == 'Stock': + base = self.JOB.Stock + + pPEB = self._preProcessEntireBase(base, m) + if pPEB is False: + PathLog.error(' -Failed to pre-process base as a whole.') + else: + (fcShp, prflShp) = pPEB + if fcShp is not False: + if fcShp is True: + PathLog.debug(' -fcShp is True.') + fShapes[m] = True + else: + fShapes[m] = [fcShp] + if prflShp is not False: + if fcShp is not False: + PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) + if vShapes[m] is not False: + PathLog.debug(' -Cutting void from base profile shape.') + adjPS = prflShp.cut(vShapes[m][0]) + self.profileShapes[m] = [adjPS] + else: + PathLog.debug(' -vShapes[m] is False.') + self.profileShapes[m] = [prflShp] + else: + PathLog.debug(' -Saving base profile shape.') + self.profileShapes[m] = [prflShp] + PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) + # Efor + + return (fShapes, vShapes) + + # private class methods + def _isReady(self, module): + '''_isReady(module)... Internal method. + Checks if required attributes are available for processing obj.Base (the Base Geometry).''' + if hasattr(self, module): + self.module = module + modMethod = getattr(self, module) # gets the attribute only + modMethod() # executes as method + else: + return False + + if not self.radius: + return False + + if not self.depthParams: + return False + + return True + + def _identifyFacesAndVoids(self, F, V): + TUPS = list() + GRP = self.JOB.Model.Group + lenGRP = len(GRP) + + # Separate selected faces into (base, face) tuples and flag model(s) for STL creation + for (bs, SBS) in self.obj.Base: + for sb in SBS: + # Flag model for STL creation + mdlIdx = None + for m in range(0, lenGRP): + if bs is GRP[m]: + self.modelSTLs[m] = True + mdlIdx = m + break + TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) + + # Apply `AvoidXFaces` value + faceCnt = len(TUPS) + add = faceCnt - self.obj.AvoidLastX_Faces + for bst in range(0, faceCnt): + (m, base, sub) = TUPS[bst] + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + faceIdx = int(sub[4:]) - 1 + if bst < add: + if F[m] is False: + F[m] = list() + F[m].append((shape, faceIdx)) + else: + if V[m] is False: + V[m] = list() + V[m].append((shape, faceIdx)) + return (F, V) + + def _preProcessFacesAndVoids(self, base, m, FACES, VOIDS): + mFS = False + mVS = False + mPS = False + mIFS = list() + + if FACES[m] is not False: + isHole = False + if self.obj.HandleMultipleFeatures == 'Collectively': + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + outFCS = list() + + # Get collective envelope slice of selected faces + for (fcshp, fcIdx) in FACES[m]: + fNum = fcIdx + 1 + fsL.append(fcshp) + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if self.obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + PathLog.debug('Attempting to get cross-section of collective faces.') + if len(outFCS) == 0: + PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) + cont = False + else: + cfsL = Part.makeCompound(outFCS) + + # Handle profile edges request + if cont is True and self.profileEdges != 'None': + ofstVal = self._calculateOffsetValue(isHole) + psOfst = extractFaceOffset(cfsL, ofstVal, self.wpc) + if psOfst is not False: + mPS = [psOfst] + if self.profileEdges == 'Only': + mFS = True + cont = False + else: + PathLog.error(' -Failed to create profile geometry for selected faces.') + cont = False + + if cont: + if self.showDebugObjects: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') + T.Shape = cfsL + T.purgeTouched() + self.tempGroup.addObject(T) + + ofstVal = self._calculateOffsetValue(isHole) + faceOfstShp = extractFaceOffset(cfsL, ofstVal, self.wpc) + if faceOfstShp is False: + PathLog.error(' -Failed to create offset face.') + cont = False + + if cont: + lenIfL = len(ifL) + if self.obj.InternalFeaturesCut is False: + if lenIfL == 0: + PathLog.debug(' -No internal features saved.') + else: + if lenIfL == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + if self.showDebugObjects: + C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') + C.Shape = casL + C.purgeTouched() + self.tempGroup.addObject(C) + ofstVal = self._calculateOffsetValue(isHole=True) + intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + mFS = [faceOfstShp] + # Eif + + elif self.obj.HandleMultipleFeatures == 'Individually': + for (fcshp, fcIdx) in FACES[m]: + cont = True + ifL = list() # avoid shape list + fNum = fcIdx + 1 + outerFace = False + + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + cont = False + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + cont = False + outerFace = False + else: + ((otrFace, raised), intWires) = gFW + outerFace = otrFace + if self.obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + if outerFace is not False: + PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) + + if self.profileEdges != 'None': + ofstVal = self._calculateOffsetValue(isHole) + psOfst = extractFaceOffset(outerFace, ofstVal, self.wpc) + if psOfst is not False: + if mPS is False: + mPS = list() + mPS.append(psOfst) + if self.profileEdges == 'Only': + if mFS is False: + mFS = list() + mFS.append(True) + cont = False + else: + PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(isHole) + faceOfstShp = extractFaceOffset(outerFace, ofstVal, self.wpc) + + lenIfl = len(ifL) + if self.obj.InternalFeaturesCut is False and lenIfl > 0: + if lenIfl == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + + ofstVal = self._calculateOffsetValue(isHole=True) + intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + if mFS is False: + mFS = list() + mFS.append(faceOfstShp) + # Eif + # Efor + # Eif + # Eif + + if len(mIFS) > 0: + if mVS is False: + mVS = list() + for ifs in mIFS: + mVS.append(ifs) + + if VOIDS[m] is not False: + PathLog.debug('Processing avoid faces.') + cont = True + isHole = False + outFCS = list() + intFEAT = list() + + for (fcshp, fcIdx) in VOIDS[m]: + fNum = fcIdx + 1 + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) + cont = False + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if self.obj.AvoidLastX_InternalFeatures is False: + if intWires is not False: + for (iFace, rsd) in intWires: + intFEAT.append(iFace) + + lenOtFcs = len(outFCS) + if lenOtFcs == 0: + cont = False + else: + if lenOtFcs == 1: + avoid = outFCS[0] + else: + avoid = Part.makeCompound(outFCS) + + if self.showDebugObjects: + PathLog.debug('*** tmpAvoidArea') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + + if cont: + if self.showDebugObjects: + PathLog.debug('*** tmpVoidCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + ofstVal = self._calculateOffsetValue(isHole, isVoid=True) + avdOfstShp = extractFaceOffset(avoid, ofstVal, self.wpc) + if avdOfstShp is False: + PathLog.error('Failed to create collective offset avoid face.') + cont = False + + if cont: + avdShp = avdOfstShp + + if self.obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: + if len(intFEAT) > 1: + ifc = Part.makeCompound(intFEAT) + else: + ifc = intFEAT[0] + ofstVal = self._calculateOffsetValue(isHole=True) + ifOfstShp = extractFaceOffset(ifc, ofstVal, self.wpc) + if ifOfstShp is False: + PathLog.error('Failed to create collective offset avoid internal features.') + else: + avdShp = avdOfstShp.cut(ifOfstShp) + + if mVS is False: + mVS = list() + mVS.append(avdShp) + + + return (mFS, mVS, mPS) + + def _getFaceWires(self, base, fcshp, fcIdx): + outFace = False + INTFCS = list() + fNum = fcIdx + 1 + warnFinDep = translate(self.module, 'Final Depth might need to be lower. Internal features detected in Face') + + PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) + WIRES = self._extractWiresFromFace(base, fcshp) + if WIRES is False: + PathLog.error('Failed to extract wires from Face{}'.format(fNum)) + return False + + # Process remaining internal features, adding to FCS list + lenW = len(WIRES) + for w in range(0, lenW): + (wire, rsd) = WIRES[w] + PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) + if wire.isClosed() is False: + PathLog.debug(' -wire is not closed.') + else: + slc = self._flattenWireToFace(wire) + if slc is False: + PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) + else: + if w == 0: + outFace = (slc, rsd) + else: + # add to VOIDS so cutter avoids area. + PathLog.warning(warnFinDep + str(fNum) + '.') + INTFCS.append((slc, rsd)) + if len(INTFCS) == 0: + return (outFace, False) + else: + return (outFace, INTFCS) + + def _preProcessEntireBase(self, base, m): + cont = True + isHole = False + prflShp = False + # Create envelope, extract cross-section and make offset co-planar shape + # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) + + try: + baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as ee: + PathLog.error(str(ee)) + shell = base.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as eee: + PathLog.error(str(eee)) + cont = False + + if cont: + csFaceShape = getShapeSlice(baseEnv) + if csFaceShape is False: + PathLog.debug('getShapeSlice(baseEnv) failed') + csFaceShape = getCrossSection(baseEnv) + if csFaceShape is False: + PathLog.debug('getCrossSection(baseEnv) failed') + csFaceShape = getSliceFromEnvelope(baseEnv) + if csFaceShape is False: + PathLog.error('Failed to slice baseEnv shape.') + cont = False + + if cont is True and self.profileEdges != 'None': + PathLog.debug(' -Attempting profile geometry for model base.') + ofstVal = self._calculateOffsetValue(isHole) + psOfst = extractFaceOffset(csFaceShape, ofstVal, self.wpc) + if psOfst is not False: + if self.profileEdges == 'Only': + return (True, psOfst) + prflShp = psOfst + else: + PathLog.error(' -Failed to create profile geometry.') + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(isHole) + faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc) + if faceOffsetShape is False: + PathLog.error('extractFaceOffset() failed.') + else: + faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) + return (faceOffsetShape, prflShp) + return False + + def _extractWiresFromFace(self, base, fc): + '''_extractWiresFromFace(base, fc) ... + Attempts to return all closed wires within a parent face, including the outer most wire of the parent. + The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). + ''' + PathLog.debug('_extractWiresFromFace()') + + WIRES = list() + lenWrs = len(fc.Wires) + PathLog.debug(' -Wire count: {}'.format(lenWrs)) + + def index0(tup): + return tup[0] + + # Cycle through wires in face + for w in range(0, lenWrs): + PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) + wire = fc.Wires[w] + checkEdges = False + cont = True + + # Check for closed edges (circles, ellipses, etc...) + for E in wire.Edges: + if E.isClosed() is True: + checkEdges = True + break + + if checkEdges is True: + PathLog.debug(' -checkEdges is True') + for e in range(0, len(wire.Edges)): + edge = wire.Edges[e] + if edge.isClosed() is True and edge.Mass > 0.01: + PathLog.debug(' -Found closed edge') + raised = False + ip = self._isPocket(base, fc, edge) + if ip is False: + raised = True + ebb = edge.BoundBox + eArea = ebb.XLength * ebb.YLength + F = Part.Face(Part.Wire([edge])) + WIRES.append((eArea, F.Wires[0], raised)) + cont = False + + if cont: + PathLog.debug(' -cont is True') + # If only one wire and not checkEdges, return first wire + if lenWrs == 1: + return [(wire, False)] + + raised = False + wbb = wire.BoundBox + wArea = wbb.XLength * wbb.YLength + if w > 0: + ip = self._isPocket(base, fc, wire) + if ip is False: + raised = True + WIRES.append((wArea, Part.Wire(wire.Edges), raised)) + + nf = len(WIRES) + if nf > 0: + PathLog.debug(' -number of wires found is {}'.format(nf)) + if nf == 1: + (area, W, raised) = WIRES[0] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + return [(OW, False), (W, raised)] + else: + return [(W, raised)] + else: + sortedWIRES = sorted(WIRES, key=index0, reverse=True) + WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + # Check if OuterWire is larger than largest in WRS list + (W, raised) = WRS[0] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + WRS.insert(0, (OW, False)) + return WRS + + return False + + def _calculateOffsetValue(self, isHole, isVoid=False): + '''_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function. + Calculate the offset for the Path.Area() function.''' + self.JOB = PathUtils.findParentJob(self.obj) + tolrnc = self.JOB.GeometryTolerance.Value + + if isVoid is False: + if isHole is True: + offset = -1 * self.obj.InternalFeaturesAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + else: + offset = -1 * self.obj.BoundaryAdjustment.Value + if self.obj.BoundaryEnforcement is True: + offset += self.radius + (tolrnc / 10.0) + else: + offset -= self.radius + (tolrnc / 10.0) + offset = 0.0 - offset + else: + offset = -1 * self.obj.BoundaryAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + + return offset + + def _isPocket(self, b, f, w): + '''_isPocket(b, f, w)... + Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. + Returns True if pocket, False if raised protrusion.''' + e = w.Edges[0] + for fi in range(0, len(b.Shape.Faces)): + face = b.Shape.Faces[fi] + for ei in range(0, len(face.Edges)): + edge = face.Edges[ei] + if e.isSame(edge) is True: + if f is face: + # Alternative: run loop to see if all edges are same + pass # same source face, look for another + else: + if face.CenterOfMass.z < f.CenterOfMass.z: + return True + return False + + def _flattenWireToFace(self, wire): + PathLog.debug('_flattenWireToFace()') + if wire.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + + # If wire is planar horizontal, convert to a face and return + if wire.BoundBox.ZLength == 0.0: + slc = Part.Face(wire) + return slc + + # Attempt to create a new wire for manipulation, if not, use original + newWire = Part.Wire(wire.Edges) + if newWire.isClosed() is True: + nWire = newWire + else: + PathLog.debug(' -newWire.isClosed() is False') + nWire = wire + + # Attempt extrusion, and then try a manual slice and then cross-section + ext = getExtrudedShape(nWire) + if ext is False: + PathLog.debug('getExtrudedShape() failed') + else: + slc = getShapeSlice(ext) + if slc is not False: + return slc + cs = getCrossSection(ext, True) + if cs is not False: + return cs + + # Attempt creating an envelope, and then try a manual slice and then cross-section + env = getShapeEnvelope(nWire) + if env is False: + PathLog.debug('getShapeEnvelope() failed') + else: + slc = getShapeSlice(env) + if slc is not False: + return slc + cs = getCrossSection(env, True) + if cs is not False: + return cs + + # Attempt creating a projection + slc = getProjectedFace(self.tempGroup, nWire) + if slc is False: + PathLog.debug('getProjectedFace() failed') + else: + return slc + + return False +# Eclass + + +# Functions for getting a shape envelope and cross-section +def getExtrudedShape(wire): + PathLog.debug('getExtrudedShape()') + wBB = wire.BoundBox + extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 + + try: + shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + except Exception as ee: + PathLog.error(' -extrude wire failed: \n{}'.format(ee)) + return False + + SHP = Part.makeSolid(shell) + return SHP + +def getShapeSlice(shape): + PathLog.debug('getShapeSlice()') + + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + xmin = bb.XMin - 1.0 + xmax = bb.XMax + 1.0 + ymin = bb.YMin - 1.0 + ymax = bb.YMax + 1.0 + p1 = FreeCAD.Vector(xmin, ymin, mid) + p2 = FreeCAD.Vector(xmax, ymin, mid) + p3 = FreeCAD.Vector(xmax, ymax, mid) + p4 = FreeCAD.Vector(xmin, ymax, mid) + + e1 = Part.makeLine(p1, p2) + e2 = Part.makeLine(p2, p3) + e3 = Part.makeLine(p3, p4) + e4 = Part.makeLine(p4, p1) + face = Part.Face(Part.Wire([e1, e2, e3, e4])) + fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area + sArea = shape.BoundBox.XLength * shape.BoundBox.YLength + midArea = (fArea + sArea) / 2.0 + + slcShp = shape.common(face) + slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength + + if slcArea < midArea: + for W in slcShp.Wires: + if W.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + if len(slcShp.Wires) == 1: + wire = slcShp.Wires[0] + slc = Part.Face(wire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + else: + fL = list() + for W in slcShp.Wires: + slc = Part.Face(W) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + fL.append(slc) + comp = Part.makeCompound(fL) + return comp + + # PathLog.debug(' -slcArea !< midArea') + # PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) + return False + +def getProjectedFace(tempGroup, wire): + import Draft + PathLog.debug('getProjectedFace()') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') + F.Shape = wire + F.purgeTouched() + tempGroup.addObject(F) + try: + prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) + prj.recompute() + prj.purgeTouched() + tempGroup.addObject(prj) + except Exception as ee: + PathLog.error(str(ee)) + return False + else: + pWire = Part.Wire(prj.Shape.Edges) + if pWire.isClosed() is False: + # PathLog.debug(' -pWire.isClosed() is False') + return False + slc = Part.Face(pWire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + +def getCrossSection(shape, withExtrude=False): + PathLog.debug('getCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) + csWire = comp.Wires[0] + if csWire.isClosed() is False: + PathLog.debug(' -comp.Wires[0] is not closed') + return False + if withExtrude is True: + ext = getExtrudedShape(csWire) + CS = getShapeSlice(ext) + if CS is False: + return False + else: + CS = Part.Face(csWire) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + +def getShapeEnvelope(shape): + PathLog.debug('getShapeEnvelope()') + + wBB = shape.BoundBox + extFwd = wBB.ZLength + 10.0 + minz = wBB.ZMin + maxz = wBB.ZMin + extFwd + stpDwn = (maxz - minz) / 4.0 + dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) + + try: + env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape + except Exception as ee: + PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) + return False + else: + return env + +def getSliceFromEnvelope(env): + PathLog.debug('getSliceFromEnvelope()') + eBB = env.BoundBox + extFwd = eBB.ZLength + 10.0 + maxz = eBB.ZMin + extFwd + + emax = math.floor(maxz - 1.0) + E = list() + for e in range(0, len(env.Edges)): + emin = env.Edges[e].BoundBox.ZMin + if emin > emax: + E.append(env.Edges[e]) + tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) + tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) + + return tf + + +# Function to extract offset face from shape +def extractFaceOffset(fcShape, offset, wpc, makeComp=True): + '''extractFaceOffset(fcShape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('extractFaceOffset()') + + if fcShape.BoundBox.ZMin != 0.0: + fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 # 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(wpc)) # Set working plane to normal at Z=1 + area.add(fcShape) + area.setParams(**areaParams) # set parameters + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + if not makeComp: + ofstFace = [ofstFace] + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + if makeComp: + ofstFace = Part.makeCompound(W) + else: + ofstFace = W + + return ofstFace # offsetShape + + +# Functions to convert path geometry into line/arc segments for OCL input or directly to g-code +def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps): + '''pathGeomToLinesPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' + PathLog.debug('pathGeomToLinesPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + chkGap = False + lnCnt = 0 + ec = len(compGeoShp.Edges) + cpa = obj.CutPatternAngle + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if cutClimb is True: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + else: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + inLine.append(tup) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + + for ei in range(1, ec): + chkGap = False + edg = compGeoShp.Edges[ei] # Get edge for vertexes + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 + + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + else: + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset collinear container + if cutClimb is True: + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + else: + sp = ep + + if cutClimb is True: + tup = (v2, v1) + if chkGap is True: + gap = abs(toolDiam - lst.sub(ep).Length) + lst = cp + else: + tup = (v1, v2) + if chkGap is True: + gap = abs(toolDiam - lst.sub(cp).Length) + lst = ep + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + tup = (vA, tup[1]) + closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < gaps[0]: + gaps.insert(0, gap) + gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + + # Handle last inLine set, reversing it. + if obj.CutPatternReversed is True: + if cpa != 0.0 and cpa % 90.0 == 0.0: + F = LINES.pop(0) + rev = list() + for iL in F: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + rev.reverse() + LINES.insert(0, rev) + + isEven = lnCnt % 2 + if isEven == 0: + PathLog.debug('Line count is ODD.') + else: + PathLog.debug('Line count is even.') + + return LINES + +def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps): + '''_pathGeomToZigzagPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings + with a ZigZag directional indicator included for each collinear group.''' + PathLog.debug('_pathGeomToZigzagPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + chkGap = False + ec = len(compGeoShp.Edges) + + if cutClimb is True: + dirFlg = -1 + else: + dirFlg = 1 + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if dirFlg == 1: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + else: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point + inLine.append(tup) + + for ei in range(1, ec): + edg = compGeoShp.Edges[ei] + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) + + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + gap = abs(toolDiam - lst.sub(cp).Length) + else: + chkGap = False + if dirFlg == -1: + inLine.reverse() + # LINES.append((dirFlg, inLine)) + LINES.append(inLine) + lnCnt += 1 + dirFlg = -1 * dirFlg # Change zig to zag + inLine = list() # reset collinear container + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + + lst = ep + if dirFlg == 1: + tup = (v1, v2) + else: + tup = (v2, v1) + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + if dirFlg == 1: + tup = (vA, tup[1]) + else: + tup = (tup[0], vB) + closedGap = True + else: + gap = round(gap, 6) + if gap < gaps[0]: + gaps.insert(0, gap) + gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + + # Fix directional issue with LAST line when line count is even + isEven = lnCnt % 2 + if isEven == 0: # Changed to != with 90 degree CutPatternAngle + PathLog.debug('Line count is even.') + else: + PathLog.debug('Line count is ODD.') + dirFlg = -1 * dirFlg + if obj.CutPatternReversed is False: + if cutClimb is True: + dirFlg = -1 * dirFlg + + if obj.CutPatternReversed: + dirFlg = -1 * dirFlg + + # Handle last inLine list + if dirFlg == 1: + rev = list() + for iL in inLine: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + + if not obj.CutPatternReversed: + rev.reverse() + else: + rev2 = list() + for iL in rev: + if iL == 'BRK': + rev2.append(iL) + else: + (p1, p2) = iL + rev2.append((p2, p1)) + rev2.reverse() + rev = rev2 + + # LINES.append((dirFlg, rev)) + LINES.append(rev) + else: + # LINES.append((dirFlg, inLine)) + LINES.append(inLine) + + return LINES + +def pathGeomToCircularPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps, COM): + '''pathGeomToCircularPointSet(obj, compGeoShp)... + Convert a compound set of arcs/circles to a set of directionally-oriented arc end points + and the corresponding center point.''' + # Extract intersection line segments for return value as list() + PathLog.debug('pathGeomToCircularPointSet()') + ARCS = list() + stpOvrEI = list() + segEI = list() + isSame = False + sameRad = None + ec = len(compGeoShp.Edges) + + def gapDist(sp, ep): + X = (ep[0] - sp[0])**2 + Y = (ep[1] - sp[1])**2 + return math.sqrt(X + Y) # the 'z' value is zero in both points + + # Separate arc data into Loops and Arcs + for ei in range(0, ec): + edg = compGeoShp.Edges[ei] + if edg.Closed is True: + stpOvrEI.append(('L', ei, False)) + else: + if isSame is False: + segEI.append(ei) + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + else: + # Check if arc is co-radial to current SEGS + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + if abs(sameRad - pnt.sub(COM).Length) > 0.00001: + isSame = False + + if isSame is True: + segEI.append(ei) + else: + # Move co-radial arc segments + stpOvrEI.append(['A', segEI, False]) + # Start new list of arc segments + segEI = [ei] + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + # Process trailing `segEI` data, if available + if isSame is True: + stpOvrEI.append(['A', segEI, False]) + + # Identify adjacent arcs with y=0 start/end points that connect + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'A': + startOnAxis = list() + endOnAxis = list() + EI = SO[1] # list of corresponding compGeoShp.Edges indexes + + # Identify startOnAxis and endOnAxis arcs + for i in range(0, len(EI)): + ei = EI[i] # edge index + E = compGeoShp.Edges[ei] # edge object + if abs(COM.y - E.Vertexes[0].Y) < 0.00001: + startOnAxis.append((i, ei, E.Vertexes[0])) + elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: + endOnAxis.append((i, ei, E.Vertexes[1])) + + # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected + lenSOA = len(startOnAxis) + lenEOA = len(endOnAxis) + if lenSOA > 0 and lenEOA > 0: + for soa in range(0, lenSOA): + (iS, eiS, vS) = startOnAxis[soa] + for eoa in range(0, len(endOnAxis)): + (iE, eiE, vE) = endOnAxis[eoa] + dist = vE.X - vS.X + if abs(dist) < 0.00001: # They connect on axis at same radius + SO[2] = (eiE, eiS) + break + elif dist > 0: + break # stop searching + # Eif + # Eif + # Efor + + # Construct arc data tuples for OCL + dirFlg = 1 + if not cutClimb: # True yields Climb when set to Conventional + dirFlg = -1 + + # Cycle through stepOver data + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'L': # L = Loop/Ring/Circle + # PathLog.debug("SO[0] == 'Loop'") + lei = SO[1] # loop Edges index + v1 = compGeoShp.Edges[lei].Vertexes[0] + + # space = obj.SampleInterval.Value / 10.0 + # space = 0.000001 + space = toolDiam * 0.005 # If too small, OCL will fail to scan the loop + + # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) + p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) # z=0.0 for waterline; z=v1.Z for 3D Surface + rad = p1.sub(COM).Length + spcRadRatio = space/rad + if spcRadRatio < 1.0: + tolrncAng = math.asin(spcRadRatio) + else: + tolrncAng = 0.99999998 * math.pi + EX = COM.x + (rad * math.cos(tolrncAng)) + EY = v1.Y - space # rad * math.sin(tolrncAng) + + sp = (v1.X, v1.Y, 0.0) + ep = (EX, EY, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + ARCS.append(('L', dirFlg, [arc])) + else: # SO[0] == 'A' A = Arc + # PathLog.debug("SO[0] == 'Arc'") + PRTS = list() + EI = SO[1] # list of corresponding Edges indexes + CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) + chkGap = False + lst = None + + if CONN is not False: + (iE, iS) = CONN + v1 = compGeoShp.Edges[iE].Vertexes[0] + v2 = compGeoShp.Edges[iS].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + lst = sp + PRTS.append(arc) + # Pop connected edge index values from arc segments index list + iEi = EI.index(iE) + iSi = EI.index(iS) + if iEi > iSi: + EI.pop(iEi) + EI.pop(iSi) + else: + EI.pop(iSi) + EI.pop(iEi) + if len(EI) > 0: + PRTS.append('BRK') + chkGap = True + cnt = 0 + for ei in EI: + if cnt > 0: + PRTS.append('BRK') + chkGap = True + v1 = compGeoShp.Edges[ei].Vertexes[0] + v2 = compGeoShp.Edges[ei].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) + lst = sp + if chkGap is True: + if gap < obj.GapThreshold.Value: + PRTS.pop() # pop off 'BRK' marker + (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current + arc = (vA, arc[1], vC) + closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < gaps[0]: + gaps.insert(0, gap) + gaps.pop() + PRTS.append(arc) + cnt += 1 + + if dirFlg == -1: + PRTS.reverse() + + ARCS.append(('A', dirFlg, PRTS)) + # Eif + if obj.CutPattern == 'CircularZigZag': + dirFlg = -1 * dirFlg + # Efor + + return ARCS + +def pathGeomToSpiralPointSet(obj, compGeoShp): + '''_pathGeomToSpiralPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directional, connected groupings.''' + PathLog.debug('_pathGeomToSpiralPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + ec = len(compGeoShp.Edges) + start = 2 + + if obj.CutPatternReversed: + edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail + ec -= 1 + start = 1 + else: + edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail + p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0) + p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0) + tup = ((p1.x, p1.y), (p2.x, p2.y)) + inLine.append(tup) + lst = p2 + + for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1 + edg = compGeoShp.Edges[ei] # Get edge for vertexes + sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point) + ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point + tup = ((sp.x, sp.y), (ep.x, ep.y)) + + if sp.sub(p2).Length < 0.000001: + inLine.append(tup) + else: + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset container + inLine.append(tup) + p1 = sp + p2 = ep + # Efor + + lnCnt += 1 + LINES.append(inLine) # Save inLine segments + + return LINES + +def pathGeomToOffsetPointSet(obj, compGeoShp): + '''pathGeomToOffsetPointSet(obj, compGeoShp)... + Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.''' + PathLog.debug('pathGeomToOffsetPointSet()') + + LINES = list() + optimize = obj.OptimizeLinearPaths + ofstCnt = len(compGeoShp) + + # Cycle through offeset loops + for ei in range(0, ofstCnt): + OS = compGeoShp[ei] + lenOS = len(OS) + + if ei > 0: + LINES.append('BRK') + + fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z) + OS.append(fp) + + # Cycle through points in each loop + prev = OS[0] + pnt = OS[1] + for v in range(1, lenOS): + nxt = OS[v + 1] + if optimize: + # iPOL = prev.isOnLineSegment(nxt, pnt) + iPOL = pnt.isOnLineSegment(prev, nxt) + if iPOL: + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + if iPOL: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + # Efor + + return [LINES] \ No newline at end of file From c0a5a8c97e84c2f96292c6fa3cd8c6e6d7846325 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Fri, 10 Apr 2020 22:51:49 -0500 Subject: [PATCH 062/142] Path: Improve Tasks editor interaction Swap setEnabled() method for show() and hide(). Include showing and hiding associated labels. Path: Hide `optimizeEnabled` input --- src/Mod/Path/PathScripts/PathWaterlineGui.py | 39 +++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py index eed15fc3d3..0616bbe6d2 100644 --- a/src/Mod/Path/PathScripts/PathWaterlineGui.py +++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py @@ -90,6 +90,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): else: self.form.optimizeEnabled.setCheckState(QtCore.Qt.Unchecked) + self.updateVisibility() + def getSignalsForUpdate(self, obj): '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' signals = [] @@ -106,21 +108,32 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): return signals def updateVisibility(self): - if self.form.algorithmSelect.currentText() == 'OCL Dropcutter': - self.form.cutPattern.setEnabled(False) - self.form.boundaryAdjustment.setEnabled(False) - self.form.stepOver.setEnabled(False) - self.form.sampleInterval.setEnabled(True) - self.form.optimizeEnabled.setEnabled(True) - else: - self.form.cutPattern.setEnabled(True) - self.form.boundaryAdjustment.setEnabled(True) + '''updateVisibility()... Updates visibility of Tasks panel objects.''' + Algorithm = self.form.algorithmSelect.currentText() + self.form.optimizeEnabled.hide() # Has no independent QLabel object + + if Algorithm == 'OCL Dropcutter': + self.form.cutPattern.hide() + self.form.cutPattern_label.hide() + self.form.boundaryAdjustment.hide() + self.form.boundaryAdjustment_label.hide() + self.form.stepOver.hide() + self.form.stepOver_label.hide() + self.form.sampleInterval.show() + self.form.sampleInterval_label.show() + elif Algorithm == 'Experimental': + self.form.cutPattern.show() + self.form.boundaryAdjustment.show() + self.form.cutPattern_label.show() + self.form.boundaryAdjustment_label.show() if self.form.cutPattern.currentText() == 'None': - self.form.stepOver.setEnabled(False) + self.form.stepOver.hide() + self.form.stepOver_label.hide() else: - self.form.stepOver.setEnabled(True) - self.form.sampleInterval.setEnabled(False) - self.form.optimizeEnabled.setEnabled(False) + self.form.stepOver.show() + self.form.stepOver_label.show() + self.form.sampleInterval.hide() + self.form.sampleInterval_label.hide() def registerSignalHandlers(self, obj): self.form.algorithmSelect.currentIndexChanged.connect(self.updateVisibility) From aa1261dc7d26117afbdc73ddaa6e0e1ca50a7c0e Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Mon, 13 Apr 2020 04:01:16 -0500 Subject: [PATCH 063/142] Path: Waterline fixes(5), new IgnoreOuterAbove, and code simplification Fix module base for getFacets() Fix isOnLineSegment() usage. Fix property visibility in Data tab. Fix missing raise to SafeHeight after clearing layer. Fix handling of `import ocl` failure Move Draft import to dependent function Raise `import ocl` test in code execution Disable face selection for Waterline and issue warning as intermediate fix. Application of face-selection from 3D Surface requires some modification for use in Waterline. This work is to be done. Some existing carryover methods should be usable in current form. Compact setup() function Sync some methods with PathSurface in preparation of extracting common methods to independent support module. Increase SampleInterval range for OCL Dropcutter algorithm. Convert OCL Dropcutter waterline to use FreeCAD.Vector() points rather than ocl.Point(). Simplify some code and delete unnecessary comments. LGTM cleanup. New feature - IgnoreOuterAbove. Ignore the outer-most waterline above this height. Designed to eliminate the model profile path created in some use cases. Adjust tooltip language for `Algorithm` --- src/Mod/Path/PathScripts/PathWaterline.py | 6876 ++++++++++----------- 1 file changed, 3392 insertions(+), 3484 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index c1c8b66cb6..80fe121553 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -1,3484 +1,3392 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2019 Russell Johnson (russ4262) * -# * Copyright (c) 2019 sliptonic * -# * * -# * 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. * -# * * -# * This program 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 this program; if not, write to the Free Software * -# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * -# * USA * -# * * -# *************************************************************************** - -from __future__ import print_function - -import FreeCAD -import Path -import PathScripts.PathLog as PathLog -import PathScripts.PathUtils as PathUtils -import PathScripts.PathOp as PathOp - -from PySide import QtCore -import time -import math - -# lazily loaded modules -from lazy_loader.lazy_loader import LazyLoader -MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') -Draft = LazyLoader('Draft', globals(), 'Draft') -Part = LazyLoader('Part', globals(), 'Part') - -if FreeCAD.GuiUp: - import FreeCADGui - -__title__ = "Path Waterline Operation" -__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Class and implementation of Mill Facing operation." - -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -# PathLog.trackModule(PathLog.thisModule()) - - -# Qt translation handling -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -# OCL must be installed -try: - import ocl -except ImportError: - FreeCAD.Console.PrintError( - translate("Path_Waterline", "This operation requires OpenCamLib to be installed.") + "\n") - import sys - sys.exit(translate("Path_Waterline", "This operation requires OpenCamLib to be installed.")) - - -class ObjectWaterline(PathOp.ObjectOp): - '''Proxy object for Surfacing operation.''' - - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - - def opFeatures(self, obj): - '''opFeatures(obj) ... return all standard features and edges based geomtries''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces - - def initOperation(self, obj): - '''initPocketOp(obj) ... - Initialize the operation - property creation and property editor status.''' - self.initOpProperties(obj) - - # For debugging - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - - if not hasattr(obj, 'DoNotSetDefaultValues'): - self.setEditorProperties(obj) - - def initOpProperties(self, obj): - '''initOpProperties(obj) ... create operation specific properties''' - PROPS = [ - ("App::PropertyBool", "ShowTempObjects", "Debug", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), - - ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")), - ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")), - - ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), - ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), - ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), - ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), - ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), - ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), - ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), - - ("App::PropertyEnumeration", "Algorithm", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental.")), - ("App::PropertyEnumeration", "BoundBox", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation. ")), - ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), - ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")), - ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")), - ("App::PropertyEnumeration", "CutMode", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), - ("App::PropertyEnumeration", "CutPattern", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), - ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), - ("App::PropertyBool", "CutPatternReversed", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), - ("App::PropertyDistance", "DepthOffset", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), - ("App::PropertyEnumeration", "LayerMode", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), - ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), - ("App::PropertyDistance", "SampleInterval", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), - ("App::PropertyPercent", "StepOver", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), - - ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), - ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), - ("App::PropertyDistance", "GapThreshold", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), - ("App::PropertyString", "GapSizes", "Optimization", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), - - ("App::PropertyVectorDistance", "StartPoint", "Start Point", - QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), - ("App::PropertyBool", "UseStartPoint", "Start Point", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) - ] - - missing = list() - for (prtyp, nm, grp, tt) in PROPS: - if not hasattr(obj, nm): - obj.addProperty(prtyp, nm, grp, tt) - missing.append(nm) - - # Set enumeration lists for enumeration properties - if len(missing) > 0: - ENUMS = self._propertyEnumerations() - for n in ENUMS: - if n in missing: - cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) - exec(cmdStr) - - self.addedAllProperties = True - - def _propertyEnumerations(self): - # Enumeration lists for App::PropertyEnumeration properties - return { - 'Algorithm': ['OCL Dropcutter', 'Experimental'], - 'BoundBox': ['BaseBoundBox', 'Stock'], - 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], - 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], - 'CutMode': ['Conventional', 'Climb'], - 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] - 'HandleMultipleFeatures': ['Collectively', 'Individually'], - 'LayerMode': ['Single-pass', 'Multi-pass'], - 'ProfileEdges': ['None', 'Only', 'First', 'Last'], - } - - def setEditorProperties(self, obj): - # Used to hide inputs in properties list - show = 0 - hide = 2 - cpShow = 0 - expMode = 0 - obj.setEditorMode('BoundaryEnforcement', hide) - obj.setEditorMode('ProfileEdges', hide) - obj.setEditorMode('InternalFeaturesAdjustment', hide) - obj.setEditorMode('InternalFeaturesCut', hide) - obj.setEditorMode('GapSizes', hide) - obj.setEditorMode('GapThreshold', hide) - obj.setEditorMode('AvoidLastX_Faces', hide) - obj.setEditorMode('AvoidLastX_InternalFeatures', hide) - obj.setEditorMode('BoundaryAdjustment', hide) - obj.setEditorMode('HandleMultipleFeatures', hide) - if hasattr(obj, 'EnableRotation'): - obj.setEditorMode('EnableRotation', hide) - if obj.CutPattern == 'None': - show = 2 - hide = 2 - cpShow = 2 - # elif obj.CutPattern in ['Line', 'ZigZag']: - # show = 0 - # hide = 2 - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - show = 2 # hide - hide = 0 # show - # obj.setEditorMode('StepOver', cpShow) - obj.setEditorMode('CutPatternAngle', show) - obj.setEditorMode('CircularCenterAt', hide) - obj.setEditorMode('CircularCenterCustom', hide) - if obj.Algorithm == 'Experimental': - expMode = 2 - obj.setEditorMode('SampleInterval', expMode) - obj.setEditorMode('LinearDeflection', expMode) - obj.setEditorMode('AngularDeflection', expMode) - - def onChanged(self, obj, prop): - if hasattr(self, 'addedAllProperties'): - if self.addedAllProperties is True: - if prop in ['Algorithm', 'CutPattern']: - self.setEditorProperties(obj) - - def opOnDocumentRestored(self, obj): - self.initOpProperties(obj) - - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - else: - obj.setEditorMode('ShowTempObjects', 0) # show - - self.setEditorProperties(obj) - - def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj, job) ... initialize defaults''' - job = PathUtils.findParentJob(obj) - - obj.OptimizeLinearPaths = True - obj.InternalFeaturesCut = True - obj.OptimizeStepOverTransitions = False - obj.BoundaryEnforcement = True - obj.UseStartPoint = False - obj.AvoidLastX_InternalFeatures = True - obj.CutPatternReversed = False - obj.StartPoint.x = 0.0 - obj.StartPoint.y = 0.0 - obj.StartPoint.z = obj.ClearanceHeight.Value - obj.Algorithm = 'OCL Dropcutter' - obj.ProfileEdges = 'None' - obj.LayerMode = 'Single-pass' - obj.CutMode = 'Conventional' - obj.CutPattern = 'None' - obj.HandleMultipleFeatures = 'Collectively' # 'Individually' - obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' - obj.GapSizes = 'No gaps identified.' - obj.ClearLastLayer = 'Off' - obj.StepOver = 100 - obj.CutPatternAngle = 0.0 - obj.DepthOffset.Value = 0.0 - obj.SampleInterval.Value = 1.0 - obj.BoundaryAdjustment.Value = 0.0 - obj.InternalFeaturesAdjustment.Value = 0.0 - obj.AvoidLastX_Faces = 0 - obj.CircularCenterCustom.x = 0.0 - obj.CircularCenterCustom.y = 0.0 - obj.CircularCenterCustom.z = 0.0 - obj.GapThreshold.Value = 0.005 - obj.LinearDeflection.Value = 0.0001 - obj.AngularDeflection.Value = 0.25 - # For debugging - obj.ShowTempObjects = False - - # need to overwrite the default depth calculations for facing - d = None - if job: - if job.Stock: - d = PathUtils.guessDepths(job.Stock.Shape, None) - PathLog.debug("job.Stock exists") - else: - PathLog.debug("job.Stock NOT exist") - else: - PathLog.debug("job NOT exist") - - if d is not None: - obj.OpFinalDepth.Value = d.final_depth - obj.OpStartDepth.Value = d.start_depth - else: - obj.OpFinalDepth.Value = -10 - obj.OpStartDepth.Value = 10 - - PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) - PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) - - def opApplyPropertyLimits(self, obj): - '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' - # Limit sample interval - if obj.SampleInterval.Value < 0.001: - obj.SampleInterval.Value = 0.001 - PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.')) - if obj.SampleInterval.Value > 25.4: - obj.SampleInterval.Value = 25.4 - PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.001 to 25.4 millimeters.')) - - # Limit cut pattern angle - if obj.CutPatternAngle < -360.0: - obj.CutPatternAngle = 0.0 - PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +-360 degrees.')) - if obj.CutPatternAngle >= 360.0: - obj.CutPatternAngle = 0.0 - PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.')) - - # Limit StepOver to natural number percentage - if obj.StepOver > 100: - obj.StepOver = 100 - if obj.StepOver < 1: - obj.StepOver = 1 - - # Limit AvoidLastX_Faces to zero and positive values - if obj.AvoidLastX_Faces < 0: - obj.AvoidLastX_Faces = 0 - PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Only zero or positive values permitted.')) - if obj.AvoidLastX_Faces > 100: - obj.AvoidLastX_Faces = 100 - PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) - - def opExecute(self, obj): - '''opExecute(obj) ... process surface operation''' - PathLog.track() - - self.modelSTLs = list() - self.safeSTLs = list() - self.modelTypes = list() - self.boundBoxes = list() - self.profileShapes = list() - self.collectiveShapes = list() - self.individualShapes = list() - self.avoidShapes = list() - self.geoTlrnc = None - self.tempGroup = None - self.CutClimb = False - self.closedGap = False - self.gaps = [0.1, 0.2, 0.3] - CMDS = list() - modelVisibility = list() - FCAD = FreeCAD.ActiveDocument - - # Set debugging behavior - self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects - self.showDebugObjects = obj.ShowTempObjects - deleteTempsFlag = True # Set to False for debugging - if PathLog.getLevel(PathLog.thisModule()) == 4: - deleteTempsFlag = False - else: - self.showDebugObjects = False - - # mark beginning of operation and identify parent Job - PathLog.info('\nBegin Waterline operation...') - startTime = time.time() - - # Identify parent Job - JOB = PathUtils.findParentJob(obj) - if JOB is None: - PathLog.error(translate('PathWaterline', "No JOB")) - return - self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin - - # set cut mode; reverse as needed - if obj.CutMode == 'Climb': - self.CutClimb = True - if obj.CutPatternReversed is True: - if self.CutClimb is True: - self.CutClimb = False - else: - self.CutClimb = True - - # Begin GCode for operation with basic information - # ... and move cutter to clearance height and startpoint - output = '' - if obj.Comment != '': - self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) - self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) - self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) - self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) - self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) - self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) - self.commandlist.append(Path.Command('N ({})'.format(output), {})) - self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - if obj.UseStartPoint is True: - self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) - - # Instantiate additional class operation variables - self.resetOpVariables() - - # Impose property limits - self.opApplyPropertyLimits(obj) - - # Create temporary group for temporary objects, removing existing - # if self.showDebugObjects is True: - tempGroupName = 'tempPathWaterlineGroup' - if FCAD.getObject(tempGroupName): - for to in FCAD.getObject(tempGroupName).Group: - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName) # remove temp directory if already exists - if FCAD.getObject(tempGroupName + '001'): - for to in FCAD.getObject(tempGroupName + '001').Group: - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists - tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) - tempGroupName = tempGroup.Name - self.tempGroup = tempGroup - tempGroup.purgeTouched() - # Add temp object to temp group folder with following code: - # ... self.tempGroup.addObject(OBJ) - - # Setup cutter for OCL and cutout value for operation - based on tool controller properties - self.cutter = self.setOclCutter(obj) - self.safeCutter = self.setOclCutter(obj, safe=True) - if self.cutter is False or self.safeCutter is False: - PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter.")) - return - toolDiam = self.cutter.getDiameter() - self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) - self.radius = toolDiam / 2.0 - self.gaps = [toolDiam, toolDiam, toolDiam] - - # Get height offset values for later use - self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value - self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value - - # Set deflection values for mesh generation - useDGT = False - try: # try/except is for Path Jobs created before GeometryTolerance - self.geoTlrnc = JOB.GeometryTolerance.Value - if self.geoTlrnc == 0.0: - useDGT = True - except AttributeError as ee: - PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) - useDGT = True - if useDGT: - import PathScripts.PathPreferences as PathPreferences - self.geoTlrnc = PathPreferences.defaultGeometryTolerance() - - # Calculate default depthparams for operation - self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) - self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 - - # make circle for workplane - self.wpc = Part.makeCircle(2.0) - - # Save model visibilities for restoration - if FreeCAD.GuiUp: - for m in range(0, len(JOB.Model.Group)): - mNm = JOB.Model.Group[m].Name - modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) - - # Setup STL, model type, and bound box containers for each model in Job - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - self.modelSTLs.append(False) - self.safeSTLs.append(False) - self.profileShapes.append(False) - # Set bound box - if obj.BoundBox == 'BaseBoundBox': - if M.TypeId.startswith('Mesh'): - self.modelTypes.append('M') # Mesh - self.boundBoxes.append(M.Mesh.BoundBox) - else: - self.modelTypes.append('S') # Solid - self.boundBoxes.append(M.Shape.BoundBox) - elif obj.BoundBox == 'Stock': - self.modelTypes.append('S') # Solid - self.boundBoxes.append(JOB.Stock.Shape.BoundBox) - - # ###### MAIN COMMANDS FOR OPERATION ###### - - # Begin processing obj.Base data and creating GCode - # Process selected faces, if available - pPM = self._preProcessModel(JOB, obj) - if pPM is False: - PathLog.error('Unable to pre-process obj.Base.') - else: - (FACES, VOIDS) = pPM - - # Create OCL.stl model objects - if obj.Algorithm == 'OCL Dropcutter': - self._prepareModelSTLs(JOB, obj) - PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value)) - PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value)) - - for m in range(0, len(JOB.Model.Group)): - Mdl = JOB.Model.Group[m] - if FACES[m] is False: - PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) - else: - if m > 0: - # Raise to clearance between models - CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) - CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) - # make stock-model-voidShapes STL model for avoidance detection on transitions - if obj.Algorithm == 'OCL Dropcutter': - self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) - # time.sleep(0.2) - # Process model/faces - OCL objects must be ready - CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m])) - - # Save gcode produced - self.commandlist.extend(CMDS) - - # ###### CLOSING COMMANDS FOR OPERATION ###### - - # Delete temporary objects - # Restore model visibilities for restoration - if FreeCAD.GuiUp: - FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - M.Visibility = modelVisibility[m] - - if deleteTempsFlag is True: - for to in tempGroup.Group: - if hasattr(to, 'Group'): - for go in to.Group: - FCAD.removeObject(go.Name) - FCAD.removeObject(to.Name) - FCAD.removeObject(tempGroupName) - else: - if len(tempGroup.Group) == 0: - FCAD.removeObject(tempGroupName) - else: - tempGroup.purgeTouched() - - # Provide user feedback for gap sizes - gaps = list() - for g in self.gaps: - if g != toolDiam: - gaps.append(g) - if len(gaps) > 0: - obj.GapSizes = '{} mm'.format(gaps) - else: - if self.closedGap is True: - obj.GapSizes = 'Closed gaps < Gap Threshold.' - else: - obj.GapSizes = 'No gaps identified.' - - # clean up class variables - self.resetOpVariables() - self.deleteOpVariables() - - self.modelSTLs = None - self.safeSTLs = None - self.modelTypes = None - self.boundBoxes = None - self.gaps = None - self.closedGap = None - self.SafeHeightOffset = None - self.ClearHeightOffset = None - self.depthParams = None - self.midDep = None - self.wpc = None - del self.modelSTLs - del self.safeSTLs - del self.modelTypes - del self.boundBoxes - del self.gaps - del self.closedGap - del self.SafeHeightOffset - del self.ClearHeightOffset - del self.depthParams - del self.midDep - del self.wpc - - execTime = time.time() - startTime - PathLog.info('Operation time: {} sec.'.format(execTime)) - - return True - - # Methods for constructing the cut area - def _preProcessModel(self, JOB, obj): - PathLog.debug('_preProcessModel()') - - FACES = list() - VOIDS = list() - fShapes = list() - vShapes = list() - preProcEr = translate('PathWaterline', 'Error pre-processing Face') - warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face') - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Crete place holders for each base model in Job - for m in range(0, lenGRP): - FACES.append(False) - VOIDS.append(False) - fShapes.append(False) - vShapes.append(False) - - # The user has selected subobjects from the base. Pre-Process each. - if obj.Base and len(obj.Base) > 0: - PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') - - (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) - - # Cycle through each base model, processing faces for each - for m in range(0, lenGRP): - base = GRP[m] - (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) - fShapes[m] = mFS - vShapes[m] = mVS - self.profileShapes[m] = mPS - else: - PathLog.debug(' -No obj.Base data.') - for m in range(0, lenGRP): - self.modelSTLs[m] = True - - # Process each model base, as a whole, as needed - # PathLog.debug(' -Pre-processing all models in Job.') - for m in range(0, lenGRP): - if fShapes[m] is False: - PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) - if obj.BoundBox == 'BaseBoundBox': - base = GRP[m] - elif obj.BoundBox == 'Stock': - base = JOB.Stock - - pPEB = self._preProcessEntireBase(obj, base, m) - if pPEB is False: - PathLog.error(' -Failed to pre-process base as a whole.') - else: - (fcShp, prflShp) = pPEB - if fcShp is not False: - if fcShp is True: - PathLog.debug(' -fcShp is True.') - fShapes[m] = True - else: - fShapes[m] = [fcShp] - if prflShp is not False: - if fcShp is not False: - PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) - if vShapes[m] is not False: - PathLog.debug(' -Cutting void from base profile shape.') - adjPS = prflShp.cut(vShapes[m][0]) - self.profileShapes[m] = [adjPS] - else: - PathLog.debug(' -vShapes[m] is False.') - self.profileShapes[m] = [prflShp] - else: - PathLog.debug(' -Saving base profile shape.') - self.profileShapes[m] = [prflShp] - PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) - # Efor - - return (fShapes, vShapes) - - def _identifyFacesAndVoids(self, JOB, obj, F, V): - TUPS = list() - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Separate selected faces into (base, face) tuples and flag model(s) for STL creation - for (bs, SBS) in obj.Base: - for sb in SBS: - # Flag model for STL creation - mdlIdx = None - for m in range(0, lenGRP): - if bs is GRP[m]: - self.modelSTLs[m] = True - mdlIdx = m - break - TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) - - # Apply `AvoidXFaces` value - faceCnt = len(TUPS) - add = faceCnt - obj.AvoidLastX_Faces - for bst in range(0, faceCnt): - (m, base, sub) = TUPS[bst] - shape = getattr(base.Shape, sub) - if isinstance(shape, Part.Face): - faceIdx = int(sub[4:]) - 1 - if bst < add: - if F[m] is False: - F[m] = list() - F[m].append((shape, faceIdx)) - else: - if V[m] is False: - V[m] = list() - V[m].append((shape, faceIdx)) - return (F, V) - - def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): - mFS = False - mVS = False - mPS = False - mIFS = list() - BB = base.Shape.BoundBox - - if FACES[m] is not False: - isHole = False - if obj.HandleMultipleFeatures == 'Collectively': - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - outFCS = list() - - # Get collective envelope slice of selected faces - for (fcshp, fcIdx) in FACES[m]: - fNum = fcIdx + 1 - fsL.append(fcshp) - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - PathLog.debug('Attempting to get cross-section of collective faces.') - if len(outFCS) == 0: - PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) - cont = False - else: - cfsL = Part.makeCompound(outFCS) - - # Handle profile edges request - if cont is True and obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(obj, cfsL, ofstVal) - if psOfst is not False: - mPS = [psOfst] - if obj.ProfileEdges == 'Only': - mFS = True - cont = False - else: - PathLog.error(' -Failed to create profile geometry for selected faces.') - cont = False - - if cont is True: - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') - T.Shape = cfsL - T.purgeTouched() - self.tempGroup.addObject(T) - - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(obj, cfsL, ofstVal) - if faceOfstShp is False: - PathLog.error(' -Failed to create offset face.') - cont = False - - if cont is True: - lenIfL = len(ifL) - if obj.InternalFeaturesCut is False: - if lenIfL == 0: - PathLog.debug(' -No internal features saved.') - else: - if lenIfL == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - if self.showDebugObjects is True: - C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') - C.Shape = casL - C.purgeTouched() - self.tempGroup.addObject(C) - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - mFS = [faceOfstShp] - # Eif - - elif obj.HandleMultipleFeatures == 'Individually': - for (fcshp, fcIdx) in FACES[m]: - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - fNum = fcIdx + 1 - outerFace = False - - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - cont = False - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - cont = False - outerFace = False - else: - ((otrFace, raised), intWires) = gFW - outerFace = otrFace - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - if outerFace is not False: - PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) - - if obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(obj, outerFace, ofstVal) - if psOfst is not False: - if mPS is False: - mPS = list() - mPS.append(psOfst) - if obj.ProfileEdges == 'Only': - if mFS is False: - mFS = list() - mFS.append(True) - cont = False - else: - PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) - cont = False - - if cont is True: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(obj, slc, ofstVal) - - lenIfl = len(ifL) - if obj.InternalFeaturesCut is False and lenIfl > 0: - if lenIfl == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(obj, casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - if mFS is False: - mFS = list() - mFS.append(faceOfstShp) - # Eif - # Efor - # Eif - # Eif - - if len(mIFS) > 0: - if mVS is False: - mVS = list() - for ifs in mIFS: - mVS.append(ifs) - - if VOIDS[m] is not False: - PathLog.debug('Processing avoid faces.') - cont = True - isHole = False - outFCS = list() - intFEAT = list() - - for (fcshp, fcIdx) in VOIDS[m]: - fNum = fcIdx + 1 - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) - cont = False - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.AvoidLastX_InternalFeatures is False: - if intWires is not False: - for (iFace, rsd) in intWires: - intFEAT.append(iFace) - - lenOtFcs = len(outFCS) - if lenOtFcs == 0: - cont = False - else: - if lenOtFcs == 1: - avoid = outFCS[0] - else: - avoid = Part.makeCompound(outFCS) - - if self.showDebugObjects is True: - PathLog.debug('*** tmpAvoidArea') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') - P.Shape = avoid - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - - if cont is True: - if self.showDebugObjects is True: - PathLog.debug('*** tmpVoidCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') - P.Shape = avoid - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) - avdOfstShp = self._extractFaceOffset(obj, avoid, ofstVal) - if avdOfstShp is False: - PathLog.error('Failed to create collective offset avoid face.') - cont = False - - if cont is True: - avdShp = avdOfstShp - - if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: - if len(intFEAT) > 1: - ifc = Part.makeCompound(intFEAT) - else: - ifc = intFEAT[0] - ofstVal = self._calculateOffsetValue(obj, isHole=True) - ifOfstShp = self._extractFaceOffset(obj, ifc, ofstVal) - if ifOfstShp is False: - PathLog.error('Failed to create collective offset avoid internal features.') - else: - avdShp = avdOfstShp.cut(ifOfstShp) - - if mVS is False: - mVS = list() - mVS.append(avdShp) - - - return (mFS, mVS, mPS) - - def _getFaceWires(self, base, fcshp, fcIdx): - outFace = False - INTFCS = list() - fNum = fcIdx + 1 - # preProcEr = translate('PathWaterline', 'Error pre-processing Face') - warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face') - - PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) - WIRES = self._extractWiresFromFace(base, fcshp) - if WIRES is False: - PathLog.error('Failed to extract wires from Face{}'.format(fNum)) - return False - - # Process remaining internal features, adding to FCS list - lenW = len(WIRES) - for w in range(0, lenW): - (wire, rsd) = WIRES[w] - PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) - if wire.isClosed() is False: - PathLog.debug(' -wire is not closed.') - else: - slc = self._flattenWireToFace(wire) - if slc is False: - PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) - else: - if w == 0: - outFace = (slc, rsd) - else: - # add to VOIDS so cutter avoids area. - PathLog.warning(warnFinDep + str(fNum) + '.') - INTFCS.append((slc, rsd)) - if len(INTFCS) == 0: - return (outFace, False) - else: - return (outFace, INTFCS) - - def _preProcessEntireBase(self, obj, base, m): - cont = True - isHole = False - prflShp = False - # Create envelope, extract cross-section and make offset co-planar shape - # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) - - try: - baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as ee: - PathLog.error(str(ee)) - shell = base.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as eee: - PathLog.error(str(eee)) - cont = False - # time.sleep(0.2) - - if cont is True: - csFaceShape = self._getShapeSlice(baseEnv) - if csFaceShape is False: - PathLog.debug('_getShapeSlice(baseEnv) failed') - csFaceShape = self._getCrossSection(baseEnv) - if csFaceShape is False: - PathLog.debug('_getCrossSection(baseEnv) failed') - csFaceShape = self._getSliceFromEnvelope(baseEnv) - if csFaceShape is False: - PathLog.error('Failed to slice baseEnv shape.') - cont = False - - if cont is True and obj.ProfileEdges != 'None': - PathLog.debug(' -Attempting profile geometry for model base.') - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(obj, csFaceShape, ofstVal) - if psOfst is not False: - if obj.ProfileEdges == 'Only': - return (True, psOfst) - prflShp = psOfst - else: - PathLog.error(' -Failed to create profile geometry.') - cont = False - - if cont is True: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOffsetShape = self._extractFaceOffset(obj, csFaceShape, ofstVal) - if faceOffsetShape is False: - PathLog.error('_extractFaceOffset() failed.') - else: - faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) - return (faceOffsetShape, prflShp) - return False - - def _extractWiresFromFace(self, base, fc): - '''_extractWiresFromFace(base, fc) ... - Attempts to return all closed wires within a parent face, including the outer most wire of the parent. - The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). - ''' - PathLog.debug('_extractWiresFromFace()') - - WIRES = list() - lenWrs = len(fc.Wires) - PathLog.debug(' -Wire count: {}'.format(lenWrs)) - - def index0(tup): - return tup[0] - - # Cycle through wires in face - for w in range(0, lenWrs): - PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) - wire = fc.Wires[w] - checkEdges = False - cont = True - - # Check for closed edges (circles, ellipses, etc...) - for E in wire.Edges: - if E.isClosed() is True: - checkEdges = True - break - - if checkEdges is True: - PathLog.debug(' -checkEdges is True') - for e in range(0, len(wire.Edges)): - edge = wire.Edges[e] - if edge.isClosed() is True and edge.Mass > 0.01: - PathLog.debug(' -Found closed edge') - raised = False - ip = self._isPocket(base, fc, edge) - if ip is False: - raised = True - ebb = edge.BoundBox - eArea = ebb.XLength * ebb.YLength - F = Part.Face(Part.Wire([edge])) - WIRES.append((eArea, F.Wires[0], raised)) - cont = False - - if cont is True: - PathLog.debug(' -cont is True') - # If only one wire and not checkEdges, return first wire - if lenWrs == 1: - return [(wire, False)] - - raised = False - wbb = wire.BoundBox - wArea = wbb.XLength * wbb.YLength - if w > 0: - ip = self._isPocket(base, fc, wire) - if ip is False: - raised = True - WIRES.append((wArea, Part.Wire(wire.Edges), raised)) - - nf = len(WIRES) - if nf > 0: - PathLog.debug(' -number of wires found is {}'.format(nf)) - if nf == 1: - (area, W, raised) = WIRES[0] - return [(W, raised)] - else: - sortedWIRES = sorted(WIRES, key=index0, reverse=True) - return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size - - return False - - def _calculateOffsetValue(self, obj, isHole, isVoid=False): - '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. - Calculate the offset for the Path.Area() function.''' - JOB = PathUtils.findParentJob(obj) - tolrnc = JOB.GeometryTolerance.Value - - if isVoid is False: - if isHole is True: - offset = -1 * obj.InternalFeaturesAdjustment.Value - offset += self.radius # (self.radius + (tolrnc / 10.0)) - else: - offset = -1 * obj.BoundaryAdjustment.Value - if obj.BoundaryEnforcement is True: - offset += self.radius # (self.radius + (tolrnc / 10.0)) - else: - offset -= self.radius # (self.radius + (tolrnc / 10.0)) - offset = 0.0 - offset - else: - offset = -1 * obj.BoundaryAdjustment.Value - offset += self.radius # (self.radius + (tolrnc / 10.0)) - - return offset - - def _extractFaceOffset(self, obj, fcShape, offset, makeComp=True): - '''_extractFaceOffset(fcShape, offset) ... internal function. - Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. - Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' - PathLog.debug('_extractFaceOffset()') - - if fcShape.BoundBox.ZMin != 0.0: - fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) - - areaParams = {} - areaParams['Offset'] = offset - areaParams['Fill'] = 1 # 1 - areaParams['Coplanar'] = 0 - areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections - areaParams['Reorient'] = True - areaParams['OpenMode'] = 0 - areaParams['MaxArcPoints'] = 400 # 400 - areaParams['Project'] = True - # areaParams['Tolerance'] = 0.001 - - area = Path.Area() # Create instance of Area() class object - # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane - area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 - area.add(fcShape) - area.setParams(**areaParams) # set parameters - - # Save parameters for debugging - # obj.AreaParams = str(area.getParams()) - # PathLog.debug("Area with params: {}".format(area.getParams())) - - offsetShape = area.getShape() - wCnt = len(offsetShape.Wires) - if wCnt == 0: - return False - elif wCnt == 1: - ofstFace = Part.Face(offsetShape.Wires[0]) - if not makeComp: - ofstFace = [ofstFace] - else: - W = list() - for wr in offsetShape.Wires: - W.append(Part.Face(wr)) - if makeComp: - ofstFace = Part.makeCompound(W) - else: - ofstFace = W - - return ofstFace # offsetShape - - def _isPocket(self, b, f, w): - '''_isPocket(b, f, w)... - Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. - Returns True if pocket, False if raised protrusion.''' - e = w.Edges[0] - for fi in range(0, len(b.Shape.Faces)): - face = b.Shape.Faces[fi] - for ei in range(0, len(face.Edges)): - edge = face.Edges[ei] - if e.isSame(edge) is True: - if f is face: - # Alternative: run loop to see if all edges are same - pass # same source face, look for another - else: - if face.CenterOfMass.z < f.CenterOfMass.z: - return True - return False - - def _flattenWireToFace(self, wire): - PathLog.debug('_flattenWireToFace()') - if wire.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - - # If wire is planar horizontal, convert to a face and return - if wire.BoundBox.ZLength == 0.0: - slc = Part.Face(wire) - return slc - - # Attempt to create a new wire for manipulation, if not, use original - newWire = Part.Wire(wire.Edges) - if newWire.isClosed() is True: - nWire = newWire - else: - PathLog.debug(' -newWire.isClosed() is False') - nWire = wire - - # Attempt extrusion, and then try a manual slice and then cross-section - ext = self._getExtrudedShape(nWire) - if ext is False: - PathLog.debug('_getExtrudedShape() failed') - else: - slc = self._getShapeSlice(ext) - if slc is not False: - return slc - cs = self._getCrossSection(ext, True) - if cs is not False: - return cs - - # Attempt creating an envelope, and then try a manual slice and then cross-section - env = self._getShapeEnvelope(nWire) - if env is False: - PathLog.debug('_getShapeEnvelope() failed') - else: - slc = self._getShapeSlice(env) - if slc is not False: - return slc - cs = self._getCrossSection(env, True) - if cs is not False: - return cs - - # Attempt creating a projection - slc = self._getProjectedFace(nWire) - if slc is False: - PathLog.debug('_getProjectedFace() failed') - else: - return slc - - return False - - def _getExtrudedShape(self, wire): - PathLog.debug('_getExtrudedShape()') - wBB = wire.BoundBox - extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 - - try: - # slower, but renders collective faces correctly. Method 5 in TESTING - shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) - except Exception as ee: - PathLog.error(' -extrude wire failed: \n{}'.format(ee)) - return False - - SHP = Part.makeSolid(shell) - return SHP - - def _getShapeSlice(self, shape): - PathLog.debug('_getShapeSlice()') - - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - xmin = bb.XMin - 1.0 - xmax = bb.XMax + 1.0 - ymin = bb.YMin - 1.0 - ymax = bb.YMax + 1.0 - p1 = FreeCAD.Vector(xmin, ymin, mid) - p2 = FreeCAD.Vector(xmax, ymin, mid) - p3 = FreeCAD.Vector(xmax, ymax, mid) - p4 = FreeCAD.Vector(xmin, ymax, mid) - - e1 = Part.makeLine(p1, p2) - e2 = Part.makeLine(p2, p3) - e3 = Part.makeLine(p3, p4) - e4 = Part.makeLine(p4, p1) - face = Part.Face(Part.Wire([e1, e2, e3, e4])) - fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area - sArea = shape.BoundBox.XLength * shape.BoundBox.YLength - midArea = (fArea + sArea) / 2.0 - - slcShp = shape.common(face) - slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength - - if slcArea < midArea: - for W in slcShp.Wires: - if W.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - if len(slcShp.Wires) == 1: - wire = slcShp.Wires[0] - slc = Part.Face(wire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - else: - fL = list() - for W in slcShp.Wires: - slc = Part.Face(W) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - fL.append(slc) - comp = Part.makeCompound(fL) - if self.showDebugObjects is True: - PathLog.debug('*** tmpSliceCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') - P.Shape = comp - # P.recompute() - P.purgeTouched() - self.tempGroup.addObject(P) - return comp - - PathLog.debug(' -slcArea !< midArea') - PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) - return False - - def _getProjectedFace(self, wire): - PathLog.debug('_getProjectedFace()') - F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') - F.Shape = wire - F.purgeTouched() - self.tempGroup.addObject(F) - try: - prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) - prj.recompute() - prj.purgeTouched() - self.tempGroup.addObject(prj) - except Exception as ee: - PathLog.error(str(ee)) - return False - else: - pWire = Part.Wire(prj.Shape.Edges) - if pWire.isClosed() is False: - # PathLog.debug(' -pWire.isClosed() is False') - return False - slc = Part.Face(pWire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - return False - - def _getCrossSection(self, shape, withExtrude=False): - PathLog.debug('_getCrossSection()') - wires = list() - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - - for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): - wires.append(i) - - if len(wires) > 0: - comp = Part.Compound(wires) # produces correct cross-section wire ! - comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) - csWire = comp.Wires[0] - if csWire.isClosed() is False: - PathLog.debug(' -comp.Wires[0] is not closed') - return False - if withExtrude is True: - ext = self._getExtrudedShape(csWire) - CS = self._getShapeSlice(ext) - else: - CS = Part.Face(csWire) - CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) - return CS - else: - PathLog.debug(' -No wires from .slice() method') - - return False - - def _getShapeEnvelope(self, shape): - PathLog.debug('_getShapeEnvelope()') - - wBB = shape.BoundBox - extFwd = wBB.ZLength + 10.0 - minz = wBB.ZMin - maxz = wBB.ZMin + extFwd - stpDwn = (maxz - minz) / 4.0 - dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) - - try: - env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape - except Exception as ee: - PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) - return False - else: - return env - - return False - - def _getSliceFromEnvelope(self, env): - PathLog.debug('_getSliceFromEnvelope()') - eBB = env.BoundBox - extFwd = eBB.ZLength + 10.0 - maxz = eBB.ZMin + extFwd - - maxMax = env.Edges[0].BoundBox.ZMin - emax = math.floor(maxz - 1.0) - E = list() - for e in range(0, len(env.Edges)): - emin = env.Edges[e].BoundBox.ZMin - if emin > emax: - E.append(env.Edges[e]) - tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) - tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) - - return tf - - def _prepareModelSTLs(self, JOB, obj): - PathLog.debug('_prepareModelSTLs()') - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - - # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") - if self.modelTypes[m] == 'M': - #TODO: test if this works - facets = M.Mesh.Facets.Points - else: - facets = Path.getFacets(M.Shape) - - if self.modelSTLs[m] is True: - stl = ocl.STLSurf() - - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2] + obj.DepthOffset.Value), - ocl.Point(tri[1][0], tri[1][1], tri[1][2] + obj.DepthOffset.Value), - ocl.Point(tri[2][0], tri[2][1], tri[2][2] + obj.DepthOffset.Value)) - stl.addTriangle(t) - self.modelSTLs[m] = stl - return - - def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): - '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... - Creates and OCL.stl object with combined data with waste stock, - model, and avoided faces. Travel lines can be checked against this - STL object to determine minimum travel height to clear stock and model.''' - PathLog.debug('_makeSafeSTL()') - - fuseShapes = list() - Mdl = JOB.Model.Group[mdlIdx] - FCAD = FreeCAD.ActiveDocument - mBB = Mdl.Shape.BoundBox - sBB = JOB.Stock.Shape.BoundBox - - # add Model shape to safeSTL shape - fuseShapes.append(Mdl.Shape) - - if obj.BoundBox == 'BaseBoundBox': - cont = False - extFwd = (sBB.ZLength) - zmin = mBB.ZMin - zmax = mBB.ZMin + extFwd - stpDwn = (zmax - zmin) / 4.0 - dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) - - try: - envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as ee: - PathLog.error(str(ee)) - shell = Mdl.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as eee: - PathLog.error(str(eee)) - - if cont is True: - stckWst = JOB.Stock.Shape.cut(envBB) - if obj.BoundaryAdjustment > 0.0: - cmpndFS = Part.makeCompound(faceShapes) - baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape - adjStckWst = stckWst.cut(baBB) - else: - adjStckWst = stckWst - fuseShapes.append(adjStckWst) - else: - PathLog.warning('Path transitions might not avoid the model. Verify paths.') - # time.sleep(0.3) - - else: - # If boundbox is Job.Stock, add hidden pad under stock as base plate - toolDiam = self.cutter.getDiameter() - zMin = JOB.Stock.Shape.BoundBox.ZMin - xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam - yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam - bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) - bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) - bH = 1.0 - crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) - B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) - fuseShapes.append(B) - - if voidShapes is not False: - voidComp = Part.makeCompound(voidShapes) - voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape - fuseShapes.append(voidEnv) - - fused = Part.makeCompound(fuseShapes) - - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') - T.Shape = fused - T.purgeTouched() - self.tempGroup.addObject(T) - - facets = Path.getFacets(fused) - - stl = ocl.STLSurf() - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - - self.safeSTLs[mdlIdx] = stl - - def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): - '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... - This method applies any avoided faces or regions to the selected faces. - It then calls the correct method.''' - PathLog.debug('_processCutAreas()') - - final = list() - base = JOB.Model.Group[mdlIdx] - - # Process faces Collectively or Individually - if obj.HandleMultipleFeatures == 'Collectively': - if FCS is True: - COMP = False - else: - ADD = Part.makeCompound(FCS) - if VDS is not False: - DEL = Part.makeCompound(VDS) - COMP = ADD.cut(DEL) - else: - COMP = ADD - - final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - if obj.Algorithm == 'OCL Dropcutter': - final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - else: - final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - - elif obj.HandleMultipleFeatures == 'Individually': - for fsi in range(0, len(FCS)): - fShp = FCS[fsi] - # self.deleteOpVariables(all=False) - self.resetOpVariables(all=False) - - if fShp is True: - COMP = False - else: - ADD = Part.makeCompound([fShp]) - if VDS is not False: - DEL = Part.makeCompound(VDS) - COMP = ADD.cut(DEL) - else: - COMP = ADD - - final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - if obj.Algorithm == 'OCL Dropcutter': - final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - else: - final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline - COMP = None - # Eif - - return final - - # Methods for creating path geometry - def _planarMakePathGeom(self, obj, faceShp): - '''_planarMakePathGeom(obj, faceShp)... - Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. - The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' - PathLog.debug('_planarMakePathGeom()') - GeoSet = list() - - # Apply drop cutter extra offset and set the max and min XY area of the operation - xmin = faceShp.BoundBox.XMin - xmax = faceShp.BoundBox.XMax - ymin = faceShp.BoundBox.YMin - ymax = faceShp.BoundBox.YMax - zmin = faceShp.BoundBox.ZMin - zmax = faceShp.BoundBox.ZMax - - # Compute weighted center of mass of all faces combined - fCnt = 0 - totArea = 0.0 - zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) - for F in faceShp.Faces: - comF = F.CenterOfMass - areaF = F.Area - totArea += areaF - fCnt += 1 - zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) - if fCnt == 0: - PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) - zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) - else: - avgArea = totArea / fCnt - zeroCOM.multiply(1 / fCnt) - zeroCOM.multiply(1 / avgArea) - COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) - - # get X, Y, Z spans; Compute center of rotation - deltaX = abs(xmax-xmin) - deltaY = abs(ymax-ymin) - deltaZ = abs(zmax-zmin) - deltaC = math.sqrt(deltaX**2 + deltaY**2) - lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - halfLL = math.ceil(lineLen / 2.0) - cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - halfPasses = math.ceil(cutPasses / 2.0) - bbC = faceShp.BoundBox.Center - - # Generate the Draft line/circle sets to be intersected with the cut-face-area - if obj.CutPattern in ['ZigZag', 'Line']: - MaxLC = -1 - centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model - cAng = math.atan(deltaX / deltaY) # BoundaryBox angle - - # Determine end points and create top lines - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - diag = None - if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: - MaxLC = math.floor(deltaY / self.cutOut) - diag = deltaY - elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: - MaxLC = math.floor(deltaX / self.cutOut) - diag = deltaX - else: - perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC - MaxLC = math.floor(perpDist / self.cutOut) - diag = perpDist - y1 = centRot.y + diag - # y2 = y1 - - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - topLineTuple = (p1, p2) - ny1 = centRot.y - diag - n1 = FreeCAD.Vector(x1, ny1, 0.0) - n2 = FreeCAD.Vector(x2, ny1, 0.0) - negTopLineTuple = (n1, n2) - - # Create end points for set of lines to intersect with cross-section face - pntTuples = list() - for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): - # if lc == (cutPasses - MaxLC - 1): - # pntTuples.append(negTopLineTuple) - # if lc == (MaxLC + 1): - # pntTuples.append(topLineTuple) - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - y1 = centRot.y + (lc * self.cutOut) - # y2 = y1 - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - pntTuples.append( (p1, p2) ) - - # Convert end points to lines - for (p1, p2) in pntTuples: - line = Part.makeLine(p1, p2) - GeoSet.append(line) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - zTgt = faceShp.BoundBox.ZMin - axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) - cntr = FreeCAD.Placement() - cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) - - if obj.CircularCenterAt == 'CenterOfMass': - cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass - elif obj.CircularCenterAt == 'CenterOfBoundBox': - cent = faceShp.BoundBox.Center - cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) - elif obj.CircularCenterAt == 'XminYmin': - cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) - elif obj.CircularCenterAt == 'Custom': - newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) - cntr.Base = newCent - - # recalculate cutPasses value, if need be - radialPasses = halfPasses - if obj.CircularCenterAt != 'CenterOfBoundBox': - # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center - EBB = faceShp.BoundBox - CORNERS = [ - FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), - FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), - ] - dMax = 0.0 - for c in range(0, 4): - dist = CORNERS[c].sub(cntr.Base).Length - if dist > dMax: - dMax = dist - lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - - # Update COM point and current CircularCenter - if obj.CircularCenterAt != 'Custom': - obj.CircularCenterCustom = cntr.Base - - minRad = self.cutter.getDiameter() * 0.45 - siX3 = 3 * obj.SampleInterval.Value - minRadSI = (siX3 / 2.0) / math.pi - if minRad < minRadSI: - minRad = minRadSI - - # Make small center circle to start pattern - if obj.StepOver > 50: - circle = Part.makeCircle(minRad, cntr.Base) - GeoSet.append(circle) - - for lc in range(1, radialPasses + 1): - rad = (lc * self.cutOut) - if rad >= minRad: - circle = Part.makeCircle(rad, cntr.Base) - GeoSet.append(circle) - # Efor - COM = cntr.Base - # Eif - - if obj.CutPatternReversed is True: - GeoSet.reverse() - - if faceShp.BoundBox.ZMin != 0.0: - faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) - - # Create compound object to bind all lines in Lineset - geomShape = Part.makeCompound(GeoSet) - - # Position and rotate the Line and ZigZag geometry - if obj.CutPattern in ['Line', 'ZigZag']: - if obj.CutPatternAngle != 0.0: - geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) - geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') - F.Shape = geomShape - F.purgeTouched() - self.tempGroup.addObject(F) - - # Identify intersection of cross-section face and lineset - cmnShape = faceShp.common(geomShape) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') - F.Shape = cmnShape - F.purgeTouched() - self.tempGroup.addObject(F) - - self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) - return cmnShape - - def _pathGeomToLinesPointSet(self, obj, compGeoShp): - '''_pathGeomToLinesPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' - PathLog.debug('_pathGeomToLinesPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - chkGap = False - lnCnt = 0 - ec = len(compGeoShp.Edges) - cutClimb = self.CutClimb - toolDiam = 2.0 * self.radius - cpa = obj.CutPatternAngle - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if cutClimb is True: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - else: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - inLine.append(tup) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - - for ei in range(1, ec): - chkGap = False - edg = compGeoShp.Edges[ei] # Get edge for vertexes - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 - - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) - iC = sp.isOnLineSegment(ep, cp) - if iC is True: - inLine.append('BRK') - chkGap = True - else: - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - lnCnt += 1 - inLine = list() # reset collinear container - if cutClimb is True: - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - else: - sp = ep - - if cutClimb is True: - tup = (v2, v1) - if chkGap is True: - gap = abs(toolDiam - lst.sub(ep).Length) - lst = cp - else: - tup = (v1, v2) - if chkGap is True: - gap = abs(toolDiam - lst.sub(cp).Length) - lst = ep - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - tup = (vA, tup[1]) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - - # Handle last inLine set, reversing it. - if obj.CutPatternReversed is True: - if cpa != 0.0 and cpa % 90.0 == 0.0: - F = LINES.pop(0) - rev = list() - for iL in F: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - rev.reverse() - LINES.insert(0, rev) - - isEven = lnCnt % 2 - if isEven == 0: - PathLog.debug('Line count is ODD.') - else: - PathLog.debug('Line count is even.') - - return LINES - - def _pathGeomToZigzagPointSet(self, obj, compGeoShp): - '''_pathGeomToZigzagPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings - with a ZigZag directional indicator included for each collinear group.''' - PathLog.debug('_pathGeomToZigzagPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - lnCnt = 0 - chkGap = False - ec = len(compGeoShp.Edges) - toolDiam = 2.0 * self.radius - - if self.CutClimb is True: - dirFlg = -1 - else: - dirFlg = 1 - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if dirFlg == 1: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - else: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point - inLine.append(tup) - otr = lst - - for ei in range(1, ec): - edg = compGeoShp.Edges[ei] - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) - - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - iC = sp.isOnLineSegment(ep, cp) - if iC is True: - inLine.append('BRK') - chkGap = True - gap = abs(toolDiam - lst.sub(cp).Length) - else: - chkGap = False - if dirFlg == -1: - inLine.reverse() - LINES.append((dirFlg, inLine)) - lnCnt += 1 - dirFlg = -1 * dirFlg # Change zig to zag - inLine = list() # reset collinear container - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - otr = ep - - lst = ep - if dirFlg == 1: - tup = (v1, v2) - else: - tup = (v2, v1) - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - if dirFlg == 1: - tup = (vA, tup[1]) - else: - #tup = (vA, tup[1]) - #tup = (tup[1], vA) - tup = (tup[0], vB) - self.closedGap = True - else: - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - - # Fix directional issue with LAST line when line count is even - isEven = lnCnt % 2 - if isEven == 0: # Changed to != with 90 degree CutPatternAngle - PathLog.debug('Line count is even.') - else: - PathLog.debug('Line count is ODD.') - dirFlg = -1 * dirFlg - if obj.CutPatternReversed is False: - if self.CutClimb is True: - dirFlg = -1 * dirFlg - - if obj.CutPatternReversed is True: - dirFlg = -1 * dirFlg - - # Handle last inLine list - if dirFlg == 1: - rev = list() - for iL in inLine: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - - if obj.CutPatternReversed is False: - rev.reverse() - else: - rev2 = list() - for iL in rev: - if iL == 'BRK': - rev2.append(iL) - else: - (p1, p2) = iL - rev2.append((p2, p1)) - rev2.reverse() - rev = rev2 - - LINES.append((dirFlg, rev)) - else: - LINES.append((dirFlg, inLine)) - - return LINES - - def _pathGeomToArcPointSet(self, obj, compGeoShp): - '''_pathGeomToArcPointSet(obj, compGeoShp)... - Convert a compound set of arcs/circles to a set of directionally-oriented arc end points - and the corresponding center point.''' - # Extract intersection line segments for return value as list() - PathLog.debug('_pathGeomToArcPointSet()') - ARCS = list() - stpOvrEI = list() - segEI = list() - isSame = False - sameRad = None - COM = self.tmpCOM - toolDiam = 2.0 * self.radius - ec = len(compGeoShp.Edges) - - def gapDist(sp, ep): - X = (ep[0] - sp[0])**2 - Y = (ep[1] - sp[1])**2 - Z = (ep[2] - sp[2])**2 - # return math.sqrt(X + Y + Z) - return math.sqrt(X + Y) # the 'z' value is zero in both points - - # Separate arc data into Loops and Arcs - for ei in range(0, ec): - edg = compGeoShp.Edges[ei] - if edg.Closed is True: - stpOvrEI.append(('L', ei, False)) - else: - if isSame is False: - segEI.append(ei) - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - else: - # Check if arc is co-radial to current SEGS - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - if abs(sameRad - pnt.sub(COM).Length) > 0.00001: - isSame = False - - if isSame is True: - segEI.append(ei) - else: - # Move co-radial arc segments - stpOvrEI.append(['A', segEI, False]) - # Start new list of arc segments - segEI = [ei] - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - # Process trailing `segEI` data, if available - if isSame is True: - stpOvrEI.append(['A', segEI, False]) - - # Identify adjacent arcs with y=0 start/end points that connect - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'A': - startOnAxis = list() - endOnAxis = list() - EI = SO[1] # list of corresponding compGeoShp.Edges indexes - - # Identify startOnAxis and endOnAxis arcs - for i in range(0, len(EI)): - ei = EI[i] # edge index - E = compGeoShp.Edges[ei] # edge object - if abs(COM.y - E.Vertexes[0].Y) < 0.00001: - startOnAxis.append((i, ei, E.Vertexes[0])) - elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: - endOnAxis.append((i, ei, E.Vertexes[1])) - - # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected - lenSOA = len(startOnAxis) - lenEOA = len(endOnAxis) - if lenSOA > 0 and lenEOA > 0: - delIdxs = list() - lstFindIdx = 0 - for soa in range(0, lenSOA): - (iS, eiS, vS) = startOnAxis[soa] - for eoa in range(0, len(endOnAxis)): - (iE, eiE, vE) = endOnAxis[eoa] - dist = vE.X - vS.X - if abs(dist) < 0.00001: # They connect on axis at same radius - SO[2] = (eiE, eiS) - break - elif dist > 0: - break # stop searching - # Eif - # Eif - # Efor - - # Construct arc data tuples for OCL - dirFlg = 1 - # cutPat = obj.CutPattern - if self.CutClimb is False: # True yields Climb when set to Conventional - dirFlg = -1 - - # Cycle through stepOver data - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'L': # L = Loop/Ring/Circle - # PathLog.debug("SO[0] == 'Loop'") - lei = SO[1] # loop Edges index - v1 = compGeoShp.Edges[lei].Vertexes[0] - - # space = obj.SampleInterval.Value / 2.0 - space = 0.0000001 - - # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) - p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) - rad = p1.sub(COM).Length - spcRadRatio = space/rad - if spcRadRatio < 1.0: - tolrncAng = math.asin(spcRadRatio) - else: - tolrncAng = 0.9999998 * math.pi - EX = COM.x + (rad * math.cos(tolrncAng)) - EY = v1.Y - space # rad * math.sin(tolrncAng) - - sp = (v1.X, v1.Y, 0.0) - ep = (EX, EY, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - ARCS.append(('L', dirFlg, [arc])) - else: # SO[0] == 'A' A = Arc - # PathLog.debug("SO[0] == 'Arc'") - PRTS = list() - EI = SO[1] # list of corresponding Edges indexes - CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) - chkGap = False - lst = None - - if CONN is not False: - (iE, iS) = CONN - v1 = compGeoShp.Edges[iE].Vertexes[0] - v2 = compGeoShp.Edges[iS].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - lst = sp - PRTS.append(arc) - # Pop connected edge index values from arc segments index list - iEi = EI.index(iE) - iSi = EI.index(iS) - if iEi > iSi: - EI.pop(iEi) - EI.pop(iSi) - else: - EI.pop(iSi) - EI.pop(iEi) - if len(EI) > 0: - PRTS.append('BRK') - chkGap = True - cnt = 0 - for ei in EI: - if cnt > 0: - PRTS.append('BRK') - chkGap = True - v1 = compGeoShp.Edges[ei].Vertexes[0] - v2 = compGeoShp.Edges[ei].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) - lst = sp - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = PRTS.pop() # pop off 'BRK' marker - (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current - arc = (vA, arc[1], vC) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - PRTS.append(arc) - cnt += 1 - - if dirFlg == -1: - PRTS.reverse() - - ARCS.append(('A', dirFlg, PRTS)) - # Eif - if obj.CutPattern == 'CircularZigZag': - dirFlg = -1 * dirFlg - # Efor - - return ARCS - - def _getExperimentalWaterlinePaths(self, obj, PNTSET, csHght): - '''_getExperimentalWaterlinePaths(obj, PNTSET, csHght)... - Switching function for calling the appropriate path-geometry to OCL points conversion function - for the various cut patterns.''' - PathLog.debug('_getExperimentalWaterlinePaths()') - SCANS = list() - - if obj.CutPattern == 'Line': - stpOvr = list() - for D in PNTSET: - for SEG in D: - if SEG == 'BRK': - stpOvr.append(SEG) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = SEG - P1 = FreeCAD.Vector(A[0], A[1], csHght) - P2 = FreeCAD.Vector(B[0], B[1], csHght) - stpOvr.append((P1, P2)) - SCANS.append(stpOvr) - stpOvr = list() - elif obj.CutPattern == 'ZigZag': - stpOvr = list() - for (dirFlg, LNS) in PNTSET: - for SEG in LNS: - if SEG == 'BRK': - stpOvr.append(SEG) - else: - # D format is ((p1, p2), (p3, p4)) - (A, B) = SEG - P1 = FreeCAD.Vector(A[0], A[1], csHght) - P2 = FreeCAD.Vector(B[0], B[1], csHght) - stpOvr.append((P1, P2)) - SCANS.append(stpOvr) - stpOvr = list() - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - # PNTSET is list, by stepover. - # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) - for so in range(0, len(PNTSET)): - stpOvr = list() - erFlg = False - (aTyp, dirFlg, ARCS) = PNTSET[so] - - if dirFlg == 1: # 1 - cMode = True # Climb mode - else: - cMode = False - - for a in range(0, len(ARCS)): - Arc = ARCS[a] - if Arc == 'BRK': - stpOvr.append('BRK') - else: - (sp, ep, cp) = Arc - S = FreeCAD.Vector(sp[0], sp[1], csHght) - E = FreeCAD.Vector(ep[0], ep[1], csHght) - C = FreeCAD.Vector(cp[0], cp[1], csHght) - scan = (S, E, C, cMode) - if scan is False: - erFlg = True - else: - ##if aTyp == 'L': - ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z)) - stpOvr.append(scan) - if erFlg is False: - SCANS.append(stpOvr) - - return SCANS - - # Main planar scan functions - def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): - cmds = list() - rtpd = False - horizGC = 'G0' - hSpeed = self.horizRapid - height = obj.SafeHeight.Value - - if obj.CutPattern in ['Line', 'Circular']: - if obj.OptimizeStepOverTransitions is True: - height = minSTH + 2.0 - # if obj.LayerMode == 'Multi-pass': - # rtpd = minSTH - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: - if obj.OptimizeStepOverTransitions is True: - zChng = first.z - lstPnt.z - # PathLog.debug('first.z: {}'.format(first.z)) - # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) - # PathLog.debug('zChng: {}'.format(zChng)) - # PathLog.debug('minSTH: {}'.format(minSTH)) - if abs(zChng) < tolrnc: # transitions to same Z height - # PathLog.debug('abs(zChng) < tolrnc') - if (minSTH - first.z) > tolrnc: - # PathLog.debug('(minSTH - first.z) > tolrnc') - height = minSTH + 2.0 - else: - # PathLog.debug('ELSE (minSTH - first.z) > tolrnc') - horizGC = 'G1' - height = first.z - elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): - height = False # allow end of Zig to cut to beginning of Zag - - - # Create raise, shift, and optional lower commands - if height is not False: - cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) - cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) - if rtpd is not False: # ReturnToPreviousDepth - cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) - - return cmds - - def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): - cmds = list() - rtpd = False - horizGC = 'G0' - hSpeed = self.horizRapid - height = obj.SafeHeight.Value - - if obj.CutPattern in ['Line', 'Circular']: - if obj.OptimizeStepOverTransitions is True: - height = minSTH + 2.0 - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: - if obj.OptimizeStepOverTransitions is True: - zChng = first.z - lstPnt.z - if abs(zChng) < tolrnc: # transitions to same Z height - if (minSTH - first.z) > tolrnc: - height = minSTH + 2.0 - else: - height = first.z + 2.0 # first.z - - cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) - cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) - if rtpd is not False: # ReturnToPreviousDepth - cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) - - return cmds - - def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): - pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object - pdc.setSTL(stl) # add stl model - if useSafeCutter is True: - pdc.setCutter(self.safeCutter) # add safeCutter - else: - pdc.setCutter(self.cutter) # add cutter - pdc.setZ(finalDep) # set minimumZ (final / target depth value) - pdc.setSampling(SampleInterval) # set sampling size - return pdc - - # Main waterline functions - def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): - '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.''' - commands = [] - - t_begin = time.time() - # JOB = PathUtils.findParentJob(obj) - base = JOB.Model.Group[mdlIdx] - bb = self.boundBoxes[mdlIdx] - stl = self.modelSTLs[mdlIdx] - - # Prepare global holdpoint and layerEndPnt containers - if self.holdPoint is None: - self.holdPoint = ocl.Point(float("inf"), float("inf"), float("inf")) - if self.layerEndPnt is None: - self.layerEndPnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model - toolDiam = self.cutter.getDiameter() - cdeoX = 0.6 * toolDiam - cdeoY = 0.6 * toolDiam - - if subShp is None: - # Get correct boundbox - if obj.BoundBox == 'Stock': - BS = JOB.Stock - bb = BS.Shape.BoundBox - elif obj.BoundBox == 'BaseBoundBox': - BS = base - bb = base.Shape.BoundBox - - env = PathUtils.getEnvelope(partshape=BS.Shape, depthparams=self.depthParams) # Produces .Shape - - xmin = bb.XMin - xmax = bb.XMax - ymin = bb.YMin - ymax = bb.YMax - zmin = bb.ZMin - zmax = bb.ZMax - else: - xmin = subShp.BoundBox.XMin - xmax = subShp.BoundBox.XMax - ymin = subShp.BoundBox.YMin - ymax = subShp.BoundBox.YMax - zmin = subShp.BoundBox.ZMin - zmax = subShp.BoundBox.ZMax - - smplInt = obj.SampleInterval.Value - minSampInt = 0.001 # value is mm - if smplInt < minSampInt: - smplInt = minSampInt - - # Determine bounding box length for the OCL scan - bbLength = math.fabs(ymax - ymin) - numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == 'Single-pass': - depthparams = [obj.FinalDepth.Value] - else: - depthparams = [dp for dp in self.depthParams] - lenDP = len(depthparams) - - # Prepare PathDropCutter objects with STL data - safePDC = self._planarGetPDC(self.safeSTLs[mdlIdx], - depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) - - # Scan the piece to depth at smplInt - oclScan = [] - oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines) - # oclScan = SCANS - lenOS = len(oclScan) - ptPrLn = int(lenOS / numScanLines) - - # Convert oclScan list of points to multi-dimensional list - scanLines = [] - for L in range(0, numScanLines): - scanLines.append([]) - for P in range(0, ptPrLn): - pi = L * ptPrLn + P - scanLines[L].append(oclScan[pi]) - lenSL = len(scanLines) - pntsPerLine = len(scanLines[0]) - PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") - - # Extract Wl layers per depthparams - lyr = 0 - cmds = [] - layTime = time.time() - self.topoMap = [] - for layDep in depthparams: - cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) - commands.extend(cmds) - lyr += 1 - PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s") - return commands - - def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines): - '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ... - Perform OCL scan for waterline purpose.''' - pdc = ocl.PathDropCutter() # create a pdc - pdc.setSTL(stl) - pdc.setCutter(self.cutter) - pdc.setZ(fd) # set minimumZ (final / target depth value) - pdc.setSampling(smplInt) - - # Create line object as path - path = ocl.Path() # create an empty path object - for nSL in range(0, numScanLines): - yVal = ymin + (nSL * smplInt) - p1 = ocl.Point(xmin, yVal, fd) # start-point of line - p2 = ocl.Point(xmax, yVal, fd) # end-point of line - path.append(ocl.Line(p1, p2)) - # path.append(l) # add the line to the path - pdc.setPath(path) - pdc.run() # run drop-cutter on the path - - # return the list the points - return pdc.getCLPoints() - - def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine): - '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.''' - commands = [] - cmds = [] - loopList = [] - self.topoMap = [] - # Create topo map from scanLines (highs and lows) - self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) - # Add buffer lines and columns to topo map - self._bufferTopoMap(lenSL, pntsPerLine) - # Identify layer waterline from OCL scan - self._highlightWaterline(4, 9) - # Extract waterline and convert to gcode - loopList = self._extractWaterlines(obj, scanLines, lyr, layDep) - # save commands - for loop in loopList: - cmds = self._loopToGcode(obj, layDep, loop) - commands.extend(cmds) - return commands - - def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine): - '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.''' - topoMap = [] - for L in range(0, lenSL): - topoMap.append([]) - for P in range(0, pntsPerLine): - if scanLines[L][P].z > layDep: - topoMap[L].append(2) - else: - topoMap[L].append(0) - return topoMap - - def _bufferTopoMap(self, lenSL, pntsPerLine): - '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.''' - pre = [0, 0] - post = [0, 0] - for p in range(0, pntsPerLine): - pre.append(0) - post.append(0) - for l in range(0, lenSL): - self.topoMap[l].insert(0, 0) - self.topoMap[l].append(0) - self.topoMap.insert(0, pre) - self.topoMap.append(post) - return True - - def _highlightWaterline(self, extraMaterial, insCorn): - '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.''' - TM = self.topoMap - lastPnt = len(TM[1]) - 1 - lastLn = len(TM) - 1 - highFlag = 0 - - # ("--Convert parallel data to ridges") - for lin in range(1, lastLn): - for pt in range(1, lastPnt): # Ignore first and last points - if TM[lin][pt] == 0: - if TM[lin][pt + 1] == 2: # step up - TM[lin][pt] = 1 - if TM[lin][pt - 1] == 2: # step down - TM[lin][pt] = 1 - - # ("--Convert perpendicular data to ridges and highlight ridges") - for pt in range(1, lastPnt): # Ignore first and last points - for lin in range(1, lastLn): - if TM[lin][pt] == 0: - highFlag = 0 - if TM[lin + 1][pt] == 2: # step up - TM[lin][pt] = 1 - if TM[lin - 1][pt] == 2: # step down - TM[lin][pt] = 1 - elif TM[lin][pt] == 2: - highFlag += 1 - if highFlag == 3: - if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2: - highFlag = 2 - else: - TM[lin - 1][pt] = extraMaterial - highFlag = 2 - - # ("--Square corners") - for pt in range(1, lastPnt): - for lin in range(1, lastLn): - if TM[lin][pt] == 1: # point == 1 - cont = True - if TM[lin + 1][pt] == 0: # forward == 0 - if TM[lin + 1][pt - 1] == 1: # forward left == 1 - if TM[lin][pt - 1] == 2: # left == 2 - TM[lin + 1][pt] = 1 # square the corner - cont = False - - if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1 - if TM[lin][pt + 1] == 2: # right == 2 - TM[lin + 1][pt] = 1 # square the corner - cont = True - - if TM[lin - 1][pt] == 0: # back == 0 - if TM[lin - 1][pt - 1] == 1: # back left == 1 - if TM[lin][pt - 1] == 2: # left == 2 - TM[lin - 1][pt] = 1 # square the corner - cont = False - - if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1 - if TM[lin][pt + 1] == 2: # right == 2 - TM[lin - 1][pt] = 1 # square the corner - - # remove inside corners - for pt in range(1, lastPnt): - for lin in range(1, lastLn): - if TM[lin][pt] == 1: # point == 1 - if TM[lin][pt + 1] == 1: - if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1: - TM[lin][pt + 1] = insCorn - elif TM[lin][pt - 1] == 1: - if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1: - TM[lin][pt - 1] = insCorn - - return True - - def _extractWaterlines(self, obj, oclScan, lyr, layDep): - '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.''' - srch = True - lastPnt = len(self.topoMap[0]) - 1 - lastLn = len(self.topoMap) - 1 - maxSrchs = 5 - srchCnt = 1 - loopList = [] - loop = [] - loopNum = 0 - - if self.CutClimb is True: - lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0] - pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] - else: - lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0] - pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] - - while srch is True: - srch = False - if srchCnt > maxSrchs: - PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!") - break - for L in range(1, lastLn): - for P in range(1, lastPnt): - if self.topoMap[L][P] == 1: - # start loop follow - srch = True - loopNum += 1 - loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum) - self.topoMap[L][P] = 0 # Mute the starting point - loopList.append(loop) - srchCnt += 1 - PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.") - return loopList - - def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): - '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.''' - loop = [oclScan[L - 1][P - 1]] # Start loop point list - cur = [L, P, 1] - prv = [L, P - 1, 1] - nxt = [L, P + 1, 1] - follow = True - ptc = 0 - ptLmt = 200000 - while follow is True: - ptc += 1 - if ptc > ptLmt: - PathLog.debug("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.") - break - nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point - loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list - self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem - if nxt[0] == L and nxt[1] == P: # check if loop complete - follow = False - elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected - follow = False - prv = cur - cur = nxt - return loop - - def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp): - '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ... - Find the next waterline point in the point cloud layer provided.''' - dl = cl - pl - dp = cp - pp - num = 0 - i = 3 - s = 0 - mtch = 0 - found = False - while mtch < 8: # check all 8 points around current point - if lC[i] == dl: - if pC[i] == dp: - s = i - 3 - found = True - # Check for y branch where current point is connection between branches - for y in range(1, mtch): - if lC[i + y] == dl: - if pC[i + y] == dp: - num = 1 - break - break - i += 1 - mtch += 1 - if found is False: - # ("_findNext: No start point found.") - return [cl, cp, num] - - for r in range(0, 8): - l = cl + lC[s + r] - p = cp + pC[s + r] - if self.topoMap[l][p] == 1: - return [l, p, num] - - # ("_findNext: No next pnt found") - return [cl, cp, num] - - def _loopToGcode(self, obj, layDep, loop): - '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.''' - # generate the path commands - output = [] - optimize = obj.OptimizeLinearPaths - - prev = ocl.Point(float("inf"), float("inf"), float("inf")) - nxt = ocl.Point(float("inf"), float("inf"), float("inf")) - pnt = ocl.Point(float("inf"), float("inf"), float("inf")) - - # Create first point - pnt.x = loop[0].x - pnt.y = loop[0].y - pnt.z = layDep - - # Position cutter to begin loop - output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) - output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) - output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) - - lenCLP = len(loop) - lastIdx = lenCLP - 1 - # Cycle through each point on loop - for i in range(0, lenCLP): - if i < lastIdx: - nxt.x = loop[i + 1].x - nxt.y = loop[i + 1].y - nxt.z = layDep - else: - optimize = False - - if not optimize or not FreeCAD.Vector(prev.x, prev.y, prev.z).isOnLineSegment(FreeCAD.Vector(nxt.x, nxt.y, nxt.z), FreeCAD.Vector(pnt.x, pnt.y, pnt.z)): - output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed})) - - # Rotate point data - prev.x = pnt.x - prev.y = pnt.y - prev.z = pnt.z - pnt.x = nxt.x - pnt.y = nxt.y - pnt.z = nxt.z - - # Save layer end point for use in transitioning to next layer - self.layerEndPnt.x = pnt.x - self.layerEndPnt.y = pnt.y - self.layerEndPnt.z = pnt.z - - return output - - # Main waterline functions - def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): - '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ... - Main waterline function to perform waterline extraction from model.''' - PathLog.debug('_experimentalWaterlineOp()') - - msg = translate('PathWaterline', 'Experimental Waterline does not currently support selected faces.') - PathLog.info('\n..... ' + msg) - - commands = [] - t_begin = time.time() - base = JOB.Model.Group[mdlIdx] - bb = self.boundBoxes[mdlIdx] - stl = self.modelSTLs[mdlIdx] - safeSTL = self.safeSTLs[mdlIdx] - self.endVector = None - - finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0) - depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep) - - # Compute number and size of stepdowns, and final depth - if obj.LayerMode == 'Single-pass': - depthparams = [finDep] - else: - depthparams = [dp for dp in depthParams] - lenDP = len(depthparams) - PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams)) - - # Prepare PathDropCutter objects with STL data - # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) - - buffer = self.cutter.getDiameter() * 2.0 - borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0)) - - # Get correct boundbox - if obj.BoundBox == 'Stock': - stockEnv = self._getShapeEnvelope(JOB.Stock.Shape) - bbFace = self._getCrossSection(stockEnv) # returned at Z=0.0 - elif obj.BoundBox == 'BaseBoundBox': - baseEnv = self._getShapeEnvelope(base.Shape) - bbFace = self._getCrossSection(baseEnv) # returned at Z=0.0 - - trimFace = borderFace.cut(bbFace) - if self.showDebugObjects is True: - TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace') - TF.Shape = trimFace - TF.purgeTouched() - self.tempGroup.addObject(TF) - - # Cycle through layer depths - CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace) - if not CUTAREAS: - PathLog.error('No cross-section cut areas identified.') - return commands - - caCnt = 0 - ofst = obj.BoundaryAdjustment.Value - ofst -= self.radius # (self.radius + (tolrnc / 10.0)) - caLen = len(CUTAREAS) - lastCA = caLen - 1 - lastClearArea = None - lastCsHght = None - clearLastLayer = True - for ca in range(0, caLen): - area = CUTAREAS[ca] - csHght = area.BoundBox.ZMin - csHght += obj.DepthOffset.Value - cont = False - caCnt += 1 - if area.Area > 0.0: - cont = True - caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire - PathLog.debug('cutAreaWireCnt: {}'.format(caWireCnt)) - if self.showDebugObjects is True: - CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt)) - CA.Shape = area - CA.purgeTouched() - self.tempGroup.addObject(CA) - else: - PathLog.error('Cut area at {} is zero.'.format(round(csHght, 4))) - - # get offset wire(s) based upon cross-section cut area - if cont: - area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin)) - activeArea = area.cut(trimFace) - activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire - PathLog.debug('activeAreaWireCnt: {}'.format(activeAreaWireCnt)) - if self.showDebugObjects is True: - CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt)) - CA.Shape = activeArea - CA.purgeTouched() - self.tempGroup.addObject(CA) - ofstArea = self._extractFaceOffset(obj, activeArea, ofst, makeComp=False) - if not ofstArea: - PathLog.error('No offset area returned for cut area depth: {}'.format(csHght)) - cont = False - - if cont: - # Identify solid areas in the offset data - ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea) - if ofstSolidFacesList: - clearArea = Part.makeCompound(ofstSolidFacesList) - if self.showDebugObjects is True: - CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt)) - CA.Shape = clearArea - CA.purgeTouched() - self.tempGroup.addObject(CA) - else: - cont = False - PathLog.error('ofstSolids is False.') - - if cont: - # Make waterline path for current CUTAREA depth (csHght) - commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght)) - clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin)) - lastClearArea = clearArea - lastCsHght = csHght - - # Clear layer as needed - (useOfst, usePat, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer) - ##if self.showDebugObjects is True and (usePat or useOfst): - ## OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearPatternArea_{}'.format(round(csHght, 2))) - ## OA.Shape = clearArea - ## OA.purgeTouched() - ## self.tempGroup.addObject(OA) - if usePat: - commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght)) - if useOfst: - commands.extend(self._makeOffsetLayerPaths(JOB, obj, clearArea, csHght)) - # Efor - - if clearLastLayer: - (useOfst, usePat, cLL) = self._clearLayer(obj, 1, 1, False) - clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin)) - if usePat: - commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght)) - - if useOfst: - commands.extend(self._makeOffsetLayerPaths(JOB, obj, lastClearArea, lastCsHght)) - - PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s") - return commands - - def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace): - '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ... - Takes shape, depthparams and base-envelope-cross-section, and - returns a list of cut areas - one for each depth.''' - PathLog.debug('_getCutAreas()') - - CUTAREAS = list() - lastLayComp = None - isFirst = True - lenDP = len(depthparams) - - # Cycle through layer depths - for dp in range(0, lenDP): - csHght = depthparams[dp] - PathLog.debug('Depth {} is {}'.format(dp + 1, csHght)) - - # Get slice at depth of shape - csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0 - if not csFaces: - PathLog.error('No cross-section wires at {}'.format(csHght)) - else: - PathLog.debug('cross-section face count {}'.format(len(csFaces))) - if len(csFaces) > 0: - useFaces = self._getSolidAreasFromPlanarFaces(csFaces) - else: - useFaces = False - - if useFaces: - PathLog.debug('useFacesCnt: {}'.format(len(useFaces))) - compAdjFaces = Part.makeCompound(useFaces) - - if self.showDebugObjects is True: - CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1)) - CA.Shape = compAdjFaces - CA.purgeTouched() - self.tempGroup.addObject(CA) - - if isFirst: - allPrevComp = compAdjFaces - cutArea = borderFace.cut(compAdjFaces) - else: - preCutArea = borderFace.cut(compAdjFaces) - cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas - allPrevComp = allPrevComp.fuse(compAdjFaces) - cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin)) - CUTAREAS.append(cutArea) - isFirst = False - else: - PathLog.error('No waterline at depth: {} mm.'.format(csHght)) - # Efor - - if len(CUTAREAS) > 0: - return CUTAREAS - - return False - - def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght): - PathLog.debug('_wiresToWaterlinePath()') - commands = list() - - # Translate path geometry to layer height - ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin)) - if self.showDebugObjects is True: - OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2))) - OA.Shape = ofstPlnrShp - OA.purgeTouched() - self.tempGroup.addObject(OA) - - commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2)))) - for w in range(0, len(ofstPlnrShp.Wires)): - wire = ofstPlnrShp.Wires[w] - V = wire.Vertexes - if obj.CutMode == 'Climb': - lv = len(V) - 1 - startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) - else: - startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) - - commands.append(Path.Command('N (Wire {}.)'.format(w))) - (cmds, endVect) = self._wireToPath(obj, wire, startVect) - commands.extend(cmds) - commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - - return commands - - def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght): - PathLog.debug('_makeCutPatternLayerPaths()') - commands = [] - - clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin)) - pathGeom = self._planarMakePathGeom(obj, clrAreaShp) - pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin)) - # clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) - - if self.showDebugObjects is True: - OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2))) - OA.Shape = pathGeom - OA.purgeTouched() - self.tempGroup.addObject(OA) - - # Convert pathGeom to gcode more efficiently - if True: - if obj.CutPattern == 'Offset': - commands.extend(self._makeOffsetLayerPaths(JOB, obj, clrAreaShp, csHght)) - else: - clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) - if obj.CutPattern == 'Line': - pntSet = self._pathGeomToLinesPointSet(obj, pathGeom) - elif obj.CutPattern == 'ZigZag': - pntSet = self._pathGeomToZigzagPointSet(obj, pathGeom) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - pntSet = self._pathGeomToArcPointSet(obj, pathGeom) - stpOVRS = self._getExperimentalWaterlinePaths(obj, pntSet, csHght) - # PathLog.debug('stpOVRS:\n{}'.format(stpOVRS)) - safePDC = False - cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, csHght) - commands.extend(cmds) - else: - # Use Path.fromShape() to convert edges to paths - for w in range(0, len(pathGeom.Edges)): - wire = pathGeom.Edges[w] - V = wire.Vertexes - if obj.CutMode == 'Climb': - lv = len(V) - 1 - startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) - else: - startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) - - commands.append(Path.Command('N (Wire {}.)'.format(w))) - (cmds, endVect) = self._wireToPath(obj, wire, startVect) - commands.extend(cmds) - commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - - return commands - - def _makeOffsetLayerPaths(self, JOB, obj, clrAreaShp, csHght): - PathLog.debug('_makeOffsetLayerPaths()') - PathLog.warning('Using `Offset` for clearing bottom layer.') - cmds = list() - # ofst = obj.BoundaryAdjustment.Value - ofst = 0.0 - self.cutOut # - self.cutter.getDiameter() # (self.radius + (tolrnc / 10.0)) - shape = clrAreaShp - cont = True - cnt = 0 - while cont: - ofstArea = self._extractFaceOffset(obj, shape, ofst, makeComp=True) - if not ofstArea: - PathLog.warning('No offset clearing area returned.') - break - for F in ofstArea.Faces: - cmds.extend(self._wiresToWaterlinePath(obj, F, csHght)) - shape = ofstArea - if cnt == 0: - ofst = 0.0 - self.cutOut # self.cutter.Diameter() - cnt += 1 - return cmds - - def _clearGeomToPaths(self, JOB, obj, safePDC, SCANDATA, csHght): - PathLog.debug('_clearGeomToPaths()') - - GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] - tolrnc = JOB.GeometryTolerance.Value - prevDepth = obj.SafeHeight.Value - lenSCANDATA = len(SCANDATA) - gDIR = ['G3', 'G2'] - - if self.CutClimb is True: - gDIR = ['G2', 'G3'] - - # Send cutter to x,y position of first point on first line - first = SCANDATA[0][0][0] # [step][item][point] - GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) - - # Cycle through step-over sections (line segments or arcs) - odd = True - lstStpEnd = None - prevDepth = obj.SafeHeight.Value # Not used for Single-pass - for so in range(0, lenSCANDATA): - cmds = list() - PRTS = SCANDATA[so] - lenPRTS = len(PRTS) - first = PRTS[0][0] # first point of arc/line stepover group - start = PRTS[0][0] # will change with each line/arc segment - last = None - cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) - - if so > 0: - if obj.CutPattern == 'CircularZigZag': - if odd is True: - odd = False - else: - odd = True - # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL - minTrnsHght = obj.SafeHeight.Value - # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) - cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) - - # Cycle through current step-over parts - for i in range(0, lenPRTS): - prt = PRTS[i] - lenPrt = len(prt) - # PathLog.debug('prt: {}'.format(prt)) - if prt == 'BRK': - nxtStart = PRTS[i + 1][0] - # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL - minSTH = obj.SafeHeight.Value - cmds.append(Path.Command('N (Break)', {})) - cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) - else: - cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) - if obj.CutPattern in ['Line', 'ZigZag']: - start, last = prt - cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed})) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - start, last, centPnt, cMode = prt - gcode = self._makeGcodeArc(start, last, odd, gDIR, tolrnc) - cmds.extend(gcode) - cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) - GCODE.extend(cmds) # save line commands - lstStpEnd = last - # Efor - - return GCODE - - def _getSolidAreasFromPlanarFaces(self, csFaces): - PathLog.debug('_getSolidAreasFromPlanarFaces()') - holds = list() - cutFaces = list() - useFaces = list() - lenCsF = len(csFaces) - PathLog.debug('lenCsF: {}'.format(lenCsF)) - - if lenCsF == 1: - useFaces = csFaces - else: - fIds = list() - aIds = list() - pIds = list() - cIds = list() - - for af in range(0, lenCsF): - fIds.append(af) # face ids - aIds.append(af) # face ids - pIds.append(-1) # parent ids - cIds.append(False) # cut ids - holds.append(False) - - while len(fIds) > 0: - li = fIds.pop() - low = csFaces[li] # senior face - pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low) - # Ewhile - ##PathLog.info('fIds: {}'.format(fIds)) - ##PathLog.info('pIds: {}'.format(pIds)) - - for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first - ##PathLog.info('af: {}'.format(af)) - prnt = pIds[af] - ##PathLog.info('prnt: {}'.format(prnt)) - if prnt == -1: - stack = -1 - else: - stack = [af] - # get_face_ids_to_parent - stack.insert(0, prnt) - nxtPrnt = pIds[prnt] - # find af value for nxtPrnt - while nxtPrnt != -1: - stack.insert(0, nxtPrnt) - nxtPrnt = pIds[nxtPrnt] - cIds[af] = stack - # PathLog.debug('cIds: {}\n'.format(cIds)) - - for af in range(0, lenCsF): - # PathLog.debug('af is {}'.format(af)) - pFc = cIds[af] - if pFc == -1: - # Simple, independent region - holds[af] = csFaces[af] # place face in hold - # PathLog.debug('pFc == -1') - else: - # Compound region - # PathLog.debug('pFc is not -1') - cnt = len(pFc) - if cnt % 2.0 == 0.0: - # even is donut cut - # PathLog.debug('cnt is even') - inr = pFc[cnt - 1] - otr = pFc[cnt - 2] - # PathLog.debug('inr / otr: {} / {}'.format(inr, otr)) - holds[otr] = holds[otr].cut(csFaces[inr]) - else: - # odd is floating solid - # PathLog.debug('cnt is ODD') - holds[af] = csFaces[af] - # Efor - - for af in range(0, lenCsF): - if holds[af]: - useFaces.append(holds[af]) # save independent solid - - # Eif - - if len(useFaces) > 0: - return useFaces - - return False - - def _getModelCrossSection(self, shape, csHght): - PathLog.debug('_getCrossSection()') - wires = list() - - def byArea(fc): - return fc.Area - - for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght): - wires.append(i) - - if len(wires) > 0: - for w in wires: - if w.isClosed() is False: - return False - FCS = list() - for w in wires: - w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin)) - FCS.append(Part.Face(w)) - FCS.sort(key=byArea, reverse=True) - return FCS - else: - PathLog.debug(' -No wires from .slice() method') - - return False - - def _isInBoundBox(self, outShp, inShp): - obb = outShp.BoundBox - ibb = inShp.BoundBox - - if obb.XMin < ibb.XMin: - if obb.XMax > ibb.XMax: - if obb.YMin < ibb.YMin: - if obb.YMax > ibb.YMax: - return True - return False - - def _idInternalFeature(self, csFaces, fIds, pIds, li, low): - Ids = list() - for i in fIds: - Ids.append(i) - while len(Ids) > 0: - hi = Ids.pop() - high = csFaces[hi] - if self._isInBoundBox(high, low): - cmn = high.common(low) - if cmn.Area > 0.0: - pIds[li] = hi - break - # Ewhile - return pIds - - def _wireToPath(self, obj, wire, startVect): - '''_wireToPath(obj, wire, startVect) ... wire to path.''' - PathLog.track() - - paths = [] - pathParams = {} # pylint: disable=assignment-from-no-return - V = wire.Vertexes - - pathParams['shapes'] = [wire] - pathParams['feedrate'] = self.horizFeed - pathParams['feedrate_v'] = self.vertFeed - pathParams['verbose'] = True - pathParams['resume_height'] = obj.SafeHeight.Value - pathParams['retraction'] = obj.ClearanceHeight.Value - pathParams['return_end'] = True - # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers - pathParams['preamble'] = False - pathParams['start'] = startVect - - (pp, end_vector) = Path.fromShapes(**pathParams) - paths.extend(pp.Commands) - # PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) - - self.endVector = end_vector # pylint: disable=attribute-defined-outside-init - - return (paths, end_vector) - - def _makeExtendedBoundBox(self, wBB, bbBfr, zDep): - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - - p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) - p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) - p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) - p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) - bb = Part.makePolygon([p1, p2, p3, p4, p1]) - - return bb - - def _makeGcodeArc(self, strtPnt, endPnt, odd, gDIR, tolrnc): - cmds = list() - isCircle = False - inrPnt = None - gdi = 0 - if odd is True: - gdi = 1 - - # Test if pnt set is circle - if abs(strtPnt.x - endPnt.x) < tolrnc: - if abs(strtPnt.y - endPnt.y) < tolrnc: - isCircle = True - isCircle = False - - if isCircle is True: - # convert LN to G2/G3 arc, consolidating GCode - # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc - # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ - # Dividing circle into two arcs allows for G2/G3 on inclined surfaces - - # ijk = self.tmpCOM - strtPnt # vector from start to center - ijk = self.tmpCOM - strtPnt # vector from start to center - xyz = self.tmpCOM.add(ijk) # end point - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) - ijk = self.tmpCOM - xyz # vector from start to center - rst = strtPnt # end point - cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - else: - # ijk = self.tmpCOM - strtPnt - ijk = self.tmpCOM.sub(strtPnt) # vector from start to center - xyz = endPnt - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) - - return cmds - - def _clearLayer(self, obj, ca, lastCA, clearLastLayer): - PathLog.debug('_clearLayer()') - usePat = False - useOfst = False - - if obj.ClearLastLayer == 'Off': - if obj.CutPattern != 'None': - usePat = True - else: - if ca == lastCA: - PathLog.debug('... Clearing bottom layer.') - if obj.ClearLastLayer == 'Offset': - obj.CutPattern = 'None' - useOfst = True - else: - obj.CutPattern = obj.ClearLastLayer - usePat = True - clearLastLayer = False - - return (useOfst, usePat, clearLastLayer) - - def resetOpVariables(self, all=True): - '''resetOpVariables() ... Reset class variables used for instance of operation.''' - self.holdPoint = None - self.layerEndPnt = None - self.onHold = False - self.SafeHeightOffset = 2.0 - self.ClearHeightOffset = 4.0 - self.layerEndzMax = 0.0 - self.resetTolerance = 0.0 - self.holdPntCnt = 0 - self.bbRadius = 0.0 - self.axialFeed = 0.0 - self.axialRapid = 0.0 - self.FinalDepth = 0.0 - self.clearHeight = 0.0 - self.safeHeight = 0.0 - self.faceZMax = -999999999999.0 - if all is True: - self.cutter = None - self.stl = None - self.fullSTL = None - self.cutOut = 0.0 - self.radius = 0.0 - self.useTiltCutter = False - return True - - def deleteOpVariables(self, all=True): - '''deleteOpVariables() ... Reset class variables used for instance of operation.''' - del self.holdPoint - del self.layerEndPnt - del self.onHold - del self.SafeHeightOffset - del self.ClearHeightOffset - del self.layerEndzMax - del self.resetTolerance - del self.holdPntCnt - del self.bbRadius - del self.axialFeed - del self.axialRapid - del self.FinalDepth - del self.clearHeight - del self.safeHeight - del self.faceZMax - if all is True: - del self.cutter - del self.stl - del self.fullSTL - del self.cutOut - del self.radius - del self.useTiltCutter - return True - - def setOclCutter(self, obj, safe=False): - ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' - # Set cutter details - # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details - diam_1 = float(obj.ToolController.Tool.Diameter) - lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 - FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 - CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 - CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 - - # Make safeCutter with 2 mm buffer around physical cutter - if safe is True: - diam_1 += 4.0 - if FR != 0.0: - FR += 2.0 - - PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) - if obj.ToolController.Tool.ToolType == 'EndMill': - # Standard End Mill - return ocl.CylCutter(diam_1, (CEH + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: - # Standard Ball End Mill - # OCL -> BallCutter::BallCutter(diameter, length) - self.useTiltCutter = True - return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> BallCutter::BallCutter(diameter, length) - return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) - - elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) - - elif obj.ToolController.Tool.ToolType == 'ChamferMill': - # Bull Nose or Corner Radius cutter - # Reference: https://www.fine-tools.com/halbstabfraeser.html - # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) - return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) - else: - # Default to standard end mill - PathLog.warning("Defaulting cutter to standard end mill.") - return ocl.CylCutter(diam_1, (CEH + lenOfst)) - - # http://www.carbidecutter.net/products/carbide-burr-cone-shape-sm.html - ''' - # Available FreeCAD cutter types - some still need translation to available OCL cutter classes. - Drill, CenterDrill, CounterSink, CounterBore, FlyCutter, Reamer, Tap, - EndMill, SlotCutter, BallEndMill, ChamferMill, CornerRound, Engraver - ''' - # Adittional problem is with new ToolBit user-defined cutter shapes. - # Some sort of translation/conversion will have to be defined to make compatible with OCL. - PathLog.error('Unable to set OCL cutter.') - return False - - -def SetupProperties(): - ''' SetupProperties() ... Return list of properties required for operation.''' - setup = [] - setup.append('Algorithm') - setup.append('AngularDeflection') - setup.append('AvoidLastX_Faces') - setup.append('AvoidLastX_InternalFeatures') - setup.append('BoundBox') - setup.append('BoundaryAdjustment') - setup.append('CircularCenterAt') - setup.append('CircularCenterCustom') - setup.append('ClearLastLayer') - setup.append('CutMode') - setup.append('CutPattern') - setup.append('CutPatternAngle') - setup.append('CutPatternReversed') - setup.append('DepthOffset') - setup.append('GapSizes') - setup.append('GapThreshold') - setup.append('HandleMultipleFeatures') - setup.append('InternalFeaturesCut') - setup.append('InternalFeaturesAdjustment') - setup.append('LayerMode') - setup.append('LinearDeflection') - setup.append('OptimizeStepOverTransitions') - setup.append('ProfileEdges') - setup.append('BoundaryEnforcement') - setup.append('SampleInterval') - setup.append('StartPoint') - setup.append('StepOver') - setup.append('UseStartPoint') - # For debugging - setup.append('ShowTempObjects') - return setup - - -def Create(name, obj=None): - '''Create(name) ... Creates and returns a Waterline operation.''' - if obj is None: - obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) - obj.Proxy = ObjectWaterline(obj, name) - return obj +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2019 Russell Johnson (russ4262) * +# * Copyright (c) 2019 sliptonic * +# * * +# * 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. * +# * * +# * This program 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 this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +__title__ = "Path Waterline Operation" +__author__ = "russ4262 (Russell Johnson), sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Class and implementation of Waterline operation." +__contributors__ = "" + +import FreeCAD +from PySide import QtCore + +# OCL must be installed +try: + import ocl +except ImportError: + msg = QtCore.QCoreApplication.translate("PathWaterline", "This operation requires OpenCamLib to be installed.") + FreeCAD.Console.PrintError(msg + "\n") + raise ImportError + # import sys + # sys.exit(msg) + +import MeshPart +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils +import PathScripts.PathOp as PathOp +import time +import math +import Part + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') +Draft = LazyLoader('Draft', globals(), 'Draft') +Part = LazyLoader('Part', globals(), 'Part') + +if FreeCAD.GuiUp: + import FreeCADGui + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class ObjectWaterline(PathOp.ObjectOp): + '''Proxy object for Surfacing operation.''' + + def baseObject(self): + '''baseObject() ... returns super of receiver + Used to call base implementation in overwritten functions.''' + return super(self.__class__, self) + + def opFeatures(self, obj): + '''opFeatures(obj) ... return all standard features and edges based geomtries''' + return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces + + def initOperation(self, obj): + '''initPocketOp(obj) ... + Initialize the operation - property creation and property editor status.''' + self.initOpProperties(obj) + + # For debugging + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + + if not hasattr(obj, 'DoNotSetDefaultValues'): + self.setEditorProperties(obj) + + def initOpProperties(self, obj): + '''initOpProperties(obj) ... create operation specific properties''' + PROPS = [ + ("App::PropertyBool", "ShowTempObjects", "Debug", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), + + ("App::PropertyDistance", "AngularDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values increase processing time a lot.")), + ("App::PropertyDistance", "LinearDeflection", "Mesh Conversion", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Smaller values yield a finer, more accurate the mesh. Smaller values do not increase processing time much.")), + + ("App::PropertyInteger", "AvoidLastX_Faces", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.")), + ("App::PropertyBool", "AvoidLastX_InternalFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Do not cut internal features on avoided faces.")), + ("App::PropertyDistance", "BoundaryAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.")), + ("App::PropertyBool", "BoundaryEnforcement", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "If true, the cutter will remain inside the boundaries of the model or selected face(s).")), + ("App::PropertyEnumeration", "HandleMultipleFeatures", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose how to process multiple Base Geometry features.")), + ("App::PropertyDistance", "InternalFeaturesAdjustment", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Positive values push the cutter toward, or into, the feature. Negative values retract the cutter away from the feature.")), + ("App::PropertyBool", "InternalFeaturesCut", "Selected Geometry Settings", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore internal feature areas within a larger selected face.")), + + ("App::PropertyEnumeration", "Algorithm", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).")), + ("App::PropertyEnumeration", "BoundBox", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation.")), + ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), + ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")), + ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")), + ("App::PropertyEnumeration", "CutMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the direction for the cutting tool to engage the material: Climb (ClockWise) or Conventional (CounterClockWise)")), + ("App::PropertyEnumeration", "CutPattern", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the geometric clearing pattern to use for the operation.")), + ("App::PropertyFloat", "CutPatternAngle", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The yaw angle used for certain clearing patterns")), + ("App::PropertyBool", "CutPatternReversed", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse the cut order of the stepover paths. For circular cut patterns, begin at the outside and work toward the center.")), + ("App::PropertyDistance", "DepthOffset", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the Z-axis depth offset from the target surface.")), + ("App::PropertyDistance", "IgnoreOuterAbove", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore outer waterlines above this height.")), + ("App::PropertyEnumeration", "LayerMode", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), + ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), + ("App::PropertyDistance", "SampleInterval", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), + ("App::PropertyPercent", "StepOver", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), + + ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.")), + ("App::PropertyBool", "OptimizeStepOverTransitions", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable separate optimization of transitions between, and breaks within, each step over path.")), + ("App::PropertyDistance", "GapThreshold", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Collinear and co-radial artifact gaps that are smaller than this threshold are closed in the path.")), + ("App::PropertyString", "GapSizes", "Optimization", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Feedback: three smallest gaps identified in the path geometry.")), + + ("App::PropertyVectorDistance", "StartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The custom start point for the path of this operation")), + ("App::PropertyBool", "UseStartPoint", "Start Point", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) + ] + + missing = list() + for (prtyp, nm, grp, tt) in PROPS: + if not hasattr(obj, nm): + obj.addProperty(prtyp, nm, grp, tt) + missing.append(nm) + + # Set enumeration lists for enumeration properties + if len(missing) > 0: + ENUMS = self._propertyEnumerations() + for n in ENUMS: + if n in missing: + cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) + exec(cmdStr) + + self.addedAllProperties = True + + def _propertyEnumerations(self): + # Enumeration lists for App::PropertyEnumeration properties + return { + 'Algorithm': ['OCL Dropcutter', 'Experimental'], + 'BoundBox': ['BaseBoundBox', 'Stock'], + 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], + 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], + 'CutMode': ['Conventional', 'Climb'], + 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] + 'HandleMultipleFeatures': ['Collectively', 'Individually'], + 'LayerMode': ['Single-pass', 'Multi-pass'], + 'ProfileEdges': ['None', 'Only', 'First', 'Last'], + } + + def setEditorProperties(self, obj): + # Used to hide inputs in properties list + show = 0 + hide = A = 2 + if hasattr(obj, 'EnableRotation'): + obj.setEditorMode('EnableRotation', hide) + + obj.setEditorMode('BoundaryEnforcement', hide) + obj.setEditorMode('ProfileEdges', hide) + obj.setEditorMode('InternalFeaturesAdjustment', hide) + obj.setEditorMode('InternalFeaturesCut', hide) + obj.setEditorMode('AvoidLastX_Faces', hide) + obj.setEditorMode('AvoidLastX_InternalFeatures', hide) + obj.setEditorMode('BoundaryAdjustment', hide) + obj.setEditorMode('HandleMultipleFeatures', hide) + obj.setEditorMode('OptimizeLinearPaths', hide) + obj.setEditorMode('OptimizeStepOverTransitions', hide) + obj.setEditorMode('GapThreshold', hide) + obj.setEditorMode('GapSizes', hide) + + if obj.Algorithm == 'OCL Dropcutter': + expMode = 0 + obj.setEditorMode('ClearLastLayer', hide) + elif obj.Algorithm == 'Experimental': + A = 0 + expMode = 2 + if obj.CutPattern == 'None': + show = hide = A = 2 + elif obj.CutPattern in ['Line', 'ZigZag']: + show = 0 + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + show = 2 # hide + hide = 0 # show + + obj.setEditorMode('CutPatternAngle', show) + obj.setEditorMode('CircularCenterAt', hide) + obj.setEditorMode('CircularCenterCustom', hide) + + obj.setEditorMode('CutPatternReversed', A) + obj.setEditorMode('ClearLastLayer', A) + obj.setEditorMode('StepOver', A) + + obj.setEditorMode('IgnoreOuterAbove', A) + obj.setEditorMode('SampleInterval', expMode) + obj.setEditorMode('LinearDeflection', expMode) + obj.setEditorMode('AngularDeflection', expMode) + + def onChanged(self, obj, prop): + if hasattr(self, 'addedAllProperties'): + if self.addedAllProperties is True: + if prop in ['Algorithm', 'CutPattern']: + self.setEditorProperties(obj) + + def opOnDocumentRestored(self, obj): + self.initOpProperties(obj) + + if PathLog.getLevel(PathLog.thisModule()) != 4: + obj.setEditorMode('ShowTempObjects', 2) # hide + else: + obj.setEditorMode('ShowTempObjects', 0) # show + + self.setEditorProperties(obj) + + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj, job) ... initialize defaults''' + job = PathUtils.findParentJob(obj) + + obj.OptimizeLinearPaths = True + obj.InternalFeaturesCut = True + obj.OptimizeStepOverTransitions = False + obj.BoundaryEnforcement = True + obj.UseStartPoint = False + obj.AvoidLastX_InternalFeatures = True + obj.CutPatternReversed = False + obj.IgnoreOuterAbove = obj.StartDepth.Value + 0.00001 + obj.StartPoint.x = 0.0 + obj.StartPoint.y = 0.0 + obj.StartPoint.z = obj.ClearanceHeight.Value + obj.Algorithm = 'OCL Dropcutter' + obj.ProfileEdges = 'None' + obj.LayerMode = 'Single-pass' + obj.CutMode = 'Conventional' + obj.CutPattern = 'None' + obj.HandleMultipleFeatures = 'Collectively' # 'Individually' + obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' + obj.GapSizes = 'No gaps identified.' + obj.ClearLastLayer = 'Off' + obj.StepOver = 100 + obj.CutPatternAngle = 0.0 + obj.DepthOffset.Value = 0.0 + obj.SampleInterval.Value = 1.0 + obj.BoundaryAdjustment.Value = 0.0 + obj.InternalFeaturesAdjustment.Value = 0.0 + obj.AvoidLastX_Faces = 0 + obj.CircularCenterCustom.x = 0.0 + obj.CircularCenterCustom.y = 0.0 + obj.CircularCenterCustom.z = 0.0 + obj.GapThreshold.Value = 0.005 + obj.LinearDeflection.Value = 0.0001 + obj.AngularDeflection.Value = 0.25 + # For debugging + obj.ShowTempObjects = False + + # need to overwrite the default depth calculations for facing + d = None + if job: + if job.Stock: + d = PathUtils.guessDepths(job.Stock.Shape, None) + PathLog.debug("job.Stock exists") + else: + PathLog.debug("job.Stock NOT exist") + else: + PathLog.debug("job NOT exist") + + if d is not None: + obj.OpFinalDepth.Value = d.final_depth + obj.OpStartDepth.Value = d.start_depth + else: + obj.OpFinalDepth.Value = -10 + obj.OpStartDepth.Value = 10 + + PathLog.debug('Default OpFinalDepth: {}'.format(obj.OpFinalDepth.Value)) + PathLog.debug('Defualt OpStartDepth: {}'.format(obj.OpStartDepth.Value)) + + def opApplyPropertyLimits(self, obj): + '''opApplyPropertyLimits(obj) ... Apply necessary limits to user input property values before performing main operation.''' + # Limit sample interval + if obj.SampleInterval.Value < 0.0001: + obj.SampleInterval.Value = 0.0001 + PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.0001 to 25.4 millimeters.')) + if obj.SampleInterval.Value > 25.4: + obj.SampleInterval.Value = 25.4 + PathLog.error(translate('PathWaterline', 'Sample interval limits are 0.0001 to 25.4 millimeters.')) + + # Limit cut pattern angle + if obj.CutPatternAngle < -360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +-360 degrees.')) + if obj.CutPatternAngle >= 360.0: + obj.CutPatternAngle = 0.0 + PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.')) + + # Limit StepOver to natural number percentage + if obj.StepOver > 100: + obj.StepOver = 100 + if obj.StepOver < 1: + obj.StepOver = 1 + + # Limit AvoidLastX_Faces to zero and positive values + if obj.AvoidLastX_Faces < 0: + obj.AvoidLastX_Faces = 0 + PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Only zero or positive values permitted.')) + if obj.AvoidLastX_Faces > 100: + obj.AvoidLastX_Faces = 100 + PathLog.error(translate('PathWaterline', 'AvoidLastX_Faces: Avoid last X faces count limited to 100.')) + + def opExecute(self, obj): + '''opExecute(obj) ... process surface operation''' + PathLog.track() + + self.modelSTLs = list() + self.safeSTLs = list() + self.modelTypes = list() + self.boundBoxes = list() + self.profileShapes = list() + self.collectiveShapes = list() + self.individualShapes = list() + self.avoidShapes = list() + self.geoTlrnc = None + self.tempGroup = None + self.CutClimb = False + self.closedGap = False + self.gaps = [0.1, 0.2, 0.3] + CMDS = list() + modelVisibility = list() + FCAD = FreeCAD.ActiveDocument + + # Set debugging behavior + self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects + self.showDebugObjects = obj.ShowTempObjects + deleteTempsFlag = True # Set to False for debugging + if PathLog.getLevel(PathLog.thisModule()) == 4: + deleteTempsFlag = False + else: + self.showDebugObjects = False + + # mark beginning of operation and identify parent Job + PathLog.info('\nBegin Waterline operation...') + startTime = time.time() + + # Identify parent Job + JOB = PathUtils.findParentJob(obj) + if JOB is None: + PathLog.error(translate('PathWaterline', "No JOB")) + return + self.stockZMin = JOB.Stock.Shape.BoundBox.ZMin + + # set cut mode; reverse as needed + if obj.CutMode == 'Climb': + self.CutClimb = True + if obj.CutPatternReversed is True: + if self.CutClimb is True: + self.CutClimb = False + else: + self.CutClimb = True + + # Begin GCode for operation with basic information + # ... and move cutter to clearance height and startpoint + output = '' + if obj.Comment != '': + self.commandlist.append(Path.Command('N ({})'.format(str(obj.Comment)), {})) + self.commandlist.append(Path.Command('N ({})'.format(obj.Label), {})) + self.commandlist.append(Path.Command('N (Tool type: {})'.format(str(obj.ToolController.Tool.ToolType)), {})) + self.commandlist.append(Path.Command('N (Compensated Tool Path. Diameter: {})'.format(str(obj.ToolController.Tool.Diameter)), {})) + self.commandlist.append(Path.Command('N (Sample interval: {})'.format(str(obj.SampleInterval.Value)), {})) + self.commandlist.append(Path.Command('N (Step over %: {})'.format(str(obj.StepOver)), {})) + self.commandlist.append(Path.Command('N ({})'.format(output), {})) + self.commandlist.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + if obj.UseStartPoint: + self.commandlist.append(Path.Command('G0', {'X': obj.StartPoint.x, 'Y': obj.StartPoint.y, 'F': self.horizRapid})) + + # Instantiate additional class operation variables + self.resetOpVariables() + + # Impose property limits + self.opApplyPropertyLimits(obj) + + # Create temporary group for temporary objects, removing existing + # if self.showDebugObjects is True: + tempGroupName = 'tempPathWaterlineGroup' + if FCAD.getObject(tempGroupName): + for to in FCAD.getObject(tempGroupName).Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) # remove temp directory if already exists + if FCAD.getObject(tempGroupName + '001'): + for to in FCAD.getObject(tempGroupName + '001').Group: + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName + '001') # remove temp directory if already exists + tempGroup = FCAD.addObject('App::DocumentObjectGroup', tempGroupName) + tempGroupName = tempGroup.Name + self.tempGroup = tempGroup + tempGroup.purgeTouched() + # Add temp object to temp group folder with following code: + # ... self.tempGroup.addObject(OBJ) + + # Setup cutter for OCL and cutout value for operation - based on tool controller properties + self.cutter = self.setOclCutter(obj) + self.safeCutter = self.setOclCutter(obj, safe=True) + if self.cutter is False or self.safeCutter is False: + PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter.")) + return + toolDiam = self.cutter.getDiameter() + self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) + self.radius = toolDiam / 2.0 + self.gaps = [toolDiam, toolDiam, toolDiam] + + # Get height offset values for later use + self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value + self.ClearHeightOffset = JOB.SetupSheet.ClearanceHeightOffset.Value + + # Set deflection values for mesh generation + useDGT = False + try: # try/except is for Path Jobs created before GeometryTolerance + self.geoTlrnc = JOB.GeometryTolerance.Value + if self.geoTlrnc == 0.0: + useDGT = True + except AttributeError as ee: + PathLog.warning('{}\nPlease set Job.GeometryTolerance to an acceptable value. Using PathPreferences.defaultGeometryTolerance().'.format(ee)) + useDGT = True + if useDGT: + import PathScripts.PathPreferences as PathPreferences + self.geoTlrnc = PathPreferences.defaultGeometryTolerance() + + # Calculate default depthparams for operation + self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) + self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 + + # make circle for workplane + self.wpc = Part.makeCircle(2.0) + + # Save model visibilities for restoration + if FreeCAD.GuiUp: + for m in range(0, len(JOB.Model.Group)): + mNm = JOB.Model.Group[m].Name + modelVisibility.append(FreeCADGui.ActiveDocument.getObject(mNm).Visibility) + + # Setup STL, model type, and bound box containers for each model in Job + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + self.modelSTLs.append(False) + self.safeSTLs.append(False) + self.profileShapes.append(False) + # Set bound box + if obj.BoundBox == 'BaseBoundBox': + if M.TypeId.startswith('Mesh'): + self.modelTypes.append('M') # Mesh + self.boundBoxes.append(M.Mesh.BoundBox) + else: + self.modelTypes.append('S') # Solid + self.boundBoxes.append(M.Shape.BoundBox) + elif obj.BoundBox == 'Stock': + self.modelTypes.append('S') # Solid + self.boundBoxes.append(JOB.Stock.Shape.BoundBox) + + # ###### MAIN COMMANDS FOR OPERATION ###### + + # Begin processing obj.Base data and creating GCode + # Process selected faces, if available + pPM = self._preProcessModel(JOB, obj) + if pPM is False: + PathLog.error('Unable to pre-process obj.Base.') + else: + (FACES, VOIDS) = pPM + + # Create OCL.stl model objects + if obj.Algorithm == 'OCL Dropcutter': + self._prepareModelSTLs(JOB, obj) + PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value)) + PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value)) + + for m in range(0, len(JOB.Model.Group)): + Mdl = JOB.Model.Group[m] + if FACES[m] is False: + PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) + else: + if m > 0: + # Raise to clearance between models + CMDS.append(Path.Command('N (Transition to base: {}.)'.format(Mdl.Label))) + CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) + # make stock-model-voidShapes STL model for avoidance detection on transitions + if obj.Algorithm == 'OCL Dropcutter': + self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) + # Process model/faces - OCL objects must be ready + CMDS.extend(self._processWaterlineAreas(JOB, obj, m, FACES[m], VOIDS[m])) + + # Save gcode produced + self.commandlist.extend(CMDS) + + # ###### CLOSING COMMANDS FOR OPERATION ###### + + # Delete temporary objects + # Restore model visibilities for restoration + if FreeCAD.GuiUp: + FreeCADGui.ActiveDocument.getObject(tempGroupName).Visibility = False + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + M.Visibility = modelVisibility[m] + + if deleteTempsFlag is True: + for to in tempGroup.Group: + if hasattr(to, 'Group'): + for go in to.Group: + FCAD.removeObject(go.Name) + FCAD.removeObject(to.Name) + FCAD.removeObject(tempGroupName) + else: + if len(tempGroup.Group) == 0: + FCAD.removeObject(tempGroupName) + else: + tempGroup.purgeTouched() + + # Provide user feedback for gap sizes + gaps = list() + for g in self.gaps: + if g != toolDiam: + gaps.append(g) + if len(gaps) > 0: + obj.GapSizes = '{} mm'.format(gaps) + else: + if self.closedGap is True: + obj.GapSizes = 'Closed gaps < Gap Threshold.' + else: + obj.GapSizes = 'No gaps identified.' + + # clean up class variables + self.resetOpVariables() + self.deleteOpVariables() + + self.modelSTLs = None + self.safeSTLs = None + self.modelTypes = None + self.boundBoxes = None + self.gaps = None + self.closedGap = None + self.SafeHeightOffset = None + self.ClearHeightOffset = None + self.depthParams = None + self.midDep = None + self.wpc = None + del self.modelSTLs + del self.safeSTLs + del self.modelTypes + del self.boundBoxes + del self.gaps + del self.closedGap + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.depthParams + del self.midDep + del self.wpc + + execTime = time.time() - startTime + PathLog.info('Operation time: {} sec.'.format(execTime)) + + return True + + # Methods for constructing the cut area + def _preProcessModel(self, JOB, obj): + PathLog.debug('_preProcessModel()') + + FACES = list() + VOIDS = list() + fShapes = list() + vShapes = list() + GRP = JOB.Model.Group + lenGRP = len(GRP) + noFaces = translate('PathWaterline', + 'Face selection is still under development for Waterline. Ignoring selected faces.') + + # Crete place holders for each base model in Job + for m in range(0, lenGRP): + FACES.append(False) + VOIDS.append(False) + fShapes.append(False) + vShapes.append(False) + + checkBase = False + if obj.Base: + if len(obj.Base) > 0: + checkBase = True + if obj.Algorithm in ['OCL Dropcutter', 'Experimental']: + checkBase = False + PathLog.warning(noFaces) + + # The user has selected subobjects from the base. Pre-Process each. + if checkBase: + PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') + + (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) + + # Cycle through each base model, processing faces for each + for m in range(0, lenGRP): + base = GRP[m] + (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) + fShapes[m] = mFS + vShapes[m] = mVS + self.profileShapes[m] = mPS + else: + PathLog.debug(' -No obj.Base data.') + for m in range(0, lenGRP): + self.modelSTLs[m] = True + + # Process each model base, as a whole, as needed + # PathLog.debug(' -Pre-processing all models in Job.') + for m in range(0, lenGRP): + if fShapes[m] is False: + PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) + if obj.BoundBox == 'BaseBoundBox': + base = GRP[m] + elif obj.BoundBox == 'Stock': + base = JOB.Stock + + pPEB = self._preProcessEntireBase(obj, base, m) + if pPEB is False: + PathLog.error(' -Failed to pre-process base as a whole.') + else: + (fcShp, prflShp) = pPEB + if fcShp is not False: + if fcShp is True: + PathLog.debug(' -fcShp is True.') + fShapes[m] = True + else: + fShapes[m] = [fcShp] + if prflShp is not False: + if fcShp is not False: + PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) + if vShapes[m] is not False: + PathLog.debug(' -Cutting void from base profile shape.') + adjPS = prflShp.cut(vShapes[m][0]) + self.profileShapes[m] = [adjPS] + else: + PathLog.debug(' -vShapes[m] is False.') + self.profileShapes[m] = [prflShp] + else: + PathLog.debug(' -Saving base profile shape.') + self.profileShapes[m] = [prflShp] + PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) + # Efor + + return (fShapes, vShapes) + + def _identifyFacesAndVoids(self, JOB, obj, F, V): + TUPS = list() + GRP = JOB.Model.Group + lenGRP = len(GRP) + + # Separate selected faces into (base, face) tuples and flag model(s) for STL creation + for (bs, SBS) in obj.Base: + for sb in SBS: + # Flag model for STL creation + mdlIdx = None + for m in range(0, lenGRP): + if bs is GRP[m]: + self.modelSTLs[m] = True + mdlIdx = m + break + TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) + + # Apply `AvoidXFaces` value + faceCnt = len(TUPS) + add = faceCnt - obj.AvoidLastX_Faces + for bst in range(0, faceCnt): + (m, base, sub) = TUPS[bst] + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + faceIdx = int(sub[4:]) - 1 + if bst < add: + if F[m] is False: + F[m] = list() + F[m].append((shape, faceIdx)) + else: + if V[m] is False: + V[m] = list() + V[m].append((shape, faceIdx)) + return (F, V) + + def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): + mFS = False + mVS = False + mPS = False + mIFS = list() + + if FACES[m] is not False: + isHole = False + if obj.HandleMultipleFeatures == 'Collectively': + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + outFCS = list() + + # Get collective envelope slice of selected faces + for (fcshp, fcIdx) in FACES[m]: + fNum = fcIdx + 1 + fsL.append(fcshp) + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + PathLog.debug('Attempting to get cross-section of collective faces.') + if len(outFCS) == 0: + PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) + cont = False + else: + cfsL = Part.makeCompound(outFCS) + + # Handle profile edges request + if cont is True and obj.ProfileEdges != 'None': + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(cfsL, ofstVal) + if psOfst is not False: + mPS = [psOfst] + if obj.ProfileEdges == 'Only': + mFS = True + cont = False + else: + PathLog.error(' -Failed to create profile geometry for selected faces.') + cont = False + + if cont: + if self.showDebugObjects is True: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') + T.Shape = cfsL + T.purgeTouched() + self.tempGroup.addObject(T) + + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOfstShp = self._extractFaceOffset(cfsL, ofstVal) + if faceOfstShp is False: + PathLog.error(' -Failed to create offset face.') + cont = False + + if cont: + lenIfL = len(ifL) + if obj.InternalFeaturesCut is False: + if lenIfL == 0: + PathLog.debug(' -No internal features saved.') + else: + if lenIfL == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + if self.showDebugObjects is True: + C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') + C.Shape = casL + C.purgeTouched() + self.tempGroup.addObject(C) + ofstVal = self._calculateOffsetValue(obj, isHole=True) + intOfstShp = self._extractFaceOffset(casL, ofstVal) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + mFS = [faceOfstShp] + # Eif + + elif obj.HandleMultipleFeatures == 'Individually': + for (fcshp, fcIdx) in FACES[m]: + cont = True + ifL = list() # avoid shape list + fNum = fcIdx + 1 + outerFace = False + + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + cont = False + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + cont = False + outerFace = False + else: + ((otrFace, raised), intWires) = gFW + outerFace = otrFace + if obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + if outerFace is not False: + PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) + + if obj.ProfileEdges != 'None': + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(outerFace, ofstVal) + if psOfst is not False: + if mPS is False: + mPS = list() + mPS.append(psOfst) + if obj.ProfileEdges == 'Only': + if mFS is False: + mFS = list() + mFS.append(True) + cont = False + else: + PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOfstShp = self._extractFaceOffset(outerFace, ofstVal) + + lenIfl = len(ifL) + if obj.InternalFeaturesCut is False and lenIfl > 0: + if lenIfl == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + + ofstVal = self._calculateOffsetValue(obj, isHole=True) + intOfstShp = self._extractFaceOffset(casL, ofstVal) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + if mFS is False: + mFS = list() + mFS.append(faceOfstShp) + # Eif + # Efor + # Eif + # Eif + + if len(mIFS) > 0: + if mVS is False: + mVS = list() + for ifs in mIFS: + mVS.append(ifs) + + if VOIDS[m] is not False: + PathLog.debug('Processing avoid faces.') + cont = True + isHole = False + outFCS = list() + intFEAT = list() + + for (fcshp, fcIdx) in VOIDS[m]: + fNum = fcIdx + 1 + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) + cont = False + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if obj.AvoidLastX_InternalFeatures is False: + if intWires is not False: + for (iFace, rsd) in intWires: + intFEAT.append(iFace) + + lenOtFcs = len(outFCS) + if lenOtFcs == 0: + cont = False + else: + if lenOtFcs == 1: + avoid = outFCS[0] + else: + avoid = Part.makeCompound(outFCS) + + if self.showDebugObjects is True: + PathLog.debug('*** tmpAvoidArea') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + + if cont: + if self.showDebugObjects is True: + PathLog.debug('*** tmpVoidCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) + avdOfstShp = self._extractFaceOffset(avoid, ofstVal) + if avdOfstShp is False: + PathLog.error('Failed to create collective offset avoid face.') + cont = False + + if cont: + avdShp = avdOfstShp + + if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: + if len(intFEAT) > 1: + ifc = Part.makeCompound(intFEAT) + else: + ifc = intFEAT[0] + ofstVal = self._calculateOffsetValue(obj, isHole=True) + ifOfstShp = self._extractFaceOffset(ifc, ofstVal) + if ifOfstShp is False: + PathLog.error('Failed to create collective offset avoid internal features.') + else: + avdShp = avdOfstShp.cut(ifOfstShp) + + if mVS is False: + mVS = list() + mVS.append(avdShp) + + + return (mFS, mVS, mPS) + + def _getFaceWires(self, base, fcshp, fcIdx): + outFace = False + INTFCS = list() + fNum = fcIdx + 1 + # preProcEr = translate('PathWaterline', 'Error pre-processing Face') + warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face') + + PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) + WIRES = self._extractWiresFromFace(base, fcshp) + if WIRES is False: + PathLog.error('Failed to extract wires from Face{}'.format(fNum)) + return False + + # Process remaining internal features, adding to FCS list + lenW = len(WIRES) + for w in range(0, lenW): + (wire, rsd) = WIRES[w] + PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) + if wire.isClosed() is False: + PathLog.debug(' -wire is not closed.') + else: + slc = self._flattenWireToFace(wire) + if slc is False: + PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) + else: + if w == 0: + outFace = (slc, rsd) + else: + # add to VOIDS so cutter avoids area. + PathLog.warning(warnFinDep + str(fNum) + '.') + INTFCS.append((slc, rsd)) + if len(INTFCS) == 0: + return (outFace, False) + else: + return (outFace, INTFCS) + + def _preProcessEntireBase(self, obj, base, m): + cont = True + isHole = False + prflShp = False + # Create envelope, extract cross-section and make offset co-planar shape + # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) + + try: + baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as ee: + PathLog.error(str(ee)) + shell = base.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as eee: + PathLog.error(str(eee)) + cont = False + + if cont: + csFaceShape = self._getShapeSlice(baseEnv) + if csFaceShape is False: + PathLog.debug('_getShapeSlice(baseEnv) failed') + csFaceShape = self._getCrossSection(baseEnv) + if csFaceShape is False: + PathLog.debug('_getCrossSection(baseEnv) failed') + csFaceShape = self._getSliceFromEnvelope(baseEnv) + if csFaceShape is False: + PathLog.error('Failed to slice baseEnv shape.') + cont = False + + if cont is True and obj.ProfileEdges != 'None': + PathLog.debug(' -Attempting profile geometry for model base.') + ofstVal = self._calculateOffsetValue(obj, isHole) + psOfst = self._extractFaceOffset(csFaceShape, ofstVal) + if psOfst is not False: + if obj.ProfileEdges == 'Only': + return (True, psOfst) + prflShp = psOfst + else: + PathLog.error(' -Failed to create profile geometry.') + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(obj, isHole) + faceOffsetShape = self._extractFaceOffset(csFaceShape, ofstVal) + if faceOffsetShape is False: + PathLog.error('_extractFaceOffset() failed.') + else: + faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) + return (faceOffsetShape, prflShp) + return False + + def _extractWiresFromFace(self, base, fc): + '''_extractWiresFromFace(base, fc) ... + Attempts to return all closed wires within a parent face, including the outer most wire of the parent. + The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). + ''' + PathLog.debug('_extractWiresFromFace()') + + WIRES = list() + lenWrs = len(fc.Wires) + PathLog.debug(' -Wire count: {}'.format(lenWrs)) + + def index0(tup): + return tup[0] + + # Cycle through wires in face + for w in range(0, lenWrs): + PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) + wire = fc.Wires[w] + checkEdges = False + cont = True + + # Check for closed edges (circles, ellipses, etc...) + for E in wire.Edges: + if E.isClosed() is True: + checkEdges = True + break + + if checkEdges is True: + PathLog.debug(' -checkEdges is True') + for e in range(0, len(wire.Edges)): + edge = wire.Edges[e] + if edge.isClosed() is True and edge.Mass > 0.01: + PathLog.debug(' -Found closed edge') + raised = False + ip = self._isPocket(base, fc, edge) + if ip is False: + raised = True + ebb = edge.BoundBox + eArea = ebb.XLength * ebb.YLength + F = Part.Face(Part.Wire([edge])) + WIRES.append((eArea, F.Wires[0], raised)) + cont = False + + if cont: + PathLog.debug(' -cont is True') + # If only one wire and not checkEdges, return first wire + if lenWrs == 1: + return [(wire, False)] + + raised = False + wbb = wire.BoundBox + wArea = wbb.XLength * wbb.YLength + if w > 0: + ip = self._isPocket(base, fc, wire) + if ip is False: + raised = True + WIRES.append((wArea, Part.Wire(wire.Edges), raised)) + + nf = len(WIRES) + if nf > 0: + PathLog.debug(' -number of wires found is {}'.format(nf)) + if nf == 1: + (area, W, raised) = WIRES[0] + return [(W, raised)] + else: + sortedWIRES = sorted(WIRES, key=index0, reverse=True) + return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + + return False + + def _calculateOffsetValue(self, obj, isHole, isVoid=False): + '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. + Calculate the offset for the Path.Area() function.''' + JOB = PathUtils.findParentJob(obj) + tolrnc = JOB.GeometryTolerance.Value + + if isVoid is False: + if isHole is True: + offset = -1 * obj.InternalFeaturesAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + else: + offset = -1 * obj.BoundaryAdjustment.Value + if obj.BoundaryEnforcement is True: + offset += self.radius + (tolrnc / 10.0) + else: + offset -= self.radius + (tolrnc / 10.0) + offset = 0.0 - offset + else: + offset = -1 * obj.BoundaryAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + + return offset + + def _extractFaceOffset(self, fcShape, offset, makeComp=True): + '''_extractFaceOffset(fcShape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('_extractFaceOffset()') + + if fcShape.BoundBox.ZMin != 0.0: + fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 # 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 + area.add(fcShape) + area.setParams(**areaParams) # set parameters + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + if not makeComp: + ofstFace = [ofstFace] + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + if makeComp: + ofstFace = Part.makeCompound(W) + else: + ofstFace = W + + return ofstFace # offsetShape + + def _isPocket(self, b, f, w): + '''_isPocket(b, f, w)... + Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. + Returns True if pocket, False if raised protrusion.''' + e = w.Edges[0] + for fi in range(0, len(b.Shape.Faces)): + face = b.Shape.Faces[fi] + for ei in range(0, len(face.Edges)): + edge = face.Edges[ei] + if e.isSame(edge) is True: + if f is face: + # Alternative: run loop to see if all edges are same + pass # same source face, look for another + else: + if face.CenterOfMass.z < f.CenterOfMass.z: + return True + return False + + def _flattenWireToFace(self, wire): + PathLog.debug('_flattenWireToFace()') + if wire.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + + # If wire is planar horizontal, convert to a face and return + if wire.BoundBox.ZLength == 0.0: + slc = Part.Face(wire) + return slc + + # Attempt to create a new wire for manipulation, if not, use original + newWire = Part.Wire(wire.Edges) + if newWire.isClosed() is True: + nWire = newWire + else: + PathLog.debug(' -newWire.isClosed() is False') + nWire = wire + + # Attempt extrusion, and then try a manual slice and then cross-section + ext = self._getExtrudedShape(nWire) + if ext is False: + PathLog.debug('_getExtrudedShape() failed') + else: + slc = self._getShapeSlice(ext) + if slc is not False: + return slc + cs = self._getCrossSection(ext, True) + if cs is not False: + return cs + + # Attempt creating an envelope, and then try a manual slice and then cross-section + env = self._getShapeEnvelope(nWire) + if env is False: + PathLog.debug('_getShapeEnvelope() failed') + else: + slc = self._getShapeSlice(env) + if slc is not False: + return slc + cs = self._getCrossSection(env, True) + if cs is not False: + return cs + + # Attempt creating a projection + slc = self._getProjectedFace(nWire) + if slc is False: + PathLog.debug('_getProjectedFace() failed') + else: + return slc + + return False + + def _getExtrudedShape(self, wire): + PathLog.debug('_getExtrudedShape()') + wBB = wire.BoundBox + extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 + + try: + # slower, but renders collective faces correctly. Method 5 in TESTING + shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + except Exception as ee: + PathLog.error(' -extrude wire failed: \n{}'.format(ee)) + return False + + SHP = Part.makeSolid(shell) + return SHP + + def _getShapeSlice(self, shape): + PathLog.debug('_getShapeSlice()') + + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + xmin = bb.XMin - 1.0 + xmax = bb.XMax + 1.0 + ymin = bb.YMin - 1.0 + ymax = bb.YMax + 1.0 + p1 = FreeCAD.Vector(xmin, ymin, mid) + p2 = FreeCAD.Vector(xmax, ymin, mid) + p3 = FreeCAD.Vector(xmax, ymax, mid) + p4 = FreeCAD.Vector(xmin, ymax, mid) + + e1 = Part.makeLine(p1, p2) + e2 = Part.makeLine(p2, p3) + e3 = Part.makeLine(p3, p4) + e4 = Part.makeLine(p4, p1) + face = Part.Face(Part.Wire([e1, e2, e3, e4])) + fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area + sArea = shape.BoundBox.XLength * shape.BoundBox.YLength + midArea = (fArea + sArea) / 2.0 + + slcShp = shape.common(face) + slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength + + if slcArea < midArea: + for W in slcShp.Wires: + if W.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + if len(slcShp.Wires) == 1: + wire = slcShp.Wires[0] + slc = Part.Face(wire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + else: + fL = list() + for W in slcShp.Wires: + slc = Part.Face(W) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + fL.append(slc) + comp = Part.makeCompound(fL) + if self.showDebugObjects is True: + PathLog.debug('*** tmpSliceCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') + P.Shape = comp + P.purgeTouched() + self.tempGroup.addObject(P) + return comp + + PathLog.debug(' -slcArea !< midArea') + PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) + return False + + def _getProjectedFace(self, wire): + import Draft + PathLog.debug('_getProjectedFace()') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') + F.Shape = wire + F.purgeTouched() + self.tempGroup.addObject(F) + try: + prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) + prj.recompute() + prj.purgeTouched() + self.tempGroup.addObject(prj) + except Exception as ee: + PathLog.error(str(ee)) + return False + else: + pWire = Part.Wire(prj.Shape.Edges) + if pWire.isClosed() is False: + # PathLog.debug(' -pWire.isClosed() is False') + return False + slc = Part.Face(pWire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + + def _getCrossSection(self, shape, withExtrude=False): + PathLog.debug('_getCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) + csWire = comp.Wires[0] + if csWire.isClosed() is False: + PathLog.debug(' -comp.Wires[0] is not closed') + return False + if withExtrude is True: + ext = self._getExtrudedShape(csWire) + CS = self._getShapeSlice(ext) + if CS is False: + return False + else: + CS = Part.Face(csWire) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + + def _getShapeEnvelope(self, shape): + PathLog.debug('_getShapeEnvelope()') + + wBB = shape.BoundBox + extFwd = wBB.ZLength + 10.0 + minz = wBB.ZMin + maxz = wBB.ZMin + extFwd + stpDwn = (maxz - minz) / 4.0 + dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) + + try: + env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape + except Exception as ee: + PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) + return False + else: + return env + + def _getSliceFromEnvelope(self, env): + PathLog.debug('_getSliceFromEnvelope()') + eBB = env.BoundBox + extFwd = eBB.ZLength + 10.0 + maxz = eBB.ZMin + extFwd + + emax = math.floor(maxz - 1.0) + E = list() + for e in range(0, len(env.Edges)): + emin = env.Edges[e].BoundBox.ZMin + if emin > emax: + E.append(env.Edges[e]) + tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) + tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) + + return tf + + def _prepareModelSTLs(self, JOB, obj): + PathLog.debug('_prepareModelSTLs()') + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + + if self.modelTypes[m] == 'M': + facets = M.Mesh.Facets.Points + else: + facets = Part.getFacets(M.Shape) + + if self.modelSTLs[m] is True: + stl = ocl.STLSurf() + + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + self.modelSTLs[m] = stl + return + + def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): + '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... + Creates and OCL.stl object with combined data with waste stock, + model, and avoided faces. Travel lines can be checked against this + STL object to determine minimum travel height to clear stock and model.''' + PathLog.debug('_makeSafeSTL()') + + fuseShapes = list() + Mdl = JOB.Model.Group[mdlIdx] + mBB = Mdl.Shape.BoundBox + sBB = JOB.Stock.Shape.BoundBox + + # add Model shape to safeSTL shape + fuseShapes.append(Mdl.Shape) + + if obj.BoundBox == 'BaseBoundBox': + cont = False + extFwd = (sBB.ZLength) + zmin = mBB.ZMin + zmax = mBB.ZMin + extFwd + stpDwn = (zmax - zmin) / 4.0 + dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) + + try: + envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as ee: + PathLog.error(str(ee)) + shell = Mdl.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as eee: + PathLog.error(str(eee)) + + if cont: + stckWst = JOB.Stock.Shape.cut(envBB) + if obj.BoundaryAdjustment > 0.0: + cmpndFS = Part.makeCompound(faceShapes) + baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape + adjStckWst = stckWst.cut(baBB) + else: + adjStckWst = stckWst + fuseShapes.append(adjStckWst) + else: + PathLog.warning('Path transitions might not avoid the model. Verify paths.') + else: + # If boundbox is Job.Stock, add hidden pad under stock as base plate + toolDiam = self.cutter.getDiameter() + zMin = JOB.Stock.Shape.BoundBox.ZMin + xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam + yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam + bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) + bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) + bH = 1.0 + crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) + B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) + fuseShapes.append(B) + + if voidShapes is not False: + voidComp = Part.makeCompound(voidShapes) + voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape + fuseShapes.append(voidEnv) + + fused = Part.makeCompound(fuseShapes) + + if self.showDebugObjects is True: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') + T.Shape = fused + T.purgeTouched() + self.tempGroup.addObject(T) + + facets = Part.getFacets(fused) + + stl = ocl.STLSurf() + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + + self.safeSTLs[mdlIdx] = stl + + def _processWaterlineAreas(self, JOB, obj, mdlIdx, FCS, VDS): + '''_processWaterlineAreas(JOB, obj, mdlIdx, FCS, VDS)... + This method applies any avoided faces or regions to the selected faces. + It then calls the correct method.''' + PathLog.debug('_processWaterlineAreas()') + + final = list() + + # Process faces Collectively or Individually + if obj.HandleMultipleFeatures == 'Collectively': + if FCS is True: + COMP = False + else: + ADD = Part.makeCompound(FCS) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + if obj.Algorithm == 'OCL Dropcutter': + final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + else: + final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + + elif obj.HandleMultipleFeatures == 'Individually': + for fsi in range(0, len(FCS)): + fShp = FCS[fsi] + # self.deleteOpVariables(all=False) + self.resetOpVariables(all=False) + + if fShp is True: + COMP = False + else: + ADD = Part.makeCompound([fShp]) + if VDS is not False: + DEL = Part.makeCompound(VDS) + COMP = ADD.cut(DEL) + else: + COMP = ADD + + final.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + if obj.Algorithm == 'OCL Dropcutter': + final.extend(self._oclWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + else: + final.extend(self._experimentalWaterlineOp(JOB, obj, mdlIdx, COMP)) # independent method set for Waterline + COMP = None + # Eif + + return final + + # Methods for creating path geometry + def _planarMakePathGeom(self, obj, faceShp): + '''_planarMakePathGeom(obj, faceShp)... + Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. + The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' + PathLog.debug('_planarMakePathGeom()') + GeoSet = list() + + # Apply drop cutter extra offset and set the max and min XY area of the operation + xmin = faceShp.BoundBox.XMin + xmax = faceShp.BoundBox.XMax + ymin = faceShp.BoundBox.YMin + ymax = faceShp.BoundBox.YMax + zmin = faceShp.BoundBox.ZMin + zmax = faceShp.BoundBox.ZMax + + # Compute weighted center of mass of all faces combined + fCnt = 0 + totArea = 0.0 + zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) + for F in faceShp.Faces: + comF = F.CenterOfMass + areaF = F.Area + totArea += areaF + fCnt += 1 + zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) + if fCnt == 0: + PathLog.error(translate('PathWaterline', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) + zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) + else: + avgArea = totArea / fCnt + zeroCOM.multiply(1 / fCnt) + zeroCOM.multiply(1 / avgArea) + COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + + # get X, Y, Z spans; Compute center of rotation + deltaX = abs(xmax-xmin) + deltaY = abs(ymax-ymin) + deltaC = math.sqrt(deltaX**2 + deltaY**2) + lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end + halfLL = math.ceil(lineLen / 2.0) + cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen + halfPasses = math.ceil(cutPasses / 2.0) + bbC = faceShp.BoundBox.Center + + # Generate the line/circle sets to be intersected with the cut-face-area + if obj.CutPattern in ['ZigZag', 'Line']: + centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model + cAng = math.atan(deltaX / deltaY) # BoundaryBox angle + + # Determine end points and create top lines + x1 = centRot.x - halfLL + x2 = centRot.x + halfLL + diag = None + if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: + diag = deltaY + elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: + diag = deltaX + else: + perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC + diag = perpDist + y1 = centRot.y + diag + # y2 = y1 + + # Create end points for set of lines to intersect with cross-section face + pntTuples = list() + for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): + x1 = centRot.x - halfLL + x2 = centRot.x + halfLL + y1 = centRot.y + (lc * self.cutOut) + # y2 = y1 + p1 = FreeCAD.Vector(x1, y1, 0.0) + p2 = FreeCAD.Vector(x2, y1, 0.0) + pntTuples.append( (p1, p2) ) + + # Convert end points to lines + for (p1, p2) in pntTuples: + line = Part.makeLine(p1, p2) + GeoSet.append(line) + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + zTgt = faceShp.BoundBox.ZMin + axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) + cntr = FreeCAD.Placement() + cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) + + if obj.CircularCenterAt == 'CenterOfMass': + cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass + elif obj.CircularCenterAt == 'CenterOfBoundBox': + cent = faceShp.BoundBox.Center + cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) + elif obj.CircularCenterAt == 'XminYmin': + cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) + elif obj.CircularCenterAt == 'Custom': + newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) + cntr.Base = newCent + + # recalculate cutPasses value, if need be + radialPasses = halfPasses + if obj.CircularCenterAt != 'CenterOfBoundBox': + # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center + EBB = faceShp.BoundBox + CORNERS = [ + FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), + FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), + ] + dMax = 0.0 + for c in range(0, 4): + dist = CORNERS[c].sub(cntr.Base).Length + if dist > dMax: + dMax = dist + lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end + radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen + + # Update COM point and current CircularCenter + if obj.CircularCenterAt != 'Custom': + obj.CircularCenterCustom = cntr.Base + + minRad = self.cutter.getDiameter() * 0.45 + siX3 = 3 * obj.SampleInterval.Value + minRadSI = (siX3 / 2.0) / math.pi + if minRad < minRadSI: + minRad = minRadSI + + # Make small center circle to start pattern + if obj.StepOver > 50: + circle = Part.makeCircle(minRad, cntr.Base) + GeoSet.append(circle) + + for lc in range(1, radialPasses + 1): + rad = (lc * self.cutOut) + if rad >= minRad: + circle = Part.makeCircle(rad, cntr.Base) + GeoSet.append(circle) + # Efor + COM = cntr.Base + # Eif + + if obj.CutPatternReversed is True: + GeoSet.reverse() + + if faceShp.BoundBox.ZMin != 0.0: + faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) + + # Create compound object to bind all lines in Lineset + geomShape = Part.makeCompound(GeoSet) + + # Position and rotate the Line and ZigZag geometry + if obj.CutPattern in ['Line', 'ZigZag']: + if obj.CutPatternAngle != 0.0: + geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) + geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) + + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') + F.Shape = geomShape + F.purgeTouched() + self.tempGroup.addObject(F) + + # Identify intersection of cross-section face and lineset + cmnShape = faceShp.common(geomShape) + + if self.showDebugObjects is True: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') + F.Shape = cmnShape + F.purgeTouched() + self.tempGroup.addObject(F) + + self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) + return cmnShape + + def _pathGeomToLinesPointSet(self, obj, compGeoShp): + '''_pathGeomToLinesPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' + PathLog.debug('_pathGeomToLinesPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + chkGap = False + lnCnt = 0 + ec = len(compGeoShp.Edges) + cutClimb = self.CutClimb + toolDiam = 2.0 * self.radius + cpa = obj.CutPatternAngle + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if cutClimb is True: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + else: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + inLine.append(tup) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + + for ei in range(1, ec): + chkGap = False + edg = compGeoShp.Edges[ei] # Get edge for vertexes + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 + + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + else: + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset collinear container + if cutClimb is True: + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + else: + sp = ep + + if cutClimb is True: + tup = (v2, v1) + if chkGap is True: + gap = abs(toolDiam - lst.sub(ep).Length) + lst = cp + else: + tup = (v1, v2) + if chkGap is True: + gap = abs(toolDiam - lst.sub(cp).Length) + lst = ep + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + tup = (vA, tup[1]) + self.closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + + # Handle last inLine set, reversing it. + if obj.CutPatternReversed is True: + if cpa != 0.0 and cpa % 90.0 == 0.0: + F = LINES.pop(0) + rev = list() + for iL in F: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + rev.reverse() + LINES.insert(0, rev) + + isEven = lnCnt % 2 + if isEven == 0: + PathLog.debug('Line count is ODD.') + else: + PathLog.debug('Line count is even.') + + return LINES + + def _pathGeomToZigzagPointSet(self, obj, compGeoShp): + '''_pathGeomToZigzagPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings + with a ZigZag directional indicator included for each collinear group.''' + PathLog.debug('_pathGeomToZigzagPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + chkGap = False + ec = len(compGeoShp.Edges) + toolDiam = 2.0 * self.radius + + if self.CutClimb is True: + dirFlg = -1 + else: + dirFlg = 1 + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if dirFlg == 1: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + else: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point + inLine.append(tup) + + for ei in range(1, ec): + edg = compGeoShp.Edges[ei] + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) + + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + gap = abs(toolDiam - lst.sub(cp).Length) + else: + chkGap = False + if dirFlg == -1: + inLine.reverse() + LINES.append((dirFlg, inLine)) + lnCnt += 1 + dirFlg = -1 * dirFlg # Change zig to zag + inLine = list() # reset collinear container + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + + lst = ep + if dirFlg == 1: + tup = (v1, v2) + else: + tup = (v2, v1) + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + if dirFlg == 1: + tup = (vA, tup[1]) + else: + #tup = (vA, tup[1]) + #tup = (tup[1], vA) + tup = (tup[0], vB) + self.closedGap = True + else: + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + + # Fix directional issue with LAST line when line count is even + isEven = lnCnt % 2 + if isEven == 0: # Changed to != with 90 degree CutPatternAngle + PathLog.debug('Line count is even.') + else: + PathLog.debug('Line count is ODD.') + dirFlg = -1 * dirFlg + if obj.CutPatternReversed is False: + if self.CutClimb is True: + dirFlg = -1 * dirFlg + + if obj.CutPatternReversed is True: + dirFlg = -1 * dirFlg + + # Handle last inLine list + if dirFlg == 1: + rev = list() + for iL in inLine: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + + if obj.CutPatternReversed is False: + rev.reverse() + else: + rev2 = list() + for iL in rev: + if iL == 'BRK': + rev2.append(iL) + else: + (p1, p2) = iL + rev2.append((p2, p1)) + rev2.reverse() + rev = rev2 + + LINES.append((dirFlg, rev)) + else: + LINES.append((dirFlg, inLine)) + + return LINES + + def _pathGeomToArcPointSet(self, obj, compGeoShp): + '''_pathGeomToArcPointSet(obj, compGeoShp)... + Convert a compound set of arcs/circles to a set of directionally-oriented arc end points + and the corresponding center point.''' + # Extract intersection line segments for return value as list() + PathLog.debug('_pathGeomToArcPointSet()') + ARCS = list() + stpOvrEI = list() + segEI = list() + isSame = False + sameRad = None + COM = self.tmpCOM + toolDiam = 2.0 * self.radius + ec = len(compGeoShp.Edges) + + def gapDist(sp, ep): + X = (ep[0] - sp[0])**2 + Y = (ep[1] - sp[1])**2 + # Z = (ep[2] - sp[2])**2 + # return math.sqrt(X + Y + Z) + return math.sqrt(X + Y) # the 'z' value is zero in both points + + # Separate arc data into Loops and Arcs + for ei in range(0, ec): + edg = compGeoShp.Edges[ei] + if edg.Closed is True: + stpOvrEI.append(('L', ei, False)) + else: + if isSame is False: + segEI.append(ei) + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + else: + # Check if arc is co-radial to current SEGS + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + if abs(sameRad - pnt.sub(COM).Length) > 0.00001: + isSame = False + + if isSame is True: + segEI.append(ei) + else: + # Move co-radial arc segments + stpOvrEI.append(['A', segEI, False]) + # Start new list of arc segments + segEI = [ei] + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + # Process trailing `segEI` data, if available + if isSame is True: + stpOvrEI.append(['A', segEI, False]) + + # Identify adjacent arcs with y=0 start/end points that connect + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'A': + startOnAxis = list() + endOnAxis = list() + EI = SO[1] # list of corresponding compGeoShp.Edges indexes + + # Identify startOnAxis and endOnAxis arcs + for i in range(0, len(EI)): + ei = EI[i] # edge index + E = compGeoShp.Edges[ei] # edge object + if abs(COM.y - E.Vertexes[0].Y) < 0.00001: + startOnAxis.append((i, ei, E.Vertexes[0])) + elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: + endOnAxis.append((i, ei, E.Vertexes[1])) + + # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected + lenSOA = len(startOnAxis) + lenEOA = len(endOnAxis) + if lenSOA > 0 and lenEOA > 0: + for soa in range(0, lenSOA): + (iS, eiS, vS) = startOnAxis[soa] + for eoa in range(0, len(endOnAxis)): + (iE, eiE, vE) = endOnAxis[eoa] + dist = vE.X - vS.X + if abs(dist) < 0.00001: # They connect on axis at same radius + SO[2] = (eiE, eiS) + break + elif dist > 0: + break # stop searching + # Eif + # Eif + # Efor + + # Construct arc data tuples for OCL + dirFlg = 1 + # cutPat = obj.CutPattern + if self.CutClimb is False: # True yields Climb when set to Conventional + dirFlg = -1 + + # Cycle through stepOver data + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'L': # L = Loop/Ring/Circle + # PathLog.debug("SO[0] == 'Loop'") + lei = SO[1] # loop Edges index + v1 = compGeoShp.Edges[lei].Vertexes[0] + + # space = obj.SampleInterval.Value / 2.0 + space = 0.0000001 + + # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) + p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) # z=0.0 for waterline; z=v1.Z for 3D Surface + rad = p1.sub(COM).Length + spcRadRatio = space/rad + if spcRadRatio < 1.0: + tolrncAng = math.asin(spcRadRatio) + else: + tolrncAng = 0.9999998 * math.pi + EX = COM.x + (rad * math.cos(tolrncAng)) + EY = v1.Y - space # rad * math.sin(tolrncAng) + + sp = (v1.X, v1.Y, 0.0) + ep = (EX, EY, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + ARCS.append(('L', dirFlg, [arc])) + else: # SO[0] == 'A' A = Arc + # PathLog.debug("SO[0] == 'Arc'") + PRTS = list() + EI = SO[1] # list of corresponding Edges indexes + CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) + chkGap = False + lst = None + + if CONN is not False: + (iE, iS) = CONN + v1 = compGeoShp.Edges[iE].Vertexes[0] + v2 = compGeoShp.Edges[iS].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + lst = sp + PRTS.append(arc) + # Pop connected edge index values from arc segments index list + iEi = EI.index(iE) + iSi = EI.index(iS) + if iEi > iSi: + EI.pop(iEi) + EI.pop(iSi) + else: + EI.pop(iSi) + EI.pop(iEi) + if len(EI) > 0: + PRTS.append('BRK') + chkGap = True + cnt = 0 + for ei in EI: + if cnt > 0: + PRTS.append('BRK') + chkGap = True + v1 = compGeoShp.Edges[ei].Vertexes[0] + v2 = compGeoShp.Edges[ei].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) + lst = sp + if chkGap is True: + if gap < obj.GapThreshold.Value: + PRTS.pop() # pop off 'BRK' marker + (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current + arc = (vA, arc[1], vC) + self.closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < self.gaps[0]: + self.gaps.insert(0, gap) + self.gaps.pop() + PRTS.append(arc) + cnt += 1 + + if dirFlg == -1: + PRTS.reverse() + + ARCS.append(('A', dirFlg, PRTS)) + # Eif + if obj.CutPattern == 'CircularZigZag': + dirFlg = -1 * dirFlg + # Efor + + return ARCS + + def _getExperimentalWaterlinePaths(self, obj, PNTSET, csHght): + '''_getExperimentalWaterlinePaths(obj, PNTSET, csHght)... + Switching function for calling the appropriate path-geometry to OCL points conversion function + for the various cut patterns.''' + PathLog.debug('_getExperimentalWaterlinePaths()') + SCANS = list() + + if obj.CutPattern == 'Line': + stpOvr = list() + for D in PNTSET: + for SEG in D: + if SEG == 'BRK': + stpOvr.append(SEG) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = SEG + P1 = FreeCAD.Vector(A[0], A[1], csHght) + P2 = FreeCAD.Vector(B[0], B[1], csHght) + stpOvr.append((P1, P2)) + SCANS.append(stpOvr) + stpOvr = list() + elif obj.CutPattern == 'ZigZag': + stpOvr = list() + for (dirFlg, LNS) in PNTSET: + for SEG in LNS: + if SEG == 'BRK': + stpOvr.append(SEG) + else: + # D format is ((p1, p2), (p3, p4)) + (A, B) = SEG + P1 = FreeCAD.Vector(A[0], A[1], csHght) + P2 = FreeCAD.Vector(B[0], B[1], csHght) + stpOvr.append((P1, P2)) + SCANS.append(stpOvr) + stpOvr = list() + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + # PNTSET is list, by stepover. + # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) + for so in range(0, len(PNTSET)): + stpOvr = list() + erFlg = False + (aTyp, dirFlg, ARCS) = PNTSET[so] + + if dirFlg == 1: # 1 + cMode = True # Climb mode + else: + cMode = False + + for a in range(0, len(ARCS)): + Arc = ARCS[a] + if Arc == 'BRK': + stpOvr.append('BRK') + else: + (sp, ep, cp) = Arc + S = FreeCAD.Vector(sp[0], sp[1], csHght) + E = FreeCAD.Vector(ep[0], ep[1], csHght) + C = FreeCAD.Vector(cp[0], cp[1], csHght) + scan = (S, E, C, cMode) + if scan is False: + erFlg = True + else: + ##if aTyp == 'L': + ## stpOvr.append(FreeCAD.Vector(scan[0][0].x, scan[0][0].y, scan[0][0].z)) + stpOvr.append(scan) + if erFlg is False: + SCANS.append(stpOvr) + + return SCANS + + # Main planar scan functions + def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + # if obj.LayerMode == 'Multi-pass': + # rtpd = minSTH + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + # PathLog.debug('first.z: {}'.format(first.z)) + # PathLog.debug('lstPnt.z: {}'.format(lstPnt.z)) + # PathLog.debug('zChng: {}'.format(zChng)) + # PathLog.debug('minSTH: {}'.format(minSTH)) + if abs(zChng) < tolrnc: # transitions to same Z height + # PathLog.debug('abs(zChng) < tolrnc') + if (minSTH - first.z) > tolrnc: + # PathLog.debug('(minSTH - first.z) > tolrnc') + height = minSTH + 2.0 + else: + # PathLog.debug('ELSE (minSTH - first.z) > tolrnc') + horizGC = 'G1' + height = first.z + elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): + height = False # allow end of Zig to cut to beginning of Zag + + + # Create raise, shift, and optional lower commands + if height is not False: + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): + cmds = list() + rtpd = False + horizGC = 'G0' + hSpeed = self.horizRapid + height = obj.SafeHeight.Value + + if obj.CutPattern in ['Line', 'Circular']: + if obj.OptimizeStepOverTransitions is True: + height = minSTH + 2.0 + elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + if obj.OptimizeStepOverTransitions is True: + zChng = first.z - lstPnt.z + if abs(zChng) < tolrnc: # transitions to same Z height + if (minSTH - first.z) > tolrnc: + height = minSTH + 2.0 + else: + height = first.z + 2.0 # first.z + + cmds.append(Path.Command('G0', {'Z': height, 'F': self.vertRapid})) + cmds.append(Path.Command(horizGC, {'X': first.x, 'Y': first.y, 'F': hSpeed})) + if rtpd is not False: # ReturnToPreviousDepth + cmds.append(Path.Command('G0', {'Z': rtpd, 'F': self.vertRapid})) + + return cmds + + def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): + pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object + pdc.setSTL(stl) # add stl model + if useSafeCutter is True: + pdc.setCutter(self.safeCutter) # add safeCutter + else: + pdc.setCutter(self.cutter) # add cutter + pdc.setZ(finalDep) # set minimumZ (final / target depth value) + pdc.setSampling(SampleInterval) # set sampling size + return pdc + + # OCL Dropcutter waterline functions + def _oclWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): + '''_oclWaterlineOp(obj, base) ... Main waterline function to perform waterline extraction from model.''' + commands = [] + + base = JOB.Model.Group[mdlIdx] + bb = self.boundBoxes[mdlIdx] + stl = self.modelSTLs[mdlIdx] + depOfst = obj.DepthOffset.Value + + # Prepare global holdpoint and layerEndPnt containers + if self.holdPoint is None: + self.holdPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + if self.layerEndPnt is None: + self.layerEndPnt = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Set extra offset to diameter of cutter to allow cutter to move around perimeter of model + toolDiam = self.cutter.getDiameter() + + if subShp is None: + # Get correct boundbox + if obj.BoundBox == 'Stock': + BS = JOB.Stock + bb = BS.Shape.BoundBox + elif obj.BoundBox == 'BaseBoundBox': + BS = base + bb = base.Shape.BoundBox + + xmin = bb.XMin + xmax = bb.XMax + ymin = bb.YMin + ymax = bb.YMax + else: + xmin = subShp.BoundBox.XMin + xmax = subShp.BoundBox.XMax + ymin = subShp.BoundBox.YMin + ymax = subShp.BoundBox.YMax + + smplInt = obj.SampleInterval.Value + minSampInt = 0.001 # value is mm + if smplInt < minSampInt: + smplInt = minSampInt + + # Determine bounding box length for the OCL scan + bbLength = math.fabs(ymax - ymin) + numScanLines = int(math.ceil(bbLength / smplInt) + 1) # Number of lines + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [obj.FinalDepth.Value] + else: + depthparams = [dp for dp in self.depthParams] + lenDP = len(depthparams) + + # Scan the piece to depth at smplInt + oclScan = [] + oclScan = self._waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, depthparams[lenDP - 1], numScanLines) + oclScan = [FreeCAD.Vector(P.x, P.y, P.z + depOfst) for P in oclScan] + lenOS = len(oclScan) + ptPrLn = int(lenOS / numScanLines) + + # Convert oclScan list of points to multi-dimensional list + scanLines = [] + for L in range(0, numScanLines): + scanLines.append([]) + for P in range(0, ptPrLn): + pi = L * ptPrLn + P + scanLines[L].append(oclScan[pi]) + lenSL = len(scanLines) + pntsPerLine = len(scanLines[0]) + PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") + + # Extract Wl layers per depthparams + lyr = 0 + cmds = [] + layTime = time.time() + self.topoMap = [] + for layDep in depthparams: + cmds = self._getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) + commands.extend(cmds) + lyr += 1 + PathLog.debug("--All layer scans combined took " + str(time.time() - layTime) + " s") + return commands + + def _waterlineDropCutScan(self, stl, smplInt, xmin, xmax, ymin, fd, numScanLines): + '''_waterlineDropCutScan(stl, smplInt, xmin, xmax, ymin, fd, numScanLines) ... + Perform OCL scan for waterline purpose.''' + pdc = ocl.PathDropCutter() # create a pdc + pdc.setSTL(stl) + pdc.setCutter(self.cutter) + pdc.setZ(fd) # set minimumZ (final / target depth value) + pdc.setSampling(smplInt) + + # Create line object as path + path = ocl.Path() # create an empty path object + for nSL in range(0, numScanLines): + yVal = ymin + (nSL * smplInt) + p1 = ocl.Point(xmin, yVal, fd) # start-point of line + p2 = ocl.Point(xmax, yVal, fd) # end-point of line + path.append(ocl.Line(p1, p2)) + # path.append(l) # add the line to the path + pdc.setPath(path) + pdc.run() # run drop-cutter on the path + + # return the list of points + return pdc.getCLPoints() + + def _getWaterline(self, obj, scanLines, layDep, lyr, lenSL, pntsPerLine): + '''_getWaterline(obj, scanLines, layDep, lyr, lenSL, pntsPerLine) ... Get waterline.''' + commands = [] + cmds = [] + loopList = [] + self.topoMap = [] + # Create topo map from scanLines (highs and lows) + self.topoMap = self._createTopoMap(scanLines, layDep, lenSL, pntsPerLine) + # Add buffer lines and columns to topo map + self._bufferTopoMap(lenSL, pntsPerLine) + # Identify layer waterline from OCL scan + self._highlightWaterline(4, 9) + # Extract waterline and convert to gcode + loopList = self._extractWaterlines(obj, scanLines, lyr, layDep) + # save commands + for loop in loopList: + cmds = self._loopToGcode(obj, layDep, loop) + commands.extend(cmds) + return commands + + def _createTopoMap(self, scanLines, layDep, lenSL, pntsPerLine): + '''_createTopoMap(scanLines, layDep, lenSL, pntsPerLine) ... Create topo map version of OCL scan data.''' + topoMap = [] + for L in range(0, lenSL): + topoMap.append([]) + for P in range(0, pntsPerLine): + if scanLines[L][P].z > layDep: + topoMap[L].append(2) + else: + topoMap[L].append(0) + return topoMap + + def _bufferTopoMap(self, lenSL, pntsPerLine): + '''_bufferTopoMap(lenSL, pntsPerLine) ... Add buffer boarder of zeros to all sides to topoMap data.''' + pre = [0, 0] + post = [0, 0] + for p in range(0, pntsPerLine): + pre.append(0) + post.append(0) + for l in range(0, lenSL): + self.topoMap[l].insert(0, 0) + self.topoMap[l].append(0) + self.topoMap.insert(0, pre) + self.topoMap.append(post) + return True + + def _highlightWaterline(self, extraMaterial, insCorn): + '''_highlightWaterline(extraMaterial, insCorn) ... Highlight the waterline data, separating from extra material.''' + TM = self.topoMap + lastPnt = len(TM[1]) - 1 + lastLn = len(TM) - 1 + highFlag = 0 + + # ("--Convert parallel data to ridges") + for lin in range(1, lastLn): + for pt in range(1, lastPnt): # Ignore first and last points + if TM[lin][pt] == 0: + if TM[lin][pt + 1] == 2: # step up + TM[lin][pt] = 1 + if TM[lin][pt - 1] == 2: # step down + TM[lin][pt] = 1 + + # ("--Convert perpendicular data to ridges and highlight ridges") + for pt in range(1, lastPnt): # Ignore first and last points + for lin in range(1, lastLn): + if TM[lin][pt] == 0: + highFlag = 0 + if TM[lin + 1][pt] == 2: # step up + TM[lin][pt] = 1 + if TM[lin - 1][pt] == 2: # step down + TM[lin][pt] = 1 + elif TM[lin][pt] == 2: + highFlag += 1 + if highFlag == 3: + if TM[lin - 1][pt - 1] < 2 or TM[lin - 1][pt + 1] < 2: + highFlag = 2 + else: + TM[lin - 1][pt] = extraMaterial + highFlag = 2 + + # ("--Square corners") + for pt in range(1, lastPnt): + for lin in range(1, lastLn): + if TM[lin][pt] == 1: # point == 1 + cont = True + if TM[lin + 1][pt] == 0: # forward == 0 + if TM[lin + 1][pt - 1] == 1: # forward left == 1 + if TM[lin][pt - 1] == 2: # left == 2 + TM[lin + 1][pt] = 1 # square the corner + cont = False + + if cont is True and TM[lin + 1][pt + 1] == 1: # forward right == 1 + if TM[lin][pt + 1] == 2: # right == 2 + TM[lin + 1][pt] = 1 # square the corner + cont = True + + if TM[lin - 1][pt] == 0: # back == 0 + if TM[lin - 1][pt - 1] == 1: # back left == 1 + if TM[lin][pt - 1] == 2: # left == 2 + TM[lin - 1][pt] = 1 # square the corner + cont = False + + if cont is True and TM[lin - 1][pt + 1] == 1: # back right == 1 + if TM[lin][pt + 1] == 2: # right == 2 + TM[lin - 1][pt] = 1 # square the corner + + # remove inside corners + for pt in range(1, lastPnt): + for lin in range(1, lastLn): + if TM[lin][pt] == 1: # point == 1 + if TM[lin][pt + 1] == 1: + if TM[lin - 1][pt + 1] == 1 or TM[lin + 1][pt + 1] == 1: + TM[lin][pt + 1] = insCorn + elif TM[lin][pt - 1] == 1: + if TM[lin - 1][pt - 1] == 1 or TM[lin + 1][pt - 1] == 1: + TM[lin][pt - 1] = insCorn + + return True + + def _extractWaterlines(self, obj, oclScan, lyr, layDep): + '''_extractWaterlines(obj, oclScan, lyr, layDep) ... Extract water lines from OCL scan data.''' + srch = True + lastPnt = len(self.topoMap[0]) - 1 + lastLn = len(self.topoMap) - 1 + maxSrchs = 5 + srchCnt = 1 + loopList = [] + loop = [] + loopNum = 0 + + if self.CutClimb is True: + lC = [-1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0] + pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] + else: + lC = [1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0] + pC = [-1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1, -1, 0, 1, 1, 1, 0, -1, -1] + + while srch is True: + srch = False + if srchCnt > maxSrchs: + PathLog.debug("Max search scans, " + str(maxSrchs) + " reached\nPossible incomplete waterline result!") + break + for L in range(1, lastLn): + for P in range(1, lastPnt): + if self.topoMap[L][P] == 1: + # start loop follow + srch = True + loopNum += 1 + loop = self._trackLoop(oclScan, lC, pC, L, P, loopNum) + self.topoMap[L][P] = 0 # Mute the starting point + loopList.append(loop) + srchCnt += 1 + PathLog.debug("Search count for layer " + str(lyr) + " is " + str(srchCnt) + ", with " + str(loopNum) + " loops.") + return loopList + + def _trackLoop(self, oclScan, lC, pC, L, P, loopNum): + '''_trackLoop(oclScan, lC, pC, L, P, loopNum) ... Track the loop direction.''' + loop = [oclScan[L - 1][P - 1]] # Start loop point list + cur = [L, P, 1] + prv = [L, P - 1, 1] + nxt = [L, P + 1, 1] + follow = True + ptc = 0 + ptLmt = 200000 + while follow is True: + ptc += 1 + if ptc > ptLmt: + PathLog.debug("Loop number " + str(loopNum) + " at [" + str(nxt[0]) + ", " + str(nxt[1]) + "] pnt count exceeds, " + str(ptLmt) + ". Stopped following loop.") + break + nxt = self._findNextWlPoint(lC, pC, cur[0], cur[1], prv[0], prv[1]) # get next point + loop.append(oclScan[nxt[0] - 1][nxt[1] - 1]) # add it to loop point list + self.topoMap[nxt[0]][nxt[1]] = nxt[2] # Mute the point, if not Y stem + if nxt[0] == L and nxt[1] == P: # check if loop complete + follow = False + elif nxt[0] == cur[0] and nxt[1] == cur[1]: # check if line cannot be detected + follow = False + prv = cur + cur = nxt + return loop + + def _findNextWlPoint(self, lC, pC, cl, cp, pl, pp): + '''_findNextWlPoint(lC, pC, cl, cp, pl, pp) ... + Find the next waterline point in the point cloud layer provided.''' + dl = cl - pl + dp = cp - pp + num = 0 + i = 3 + s = 0 + mtch = 0 + found = False + while mtch < 8: # check all 8 points around current point + if lC[i] == dl: + if pC[i] == dp: + s = i - 3 + found = True + # Check for y branch where current point is connection between branches + for y in range(1, mtch): + if lC[i + y] == dl: + if pC[i + y] == dp: + num = 1 + break + break + i += 1 + mtch += 1 + if found is False: + # ("_findNext: No start point found.") + return [cl, cp, num] + + for r in range(0, 8): + l = cl + lC[s + r] + p = cp + pC[s + r] + if self.topoMap[l][p] == 1: + return [l, p, num] + + # ("_findNext: No next pnt found") + return [cl, cp, num] + + def _loopToGcode(self, obj, layDep, loop): + '''_loopToGcode(obj, layDep, loop) ... Convert set of loop points to Gcode.''' + # generate the path commands + output = [] + + prev = FreeCAD.Vector(2135984513.165, -58351896873.17455, 13838638431.861) + nxt = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Create first point + pnt = FreeCAD.Vector(loop[0].x, loop[0].y, layDep) + + # Position cutter to begin loop + output.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) + output.append(Path.Command('G0', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizRapid})) + output.append(Path.Command('G1', {'Z': pnt.z, 'F': self.vertFeed})) + + lenCLP = len(loop) + lastIdx = lenCLP - 1 + # Cycle through each point on loop + for i in range(0, lenCLP): + if i < lastIdx: + nxt.x = loop[i + 1].x + nxt.y = loop[i + 1].y + nxt.z = layDep + + output.append(Path.Command('G1', {'X': pnt.x, 'Y': pnt.y, 'F': self.horizFeed})) + + # Rotate point data + prev = pnt + pnt = nxt + + # Save layer end point for use in transitioning to next layer + self.layerEndPnt = pnt + + return output + + # Experimental waterline functions + def _experimentalWaterlineOp(self, JOB, obj, mdlIdx, subShp=None): + '''_waterlineOp(JOB, obj, mdlIdx, subShp=None) ... + Main waterline function to perform waterline extraction from model.''' + PathLog.debug('_experimentalWaterlineOp()') + + # msg = translate('PathWaterline', 'Experimental Waterline does not currently support selected faces.') + # PathLog.info('\n..... ' + msg) + + commands = [] + t_begin = time.time() + base = JOB.Model.Group[mdlIdx] + # bb = self.boundBoxes[mdlIdx] + # stl = self.modelSTLs[mdlIdx] + # safeSTL = self.safeSTLs[mdlIdx] + self.endVector = None + + finDep = obj.FinalDepth.Value + (self.geoTlrnc / 10.0) + depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, finDep) + + # Compute number and size of stepdowns, and final depth + if obj.LayerMode == 'Single-pass': + depthparams = [finDep] + else: + depthparams = [dp for dp in depthParams] + PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams)) + + # Prepare PathDropCutter objects with STL data + # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) + + buffer = self.cutter.getDiameter() * 10.0 + borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0)) + + # Get correct boundbox + if obj.BoundBox == 'Stock': + stockEnv = self._getShapeEnvelope(JOB.Stock.Shape) + bbFace = self._getCrossSection(stockEnv) # returned at Z=0.0 + elif obj.BoundBox == 'BaseBoundBox': + baseEnv = self._getShapeEnvelope(base.Shape) + bbFace = self._getCrossSection(baseEnv) # returned at Z=0.0 + + trimFace = borderFace.cut(bbFace) + if self.showDebugObjects is True: + TF = FreeCAD.ActiveDocument.addObject('Part::Feature', 'trimFace') + TF.Shape = trimFace + TF.purgeTouched() + self.tempGroup.addObject(TF) + + # Cycle through layer depths + CUTAREAS = self._getCutAreas(base.Shape, depthparams, bbFace, trimFace, borderFace) + if not CUTAREAS: + PathLog.error('No cross-section cut areas identified.') + return commands + + caCnt = 0 + ofst = obj.BoundaryAdjustment.Value + ofst -= self.radius # (self.radius + (tolrnc / 10.0)) + caLen = len(CUTAREAS) + lastCA = caLen - 1 + lastClearArea = None + lastCsHght = None + clearLastLayer = True + for ca in range(0, caLen): + area = CUTAREAS[ca] + csHght = area.BoundBox.ZMin + csHght += obj.DepthOffset.Value + cont = False + caCnt += 1 + if area.Area > 0.0: + cont = True + caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire + PathLog.debug('cutAreaWireCnt: {}'.format(caWireCnt)) + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt)) + CA.Shape = area + CA.purgeTouched() + self.tempGroup.addObject(CA) + else: + PathLog.error('Cut area at {} is zero.'.format(round(csHght, 4))) + + # get offset wire(s) based upon cross-section cut area + if cont: + area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin)) + activeArea = area.cut(trimFace) + activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire + PathLog.debug('activeAreaWireCnt: {}'.format(activeAreaWireCnt)) + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt)) + CA.Shape = activeArea + CA.purgeTouched() + self.tempGroup.addObject(CA) + ofstArea = self._extractFaceOffset(activeArea, ofst, makeComp=False) + if not ofstArea: + PathLog.error('No offset area returned for cut area depth: {}'.format(csHght)) + cont = False + + if cont: + # Identify solid areas in the offset data + ofstSolidFacesList = self._getSolidAreasFromPlanarFaces(ofstArea) + if ofstSolidFacesList: + clearArea = Part.makeCompound(ofstSolidFacesList) + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearArea_{}'.format(caCnt)) + CA.Shape = clearArea + CA.purgeTouched() + self.tempGroup.addObject(CA) + else: + cont = False + PathLog.error('ofstSolids is False.') + + if cont: + # Make waterline path for current CUTAREA depth (csHght) + commands.extend(self._wiresToWaterlinePath(obj, clearArea, csHght)) + clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clearArea.BoundBox.ZMin)) + lastClearArea = clearArea + lastCsHght = csHght + + # Clear layer as needed + (useOfst, usePat, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer) + ##if self.showDebugObjects is True and (usePat or useOfst): + ## OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearPatternArea_{}'.format(round(csHght, 2))) + ## OA.Shape = clearArea + ## OA.purgeTouched() + ## self.tempGroup.addObject(OA) + if usePat: + commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght)) + if useOfst: + commands.extend(self._makeOffsetLayerPaths(JOB, obj, clearArea, csHght)) + # Efor + + if clearLastLayer: + (useOfst, usePat, cLL) = self._clearLayer(obj, 1, 1, False) + clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin)) + if usePat: + commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght)) + + if useOfst: + commands.extend(self._makeOffsetLayerPaths(JOB, obj, lastClearArea, lastCsHght)) + + PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s") + return commands + + def _getCutAreas(self, shape, depthparams, bbFace, trimFace, borderFace): + '''_getCutAreas(JOB, shape, depthparams, bbFace, borderFace) ... + Takes shape, depthparams and base-envelope-cross-section, and + returns a list of cut areas - one for each depth.''' + PathLog.debug('_getCutAreas()') + + CUTAREAS = list() + isFirst = True + lenDP = len(depthparams) + + # Cycle through layer depths + for dp in range(0, lenDP): + csHght = depthparams[dp] + PathLog.debug('Depth {} is {}'.format(dp + 1, csHght)) + + # Get slice at depth of shape + csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0 + if not csFaces: + PathLog.error('No cross-section wires at {}'.format(csHght)) + else: + PathLog.debug('cross-section face count {}'.format(len(csFaces))) + if len(csFaces) > 0: + useFaces = self._getSolidAreasFromPlanarFaces(csFaces) + else: + useFaces = False + + if useFaces: + PathLog.debug('useFacesCnt: {}'.format(len(useFaces))) + compAdjFaces = Part.makeCompound(useFaces) + + if self.showDebugObjects is True: + CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSolids_{}'.format(dp + 1)) + CA.Shape = compAdjFaces + CA.purgeTouched() + self.tempGroup.addObject(CA) + + if isFirst: + allPrevComp = compAdjFaces + cutArea = borderFace.cut(compAdjFaces) + else: + preCutArea = borderFace.cut(compAdjFaces) + cutArea = preCutArea.cut(allPrevComp) # cut out higher layers to avoid cutting recessed areas + allPrevComp = allPrevComp.fuse(compAdjFaces) + cutArea.translate(FreeCAD.Vector(0.0, 0.0, csHght - cutArea.BoundBox.ZMin)) + CUTAREAS.append(cutArea) + isFirst = False + else: + PathLog.error('No waterline at depth: {} mm.'.format(csHght)) + # Efor + + if len(CUTAREAS) > 0: + return CUTAREAS + + return False + + def _wiresToWaterlinePath(self, obj, ofstPlnrShp, csHght): + PathLog.debug('_wiresToWaterlinePath()') + commands = list() + + # Translate path geometry to layer height + ofstPlnrShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - ofstPlnrShp.BoundBox.ZMin)) + if self.showDebugObjects is True: + OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'waterlinePathArea_{}'.format(round(csHght, 2))) + OA.Shape = ofstPlnrShp + OA.purgeTouched() + self.tempGroup.addObject(OA) + + commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2)))) + start = 1 + if ofstPlnrShp.BoundBox.ZMin < obj.IgnoreOuterAbove: + start = 0 + for w in range(start, len(ofstPlnrShp.Wires)): + wire = ofstPlnrShp.Wires[w] + V = wire.Vertexes + if obj.CutMode == 'Climb': + lv = len(V) - 1 + startVect = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) + else: + startVect = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) + + commands.append(Path.Command('N (Wire {}.)'.format(w))) + (cmds, endVect) = self._wireToPath(obj, wire, startVect) + commands.extend(cmds) + commands.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + + return commands + + def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght): + PathLog.debug('_makeCutPatternLayerPaths()') + commands = [] + + clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin)) + pathGeom = self._planarMakePathGeom(obj, clrAreaShp) + pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin)) + # clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) + + if self.showDebugObjects is True: + OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2))) + OA.Shape = pathGeom + OA.purgeTouched() + self.tempGroup.addObject(OA) + + # Convert pathGeom to gcode more efficiently + if obj.CutPattern == 'Offset': + commands.extend(self._makeOffsetLayerPaths(JOB, obj, clrAreaShp, csHght)) + else: + clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) + if obj.CutPattern == 'Line': + pntSet = self._pathGeomToLinesPointSet(obj, pathGeom) + elif obj.CutPattern == 'ZigZag': + pntSet = self._pathGeomToZigzagPointSet(obj, pathGeom) + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + pntSet = self._pathGeomToArcPointSet(obj, pathGeom) + stpOVRS = self._getExperimentalWaterlinePaths(obj, pntSet, csHght) + # PathLog.debug('stpOVRS:\n{}'.format(stpOVRS)) + safePDC = False + cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, csHght) + commands.extend(cmds) + + return commands + + def _makeOffsetLayerPaths(self, JOB, obj, clrAreaShp, csHght): + PathLog.debug('_makeOffsetLayerPaths()') + PathLog.warning('Using `Offset` for clearing bottom layer.') + cmds = list() + # ofst = obj.BoundaryAdjustment.Value + ofst = 0.0 - self.cutOut # - self.cutter.getDiameter() # (self.radius + (tolrnc / 10.0)) + shape = clrAreaShp + cont = True + cnt = 0 + while cont: + ofstArea = self._extractFaceOffset(shape, ofst, makeComp=True) + if not ofstArea: + PathLog.warning('No offset clearing area returned.') + break + for F in ofstArea.Faces: + cmds.extend(self._wiresToWaterlinePath(obj, F, csHght)) + shape = ofstArea + if cnt == 0: + ofst = 0.0 - self.cutOut # self.cutter.Diameter() + cnt += 1 + return cmds + + def _clearGeomToPaths(self, JOB, obj, safePDC, SCANDATA, csHght): + PathLog.debug('_clearGeomToPaths()') + + GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] + tolrnc = JOB.GeometryTolerance.Value + lenSCANDATA = len(SCANDATA) + gDIR = ['G3', 'G2'] + + if self.CutClimb is True: + gDIR = ['G2', 'G3'] + + # Send cutter to x,y position of first point on first line + first = SCANDATA[0][0][0] # [step][item][point] + GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) + + # Cycle through step-over sections (line segments or arcs) + odd = True + lstStpEnd = None + for so in range(0, lenSCANDATA): + cmds = list() + PRTS = SCANDATA[so] + lenPRTS = len(PRTS) + first = PRTS[0][0] # first point of arc/line stepover group + last = None + cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) + + if so > 0: + if obj.CutPattern == 'CircularZigZag': + if odd is True: + odd = False + else: + odd = True + # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL + minTrnsHght = obj.SafeHeight.Value + # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) + cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) + + # Cycle through current step-over parts + for i in range(0, lenPRTS): + prt = PRTS[i] + # PathLog.debug('prt: {}'.format(prt)) + if prt == 'BRK': + nxtStart = PRTS[i + 1][0] + # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL + minSTH = obj.SafeHeight.Value + cmds.append(Path.Command('N (Break)', {})) + cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) + else: + cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) + if obj.CutPattern in ['Line', 'ZigZag']: + start, last = prt + cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed})) + elif obj.CutPattern in ['Circular', 'CircularZigZag']: + start, last, centPnt, cMode = prt + gcode = self._makeGcodeArc(start, last, odd, gDIR, tolrnc) + cmds.extend(gcode) + cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) + GCODE.extend(cmds) # save line commands + lstStpEnd = last + # Efor + + # Raise to safe height after clearing + GCODE.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) + + return GCODE + + def _getSolidAreasFromPlanarFaces(self, csFaces): + PathLog.debug('_getSolidAreasFromPlanarFaces()') + holds = list() + useFaces = list() + lenCsF = len(csFaces) + PathLog.debug('lenCsF: {}'.format(lenCsF)) + + if lenCsF == 1: + useFaces = csFaces + else: + fIds = list() + aIds = list() + pIds = list() + cIds = list() + + for af in range(0, lenCsF): + fIds.append(af) # face ids + aIds.append(af) # face ids + pIds.append(-1) # parent ids + cIds.append(False) # cut ids + holds.append(False) + + while len(fIds) > 0: + li = fIds.pop() + low = csFaces[li] # senior face + pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low) + # Ewhile + ##PathLog.info('fIds: {}'.format(fIds)) + ##PathLog.info('pIds: {}'.format(pIds)) + + for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first + ##PathLog.info('af: {}'.format(af)) + prnt = pIds[af] + ##PathLog.info('prnt: {}'.format(prnt)) + if prnt == -1: + stack = -1 + else: + stack = [af] + # get_face_ids_to_parent + stack.insert(0, prnt) + nxtPrnt = pIds[prnt] + # find af value for nxtPrnt + while nxtPrnt != -1: + stack.insert(0, nxtPrnt) + nxtPrnt = pIds[nxtPrnt] + cIds[af] = stack + # PathLog.debug('cIds: {}\n'.format(cIds)) + + for af in range(0, lenCsF): + # PathLog.debug('af is {}'.format(af)) + pFc = cIds[af] + if pFc == -1: + # Simple, independent region + holds[af] = csFaces[af] # place face in hold + # PathLog.debug('pFc == -1') + else: + # Compound region + # PathLog.debug('pFc is not -1') + cnt = len(pFc) + if cnt % 2.0 == 0.0: + # even is donut cut + # PathLog.debug('cnt is even') + inr = pFc[cnt - 1] + otr = pFc[cnt - 2] + # PathLog.debug('inr / otr: {} / {}'.format(inr, otr)) + holds[otr] = holds[otr].cut(csFaces[inr]) + else: + # odd is floating solid + # PathLog.debug('cnt is ODD') + holds[af] = csFaces[af] + # Efor + + for af in range(0, lenCsF): + if holds[af]: + useFaces.append(holds[af]) # save independent solid + + # Eif + + if len(useFaces) > 0: + return useFaces + + return False + + def _getModelCrossSection(self, shape, csHght): + PathLog.debug('_getCrossSection()') + wires = list() + + def byArea(fc): + return fc.Area + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), csHght): + wires.append(i) + + if len(wires) > 0: + for w in wires: + if w.isClosed() is False: + return False + FCS = list() + for w in wires: + w.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - w.BoundBox.ZMin)) + FCS.append(Part.Face(w)) + FCS.sort(key=byArea, reverse=True) + return FCS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + + def _isInBoundBox(self, outShp, inShp): + obb = outShp.BoundBox + ibb = inShp.BoundBox + + if obb.XMin < ibb.XMin: + if obb.XMax > ibb.XMax: + if obb.YMin < ibb.YMin: + if obb.YMax > ibb.YMax: + return True + return False + + def _idInternalFeature(self, csFaces, fIds, pIds, li, low): + Ids = list() + for i in fIds: + Ids.append(i) + while len(Ids) > 0: + hi = Ids.pop() + high = csFaces[hi] + if self._isInBoundBox(high, low): + cmn = high.common(low) + if cmn.Area > 0.0: + pIds[li] = hi + break + # Ewhile + return pIds + + def _wireToPath(self, obj, wire, startVect): + '''_wireToPath(obj, wire, startVect) ... wire to path.''' + PathLog.track() + + paths = [] + pathParams = {} # pylint: disable=assignment-from-no-return + + pathParams['shapes'] = [wire] + pathParams['feedrate'] = self.horizFeed + pathParams['feedrate_v'] = self.vertFeed + pathParams['verbose'] = True + pathParams['resume_height'] = obj.SafeHeight.Value + pathParams['retraction'] = obj.ClearanceHeight.Value + pathParams['return_end'] = True + # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers + pathParams['preamble'] = False + pathParams['start'] = startVect + + (pp, end_vector) = Path.fromShapes(**pathParams) + paths.extend(pp.Commands) + # PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) + + self.endVector = end_vector # pylint: disable=attribute-defined-outside-init + + return (paths, end_vector) + + def _makeExtendedBoundBox(self, wBB, bbBfr, zDep): + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + pl.Base = FreeCAD.Vector(0, 0, 0) + + p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) + p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) + p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) + p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) + bb = Part.makePolygon([p1, p2, p3, p4, p1]) + + return bb + + def _makeGcodeArc(self, strtPnt, endPnt, odd, gDIR, tolrnc): + cmds = list() + isCircle = False + gdi = 0 + if odd is True: + gdi = 1 + + # Test if pnt set is circle + if abs(strtPnt.x - endPnt.x) < tolrnc: + if abs(strtPnt.y - endPnt.y) < tolrnc: + isCircle = True + isCircle = False + + if isCircle is True: + # convert LN to G2/G3 arc, consolidating GCode + # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc + # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ + # Dividing circle into two arcs allows for G2/G3 on inclined surfaces + + # ijk = self.tmpCOM - strtPnt # vector from start to center + ijk = self.tmpCOM - strtPnt # vector from start to center + xyz = self.tmpCOM.add(ijk) # end point + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) + ijk = self.tmpCOM - xyz # vector from start to center + rst = strtPnt # end point + cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + else: + # ijk = self.tmpCOM - strtPnt + ijk = self.tmpCOM.sub(strtPnt) # vector from start to center + xyz = endPnt + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) + + return cmds + + def _clearLayer(self, obj, ca, lastCA, clearLastLayer): + PathLog.debug('_clearLayer()') + usePat = False + useOfst = False + + if obj.ClearLastLayer == 'Off': + if obj.CutPattern != 'None': + usePat = True + else: + if ca == lastCA: + PathLog.debug('... Clearing bottom layer.') + if obj.ClearLastLayer == 'Offset': + obj.CutPattern = 'None' + useOfst = True + else: + obj.CutPattern = obj.ClearLastLayer + usePat = True + clearLastLayer = False + + return (useOfst, usePat, clearLastLayer) + + # Support methods + def resetOpVariables(self, all=True): + '''resetOpVariables() ... Reset class variables used for instance of operation.''' + self.holdPoint = None + self.layerEndPnt = None + self.onHold = False + self.SafeHeightOffset = 2.0 + self.ClearHeightOffset = 4.0 + self.layerEndzMax = 0.0 + self.resetTolerance = 0.0 + self.holdPntCnt = 0 + self.bbRadius = 0.0 + self.axialFeed = 0.0 + self.axialRapid = 0.0 + self.FinalDepth = 0.0 + self.clearHeight = 0.0 + self.safeHeight = 0.0 + self.faceZMax = -999999999999.0 + if all is True: + self.cutter = None + self.stl = None + self.fullSTL = None + self.cutOut = 0.0 + self.radius = 0.0 + self.useTiltCutter = False + return True + + def deleteOpVariables(self, all=True): + '''deleteOpVariables() ... Reset class variables used for instance of operation.''' + del self.holdPoint + del self.layerEndPnt + del self.onHold + del self.SafeHeightOffset + del self.ClearHeightOffset + del self.layerEndzMax + del self.resetTolerance + del self.holdPntCnt + del self.bbRadius + del self.axialFeed + del self.axialRapid + del self.FinalDepth + del self.clearHeight + del self.safeHeight + del self.faceZMax + if all is True: + del self.cutter + del self.stl + del self.fullSTL + del self.cutOut + del self.radius + del self.useTiltCutter + return True + + def setOclCutter(self, obj, safe=False): + ''' setOclCutter(obj) ... Translation function to convert FreeCAD tool definition to OCL formatted tool. ''' + # Set cutter details + # https://www.freecadweb.org/api/dd/dfe/classPath_1_1Tool.html#details + diam_1 = float(obj.ToolController.Tool.Diameter) + lenOfst = obj.ToolController.Tool.LengthOffset if hasattr(obj.ToolController.Tool, 'LengthOffset') else 0 + FR = obj.ToolController.Tool.FlatRadius if hasattr(obj.ToolController.Tool, 'FlatRadius') else 0 + CEH = obj.ToolController.Tool.CuttingEdgeHeight if hasattr(obj.ToolController.Tool, 'CuttingEdgeHeight') else 0 + CEA = obj.ToolController.Tool.CuttingEdgeAngle if hasattr(obj.ToolController.Tool, 'CuttingEdgeAngle') else 0 + + # Make safeCutter with 2 mm buffer around physical cutter + if safe is True: + diam_1 += 4.0 + if FR != 0.0: + FR += 2.0 + + PathLog.debug('ToolType: {}'.format(obj.ToolController.Tool.ToolType)) + if obj.ToolController.Tool.ToolType == 'EndMill': + # Standard End Mill + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR == 0.0: + # Standard Ball End Mill + # OCL -> BallCutter::BallCutter(diameter, length) + self.useTiltCutter = True + return ocl.BallCutter(diam_1, (diam_1 / 2 + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'BallEndMill' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> BallCutter::BallCutter(diameter, length) + return ocl.BullCutter(diam_1, FR, (CEH + lenOfst)) + + elif obj.ToolController.Tool.ToolType == 'Engraver' and FR > 0.0: + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + + elif obj.ToolController.Tool.ToolType == 'ChamferMill': + # Bull Nose or Corner Radius cutter + # Reference: https://www.fine-tools.com/halbstabfraeser.html + # OCL -> ConeCutter::ConeCutter(diameter, angle, lengthOffset) + return ocl.ConeCutter(diam_1, (CEA / 2), lenOfst) + else: + # Default to standard end mill + PathLog.warning("Defaulting cutter to standard end mill.") + return ocl.CylCutter(diam_1, (CEH + lenOfst)) + + +def SetupProperties(): + ''' SetupProperties() ... Return list of properties required for operation.''' + setup = ['Algorithm', 'AvoidLastX_Faces', 'AvoidLastX_InternalFeatures', 'BoundBox'] + setup.extend(['BoundaryAdjustment', 'CircularCenterAt', 'CircularCenterCustom']) + setup.extend(['ClearLastLayer', 'InternalFeaturesCut', 'InternalFeaturesAdjustment']) + setup.extend(['CutMode', 'CutPattern', 'CutPatternAngle', 'CutPatternReversed']) + setup.extend(['DepthOffset', 'GapSizes', 'GapThreshold']) + setup.extend(['HandleMultipleFeatures', 'LayerMode', 'OptimizeStepOverTransitions']) + setup.extend(['ProfileEdges', 'BoundaryEnforcement', 'SampleInterval']) + setup.extend(['StartPoint', 'StepOver', 'IgnoreOuterAbove']) + setup.extend(['UseStartPoint', 'AngularDeflection', 'LinearDeflection', 'ShowTempObjects']) + return setup + + +def Create(name, obj=None): + '''Create(name) ... Creates and returns a Waterline operation.''' + if obj is None: + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + obj.Proxy = ObjectWaterline(obj, name) + return obj From 01e95b2ac09bdda3b9a389ea0b6d9eb3351669da Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Mon, 13 Apr 2020 04:03:46 -0500 Subject: [PATCH 064/142] Path: Add missing tooltips --- .../Resources/panels/PageOpWaterlineEdit.ui | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui index 5e0edef1c9..82533fe061 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui @@ -50,6 +50,9 @@ 8 + + <html><head/><body><p>Select the overall boundary for the operation.</p></body></html> + Stock @@ -70,6 +73,9 @@ 0 + + <html><head/><body><p>Positive values push the cutter toward, or beyond, the boundary. Negative values retract the cutter away from the boundary.</p></body></html> +
    @@ -79,6 +85,9 @@ 8 + + <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html> + Single-pass @@ -93,6 +102,9 @@ + + <html><head/><body><p>Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).</p></body></html> + OCL Dropcutter @@ -107,6 +119,9 @@ + + <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html> + Optimize Linear Paths @@ -132,6 +147,9 @@ 8 + + <html><head/><body><p>Set the geometric clearing pattern to use for the operation.</p></body></html> + None @@ -213,6 +231,9 @@ + + <html><head/><body><p>Set the sampling resolution. Smaller values quickly increase processing time.</p></body></html> + mm @@ -260,8 +281,8 @@ Gui::InputField - QWidget -
    gui::inputfield.h
    + QLineEdit +
    Gui/InputField.h
    From 9e9d5ce9620182747c3202e4cbacd5099d695c43 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Mon, 13 Apr 2020 15:00:26 -0500 Subject: [PATCH 065/142] Path: Fix weakness in face analysis for unique OuterWire cases synced with PathSurface module --- src/Mod/Path/PathScripts/PathWaterline.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index 80fe121553..1953af31ee 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -1107,10 +1107,24 @@ class ObjectWaterline(PathOp.ObjectOp): PathLog.debug(' -number of wires found is {}'.format(nf)) if nf == 1: (area, W, raised) = WIRES[0] - return [(W, raised)] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + return [(OW, False), (W, raised)] + else: + return [(W, raised)] else: sortedWIRES = sorted(WIRES, key=index0, reverse=True) - return [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + # Check if OuterWire is larger than largest in WRS list + (W, raised) = WRS[0] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + WRS.insert(0, (OW, False)) + return WRS return False From a0cecce62e500907c35f060be05340686432d412 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 22:16:51 -0500 Subject: [PATCH 066/142] Path: Expose operation's property details to access via class --- src/Mod/Path/PathScripts/PathWaterline.py | 46 +++++++++++++---------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index 1953af31ee..8e1a9dae51 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -97,7 +97,29 @@ class ObjectWaterline(PathOp.ObjectOp): def initOpProperties(self, obj): '''initOpProperties(obj) ... create operation specific properties''' - PROPS = [ + missing = list() + + for (prtyp, nm, grp, tt) in self.opProperties(): + if not hasattr(obj, nm): + obj.addProperty(prtyp, nm, grp, tt) + missing.append(nm) + newPropMsg = translate('PathSurface', 'New property added: ') + nm + '. ' + newPropMsg += translate('PathSurface', 'Check its default value.') + PathLog.warning(newPropMsg) + + # Set enumeration lists for enumeration properties + if len(missing) > 0: + ENUMS = self.propertyEnumerations() + for n in ENUMS: + if n in missing: + cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) + exec(cmdStr) + + self.addedAllProperties = True + + def opProperties(self): + '''opProperties() ... return list of tuples containing operation specific properties''' + return [ ("App::PropertyBool", "ShowTempObjects", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), @@ -167,31 +189,15 @@ class ObjectWaterline(PathOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) ] - missing = list() - for (prtyp, nm, grp, tt) in PROPS: - if not hasattr(obj, nm): - obj.addProperty(prtyp, nm, grp, tt) - missing.append(nm) - - # Set enumeration lists for enumeration properties - if len(missing) > 0: - ENUMS = self._propertyEnumerations() - for n in ENUMS: - if n in missing: - cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) - exec(cmdStr) - - self.addedAllProperties = True - - def _propertyEnumerations(self): + def propertyEnumerations(self): # Enumeration lists for App::PropertyEnumeration properties return { 'Algorithm': ['OCL Dropcutter', 'Experimental'], 'BoundBox': ['BaseBoundBox', 'Stock'], 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], - 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], + 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], 'CutMode': ['Conventional', 'Climb'], - 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] + 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] 'HandleMultipleFeatures': ['Collectively', 'Individually'], 'LayerMode': ['Single-pass', 'Multi-pass'], 'ProfileEdges': ['None', 'Only', 'First', 'Last'], From ed85341cd98cc17a608a6a1e1c756a4f6e8e7b13 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 22:18:04 -0500 Subject: [PATCH 067/142] Path: Improve property visibility in Data tab --- src/Mod/Path/PathScripts/PathWaterline.py | 42 +++++++++++++---------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index 8e1a9dae51..434300f3f4 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -205,8 +205,8 @@ class ObjectWaterline(PathOp.ObjectOp): def setEditorProperties(self, obj): # Used to hide inputs in properties list - show = 0 - hide = A = 2 + expMode = G = 0 + show = hide = A = B = C = 2 if hasattr(obj, 'EnableRotation'): obj.setEditorMode('EnableRotation', hide) @@ -224,29 +224,35 @@ class ObjectWaterline(PathOp.ObjectOp): obj.setEditorMode('GapSizes', hide) if obj.Algorithm == 'OCL Dropcutter': - expMode = 0 - obj.setEditorMode('ClearLastLayer', hide) + B = 2 elif obj.Algorithm == 'Experimental': - A = 0 - expMode = 2 - if obj.CutPattern == 'None': + A = B = C = 0 + expMode = G = 2 + + cutPattern = obj.CutPattern + if obj.ClearLastLayer != 'Off': + cutPattern = obj.CutPattern + + if cutPattern == 'None': show = hide = A = 2 - elif obj.CutPattern in ['Line', 'ZigZag']: + elif cutPattern in ['Line', 'ZigZag']: show = 0 - elif obj.CutPattern in ['Circular', 'CircularZigZag']: + elif cutPattern in ['Circular', 'CircularZigZag']: show = 2 # hide hide = 0 # show + elif cutPattern == 'Spiral': + G = 0 - obj.setEditorMode('CutPatternAngle', show) - obj.setEditorMode('CircularCenterAt', hide) - obj.setEditorMode('CircularCenterCustom', hide) + obj.setEditorMode('CutPatternAngle', show) + obj.setEditorMode('CircularCenterAt', hide) + obj.setEditorMode('CircularCenterCustom', hide) + obj.setEditorMode('CutPatternReversed', A) - obj.setEditorMode('CutPatternReversed', A) - obj.setEditorMode('ClearLastLayer', A) - obj.setEditorMode('StepOver', A) - - obj.setEditorMode('IgnoreOuterAbove', A) - obj.setEditorMode('SampleInterval', expMode) + obj.setEditorMode('ClearLastLayer', C) + obj.setEditorMode('StepOver', B) + obj.setEditorMode('IgnoreOuterAbove', B) + obj.setEditorMode('CutPattern', C) + obj.setEditorMode('SampleInterval', G) obj.setEditorMode('LinearDeflection', expMode) obj.setEditorMode('AngularDeflection', expMode) From eb354c3a1d2e136cd4ab6d8aada132a3aac81936 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 22:19:06 -0500 Subject: [PATCH 068/142] Path: Improve backwards compatibility capabilities --- src/Mod/Path/PathScripts/PathWaterline.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index 434300f3f4..bd43671a3c 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -270,6 +270,19 @@ class ObjectWaterline(PathOp.ObjectOp): else: obj.setEditorMode('ShowTempObjects', 0) # show + # Repopulate enumerations in case of changes + ENUMS = self.propertyEnumerations() + for n in ENUMS: + restore = False + if hasattr(obj, n): + val = obj.getPropertyByName(n) + restore = True + cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) + exec(cmdStr) + if restore: + cmdStr = 'obj.{}={}'.format(n, "'" + val + "'") + exec(cmdStr) + self.setEditorProperties(obj) def opSetDefaultValues(self, obj, job): From aaf1eee7c5f65f351950da87595587056396cffd Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 22:20:21 -0500 Subject: [PATCH 069/142] Path: Preparation for making property defaults readable through class --- src/Mod/Path/PathScripts/PathWaterline.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index bd43671a3c..504764f139 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -297,9 +297,10 @@ class ObjectWaterline(PathOp.ObjectOp): obj.AvoidLastX_InternalFeatures = True obj.CutPatternReversed = False obj.IgnoreOuterAbove = obj.StartDepth.Value + 0.00001 - obj.StartPoint.x = 0.0 - obj.StartPoint.y = 0.0 - obj.StartPoint.z = obj.ClearanceHeight.Value + #obj.StartPoint.x = 0.0 + #obj.StartPoint.y = 0.0 + #obj.StartPoint.z = obj.ClearanceHeight.Value + obj.StartPoint = FreeCAD.Vector(5.0, 5.0, obj.ClearanceHeight.Value) obj.Algorithm = 'OCL Dropcutter' obj.ProfileEdges = 'None' obj.LayerMode = 'Single-pass' @@ -316,9 +317,10 @@ class ObjectWaterline(PathOp.ObjectOp): obj.BoundaryAdjustment.Value = 0.0 obj.InternalFeaturesAdjustment.Value = 0.0 obj.AvoidLastX_Faces = 0 - obj.CircularCenterCustom.x = 0.0 - obj.CircularCenterCustom.y = 0.0 - obj.CircularCenterCustom.z = 0.0 + #obj.CircularCenterCustom.x = 0.0 + #obj.CircularCenterCustom.y = 0.0 + #obj.CircularCenterCustom.z = 0.0 + obj.CircularCenterCustom = FreeCAD.Vector(5.0, 5.0, 5.0) obj.GapThreshold.Value = 0.005 obj.LinearDeflection.Value = 0.0001 obj.AngularDeflection.Value = 0.25 From cb796ca2c9ef523dac2f8c2c54e8e1c0ff09dfa6 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 22:36:51 -0500 Subject: [PATCH 070/142] Path: Implement new module PathSurfaceSupport; Add `Spiral` cut pattern New module is shared with 3D Surface operation. Module contains PathGeometryGenerator class. More common methods can be moved into the new module. --- src/Mod/Path/PathScripts/PathWaterline.py | 364 ++++++++-------------- 1 file changed, 124 insertions(+), 240 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index 504764f139..cb74c2a505 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -49,6 +49,7 @@ import Path import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils import PathScripts.PathOp as PathOp +import PathScripts.PathSurfaceSupport as PathSurfaceSupport import time import math import Part @@ -1609,177 +1610,6 @@ class ObjectWaterline(PathOp.ObjectOp): return final # Methods for creating path geometry - def _planarMakePathGeom(self, obj, faceShp): - '''_planarMakePathGeom(obj, faceShp)... - Creates the line/arc cut pattern geometry and returns the intersection with the received faceShp. - The resulting intersecting line/arc geometries are then converted to lines or arcs for OCL.''' - PathLog.debug('_planarMakePathGeom()') - GeoSet = list() - - # Apply drop cutter extra offset and set the max and min XY area of the operation - xmin = faceShp.BoundBox.XMin - xmax = faceShp.BoundBox.XMax - ymin = faceShp.BoundBox.YMin - ymax = faceShp.BoundBox.YMax - zmin = faceShp.BoundBox.ZMin - zmax = faceShp.BoundBox.ZMax - - # Compute weighted center of mass of all faces combined - fCnt = 0 - totArea = 0.0 - zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) - for F in faceShp.Faces: - comF = F.CenterOfMass - areaF = F.Area - totArea += areaF - fCnt += 1 - zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) - if fCnt == 0: - PathLog.error(translate('PathWaterline', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) - zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) - else: - avgArea = totArea / fCnt - zeroCOM.multiply(1 / fCnt) - zeroCOM.multiply(1 / avgArea) - COM = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) - - # get X, Y, Z spans; Compute center of rotation - deltaX = abs(xmax-xmin) - deltaY = abs(ymax-ymin) - deltaC = math.sqrt(deltaX**2 + deltaY**2) - lineLen = deltaC + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - halfLL = math.ceil(lineLen / 2.0) - cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - halfPasses = math.ceil(cutPasses / 2.0) - bbC = faceShp.BoundBox.Center - - # Generate the line/circle sets to be intersected with the cut-face-area - if obj.CutPattern in ['ZigZag', 'Line']: - centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model - cAng = math.atan(deltaX / deltaY) # BoundaryBox angle - - # Determine end points and create top lines - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - diag = None - if obj.CutPatternAngle == 0 or obj.CutPatternAngle == 180: - diag = deltaY - elif obj.CutPatternAngle == 90 or obj.CutPatternAngle == 270: - diag = deltaX - else: - perpDist = math.cos(cAng - math.radians(obj.CutPatternAngle)) * deltaC - diag = perpDist - y1 = centRot.y + diag - # y2 = y1 - - # Create end points for set of lines to intersect with cross-section face - pntTuples = list() - for lc in range((-1 * (halfPasses - 1)), halfPasses + 1): - x1 = centRot.x - halfLL - x2 = centRot.x + halfLL - y1 = centRot.y + (lc * self.cutOut) - # y2 = y1 - p1 = FreeCAD.Vector(x1, y1, 0.0) - p2 = FreeCAD.Vector(x2, y1, 0.0) - pntTuples.append( (p1, p2) ) - - # Convert end points to lines - for (p1, p2) in pntTuples: - line = Part.makeLine(p1, p2) - GeoSet.append(line) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: - zTgt = faceShp.BoundBox.ZMin - axisRot = FreeCAD.Vector(0.0, 0.0, 1.0) - cntr = FreeCAD.Placement() - cntr.Rotation = FreeCAD.Rotation(axisRot, 0.0) - - if obj.CircularCenterAt == 'CenterOfMass': - cntr.Base = FreeCAD.Vector(COM.x, COM.y, zTgt) # COM # Use center of Mass - elif obj.CircularCenterAt == 'CenterOfBoundBox': - cent = faceShp.BoundBox.Center - cntr.Base = FreeCAD.Vector(cent.x, cent.y, zTgt) - elif obj.CircularCenterAt == 'XminYmin': - cntr.Base = FreeCAD.Vector(faceShp.BoundBox.XMin, faceShp.BoundBox.YMin, zTgt) - elif obj.CircularCenterAt == 'Custom': - newCent = FreeCAD.Vector(obj.CircularCenterCustom.x, obj.CircularCenterCustom.y, zTgt) - cntr.Base = newCent - - # recalculate cutPasses value, if need be - radialPasses = halfPasses - if obj.CircularCenterAt != 'CenterOfBoundBox': - # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center - EBB = faceShp.BoundBox - CORNERS = [ - FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), - FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), - ] - dMax = 0.0 - for c in range(0, 4): - dist = CORNERS[c].sub(cntr.Base).Length - if dist > dMax: - dMax = dist - lineLen = dMax + (2.0 * self.cutter.getDiameter()) # Line length to span boundbox diag with 2x cutter diameter extra on each end - radialPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover lineLen - - # Update COM point and current CircularCenter - if obj.CircularCenterAt != 'Custom': - obj.CircularCenterCustom = cntr.Base - - minRad = self.cutter.getDiameter() * 0.45 - siX3 = 3 * obj.SampleInterval.Value - minRadSI = (siX3 / 2.0) / math.pi - if minRad < minRadSI: - minRad = minRadSI - - # Make small center circle to start pattern - if obj.StepOver > 50: - circle = Part.makeCircle(minRad, cntr.Base) - GeoSet.append(circle) - - for lc in range(1, radialPasses + 1): - rad = (lc * self.cutOut) - if rad >= minRad: - circle = Part.makeCircle(rad, cntr.Base) - GeoSet.append(circle) - # Efor - COM = cntr.Base - # Eif - - if obj.CutPatternReversed is True: - GeoSet.reverse() - - if faceShp.BoundBox.ZMin != 0.0: - faceShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceShp.BoundBox.ZMin)) - - # Create compound object to bind all lines in Lineset - geomShape = Part.makeCompound(GeoSet) - - # Position and rotate the Line and ZigZag geometry - if obj.CutPattern in ['Line', 'ZigZag']: - if obj.CutPatternAngle != 0.0: - geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), obj.CutPatternAngle) - geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') - F.Shape = geomShape - F.purgeTouched() - self.tempGroup.addObject(F) - - # Identify intersection of cross-section face and lineset - cmnShape = faceShp.common(geomShape) - - if self.showDebugObjects is True: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') - F.Shape = cmnShape - F.purgeTouched() - self.tempGroup.addObject(F) - - self.tmpCOM = FreeCAD.Vector(COM.x, COM.y, faceShp.BoundBox.ZMin) - return cmnShape - def _pathGeomToLinesPointSet(self, obj, compGeoShp): '''_pathGeomToLinesPointSet(obj, compGeoShp)... Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' @@ -2209,14 +2039,59 @@ class ObjectWaterline(PathOp.ObjectOp): return ARCS - def _getExperimentalWaterlinePaths(self, obj, PNTSET, csHght): - '''_getExperimentalWaterlinePaths(obj, PNTSET, csHght)... + def _pathGeomToSpiralPointSet(self, obj, compGeoShp): + '''_pathGeomToSpiralPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directional, connected groupings.''' + PathLog.debug('_pathGeomToSpiralPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + ec = len(compGeoShp.Edges) + start = 2 + + if obj.CutPatternReversed: + edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail + ec -= 1 + start = 1 + else: + edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail + p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0) + p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0) + tup = ((p1.x, p1.y), (p2.x, p2.y)) + inLine.append(tup) + lst = p2 + + for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1 + edg = compGeoShp.Edges[ei] # Get edge for vertexes + sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point) + ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point + tup = ((sp.x, sp.y), (ep.x, ep.y)) + + if sp.sub(p2).Length < 0.000001: + inLine.append(tup) + else: + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset container + inLine.append(tup) + p1 = sp + p2 = ep + # Efor + + lnCnt += 1 + LINES.append(inLine) # Save inLine segments + + return LINES + + def _getExperimentalWaterlinePaths(self, PNTSET, csHght, cutPattern): + '''_getExperimentalWaterlinePaths(PNTSET, csHght, cutPattern)... Switching function for calling the appropriate path-geometry to OCL points conversion function for the various cut patterns.''' PathLog.debug('_getExperimentalWaterlinePaths()') SCANS = list() - if obj.CutPattern == 'Line': + if cutPattern in ['Line', 'Spiral']: stpOvr = list() for D in PNTSET: for SEG in D: @@ -2230,7 +2105,7 @@ class ObjectWaterline(PathOp.ObjectOp): stpOvr.append((P1, P2)) SCANS.append(stpOvr) stpOvr = list() - elif obj.CutPattern == 'ZigZag': + elif cutPattern == 'ZigZag': stpOvr = list() for (dirFlg, LNS) in PNTSET: for SEG in LNS: @@ -2244,7 +2119,7 @@ class ObjectWaterline(PathOp.ObjectOp): stpOvr.append((P1, P2)) SCANS.append(stpOvr) stpOvr = list() - elif obj.CutPattern in ['Circular', 'CircularZigZag']: + elif cutPattern in ['Circular', 'CircularZigZag']: # PNTSET is list, by stepover. # Each stepover is a list containing arc/loop descriptions, (sp, ep, cp) for so in range(0, len(PNTSET)): @@ -2279,19 +2154,19 @@ class ObjectWaterline(PathOp.ObjectOp): return SCANS # Main planar scan functions - def _stepTransitionCmds(self, obj, lstPnt, first, minSTH, tolrnc): + def _stepTransitionCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc): cmds = list() rtpd = False horizGC = 'G0' hSpeed = self.horizRapid height = obj.SafeHeight.Value - if obj.CutPattern in ['Line', 'Circular']: + if cutPattern in ['Line', 'Circular', 'Spiral']: if obj.OptimizeStepOverTransitions is True: height = minSTH + 2.0 # if obj.LayerMode == 'Multi-pass': # rtpd = minSTH - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + elif cutPattern in ['ZigZag', 'CircularZigZag']: if obj.OptimizeStepOverTransitions is True: zChng = first.z - lstPnt.z # PathLog.debug('first.z: {}'.format(first.z)) @@ -2320,17 +2195,17 @@ class ObjectWaterline(PathOp.ObjectOp): return cmds - def _breakCmds(self, obj, lstPnt, first, minSTH, tolrnc): + def _breakCmds(self, obj, cutPattern, lstPnt, first, minSTH, tolrnc): cmds = list() rtpd = False horizGC = 'G0' hSpeed = self.horizRapid height = obj.SafeHeight.Value - if obj.CutPattern in ['Line', 'Circular']: + if cutPattern in ['Line', 'Circular', 'Spiral']: if obj.OptimizeStepOverTransitions is True: height = minSTH + 2.0 - elif obj.CutPattern in ['ZigZag', 'CircularZigZag']: + elif cutPattern in ['ZigZag', 'CircularZigZag']: if obj.OptimizeStepOverTransitions is True: zChng = first.z - lstPnt.z if abs(zChng) < tolrnc: # transitions to same Z height @@ -2835,26 +2710,23 @@ class ObjectWaterline(PathOp.ObjectOp): lastCsHght = csHght # Clear layer as needed - (useOfst, usePat, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer) - ##if self.showDebugObjects is True and (usePat or useOfst): - ## OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'clearPatternArea_{}'.format(round(csHght, 2))) - ## OA.Shape = clearArea - ## OA.purgeTouched() - ## self.tempGroup.addObject(OA) - if usePat: - commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght)) - if useOfst: - commands.extend(self._makeOffsetLayerPaths(JOB, obj, clearArea, csHght)) + (clrLyr, clearLastLayer) = self._clearLayer(obj, ca, lastCA, clearLastLayer) + if clrLyr == 'Offset': + commands.extend(self._makeOffsetLayerPaths(obj, clearArea, csHght)) + elif clrLyr: + cutPattern = obj.CutPattern + if clearLastLayer is False: + cutPattern = obj.ClearLastLayer + commands.extend(self._makeCutPatternLayerPaths(JOB, obj, clearArea, csHght, cutPattern)) # Efor if clearLastLayer: - (useOfst, usePat, cLL) = self._clearLayer(obj, 1, 1, False) - clearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin)) - if usePat: - commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght)) - - if useOfst: - commands.extend(self._makeOffsetLayerPaths(JOB, obj, lastClearArea, lastCsHght)) + (clrLyr, cLL) = self._clearLayer(obj, 1, 1, False) + lastClearArea.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - lastClearArea.BoundBox.ZMin)) + if clrLyr == 'Offset': + commands.extend(self._makeOffsetLayerPaths(obj, lastClearArea, lastCsHght)) + elif clrLyr: + commands.extend(self._makeCutPatternLayerPaths(JOB, obj, lastClearArea, lastCsHght, obj.ClearLastLayer)) PathLog.info("Waterline: All layer scans combined took " + str(time.time() - t_begin) + " s") return commands @@ -2928,7 +2800,7 @@ class ObjectWaterline(PathOp.ObjectOp): commands.append(Path.Command('N (Cut Area {}.)'.format(round(csHght, 2)))) start = 1 - if ofstPlnrShp.BoundBox.ZMin < obj.IgnoreOuterAbove: + if csHght < obj.IgnoreOuterAbove: start = 0 for w in range(start, len(ofstPlnrShp.Wires)): wire = ofstPlnrShp.Wires[w] @@ -2946,90 +2818,97 @@ class ObjectWaterline(PathOp.ObjectOp): return commands - def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght): + def _makeCutPatternLayerPaths(self, JOB, obj, clrAreaShp, csHght, cutPattern): PathLog.debug('_makeCutPatternLayerPaths()') commands = [] clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - clrAreaShp.BoundBox.ZMin)) - pathGeom = self._planarMakePathGeom(obj, clrAreaShp) - pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin)) - # clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) - - if self.showDebugObjects is True: - OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2))) - OA.Shape = pathGeom - OA.purgeTouched() - self.tempGroup.addObject(OA) # Convert pathGeom to gcode more efficiently - if obj.CutPattern == 'Offset': - commands.extend(self._makeOffsetLayerPaths(JOB, obj, clrAreaShp, csHght)) + if cutPattern == 'Offset': + commands.extend(self._makeOffsetLayerPaths(obj, clrAreaShp, csHght)) else: - clrAreaShp.translate(FreeCAD.Vector(0.0, 0.0, csHght - clrAreaShp.BoundBox.ZMin)) - if obj.CutPattern == 'Line': + # Request path geometry from external support class + PGG = PathSurfaceSupport.PathGeometryGenerator(obj, clrAreaShp, cutPattern) + if self.showDebugObjects: + PGG.setDebugObjectsGroup(self.tempGroup) + self.tmpCOM = PGG.getCenterOfMass() + pathGeom = PGG.getPathGeometryGenerator() + if not pathGeom: + PathLog.warning('No path geometry generated.') + return commands + pathGeom.translate(FreeCAD.Vector(0.0, 0.0, csHght - pathGeom.BoundBox.ZMin)) + + if self.showDebugObjects is True: + OA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'pathGeom_{}'.format(round(csHght, 2))) + OA.Shape = pathGeom + OA.purgeTouched() + self.tempGroup.addObject(OA) + + if cutPattern == 'Line': pntSet = self._pathGeomToLinesPointSet(obj, pathGeom) - elif obj.CutPattern == 'ZigZag': + elif cutPattern == 'ZigZag': pntSet = self._pathGeomToZigzagPointSet(obj, pathGeom) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: + elif cutPattern in ['Circular', 'CircularZigZag']: pntSet = self._pathGeomToArcPointSet(obj, pathGeom) - stpOVRS = self._getExperimentalWaterlinePaths(obj, pntSet, csHght) - # PathLog.debug('stpOVRS:\n{}'.format(stpOVRS)) + elif cutPattern == 'Spiral': + pntSet = self._pathGeomToSpiralPointSet(obj, pathGeom) + + stpOVRS = self._getExperimentalWaterlinePaths(pntSet, csHght, cutPattern) safePDC = False - cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, csHght) + cmds = self._clearGeomToPaths(JOB, obj, safePDC, stpOVRS, cutPattern) commands.extend(cmds) return commands - def _makeOffsetLayerPaths(self, JOB, obj, clrAreaShp, csHght): + def _makeOffsetLayerPaths(self, obj, clrAreaShp, csHght): PathLog.debug('_makeOffsetLayerPaths()') - PathLog.warning('Using `Offset` for clearing bottom layer.') cmds = list() - # ofst = obj.BoundaryAdjustment.Value - ofst = 0.0 - self.cutOut # - self.cutter.getDiameter() # (self.radius + (tolrnc / 10.0)) + ofst = 0.0 - self.cutOut shape = clrAreaShp cont = True cnt = 0 while cont: ofstArea = self._extractFaceOffset(shape, ofst, makeComp=True) if not ofstArea: - PathLog.warning('No offset clearing area returned.') + # PathLog.debug('No offset clearing area returned.') break for F in ofstArea.Faces: cmds.extend(self._wiresToWaterlinePath(obj, F, csHght)) shape = ofstArea if cnt == 0: - ofst = 0.0 - self.cutOut # self.cutter.Diameter() + ofst = 0.0 - self.cutOut cnt += 1 return cmds - def _clearGeomToPaths(self, JOB, obj, safePDC, SCANDATA, csHght): + def _clearGeomToPaths(self, JOB, obj, safePDC, stpOVRS, cutPattern): PathLog.debug('_clearGeomToPaths()') GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] tolrnc = JOB.GeometryTolerance.Value - lenSCANDATA = len(SCANDATA) + lenstpOVRS = len(stpOVRS) gDIR = ['G3', 'G2'] if self.CutClimb is True: gDIR = ['G2', 'G3'] # Send cutter to x,y position of first point on first line - first = SCANDATA[0][0][0] # [step][item][point] + first = stpOVRS[0][0][0] # [step][item][point] GCODE.append(Path.Command('G0', {'X': first.x, 'Y': first.y, 'F': self.horizRapid})) # Cycle through step-over sections (line segments or arcs) odd = True lstStpEnd = None - for so in range(0, lenSCANDATA): + for so in range(0, lenstpOVRS): cmds = list() - PRTS = SCANDATA[so] + PRTS = stpOVRS[so] lenPRTS = len(PRTS) first = PRTS[0][0] # first point of arc/line stepover group last = None cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) if so > 0: - if obj.CutPattern == 'CircularZigZag': + if cutPattern == 'CircularZigZag': if odd is True: odd = False else: @@ -3037,7 +2916,7 @@ class ObjectWaterline(PathOp.ObjectOp): # minTrnsHght = self._getMinSafeTravelHeight(safePDC, lstStpEnd, first) # Check safe travel height against fullSTL minTrnsHght = obj.SafeHeight.Value # cmds.append(Path.Command('N (Transition: last, first: {}, {}: minSTH: {})'.format(lstStpEnd, first, minTrnsHght), {})) - cmds.extend(self._stepTransitionCmds(obj, lstStpEnd, first, minTrnsHght, tolrnc)) + cmds.extend(self._stepTransitionCmds(obj, cutPattern, lstStpEnd, first, minTrnsHght, tolrnc)) # Cycle through current step-over parts for i in range(0, lenPRTS): @@ -3048,14 +2927,14 @@ class ObjectWaterline(PathOp.ObjectOp): # minSTH = self._getMinSafeTravelHeight(safePDC, last, nxtStart) # Check safe travel height against fullSTL minSTH = obj.SafeHeight.Value cmds.append(Path.Command('N (Break)', {})) - cmds.extend(self._breakCmds(obj, last, nxtStart, minSTH, tolrnc)) + cmds.extend(self._breakCmds(obj, cutPattern, last, nxtStart, minSTH, tolrnc)) else: cmds.append(Path.Command('N (part {}.)'.format(i + 1), {})) - if obj.CutPattern in ['Line', 'ZigZag']: + if cutPattern in ['Line', 'ZigZag', 'Spiral']: start, last = prt cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed})) cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed})) - elif obj.CutPattern in ['Circular', 'CircularZigZag']: + elif cutPattern in ['Circular', 'CircularZigZag']: start, last, centPnt, cMode = prt gcode = self._makeGcodeArc(start, last, odd, gDIR, tolrnc) cmds.extend(gcode) @@ -3291,22 +3170,27 @@ class ObjectWaterline(PathOp.ObjectOp): PathLog.debug('_clearLayer()') usePat = False useOfst = False + clrLyr = False if obj.ClearLastLayer == 'Off': if obj.CutPattern != 'None': - usePat = True + clrLyr = obj.CutPattern else: - if ca == lastCA: + obj.CutPattern = 'None' + if ca == lastCA: # if current iteration is last layer PathLog.debug('... Clearing bottom layer.') + ''' if obj.ClearLastLayer == 'Offset': - obj.CutPattern = 'None' + # obj.CutPattern = 'None' useOfst = True else: - obj.CutPattern = obj.ClearLastLayer + # obj.CutPattern = obj.ClearLastLayer usePat = True + ''' + clrLyr = obj.ClearLastLayer clearLastLayer = False - return (useOfst, usePat, clearLastLayer) + return (clrLyr, clearLastLayer) # Support methods def resetOpVariables(self, all=True): From 4cd4b2e8794c4fdb4428ee055b07e692d6b4b41f Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 22:48:45 -0500 Subject: [PATCH 071/142] Path: Comment cleanup; adjust messages; set 2 default values --- src/Mod/Path/PathScripts/PathWaterline.py | 64 ++++++----------------- 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index cb74c2a505..f2cf3c9e94 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -298,10 +298,7 @@ class ObjectWaterline(PathOp.ObjectOp): obj.AvoidLastX_InternalFeatures = True obj.CutPatternReversed = False obj.IgnoreOuterAbove = obj.StartDepth.Value + 0.00001 - #obj.StartPoint.x = 0.0 - #obj.StartPoint.y = 0.0 - #obj.StartPoint.z = obj.ClearanceHeight.Value - obj.StartPoint = FreeCAD.Vector(5.0, 5.0, obj.ClearanceHeight.Value) + obj.StartPoint = FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value) obj.Algorithm = 'OCL Dropcutter' obj.ProfileEdges = 'None' obj.LayerMode = 'Single-pass' @@ -318,10 +315,7 @@ class ObjectWaterline(PathOp.ObjectOp): obj.BoundaryAdjustment.Value = 0.0 obj.InternalFeaturesAdjustment.Value = 0.0 obj.AvoidLastX_Faces = 0 - #obj.CircularCenterCustom.x = 0.0 - #obj.CircularCenterCustom.y = 0.0 - #obj.CircularCenterCustom.z = 0.0 - obj.CircularCenterCustom = FreeCAD.Vector(5.0, 5.0, 5.0) + obj.CircularCenterCustom = FreeCAD.Vector(0.0, 0.0, 0.0) obj.GapThreshold.Value = 0.005 obj.LinearDeflection.Value = 0.0001 obj.AngularDeflection.Value = 0.25 @@ -333,6 +327,7 @@ class ObjectWaterline(PathOp.ObjectOp): if job: if job.Stock: d = PathUtils.guessDepths(job.Stock.Shape, None) + obj.IgnoreOuterAbove = job.Stock.Shape.BoundBox.ZMax + 0.000001 PathLog.debug("job.Stock exists") else: PathLog.debug("job.Stock NOT exist") @@ -397,6 +392,7 @@ class ObjectWaterline(PathOp.ObjectOp): self.tempGroup = None self.CutClimb = False self.closedGap = False + self.tmpCOM = None self.gaps = [0.1, 0.2, 0.3] CMDS = list() modelVisibility = list() @@ -2598,9 +2594,6 @@ class ObjectWaterline(PathOp.ObjectOp): Main waterline function to perform waterline extraction from model.''' PathLog.debug('_experimentalWaterlineOp()') - # msg = translate('PathWaterline', 'Experimental Waterline does not currently support selected faces.') - # PathLog.info('\n..... ' + msg) - commands = [] t_begin = time.time() base = JOB.Model.Group[mdlIdx] @@ -2663,29 +2656,29 @@ class ObjectWaterline(PathOp.ObjectOp): if area.Area > 0.0: cont = True caWireCnt = len(area.Wires) - 1 # first wire is boundFace wire - PathLog.debug('cutAreaWireCnt: {}'.format(caWireCnt)) - if self.showDebugObjects is True: + if self.showDebugObjects: CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'cutArea_{}'.format(caCnt)) CA.Shape = area CA.purgeTouched() self.tempGroup.addObject(CA) else: - PathLog.error('Cut area at {} is zero.'.format(round(csHght, 4))) + data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString + PathLog.debug('Cut area at {} is zero.'.format(data)) # get offset wire(s) based upon cross-section cut area if cont: area.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - area.BoundBox.ZMin)) activeArea = area.cut(trimFace) activeAreaWireCnt = len(activeArea.Wires) # first wire is boundFace wire - PathLog.debug('activeAreaWireCnt: {}'.format(activeAreaWireCnt)) - if self.showDebugObjects is True: + if self.showDebugObjects: CA = FreeCAD.ActiveDocument.addObject('Part::Feature', 'activeArea_{}'.format(caCnt)) CA.Shape = activeArea CA.purgeTouched() self.tempGroup.addObject(CA) ofstArea = self._extractFaceOffset(activeArea, ofst, makeComp=False) if not ofstArea: - PathLog.error('No offset area returned for cut area depth: {}'.format(csHght)) + data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString + PathLog.debug('No offset area returned for cut area depth at {}.'.format(data)) cont = False if cont: @@ -2700,7 +2693,8 @@ class ObjectWaterline(PathOp.ObjectOp): self.tempGroup.addObject(CA) else: cont = False - PathLog.error('ofstSolids is False.') + data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString + PathLog.error('Could not determine solid faces at {}.'.format(data)) if cont: # Make waterline path for current CUTAREA depth (csHght) @@ -2744,21 +2738,19 @@ class ObjectWaterline(PathOp.ObjectOp): # Cycle through layer depths for dp in range(0, lenDP): csHght = depthparams[dp] - PathLog.debug('Depth {} is {}'.format(dp + 1, csHght)) + # PathLog.debug('Depth {} is {}'.format(dp + 1, csHght)) # Get slice at depth of shape csFaces = self._getModelCrossSection(shape, csHght) # returned at Z=0.0 if not csFaces: - PathLog.error('No cross-section wires at {}'.format(csHght)) + data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString else: - PathLog.debug('cross-section face count {}'.format(len(csFaces))) if len(csFaces) > 0: useFaces = self._getSolidAreasFromPlanarFaces(csFaces) else: useFaces = False if useFaces: - PathLog.debug('useFacesCnt: {}'.format(len(useFaces))) compAdjFaces = Part.makeCompound(useFaces) if self.showDebugObjects is True: @@ -2871,7 +2863,6 @@ class ObjectWaterline(PathOp.ObjectOp): while cont: ofstArea = self._extractFaceOffset(shape, ofst, makeComp=True) if not ofstArea: - # PathLog.debug('No offset clearing area returned.') break for F in ofstArea.Faces: cmds.extend(self._wiresToWaterlinePath(obj, F, csHght)) @@ -2974,14 +2965,9 @@ class ObjectWaterline(PathOp.ObjectOp): li = fIds.pop() low = csFaces[li] # senior face pIds = self._idInternalFeature(csFaces, fIds, pIds, li, low) - # Ewhile - ##PathLog.info('fIds: {}'.format(fIds)) - ##PathLog.info('pIds: {}'.format(pIds)) for af in range(lenCsF - 1, -1, -1): # cycle from last item toward first - ##PathLog.info('af: {}'.format(af)) prnt = pIds[af] - ##PathLog.info('prnt: {}'.format(prnt)) if prnt == -1: stack = -1 else: @@ -2994,36 +2980,27 @@ class ObjectWaterline(PathOp.ObjectOp): stack.insert(0, nxtPrnt) nxtPrnt = pIds[nxtPrnt] cIds[af] = stack - # PathLog.debug('cIds: {}\n'.format(cIds)) for af in range(0, lenCsF): - # PathLog.debug('af is {}'.format(af)) pFc = cIds[af] if pFc == -1: # Simple, independent region holds[af] = csFaces[af] # place face in hold - # PathLog.debug('pFc == -1') else: # Compound region - # PathLog.debug('pFc is not -1') cnt = len(pFc) if cnt % 2.0 == 0.0: # even is donut cut - # PathLog.debug('cnt is even') inr = pFc[cnt - 1] otr = pFc[cnt - 2] - # PathLog.debug('inr / otr: {} / {}'.format(inr, otr)) holds[otr] = holds[otr].cut(csFaces[inr]) else: # odd is floating solid - # PathLog.debug('cnt is ODD') holds[af] = csFaces[af] - # Efor for af in range(0, lenCsF): if holds[af]: useFaces.append(holds[af]) # save independent solid - # Eif if len(useFaces) > 0: @@ -3079,7 +3056,7 @@ class ObjectWaterline(PathOp.ObjectOp): if cmn.Area > 0.0: pIds[li] = hi break - # Ewhile + return pIds def _wireToPath(self, obj, wire, startVect): @@ -3102,7 +3079,6 @@ class ObjectWaterline(PathOp.ObjectOp): (pp, end_vector) = Path.fromShapes(**pathParams) paths.extend(pp.Commands) - # PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) self.endVector = end_vector # pylint: disable=attribute-defined-outside-init @@ -3168,8 +3144,6 @@ class ObjectWaterline(PathOp.ObjectOp): def _clearLayer(self, obj, ca, lastCA, clearLastLayer): PathLog.debug('_clearLayer()') - usePat = False - useOfst = False clrLyr = False if obj.ClearLastLayer == 'Off': @@ -3179,14 +3153,6 @@ class ObjectWaterline(PathOp.ObjectOp): obj.CutPattern = 'None' if ca == lastCA: # if current iteration is last layer PathLog.debug('... Clearing bottom layer.') - ''' - if obj.ClearLastLayer == 'Offset': - # obj.CutPattern = 'None' - useOfst = True - else: - # obj.CutPattern = obj.ClearLastLayer - usePat = True - ''' clrLyr = obj.ClearLastLayer clearLastLayer = False From 28abb95ea5f3211d057fde3abfe8fdce132189fc Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 14 Apr 2020 22:39:08 -0500 Subject: [PATCH 072/142] Path: Add new support module for 3D Surface and Waterline --- src/Mod/Path/CMakeLists.txt | 1 + .../Path/PathScripts/PathSurfaceSupport.py | 441 ++++++++++++++++++ 2 files changed, 442 insertions(+) create mode 100644 src/Mod/Path/PathScripts/PathSurfaceSupport.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index ded7c91a93..78415e1e32 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -107,6 +107,7 @@ SET(PathScripts_SRCS PathScripts/PathStop.py PathScripts/PathSurface.py PathScripts/PathSurfaceGui.py + PathScripts/PathSurfaceSupport.py PathScripts/PathToolBit.py PathScripts/PathToolBitCmd.py PathScripts/PathToolBitEdit.py diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py new file mode 100644 index 0000000000..68abd2abc5 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py @@ -0,0 +1,441 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2020 russ4262 * +# * * +# * 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. * +# * * +# * This program 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 this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +from __future__ import print_function + +__title__ = "Path Surface Support Module" +__author__ = "russ4262 (Russell Johnson)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Support functions and classes for 3D Surface and Waterline operations." +__contributors__ = "" + +import FreeCAD +from PySide import QtCore +import Path +import PathScripts.PathLog as PathLog +import PathScripts.PathUtils as PathUtils +import math +import Part + + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule(PathLog.thisModule()) + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class PathGeometryGenerator: + '''Creates a path geometry shape from an assigned pattern for conversion to tool paths. + PathGeometryGenerator(obj, shape, pattern) + `obj` is the operation object, `shape` is the horizontal planar shape object, + and `pattern` is the name of the geometric pattern to apply. + Frist, call the getCenterOfMass() method for the CenterOfMass for patterns allowing a custom center. + Next, call the getPathGeometryGenerator() method to request the path geometry shape.''' + + # Register valid patterns here by name + # Create a corresponding processing method below. Precede the name with an underscore(_) + patterns = ('Circular', 'CircularZigZag', 'Line', 'Offset', 'Spiral', 'ZigZag') + + def __init__(self, obj, shape, pattern): + '''__init__(obj, shape, pattern)... Instantiate PathGeometryGenerator class. + Required arguments are the operation object, horizontal planar shape, and pattern name.''' + self.debugObjectsGroup = False + self.pattern = None + self.shape = None + self.pathGeometry = None + self.rawGeoList = None + self.centerOfMass = None + self.deltaX = None + self.deltaY = None + self.deltaC = None + self.halfDiag = None + self.halfPasses = None + self.obj = obj + self.toolDiam = float(obj.ToolController.Tool.Diameter) + self.cutOut = self.toolDiam * (float(obj.StepOver) / 100.0) + self.wpc = Part.makeCircle(2.0) # make circle for workplane + + # validate requested pattern + if pattern in self.patterns: + if hasattr(self, '_' + pattern): + self.pattern = pattern + + if shape.BoundBox.ZMin != 0.0: + shape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - shape.BoundBox.ZMin)) + if shape.BoundBox.ZMax == 0.0: + self.shape = shape + else: + PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax)) + + self._prepareConstants() + + def _prepareConstants(self): + # Apply drop cutter extra offset and set the max and min XY area of the operation + xmin = self.shape.BoundBox.XMin + xmax = self.shape.BoundBox.XMax + ymin = self.shape.BoundBox.YMin + ymax = self.shape.BoundBox.YMax + + # Compute weighted center of mass of all faces combined + fCnt = 0 + totArea = 0.0 + zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) + for F in self.shape.Faces: + comF = F.CenterOfMass + areaF = F.Area + totArea += areaF + fCnt += 1 + zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) + if fCnt == 0: + PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) + zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) + else: + avgArea = totArea / fCnt + zeroCOM.multiply(1 / fCnt) + zeroCOM.multiply(1 / avgArea) + self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + + # get X, Y, Z spans; Compute center of rotation + self.deltaX = self.shape.BoundBox.XLength + self.deltaY = self.shape.BoundBox.YLength + self.deltaC = self.shape.BoundBox.DiagonalLength # math.sqrt(self.deltaX**2 + self.deltaY**2) + lineLen = self.deltaC + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end + self.halfDiag = math.ceil(lineLen / 2.0) + cutPasses = math.ceil(lineLen / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal + self.halfPasses = math.ceil(cutPasses / 2.0) + + # Public methods + def setDebugObjectsGroup(self, tmpGrpObject): + '''setDebugObjectsGroup(tmpGrpObject)... + Pass the temporary object group to show temporary construction objects''' + self.debugObjectsGroup = tmpGrpObject + + def getCenterOfMass(self): + '''getCenterOfMass()... + Returns the Center Of Mass for the current class instance.''' + return self.centerOfMass + + def getPathGeometryGenerator(self): + '''getPathGeometryGenerator()... + Call this function to obtain the path geometry shape, generated by this class.''' + if self.pattern is None: + PathLog.warning('PGG: No pattern set.') + return False + + if self.shape is None: + PathLog.warning('PGG: No shape set.') + return False + + cmd = 'self._' + self.pattern + '()' + exec(cmd) + + if self.obj.CutPatternReversed is True: + self.rawGeoList.reverse() + + # Create compound object to bind all lines in Lineset + geomShape = Part.makeCompound(self.rawGeoList) + + # Position and rotate the Line and ZigZag geometry + if self.pattern in ['Line', 'ZigZag']: + if self.obj.CutPatternAngle != 0.0: + geomShape.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), self.obj.CutPatternAngle) + bbC = self.shape.BoundBox.Center + geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) + + if self.debugObjectsGroup: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') + F.Shape = geomShape + F.purgeTouched() + self.debugObjectsGroup.addObject(F) + + if self.pattern == 'Offset': + return geomShape + + # Identify intersection of cross-section face and lineset + cmnShape = self.shape.common(geomShape) + + if self.debugObjectsGroup: + F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') + F.Shape = cmnShape + F.purgeTouched() + self.debugObjectsGroup.addObject(F) + + self.tmpCOM = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0) + return cmnShape + + # Cut pattern methods + def _Circular(self): + GeoSet = list() + zTgt = 0.0 # self.shape.BoundBox.ZMin + centerAt = self.obj.CircularCenterAt + cntr = FreeCAD.Placement() + + if centerAt == 'CenterOfMass': + cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, zTgt) # self.centerOfMass # Use center of Mass + elif centerAt == 'CenterOfBoundBox': + cent = self.shape.BoundBox.Center + cntrPnt = FreeCAD.Vector(cent.x, cent.y, zTgt) + elif centerAt == 'XminYmin': + cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, zTgt) + elif centerAt == 'Custom': + newCent = FreeCAD.Vector(self.obj.CircularCenterCustom.x, self.obj.CircularCenterCustom.y, zTgt) + cntrPnt = newCent + + # recalculate number of passes, if need be + radialPasses = self.halfPasses + if centerAt != 'CenterOfBoundBox': + # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center + EBB = self.shape.BoundBox + CORNERS = [ + FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), + FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), + ] + dMax = 0.0 + for c in range(0, 4): + dist = CORNERS[c].sub(cntrPnt).Length + if dist > dMax: + dMax = dist + diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end + radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal + + # Update self.centerOfMass point and current CircularCenter + if centerAt != 'Custom': + self.obj.CircularCenterCustom = cntrPnt + + minRad = self.toolDiam * 0.45 + siX3 = 3 * self.obj.SampleInterval.Value + minRadSI = (siX3 / 2.0) / math.pi + if minRad < minRadSI: + minRad = minRadSI + + # Make small center circle to start pattern + if self.obj.StepOver > 50: + circle = Part.makeCircle(minRad, cntrPnt) + GeoSet.append(circle) + + for lc in range(1, radialPasses + 1): + rad = (lc * self.cutOut) + if rad >= minRad: + circle = Part.makeCircle(rad, cntrPnt) + GeoSet.append(circle) + # Efor + self.centerOfMass = cntrPnt + self.rawGeoList = GeoSet + + def _CircularZigZag(self): + self._Circular() # Use _Circular generator + + def _Line(self): + GeoSet = list() + centRot = FreeCAD.Vector(0.0, 0.0, 0.0) # Bottom left corner of face/selection/model + cAng = math.atan(self.deltaX / self.deltaY) # BoundaryBox angle + + # Determine end points and create top lines + x1 = centRot.x - self.halfDiag + x2 = centRot.x + self.halfDiag + diag = None + if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180: + diag = self.deltaY + elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270: + diag = self.deltaX + else: + perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC + diag = perpDist + y1 = centRot.y + diag + # y2 = y1 + + # Create end points for set of lines to intersect with cross-section face + pntTuples = list() + for lc in range((-1 * (self.halfPasses - 1)), self.halfPasses + 1): + x1 = centRot.x - self.halfDiag + x2 = centRot.x + self.halfDiag + y1 = centRot.y + (lc * self.cutOut) + # y2 = y1 + p1 = FreeCAD.Vector(x1, y1, 0.0) + p2 = FreeCAD.Vector(x2, y1, 0.0) + pntTuples.append( (p1, p2) ) + + # Convert end points to lines + for (p1, p2) in pntTuples: + line = Part.makeLine(p1, p2) + GeoSet.append(line) + + self.rawGeoList = GeoSet + + def _Offset(self): + self.rawGeoList = self._extractOffsetFaces() + + def _Spiral(self): + GeoSet = list() + SEGS = list() + draw = True + loopRadians = 0.0 # Used to keep track of complete loops/cycles + sumRadians = 0.0 + loopCnt = 0 + segCnt = 0 + twoPi = 2.0 * math.pi + maxDist = self.halfDiag + move = self.centerOfMass # FreeCAD.Vector(0.0, 0.0, 0.0) # Use to translate the center of the spiral + lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0) + + # Set tool properties and calculate cutout + cutOut = self.cutOut / twoPi + segLen = self.obj.SampleInterval.Value # CutterDiameter / 10.0 # SampleInterval.Value + stepAng = segLen / ((loopCnt + 1) * self.cutOut) # math.pi / 18.0 # 10 degrees + stopRadians = maxDist / cutOut + + if self.obj.CutPatternReversed: + if self.obj.CutMode == 'Conventional': + getPoint = self._makeOppSpiralPnt + else: + getPoint = self._makeRegSpiralPnt + + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p2, p1) + SEGS.append(lineSeg) + # Ewhile + SEGS.reverse() + else: + if self.obj.CutMode == 'Climb': + getPoint = self._makeOppSpiralPnt + else: + getPoint = self._makeRegSpiralPnt + + while draw: + radAng = sumRadians + stepAng + p1 = lastPoint + p2 = getPoint(move, cutOut, radAng) # cutOut is 'b' in the equation r = b * radAng + sumRadians += stepAng # Increment sumRadians + loopRadians += stepAng # Increment loopRadians + if loopRadians > twoPi: + loopCnt += 1 + loopRadians -= twoPi + stepAng = segLen / ((loopCnt + 1) * self.cutOut) # adjust stepAng with each loop/cycle + segCnt += 1 + lastPoint = p2 + if sumRadians > stopRadians: + draw = False + # Create line and show in Object tree + lineSeg = Part.makeLine(p1, p2) + SEGS.append(lineSeg) + # Ewhile + # Eif + spiral = Part.Wire([ls.Edges[0] for ls in SEGS]) + GeoSet.append(spiral) + + self.rawGeoList = GeoSet + + def _ZigZag(self): + self._Line() # Use _Line generator + + # Support methods + def _makeRegSpiralPnt(self, move, b, radAng): + x = b * radAng * math.cos(radAng) + y = b * radAng * math.sin(radAng) + return FreeCAD.Vector(x, y, 0.0).add(move) + + def _makeOppSpiralPnt(self, move, b, radAng): + x = b * radAng * math.cos(radAng) + y = b * radAng * math.sin(radAng) + return FreeCAD.Vector(-1 * x, y, 0.0).add(move) + + def _extractOffsetFaces(self): + PathLog.debug('_extractOffsetFaces()') + wires = list() + faces = list() + ofst = 0.0 # - self.cutOut + shape = self.shape + cont = True + cnt = 0 + while cont: + ofstArea = self._getFaceOffset(shape, ofst) + if not ofstArea: + PathLog.warning('PGG: No offset clearing area returned.') + cont = False + break + for F in ofstArea.Faces: + faces.append(F) + for w in F.Wires: + wires.append(w) + shape = ofstArea + if cnt == 0: + ofst = 0.0 - self.cutOut + cnt += 1 + return wires + + def _getFaceOffset(self, shape, offset): + '''_getFaceOffset(shape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('_getFaceOffset()') + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 # 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(shape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 + area.add(shape) + area.setParams(**areaParams) # set parameters + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + ofstFace = Part.makeCompound(W) + + return ofstFace +# Eclass From dc8befa478ab49d68de14ff9d149bcf90b7601dc Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 16 Apr 2020 00:35:05 -0500 Subject: [PATCH 073/142] Path: Move more common methods to PathSurfaceSupport module --- .../Path/PathScripts/PathSurfaceSupport.py | 1560 ++++++++++++++++- src/Mod/Path/PathScripts/PathWaterline.py | 1508 ++-------------- 2 files changed, 1585 insertions(+), 1483 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py index 68abd2abc5..e991b28163 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceSupport.py +++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py @@ -28,6 +28,7 @@ __title__ = "Path Surface Support Module" __author__ = "russ4262 (Russell Johnson)" __url__ = "http://www.freecadweb.org" __doc__ = "Support functions and classes for 3D Surface and Waterline operations." +# __name__ = "PathSurfaceSupport" __contributors__ = "" import FreeCAD @@ -53,8 +54,8 @@ class PathGeometryGenerator: PathGeometryGenerator(obj, shape, pattern) `obj` is the operation object, `shape` is the horizontal planar shape object, and `pattern` is the name of the geometric pattern to apply. - Frist, call the getCenterOfMass() method for the CenterOfMass for patterns allowing a custom center. - Next, call the getPathGeometryGenerator() method to request the path geometry shape.''' + Frist, call the getCenterOfPattern() method for the CenterOfMass for patterns allowing a custom center. + Next, call the generatePathGeometry() method to request the path geometry shape.''' # Register valid patterns here by name # Create a corresponding processing method below. Precede the name with an underscore(_) @@ -64,11 +65,12 @@ class PathGeometryGenerator: '''__init__(obj, shape, pattern)... Instantiate PathGeometryGenerator class. Required arguments are the operation object, horizontal planar shape, and pattern name.''' self.debugObjectsGroup = False - self.pattern = None + self.pattern = 'None' self.shape = None self.pathGeometry = None self.rawGeoList = None self.centerOfMass = None + self.centerofPattern = None self.deltaX = None self.deltaY = None self.deltaC = None @@ -86,7 +88,7 @@ class PathGeometryGenerator: if shape.BoundBox.ZMin != 0.0: shape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - shape.BoundBox.ZMin)) - if shape.BoundBox.ZMax == 0.0: + if shape.BoundBox.ZLength == 0.0: self.shape = shape else: PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax)) @@ -101,23 +103,30 @@ class PathGeometryGenerator: ymax = self.shape.BoundBox.YMax # Compute weighted center of mass of all faces combined - fCnt = 0 - totArea = 0.0 - zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) - for F in self.shape.Faces: - comF = F.CenterOfMass - areaF = F.Area - totArea += areaF - fCnt += 1 - zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) - if fCnt == 0: - PathLog.error(translate('PathSurface', 'Cannot calculate the Center Of Mass. Using Center of Boundbox.')) - zeroCOM = FreeCAD.Vector((xmin + xmax) / 2.0, (ymin + ymax) / 2.0, 0.0) + if self.pattern in ['Circular', 'CircularZigZag', 'Spiral']: + if self.obj.PatternCenterAt == 'CenterOfMass': + fCnt = 0 + totArea = 0.0 + zeroCOM = FreeCAD.Vector(0.0, 0.0, 0.0) + for F in self.shape.Faces: + comF = F.CenterOfMass + areaF = F.Area + totArea += areaF + fCnt += 1 + zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) + if fCnt == 0: + PathLog.error(translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.')) + bbC = self.shape.BoundBox.Center + zeroCOM = FreeCAD.Vector(bbC.x, bbC.y, 0.0) + else: + avgArea = totArea / fCnt + zeroCOM.multiply(1 / fCnt) + zeroCOM.multiply(1 / avgArea) + self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + self.centerOfPattern = self._getPatternCenter() else: - avgArea = totArea / fCnt - zeroCOM.multiply(1 / fCnt) - zeroCOM.multiply(1 / avgArea) - self.centerOfMass = FreeCAD.Vector(zeroCOM.x, zeroCOM.y, 0.0) + bbC = self.shape.BoundBox.Center + self.centerOfPattern = FreeCAD.Vector(bbC.x, bbC.y, 0.0) # get X, Y, Z spans; Compute center of rotation self.deltaX = self.shape.BoundBox.XLength @@ -134,15 +143,15 @@ class PathGeometryGenerator: Pass the temporary object group to show temporary construction objects''' self.debugObjectsGroup = tmpGrpObject - def getCenterOfMass(self): - '''getCenterOfMass()... + def getCenterOfPattern(self): + '''getCenterOfPattern()... Returns the Center Of Mass for the current class instance.''' - return self.centerOfMass + return self.centerOfPattern - def getPathGeometryGenerator(self): - '''getPathGeometryGenerator()... + def generatePathGeometry(self): + '''generatePathGeometry()... Call this function to obtain the path geometry shape, generated by this class.''' - if self.pattern is None: + if self.pattern == 'None': PathLog.warning('PGG: No pattern set.') return False @@ -167,7 +176,7 @@ class PathGeometryGenerator: geomShape.Placement.Base = FreeCAD.Vector(bbC.x, bbC.y, 0.0 - geomShape.BoundBox.ZMin) if self.debugObjectsGroup: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpGeometrySet') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpGeometrySet') F.Shape = geomShape F.purgeTouched() self.debugObjectsGroup.addObject(F) @@ -179,73 +188,36 @@ class PathGeometryGenerator: cmnShape = self.shape.common(geomShape) if self.debugObjectsGroup: - F = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpPathGeometry') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpPathGeometry') F.Shape = cmnShape F.purgeTouched() self.debugObjectsGroup.addObject(F) - self.tmpCOM = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0) return cmnShape # Cut pattern methods def _Circular(self): GeoSet = list() - zTgt = 0.0 # self.shape.BoundBox.ZMin - centerAt = self.obj.CircularCenterAt - cntr = FreeCAD.Placement() - - if centerAt == 'CenterOfMass': - cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, zTgt) # self.centerOfMass # Use center of Mass - elif centerAt == 'CenterOfBoundBox': - cent = self.shape.BoundBox.Center - cntrPnt = FreeCAD.Vector(cent.x, cent.y, zTgt) - elif centerAt == 'XminYmin': - cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, zTgt) - elif centerAt == 'Custom': - newCent = FreeCAD.Vector(self.obj.CircularCenterCustom.x, self.obj.CircularCenterCustom.y, zTgt) - cntrPnt = newCent - - # recalculate number of passes, if need be - radialPasses = self.halfPasses - if centerAt != 'CenterOfBoundBox': - # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center - EBB = self.shape.BoundBox - CORNERS = [ - FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), - FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), - FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), - ] - dMax = 0.0 - for c in range(0, 4): - dist = CORNERS[c].sub(cntrPnt).Length - if dist > dMax: - dMax = dist - diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end - radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal - - # Update self.centerOfMass point and current CircularCenter - if centerAt != 'Custom': - self.obj.CircularCenterCustom = cntrPnt - + radialPasses = self._getRadialPasses() minRad = self.toolDiam * 0.45 siX3 = 3 * self.obj.SampleInterval.Value minRadSI = (siX3 / 2.0) / math.pi + if minRad < minRadSI: minRad = minRadSI + PathLog.debug(' -centerOfPattern: {}'.format(self.centerOfPattern)) # Make small center circle to start pattern if self.obj.StepOver > 50: - circle = Part.makeCircle(minRad, cntrPnt) + circle = Part.makeCircle(minRad, self.centerOfPattern) GeoSet.append(circle) for lc in range(1, radialPasses + 1): rad = (lc * self.cutOut) if rad >= minRad: - circle = Part.makeCircle(rad, cntrPnt) + circle = Part.makeCircle(rad, self.centerOfPattern) GeoSet.append(circle) # Efor - self.centerOfMass = cntrPnt self.rawGeoList = GeoSet def _CircularZigZag(self): @@ -279,7 +251,7 @@ class PathGeometryGenerator: # y2 = y1 p1 = FreeCAD.Vector(x1, y1, 0.0) p2 = FreeCAD.Vector(x2, y1, 0.0) - pntTuples.append( (p1, p2) ) + pntTuples.append((p1, p2)) # Convert end points to lines for (p1, p2) in pntTuples: @@ -300,8 +272,8 @@ class PathGeometryGenerator: loopCnt = 0 segCnt = 0 twoPi = 2.0 * math.pi - maxDist = self.halfDiag - move = self.centerOfMass # FreeCAD.Vector(0.0, 0.0, 0.0) # Use to translate the center of the spiral + maxDist = math.ceil(self.cutOut * self._getRadialPasses()) # self.halfDiag + move = self.centerOfPattern # Use to translate the center of the spiral lastPoint = FreeCAD.Vector(0.0, 0.0, 0.0) # Set tool properties and calculate cutout @@ -369,6 +341,48 @@ class PathGeometryGenerator: self._Line() # Use _Line generator # Support methods + def _getPatternCenter(self): + centerAt = self.obj.PatternCenterAt + + if centerAt == 'CenterOfMass': + cntrPnt = FreeCAD.Vector(self.centerOfMass.x, self.centerOfMass.y, 0.0) + elif centerAt == 'CenterOfBoundBox': + cent = self.shape.BoundBox.Center + cntrPnt = FreeCAD.Vector(cent.x, cent.y, 0.0) + elif centerAt == 'XminYmin': + cntrPnt = FreeCAD.Vector(self.shape.BoundBox.XMin, self.shape.BoundBox.YMin, 0.0) + elif centerAt == 'Custom': + cntrPnt = FreeCAD.Vector(self.obj.PatternCenterCustom.x, self.obj.PatternCenterCustom.y, 0.0) + + # Update centerOfPattern point + if centerAt != 'Custom': + self.obj.PatternCenterCustom = cntrPnt + self.centerOfPattern = cntrPnt + + return cntrPnt + + def _getRadialPasses(self): + # recalculate number of passes, if need be + radialPasses = self.halfPasses + if self.obj.PatternCenterAt != 'CenterOfBoundBox': + # make 4 corners of boundbox in XY plane, find which is greatest distance to new circular center + EBB = self.shape.BoundBox + CORNERS = [ + FreeCAD.Vector(EBB.XMin, EBB.YMin, 0.0), + FreeCAD.Vector(EBB.XMin, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMax, 0.0), + FreeCAD.Vector(EBB.XMax, EBB.YMin, 0.0), + ] + dMax = 0.0 + for c in range(0, 4): + dist = CORNERS[c].sub(self.centerOfPattern).Length + if dist > dMax: + dMax = dist + diag = dMax + (2.0 * self.toolDiam) # Line length to span boundbox diag with 2x cutter diameter extra on each end + radialPasses = math.ceil(diag / self.cutOut) + 1 # Number of lines(passes) required to cover boundbox diagonal + + return radialPasses + def _makeRegSpiralPnt(self, move, b, radAng): x = b * radAng * math.cos(radAng) y = b * radAng * math.sin(radAng) @@ -439,3 +453,1403 @@ class PathGeometryGenerator: return ofstFace # Eclass + + +class ProcessSelectedFaces: + """ProcessSelectedFaces(JOB, obj) class. + This class processes the `obj.Base` object for selected geometery. + Calling the preProcessModel(module) method returns + two compound objects as a tuple: (FACES, VOIDS) or False.""" + + def __init__(self, JOB, obj): + self.modelSTLs = list() + self.profileShapes = list() + self.tempGroup = False + self.showDebugObjects = False + self.checkBase = False + self.module = None + self.radius = None + self.depthParams = None + self.msgNoFaces = translate(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.') + self.JOB = JOB + self.obj = obj + self.profileEdges = 'None' + + if hasattr(obj, 'ProfileEdges'): + self.profileEdges = obj.ProfileEdges + + # Setup STL, model type, and bound box containers for each model in Job + for m in range(0, len(JOB.Model.Group)): + M = JOB.Model.Group[m] + self.modelSTLs.append(False) + self.profileShapes.append(False) + + # make circle for workplane + self.wpc = Part.makeCircle(2.0) + + def PathSurface(self): + if self.obj.Base: + if len(self.obj.Base) > 0: + self.checkBase = True + if self.obj.ScanType == 'Rotational': + self.checkBase = False + PathLog.warning(self.msgNoFaces) + + def PathWaterline(self): + if self.obj.Base: + if len(self.obj.Base) > 0: + self.checkBase = True + if self.obj.Algorithm in ['OCL Dropcutter', 'Experimental']: + self.checkBase = False + PathLog.warning(self.msgNoFaces) + + # public class methods + def setShowDebugObjects(self, grpObj, val): + self.tempGroup = grpObj + self.showDebugObjects = val + + def preProcessModel(self, module): + PathLog.debug('preProcessModel()') + + if not self._isReady(module): + return False + + FACES = list() + VOIDS = list() + fShapes = list() + vShapes = list() + GRP = self.JOB.Model.Group + lenGRP = len(GRP) + + # Crete place holders for each base model in Job + for m in range(0, lenGRP): + FACES.append(False) + VOIDS.append(False) + fShapes.append(False) + vShapes.append(False) + + # The user has selected subobjects from the base. Pre-Process each. + if self.checkBase: + PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') + + # (FACES, VOIDS) = self._identifyFacesAndVoids(FACES, VOIDS) + (F, V) = self._identifyFacesAndVoids(FACES, VOIDS) + + # Cycle through each base model, processing faces for each + for m in range(0, lenGRP): + base = GRP[m] + (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, m, FACES, VOIDS) + fShapes[m] = mFS + vShapes[m] = mVS + self.profileShapes[m] = mPS + else: + PathLog.debug(' -No obj.Base data.') + for m in range(0, lenGRP): + self.modelSTLs[m] = True + + # Process each model base, as a whole, as needed + # PathLog.debug(' -Pre-processing all models in Job.') + for m in range(0, lenGRP): + if fShapes[m] is False: + PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) + if self.obj.BoundBox == 'BaseBoundBox': + base = GRP[m] + elif self.obj.BoundBox == 'Stock': + base = self.JOB.Stock + + pPEB = self._preProcessEntireBase(base, m) + if pPEB is False: + PathLog.error(' -Failed to pre-process base as a whole.') + else: + (fcShp, prflShp) = pPEB + if fcShp is not False: + if fcShp is True: + PathLog.debug(' -fcShp is True.') + fShapes[m] = True + else: + fShapes[m] = [fcShp] + if prflShp is not False: + if fcShp is not False: + PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) + if vShapes[m] is not False: + PathLog.debug(' -Cutting void from base profile shape.') + adjPS = prflShp.cut(vShapes[m][0]) + self.profileShapes[m] = [adjPS] + else: + PathLog.debug(' -vShapes[m] is False.') + self.profileShapes[m] = [prflShp] + else: + PathLog.debug(' -Saving base profile shape.') + self.profileShapes[m] = [prflShp] + PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) + # Efor + + return (fShapes, vShapes) + + # private class methods + def _isReady(self, module): + '''_isReady(module)... Internal method. + Checks if required attributes are available for processing obj.Base (the Base Geometry).''' + if hasattr(self, module): + self.module = module + modMethod = getattr(self, module) # gets the attribute only + modMethod() # executes as method + else: + return False + + if not self.radius: + return False + + if not self.depthParams: + return False + + return True + + def _identifyFacesAndVoids(self, F, V): + TUPS = list() + GRP = self.JOB.Model.Group + lenGRP = len(GRP) + + # Separate selected faces into (base, face) tuples and flag model(s) for STL creation + for (bs, SBS) in self.obj.Base: + for sb in SBS: + # Flag model for STL creation + mdlIdx = None + for m in range(0, lenGRP): + if bs is GRP[m]: + self.modelSTLs[m] = True + mdlIdx = m + break + TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) + + # Apply `AvoidXFaces` value + faceCnt = len(TUPS) + add = faceCnt - self.obj.AvoidLastX_Faces + for bst in range(0, faceCnt): + (m, base, sub) = TUPS[bst] + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + faceIdx = int(sub[4:]) - 1 + if bst < add: + if F[m] is False: + F[m] = list() + F[m].append((shape, faceIdx)) + else: + if V[m] is False: + V[m] = list() + V[m].append((shape, faceIdx)) + return (F, V) + + def _preProcessFacesAndVoids(self, base, m, FACES, VOIDS): + mFS = False + mVS = False + mPS = False + mIFS = list() + + if FACES[m] is not False: + isHole = False + if self.obj.HandleMultipleFeatures == 'Collectively': + cont = True + fsL = list() # face shape list + ifL = list() # avoid shape list + outFCS = list() + + # Get collective envelope slice of selected faces + for (fcshp, fcIdx) in FACES[m]: + fNum = fcIdx + 1 + fsL.append(fcshp) + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if self.obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + PathLog.debug('Attempting to get cross-section of collective faces.') + if len(outFCS) == 0: + PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) + cont = False + else: + cfsL = Part.makeCompound(outFCS) + + # Handle profile edges request + if cont is True and self.profileEdges != 'None': + ofstVal = self._calculateOffsetValue(isHole) + psOfst = extractFaceOffset(cfsL, ofstVal, self.wpc) + if psOfst is not False: + mPS = [psOfst] + if self.profileEdges == 'Only': + mFS = True + cont = False + else: + PathLog.error(' -Failed to create profile geometry for selected faces.') + cont = False + + if cont: + if self.showDebugObjects: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') + T.Shape = cfsL + T.purgeTouched() + self.tempGroup.addObject(T) + + ofstVal = self._calculateOffsetValue(isHole) + faceOfstShp = extractFaceOffset(cfsL, ofstVal, self.wpc) + if faceOfstShp is False: + PathLog.error(' -Failed to create offset face.') + cont = False + + if cont: + lenIfL = len(ifL) + if self.obj.InternalFeaturesCut is False: + if lenIfL == 0: + PathLog.debug(' -No internal features saved.') + else: + if lenIfL == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + if self.showDebugObjects: + C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') + C.Shape = casL + C.purgeTouched() + self.tempGroup.addObject(C) + ofstVal = self._calculateOffsetValue(isHole=True) + intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + mFS = [faceOfstShp] + # Eif + + elif self.obj.HandleMultipleFeatures == 'Individually': + for (fcshp, fcIdx) in FACES[m]: + cont = True + ifL = list() # avoid shape list + fNum = fcIdx + 1 + outerFace = False + + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from Face{}'.format(fNum)) + cont = False + elif gFW[0] is False: + PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) + cont = False + outerFace = False + else: + ((otrFace, raised), intWires) = gFW + outerFace = otrFace + if self.obj.InternalFeaturesCut is False: + if intWires is not False: + for (iFace, rsd) in intWires: + ifL.append(iFace) + + if outerFace is not False: + PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) + + if self.profileEdges != 'None': + ofstVal = self._calculateOffsetValue(isHole) + psOfst = extractFaceOffset(outerFace, ofstVal, self.wpc) + if psOfst is not False: + if mPS is False: + mPS = list() + mPS.append(psOfst) + if self.profileEdges == 'Only': + if mFS is False: + mFS = list() + mFS.append(True) + cont = False + else: + PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(isHole) + faceOfstShp = extractFaceOffset(outerFace, ofstVal, self.wpc) + + lenIfl = len(ifL) + if self.obj.InternalFeaturesCut is False and lenIfl > 0: + if lenIfl == 1: + casL = ifL[0] + else: + casL = Part.makeCompound(ifL) + + ofstVal = self._calculateOffsetValue(isHole=True) + intOfstShp = extractFaceOffset(casL, ofstVal, self.wpc) + mIFS.append(intOfstShp) + # faceOfstShp = faceOfstShp.cut(intOfstShp) + + if mFS is False: + mFS = list() + mFS.append(faceOfstShp) + # Eif + # Efor + # Eif + # Eif + + if len(mIFS) > 0: + if mVS is False: + mVS = list() + for ifs in mIFS: + mVS.append(ifs) + + if VOIDS[m] is not False: + PathLog.debug('Processing avoid faces.') + cont = True + isHole = False + outFCS = list() + intFEAT = list() + + for (fcshp, fcIdx) in VOIDS[m]: + fNum = fcIdx + 1 + gFW = self._getFaceWires(base, fcshp, fcIdx) + if gFW is False: + PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) + cont = False + else: + ((otrFace, raised), intWires) = gFW + outFCS.append(otrFace) + if self.obj.AvoidLastX_InternalFeatures is False: + if intWires is not False: + for (iFace, rsd) in intWires: + intFEAT.append(iFace) + + lenOtFcs = len(outFCS) + if lenOtFcs == 0: + cont = False + else: + if lenOtFcs == 1: + avoid = outFCS[0] + else: + avoid = Part.makeCompound(outFCS) + + if self.showDebugObjects: + PathLog.debug('*** tmpAvoidArea') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + + if cont: + if self.showDebugObjects: + PathLog.debug('*** tmpVoidCompound') + P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') + P.Shape = avoid + P.purgeTouched() + self.tempGroup.addObject(P) + ofstVal = self._calculateOffsetValue(isHole, isVoid=True) + avdOfstShp = extractFaceOffset(avoid, ofstVal, self.wpc) + if avdOfstShp is False: + PathLog.error('Failed to create collective offset avoid face.') + cont = False + + if cont: + avdShp = avdOfstShp + + if self.obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: + if len(intFEAT) > 1: + ifc = Part.makeCompound(intFEAT) + else: + ifc = intFEAT[0] + ofstVal = self._calculateOffsetValue(isHole=True) + ifOfstShp = extractFaceOffset(ifc, ofstVal, self.wpc) + if ifOfstShp is False: + PathLog.error('Failed to create collective offset avoid internal features.') + else: + avdShp = avdOfstShp.cut(ifOfstShp) + + if mVS is False: + mVS = list() + mVS.append(avdShp) + + + return (mFS, mVS, mPS) + + def _getFaceWires(self, base, fcshp, fcIdx): + outFace = False + INTFCS = list() + fNum = fcIdx + 1 + warnFinDep = translate(self.module, 'Final Depth might need to be lower. Internal features detected in Face') + + PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) + WIRES = self._extractWiresFromFace(base, fcshp) + if WIRES is False: + PathLog.error('Failed to extract wires from Face{}'.format(fNum)) + return False + + # Process remaining internal features, adding to FCS list + lenW = len(WIRES) + for w in range(0, lenW): + (wire, rsd) = WIRES[w] + PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) + if wire.isClosed() is False: + PathLog.debug(' -wire is not closed.') + else: + slc = self._flattenWireToFace(wire) + if slc is False: + PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) + else: + if w == 0: + outFace = (slc, rsd) + else: + # add to VOIDS so cutter avoids area. + PathLog.warning(warnFinDep + str(fNum) + '.') + INTFCS.append((slc, rsd)) + if len(INTFCS) == 0: + return (outFace, False) + else: + return (outFace, INTFCS) + + def _preProcessEntireBase(self, base, m): + cont = True + isHole = False + prflShp = False + # Create envelope, extract cross-section and make offset co-planar shape + # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) + + try: + baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as ee: + PathLog.error(str(ee)) + shell = base.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape + except Exception as eee: + PathLog.error(str(eee)) + cont = False + + if cont: + csFaceShape = getShapeSlice(baseEnv) + if csFaceShape is False: + PathLog.debug('getShapeSlice(baseEnv) failed') + csFaceShape = getCrossSection(baseEnv) + if csFaceShape is False: + PathLog.debug('getCrossSection(baseEnv) failed') + csFaceShape = getSliceFromEnvelope(baseEnv) + if csFaceShape is False: + PathLog.error('Failed to slice baseEnv shape.') + cont = False + + if cont is True and self.profileEdges != 'None': + PathLog.debug(' -Attempting profile geometry for model base.') + ofstVal = self._calculateOffsetValue(isHole) + psOfst = extractFaceOffset(csFaceShape, ofstVal, self.wpc) + if psOfst is not False: + if self.profileEdges == 'Only': + return (True, psOfst) + prflShp = psOfst + else: + PathLog.error(' -Failed to create profile geometry.') + cont = False + + if cont: + ofstVal = self._calculateOffsetValue(isHole) + faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc) + if faceOffsetShape is False: + PathLog.error('extractFaceOffset() failed.') + else: + faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) + return (faceOffsetShape, prflShp) + return False + + def _extractWiresFromFace(self, base, fc): + '''_extractWiresFromFace(base, fc) ... + Attempts to return all closed wires within a parent face, including the outer most wire of the parent. + The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). + ''' + PathLog.debug('_extractWiresFromFace()') + + WIRES = list() + lenWrs = len(fc.Wires) + PathLog.debug(' -Wire count: {}'.format(lenWrs)) + + def index0(tup): + return tup[0] + + # Cycle through wires in face + for w in range(0, lenWrs): + PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) + wire = fc.Wires[w] + checkEdges = False + cont = True + + # Check for closed edges (circles, ellipses, etc...) + for E in wire.Edges: + if E.isClosed() is True: + checkEdges = True + break + + if checkEdges is True: + PathLog.debug(' -checkEdges is True') + for e in range(0, len(wire.Edges)): + edge = wire.Edges[e] + if edge.isClosed() is True and edge.Mass > 0.01: + PathLog.debug(' -Found closed edge') + raised = False + ip = self._isPocket(base, fc, edge) + if ip is False: + raised = True + ebb = edge.BoundBox + eArea = ebb.XLength * ebb.YLength + F = Part.Face(Part.Wire([edge])) + WIRES.append((eArea, F.Wires[0], raised)) + cont = False + + if cont: + PathLog.debug(' -cont is True') + # If only one wire and not checkEdges, return first wire + if lenWrs == 1: + return [(wire, False)] + + raised = False + wbb = wire.BoundBox + wArea = wbb.XLength * wbb.YLength + if w > 0: + ip = self._isPocket(base, fc, wire) + if ip is False: + raised = True + WIRES.append((wArea, Part.Wire(wire.Edges), raised)) + + nf = len(WIRES) + if nf > 0: + PathLog.debug(' -number of wires found is {}'.format(nf)) + if nf == 1: + (area, W, raised) = WIRES[0] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + return [(OW, False), (W, raised)] + else: + return [(W, raised)] + else: + sortedWIRES = sorted(WIRES, key=index0, reverse=True) + WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size + # Check if OuterWire is larger than largest in WRS list + (W, raised) = WRS[0] + owLen = fc.OuterWire.Length + wLen = W.Length + if abs(owLen - wLen) > 0.0000001: + OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) + WRS.insert(0, (OW, False)) + return WRS + + return False + + def _calculateOffsetValue(self, isHole, isVoid=False): + '''_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function. + Calculate the offset for the Path.Area() function.''' + self.JOB = PathUtils.findParentJob(self.obj) + tolrnc = self.JOB.GeometryTolerance.Value + + if isVoid is False: + if isHole is True: + offset = -1 * self.obj.InternalFeaturesAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + else: + offset = -1 * self.obj.BoundaryAdjustment.Value + if self.obj.BoundaryEnforcement is True: + offset += self.radius + (tolrnc / 10.0) + else: + offset -= self.radius + (tolrnc / 10.0) + offset = 0.0 - offset + else: + offset = -1 * self.obj.BoundaryAdjustment.Value + offset += self.radius + (tolrnc / 10.0) + + return offset + + def _isPocket(self, b, f, w): + '''_isPocket(b, f, w)... + Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. + Returns True if pocket, False if raised protrusion.''' + e = w.Edges[0] + for fi in range(0, len(b.Shape.Faces)): + face = b.Shape.Faces[fi] + for ei in range(0, len(face.Edges)): + edge = face.Edges[ei] + if e.isSame(edge) is True: + if f is face: + # Alternative: run loop to see if all edges are same + pass # same source face, look for another + else: + if face.CenterOfMass.z < f.CenterOfMass.z: + return True + return False + + def _flattenWireToFace(self, wire): + PathLog.debug('_flattenWireToFace()') + if wire.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + + # If wire is planar horizontal, convert to a face and return + if wire.BoundBox.ZLength == 0.0: + slc = Part.Face(wire) + return slc + + # Attempt to create a new wire for manipulation, if not, use original + newWire = Part.Wire(wire.Edges) + if newWire.isClosed() is True: + nWire = newWire + else: + PathLog.debug(' -newWire.isClosed() is False') + nWire = wire + + # Attempt extrusion, and then try a manual slice and then cross-section + ext = getExtrudedShape(nWire) + if ext is False: + PathLog.debug('getExtrudedShape() failed') + else: + slc = getShapeSlice(ext) + if slc is not False: + return slc + cs = getCrossSection(ext, True) + if cs is not False: + return cs + + # Attempt creating an envelope, and then try a manual slice and then cross-section + env = getShapeEnvelope(nWire) + if env is False: + PathLog.debug('getShapeEnvelope() failed') + else: + slc = getShapeSlice(env) + if slc is not False: + return slc + cs = getCrossSection(env, True) + if cs is not False: + return cs + + # Attempt creating a projection + slc = getProjectedFace(self.tempGroup, nWire) + if slc is False: + PathLog.debug('getProjectedFace() failed') + else: + return slc + + return False +# Eclass + + +# Functions for getting a shape envelope and cross-section +def getExtrudedShape(wire): + PathLog.debug('getExtrudedShape()') + wBB = wire.BoundBox + extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 + + try: + shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + except Exception as ee: + PathLog.error(' -extrude wire failed: \n{}'.format(ee)) + return False + + SHP = Part.makeSolid(shell) + return SHP + +def getShapeSlice(shape): + PathLog.debug('getShapeSlice()') + + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + xmin = bb.XMin - 1.0 + xmax = bb.XMax + 1.0 + ymin = bb.YMin - 1.0 + ymax = bb.YMax + 1.0 + p1 = FreeCAD.Vector(xmin, ymin, mid) + p2 = FreeCAD.Vector(xmax, ymin, mid) + p3 = FreeCAD.Vector(xmax, ymax, mid) + p4 = FreeCAD.Vector(xmin, ymax, mid) + + e1 = Part.makeLine(p1, p2) + e2 = Part.makeLine(p2, p3) + e3 = Part.makeLine(p3, p4) + e4 = Part.makeLine(p4, p1) + face = Part.Face(Part.Wire([e1, e2, e3, e4])) + fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area + sArea = shape.BoundBox.XLength * shape.BoundBox.YLength + midArea = (fArea + sArea) / 2.0 + + slcShp = shape.common(face) + slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength + + if slcArea < midArea: + for W in slcShp.Wires: + if W.isClosed() is False: + PathLog.debug(' -wire.isClosed() is False') + return False + if len(slcShp.Wires) == 1: + wire = slcShp.Wires[0] + slc = Part.Face(wire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + else: + fL = list() + for W in slcShp.Wires: + slc = Part.Face(W) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + fL.append(slc) + comp = Part.makeCompound(fL) + return comp + + # PathLog.debug(' -slcArea !< midArea') + # PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) + return False + +def getProjectedFace(tempGroup, wire): + import Draft + PathLog.debug('getProjectedFace()') + F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') + F.Shape = wire + F.purgeTouched() + tempGroup.addObject(F) + try: + prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) + prj.recompute() + prj.purgeTouched() + tempGroup.addObject(prj) + except Exception as ee: + PathLog.error(str(ee)) + return False + else: + pWire = Part.Wire(prj.Shape.Edges) + if pWire.isClosed() is False: + # PathLog.debug(' -pWire.isClosed() is False') + return False + slc = Part.Face(pWire) + slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) + return slc + +def getCrossSection(shape, withExtrude=False): + PathLog.debug('getCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) + csWire = comp.Wires[0] + if csWire.isClosed() is False: + PathLog.debug(' -comp.Wires[0] is not closed') + return False + if withExtrude is True: + ext = getExtrudedShape(csWire) + CS = getShapeSlice(ext) + if CS is False: + return False + else: + CS = Part.Face(csWire) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + else: + PathLog.debug(' -No wires from .slice() method') + + return False + +def getShapeEnvelope(shape): + PathLog.debug('getShapeEnvelope()') + + wBB = shape.BoundBox + extFwd = wBB.ZLength + 10.0 + minz = wBB.ZMin + maxz = wBB.ZMin + extFwd + stpDwn = (maxz - minz) / 4.0 + dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) + + try: + env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape + except Exception as ee: + PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) + return False + else: + return env + +def getSliceFromEnvelope(env): + PathLog.debug('getSliceFromEnvelope()') + eBB = env.BoundBox + extFwd = eBB.ZLength + 10.0 + maxz = eBB.ZMin + extFwd + + emax = math.floor(maxz - 1.0) + E = list() + for e in range(0, len(env.Edges)): + emin = env.Edges[e].BoundBox.ZMin + if emin > emax: + E.append(env.Edges[e]) + tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) + tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) + + return tf + + +# Function to extract offset face from shape +def extractFaceOffset(fcShape, offset, wpc, makeComp=True): + '''extractFaceOffset(fcShape, offset) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('extractFaceOffset()') + + if fcShape.BoundBox.ZMin != 0.0: + fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) + + areaParams = {} + areaParams['Offset'] = offset + areaParams['Fill'] = 1 # 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + + area = Path.Area() # Create instance of Area() class object + # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane + area.setPlane(PathUtils.makeWorkplane(wpc)) # Set working plane to normal at Z=1 + area.add(fcShape) + area.setParams(**areaParams) # set parameters + + offsetShape = area.getShape() + wCnt = len(offsetShape.Wires) + if wCnt == 0: + return False + elif wCnt == 1: + ofstFace = Part.Face(offsetShape.Wires[0]) + if not makeComp: + ofstFace = [ofstFace] + else: + W = list() + for wr in offsetShape.Wires: + W.append(Part.Face(wr)) + if makeComp: + ofstFace = Part.makeCompound(W) + else: + ofstFace = W + + return ofstFace # offsetShape + + +# Functions to convert path geometry into line/arc segments for OCL input or directly to g-code +def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps): + '''pathGeomToLinesPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' + PathLog.debug('pathGeomToLinesPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + chkGap = False + lnCnt = 0 + ec = len(compGeoShp.Edges) + cpa = obj.CutPatternAngle + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if cutClimb is True: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + else: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + inLine.append(tup) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + + for ei in range(1, ec): + chkGap = False + edg = compGeoShp.Edges[ei] # Get edge for vertexes + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 + + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + else: + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset collinear container + if cutClimb is True: + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + else: + sp = ep + + if cutClimb is True: + tup = (v2, v1) + if chkGap is True: + gap = abs(toolDiam - lst.sub(ep).Length) + lst = cp + else: + tup = (v1, v2) + if chkGap is True: + gap = abs(toolDiam - lst.sub(cp).Length) + lst = ep + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + tup = (vA, tup[1]) + closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < gaps[0]: + gaps.insert(0, gap) + gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + if cutClimb is True: + inLine.reverse() + LINES.append(inLine) # Save inLine segments + + # Handle last inLine set, reversing it. + if obj.CutPatternReversed is True: + if cpa != 0.0 and cpa % 90.0 == 0.0: + F = LINES.pop(0) + rev = list() + for iL in F: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + rev.reverse() + LINES.insert(0, rev) + + isEven = lnCnt % 2 + if isEven == 0: + PathLog.debug('Line count is ODD.') + else: + PathLog.debug('Line count is even.') + + return LINES + +def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps): + '''_pathGeomToZigzagPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directionally-oriented collinear groupings + with a ZigZag directional indicator included for each collinear group.''' + PathLog.debug('_pathGeomToZigzagPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + chkGap = False + ec = len(compGeoShp.Edges) + + if cutClimb is True: + dirFlg = -1 + else: + dirFlg = 1 + + edg0 = compGeoShp.Edges[0] + p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) + p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) + if dirFlg == 1: + tup = (p1, p2) + lst = FreeCAD.Vector(p2[0], p2[1], 0.0) + sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point + else: + tup = (p2, p1) + lst = FreeCAD.Vector(p1[0], p1[1], 0.0) + sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point + inLine.append(tup) + + for ei in range(1, ec): + edg = compGeoShp.Edges[ei] + v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) + v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) + + cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) + ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point + # iC = sp.isOnLineSegment(ep, cp) + iC = cp.isOnLineSegment(sp, ep) + if iC is True: + inLine.append('BRK') + chkGap = True + gap = abs(toolDiam - lst.sub(cp).Length) + else: + chkGap = False + if dirFlg == -1: + inLine.reverse() + # LINES.append((dirFlg, inLine)) + LINES.append(inLine) + lnCnt += 1 + dirFlg = -1 * dirFlg # Change zig to zag + inLine = list() # reset collinear container + sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) + + lst = ep + if dirFlg == 1: + tup = (v1, v2) + else: + tup = (v2, v1) + + if chkGap is True: + if gap < obj.GapThreshold.Value: + b = inLine.pop() # pop off 'BRK' marker + (vA, vB) = inLine.pop() # pop off previous line segment for combining with current + if dirFlg == 1: + tup = (vA, tup[1]) + else: + tup = (tup[0], vB) + closedGap = True + else: + gap = round(gap, 6) + if gap < gaps[0]: + gaps.insert(0, gap) + gaps.pop() + inLine.append(tup) + # Efor + lnCnt += 1 + + # Fix directional issue with LAST line when line count is even + isEven = lnCnt % 2 + if isEven == 0: # Changed to != with 90 degree CutPatternAngle + PathLog.debug('Line count is even.') + else: + PathLog.debug('Line count is ODD.') + dirFlg = -1 * dirFlg + if obj.CutPatternReversed is False: + if cutClimb is True: + dirFlg = -1 * dirFlg + + if obj.CutPatternReversed: + dirFlg = -1 * dirFlg + + # Handle last inLine list + if dirFlg == 1: + rev = list() + for iL in inLine: + if iL == 'BRK': + rev.append(iL) + else: + (p1, p2) = iL + rev.append((p2, p1)) + + if not obj.CutPatternReversed: + rev.reverse() + else: + rev2 = list() + for iL in rev: + if iL == 'BRK': + rev2.append(iL) + else: + (p1, p2) = iL + rev2.append((p2, p1)) + rev2.reverse() + rev = rev2 + + # LINES.append((dirFlg, rev)) + LINES.append(rev) + else: + # LINES.append((dirFlg, inLine)) + LINES.append(inLine) + + return LINES + +def pathGeomToCircularPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps, COM): + '''pathGeomToCircularPointSet(obj, compGeoShp)... + Convert a compound set of arcs/circles to a set of directionally-oriented arc end points + and the corresponding center point.''' + # Extract intersection line segments for return value as list() + PathLog.debug('pathGeomToCircularPointSet()') + ARCS = list() + stpOvrEI = list() + segEI = list() + isSame = False + sameRad = None + ec = len(compGeoShp.Edges) + + def gapDist(sp, ep): + X = (ep[0] - sp[0])**2 + Y = (ep[1] - sp[1])**2 + return math.sqrt(X + Y) # the 'z' value is zero in both points + + # Separate arc data into Loops and Arcs + for ei in range(0, ec): + edg = compGeoShp.Edges[ei] + if edg.Closed is True: + stpOvrEI.append(('L', ei, False)) + else: + if isSame is False: + segEI.append(ei) + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + else: + # Check if arc is co-radial to current SEGS + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + if abs(sameRad - pnt.sub(COM).Length) > 0.00001: + isSame = False + + if isSame is True: + segEI.append(ei) + else: + # Move co-radial arc segments + stpOvrEI.append(['A', segEI, False]) + # Start new list of arc segments + segEI = [ei] + isSame = True + pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) + sameRad = pnt.sub(COM).Length + # Process trailing `segEI` data, if available + if isSame is True: + stpOvrEI.append(['A', segEI, False]) + + # Identify adjacent arcs with y=0 start/end points that connect + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'A': + startOnAxis = list() + endOnAxis = list() + EI = SO[1] # list of corresponding compGeoShp.Edges indexes + + # Identify startOnAxis and endOnAxis arcs + for i in range(0, len(EI)): + ei = EI[i] # edge index + E = compGeoShp.Edges[ei] # edge object + if abs(COM.y - E.Vertexes[0].Y) < 0.00001: + startOnAxis.append((i, ei, E.Vertexes[0])) + elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: + endOnAxis.append((i, ei, E.Vertexes[1])) + + # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected + lenSOA = len(startOnAxis) + lenEOA = len(endOnAxis) + if lenSOA > 0 and lenEOA > 0: + for soa in range(0, lenSOA): + (iS, eiS, vS) = startOnAxis[soa] + for eoa in range(0, len(endOnAxis)): + (iE, eiE, vE) = endOnAxis[eoa] + dist = vE.X - vS.X + if abs(dist) < 0.00001: # They connect on axis at same radius + SO[2] = (eiE, eiS) + break + elif dist > 0: + break # stop searching + # Eif + # Eif + # Efor + + # Construct arc data tuples for OCL + dirFlg = 1 + if not cutClimb: # True yields Climb when set to Conventional + dirFlg = -1 + + # Cycle through stepOver data + for so in range(0, len(stpOvrEI)): + SO = stpOvrEI[so] + if SO[0] == 'L': # L = Loop/Ring/Circle + # PathLog.debug("SO[0] == 'Loop'") + lei = SO[1] # loop Edges index + v1 = compGeoShp.Edges[lei].Vertexes[0] + + # space = obj.SampleInterval.Value / 10.0 + # space = 0.000001 + space = toolDiam * 0.005 # If too small, OCL will fail to scan the loop + + # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) + p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) # z=0.0 for waterline; z=v1.Z for 3D Surface + rad = p1.sub(COM).Length + spcRadRatio = space/rad + if spcRadRatio < 1.0: + tolrncAng = math.asin(spcRadRatio) + else: + tolrncAng = 0.99999998 * math.pi + EX = COM.x + (rad * math.cos(tolrncAng)) + EY = v1.Y - space # rad * math.sin(tolrncAng) + + sp = (v1.X, v1.Y, 0.0) + ep = (EX, EY, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + ARCS.append(('L', dirFlg, [arc])) + else: # SO[0] == 'A' A = Arc + # PathLog.debug("SO[0] == 'Arc'") + PRTS = list() + EI = SO[1] # list of corresponding Edges indexes + CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) + chkGap = False + lst = None + + if CONN is not False: + (iE, iS) = CONN + v1 = compGeoShp.Edges[iE].Vertexes[0] + v2 = compGeoShp.Edges[iS].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + lst = sp + PRTS.append(arc) + # Pop connected edge index values from arc segments index list + iEi = EI.index(iE) + iSi = EI.index(iS) + if iEi > iSi: + EI.pop(iEi) + EI.pop(iSi) + else: + EI.pop(iSi) + EI.pop(iEi) + if len(EI) > 0: + PRTS.append('BRK') + chkGap = True + cnt = 0 + for ei in EI: + if cnt > 0: + PRTS.append('BRK') + chkGap = True + v1 = compGeoShp.Edges[ei].Vertexes[0] + v2 = compGeoShp.Edges[ei].Vertexes[1] + sp = (v1.X, v1.Y, 0.0) + ep = (v2.X, v2.Y, 0.0) + cp = (COM.x, COM.y, 0.0) + if dirFlg == 1: + arc = (sp, ep, cp) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) + lst = ep + else: + arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) + if chkGap is True: + gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) + lst = sp + if chkGap is True: + if gap < obj.GapThreshold.Value: + PRTS.pop() # pop off 'BRK' marker + (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current + arc = (vA, arc[1], vC) + closedGap = True + else: + # PathLog.debug('---- Gap: {} mm'.format(gap)) + gap = round(gap, 6) + if gap < gaps[0]: + gaps.insert(0, gap) + gaps.pop() + PRTS.append(arc) + cnt += 1 + + if dirFlg == -1: + PRTS.reverse() + + ARCS.append(('A', dirFlg, PRTS)) + # Eif + if obj.CutPattern == 'CircularZigZag': + dirFlg = -1 * dirFlg + # Efor + + return ARCS + +def pathGeomToSpiralPointSet(obj, compGeoShp): + '''_pathGeomToSpiralPointSet(obj, compGeoShp)... + Convert a compound set of sequential line segments to directional, connected groupings.''' + PathLog.debug('_pathGeomToSpiralPointSet()') + # Extract intersection line segments for return value as list() + LINES = list() + inLine = list() + lnCnt = 0 + ec = len(compGeoShp.Edges) + start = 2 + + if obj.CutPatternReversed: + edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail + ec -= 1 + start = 1 + else: + edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail + p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0) + p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0) + tup = ((p1.x, p1.y), (p2.x, p2.y)) + inLine.append(tup) + lst = p2 + + for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1 + edg = compGeoShp.Edges[ei] # Get edge for vertexes + sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point) + ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point + tup = ((sp.x, sp.y), (ep.x, ep.y)) + + if sp.sub(p2).Length < 0.000001: + inLine.append(tup) + else: + LINES.append(inLine) # Save inLine segments + lnCnt += 1 + inLine = list() # reset container + inLine.append(tup) + p1 = sp + p2 = ep + # Efor + + lnCnt += 1 + LINES.append(inLine) # Save inLine segments + + return LINES + +def pathGeomToOffsetPointSet(obj, compGeoShp): + '''pathGeomToOffsetPointSet(obj, compGeoShp)... + Convert a compound set of 3D profile segmented wires to 2D segments, applying linear optimization.''' + PathLog.debug('pathGeomToOffsetPointSet()') + + LINES = list() + optimize = obj.OptimizeLinearPaths + ofstCnt = len(compGeoShp) + + # Cycle through offeset loops + for ei in range(0, ofstCnt): + OS = compGeoShp[ei] + lenOS = len(OS) + + if ei > 0: + LINES.append('BRK') + + fp = FreeCAD.Vector(OS[0].x, OS[0].y, OS[0].z) + OS.append(fp) + + # Cycle through points in each loop + prev = OS[0] + pnt = OS[1] + for v in range(1, lenOS): + nxt = OS[v + 1] + if optimize: + # iPOL = prev.isOnLineSegment(nxt, pnt) + iPOL = pnt.isOnLineSegment(prev, nxt) + if iPOL: + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + else: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + prev = pnt + pnt = nxt + if iPOL: + tup = ((prev.x, prev.y), (pnt.x, pnt.y)) + LINES.append(tup) + # Efor + + return [LINES] \ No newline at end of file diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index f2cf3c9e94..ba930db881 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -44,7 +44,6 @@ except ImportError: # import sys # sys.exit(msg) -import MeshPart import Path import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils @@ -52,12 +51,10 @@ import PathScripts.PathOp as PathOp import PathScripts.PathSurfaceSupport as PathSurfaceSupport import time import math -import Part # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') -Draft = LazyLoader('Draft', globals(), 'Draft') Part = LazyLoader('Part', globals(), 'Part') if FreeCAD.GuiUp: @@ -96,7 +93,7 @@ class ObjectWaterline(PathOp.ObjectOp): if not hasattr(obj, 'DoNotSetDefaultValues'): self.setEditorProperties(obj) - def initOpProperties(self, obj): + def initOpProperties(self, obj, warn=False): '''initOpProperties(obj) ... create operation specific properties''' missing = list() @@ -104,17 +101,17 @@ class ObjectWaterline(PathOp.ObjectOp): if not hasattr(obj, nm): obj.addProperty(prtyp, nm, grp, tt) missing.append(nm) - newPropMsg = translate('PathSurface', 'New property added: ') + nm + '. ' - newPropMsg += translate('PathSurface', 'Check its default value.') - PathLog.warning(newPropMsg) + if warn: + newPropMsg = translate('PathWaterline', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. ' + newPropMsg += translate('PathWaterline', 'Check its default value.') + PathLog.warning(newPropMsg) # Set enumeration lists for enumeration properties if len(missing) > 0: ENUMS = self.propertyEnumerations() for n in ENUMS: if n in missing: - cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) - exec(cmdStr) + setattr(obj, n, ENUMS[n]) self.addedAllProperties = True @@ -148,10 +145,6 @@ class ObjectWaterline(PathOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the algorithm to use: OCL Dropcutter*, or Experimental (Not OCL based).")), ("App::PropertyEnumeration", "BoundBox", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Select the overall boundary for the operation.")), - ("App::PropertyVectorDistance", "CircularCenterCustom", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for circular cut patterns.")), - ("App::PropertyEnumeration", "CircularCenterAt", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the circular pattern.")), ("App::PropertyEnumeration", "ClearLastLayer", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set to clear last layer in a `Multi-pass` operation.")), ("App::PropertyEnumeration", "CutMode", "Clearing Options", @@ -168,8 +161,10 @@ class ObjectWaterline(PathOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Ignore outer waterlines above this height.")), ("App::PropertyEnumeration", "LayerMode", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Complete the operation in a single pass at depth, or mulitiple passes to final depth.")), - ("App::PropertyEnumeration", "ProfileEdges", "Clearing Options", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), + ("App::PropertyVectorDistance", "PatternCenterCustom", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the start point for the cut pattern.")), + ("App::PropertyEnumeration", "PatternCenterAt", "Clearing Options", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the cut pattern.")), ("App::PropertyDistance", "SampleInterval", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), ("App::PropertyPercent", "StepOver", "Clearing Options", @@ -195,13 +190,12 @@ class ObjectWaterline(PathOp.ObjectOp): return { 'Algorithm': ['OCL Dropcutter', 'Experimental'], 'BoundBox': ['BaseBoundBox', 'Stock'], - 'CircularCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], + 'PatternCenterAt': ['CenterOfMass', 'CenterOfBoundBox', 'XminYmin', 'Custom'], 'ClearLastLayer': ['Off', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], 'CutMode': ['Conventional', 'Climb'], 'CutPattern': ['None', 'Line', 'Circular', 'CircularZigZag', 'Offset', 'Spiral', 'ZigZag'], # Additional goals ['Offset', 'Spiral', 'ZigZagOffset', 'Grid', 'Triangle'] 'HandleMultipleFeatures': ['Collectively', 'Individually'], 'LayerMode': ['Single-pass', 'Multi-pass'], - 'ProfileEdges': ['None', 'Only', 'First', 'Last'], } def setEditorProperties(self, obj): @@ -212,7 +206,6 @@ class ObjectWaterline(PathOp.ObjectOp): obj.setEditorMode('EnableRotation', hide) obj.setEditorMode('BoundaryEnforcement', hide) - obj.setEditorMode('ProfileEdges', hide) obj.setEditorMode('InternalFeaturesAdjustment', hide) obj.setEditorMode('InternalFeaturesCut', hide) obj.setEditorMode('AvoidLastX_Faces', hide) @@ -225,14 +218,14 @@ class ObjectWaterline(PathOp.ObjectOp): obj.setEditorMode('GapSizes', hide) if obj.Algorithm == 'OCL Dropcutter': - B = 2 + pass elif obj.Algorithm == 'Experimental': A = B = C = 0 - expMode = G = 2 + expMode = G = show = hide = 2 cutPattern = obj.CutPattern if obj.ClearLastLayer != 'Off': - cutPattern = obj.CutPattern + cutPattern = obj.ClearLastLayer if cutPattern == 'None': show = hide = A = 2 @@ -242,11 +235,11 @@ class ObjectWaterline(PathOp.ObjectOp): show = 2 # hide hide = 0 # show elif cutPattern == 'Spiral': - G = 0 + G = hide = 0 obj.setEditorMode('CutPatternAngle', show) - obj.setEditorMode('CircularCenterAt', hide) - obj.setEditorMode('CircularCenterCustom', hide) + obj.setEditorMode('PatternCenterAt', hide) + obj.setEditorMode('PatternCenterCustom', hide) obj.setEditorMode('CutPatternReversed', A) obj.setEditorMode('ClearLastLayer', C) @@ -264,7 +257,7 @@ class ObjectWaterline(PathOp.ObjectOp): self.setEditorProperties(obj) def opOnDocumentRestored(self, obj): - self.initOpProperties(obj) + self.initOpProperties(obj, warn=True) if PathLog.getLevel(PathLog.thisModule()) != 4: obj.setEditorMode('ShowTempObjects', 2) # hide @@ -278,11 +271,9 @@ class ObjectWaterline(PathOp.ObjectOp): if hasattr(obj, n): val = obj.getPropertyByName(n) restore = True - cmdStr = 'obj.{}={}'.format(n, ENUMS[n]) - exec(cmdStr) + setattr(obj, n, ENUMS[n]) if restore: - cmdStr = 'obj.{}={}'.format(n, "'" + val + "'") - exec(cmdStr) + setattr(obj, n, val) self.setEditorProperties(obj) @@ -300,12 +291,11 @@ class ObjectWaterline(PathOp.ObjectOp): obj.IgnoreOuterAbove = obj.StartDepth.Value + 0.00001 obj.StartPoint = FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value) obj.Algorithm = 'OCL Dropcutter' - obj.ProfileEdges = 'None' obj.LayerMode = 'Single-pass' obj.CutMode = 'Conventional' obj.CutPattern = 'None' obj.HandleMultipleFeatures = 'Collectively' # 'Individually' - obj.CircularCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' + obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' obj.GapSizes = 'No gaps identified.' obj.ClearLastLayer = 'Off' obj.StepOver = 100 @@ -315,7 +305,7 @@ class ObjectWaterline(PathOp.ObjectOp): obj.BoundaryAdjustment.Value = 0.0 obj.InternalFeaturesAdjustment.Value = 0.0 obj.AvoidLastX_Faces = 0 - obj.CircularCenterCustom = FreeCAD.Vector(0.0, 0.0, 0.0) + obj.PatternCenterCustom = FreeCAD.Vector(0.0, 0.0, 0.0) obj.GapThreshold.Value = 0.005 obj.LinearDeflection.Value = 0.0001 obj.AngularDeflection.Value = 0.25 @@ -398,6 +388,15 @@ class ObjectWaterline(PathOp.ObjectOp): modelVisibility = list() FCAD = FreeCAD.ActiveDocument + try: + dotIdx = __name__.index('.') + 1 + except Exception: + dotIdx = 0 + self.module = __name__[dotIdx:] + + # make circle for workplane + self.wpc = Part.makeCircle(2.0) + # Set debugging behavior self.showDebugObjects = False # Set to true if you want a visual DocObjects created for some path construction objects self.showDebugObjects = obj.ShowTempObjects @@ -468,14 +467,13 @@ class ObjectWaterline(PathOp.ObjectOp): # Setup cutter for OCL and cutout value for operation - based on tool controller properties self.cutter = self.setOclCutter(obj) - self.safeCutter = self.setOclCutter(obj, safe=True) - if self.cutter is False or self.safeCutter is False: + if self.cutter is False: PathLog.error(translate('PathWaterline', "Canceling Waterline operation. Error creating OCL cutter.")) return - toolDiam = self.cutter.getDiameter() - self.cutOut = (toolDiam * (float(obj.StepOver) / 100.0)) - self.radius = toolDiam / 2.0 - self.gaps = [toolDiam, toolDiam, toolDiam] + self.toolDiam = self.cutter.getDiameter() + self.radius = self.toolDiam / 2.0 + self.cutOut = (self.toolDiam * (float(obj.StepOver) / 100.0)) + self.gaps = [self.toolDiam, self.toolDiam, self.toolDiam] # Get height offset values for later use self.SafeHeightOffset = JOB.SetupSheet.SafeHeightOffset.Value @@ -498,9 +496,6 @@ class ObjectWaterline(PathOp.ObjectOp): self.depthParams = PathUtils.depth_params(obj.ClearanceHeight.Value, obj.SafeHeight.Value, obj.StartDepth.Value, obj.StepDown.Value, 0.0, obj.FinalDepth.Value) self.midDep = (obj.StartDepth.Value + obj.FinalDepth.Value) / 2.0 - # make circle for workplane - self.wpc = Part.makeCircle(2.0) - # Save model visibilities for restoration if FreeCAD.GuiUp: for m in range(0, len(JOB.Model.Group)): @@ -528,12 +523,18 @@ class ObjectWaterline(PathOp.ObjectOp): # ###### MAIN COMMANDS FOR OPERATION ###### # Begin processing obj.Base data and creating GCode + PSF = PathSurfaceSupport.ProcessSelectedFaces(JOB, obj) + PSF.setShowDebugObjects(tempGroup, self.showDebugObjects) + PSF.radius = self.radius + PSF.depthParams = self.depthParams + pPM = PSF.preProcessModel(self.module) # Process selected faces, if available - pPM = self._preProcessModel(JOB, obj) if pPM is False: PathLog.error('Unable to pre-process obj.Base.') else: (FACES, VOIDS) = pPM + self.modelSTLs = PSF.modelSTLs + self.profileShapes = PSF.profileShapes # Create OCL.stl model objects if obj.Algorithm == 'OCL Dropcutter': @@ -586,7 +587,7 @@ class ObjectWaterline(PathOp.ObjectOp): # Provide user feedback for gap sizes gaps = list() for g in self.gaps: - if g != toolDiam: + if g != self.toolDiam: gaps.append(g) if len(gaps) > 0: obj.GapSizes = '{} mm'.format(gaps) @@ -610,7 +611,6 @@ class ObjectWaterline(PathOp.ObjectOp): self.ClearHeightOffset = None self.depthParams = None self.midDep = None - self.wpc = None del self.modelSTLs del self.safeSTLs del self.modelTypes @@ -621,7 +621,6 @@ class ObjectWaterline(PathOp.ObjectOp): del self.ClearHeightOffset del self.depthParams del self.midDep - del self.wpc execTime = time.time() - startTime PathLog.info('Operation time: {} sec.'.format(execTime)) @@ -629,831 +628,14 @@ class ObjectWaterline(PathOp.ObjectOp): return True # Methods for constructing the cut area - def _preProcessModel(self, JOB, obj): - PathLog.debug('_preProcessModel()') - - FACES = list() - VOIDS = list() - fShapes = list() - vShapes = list() - GRP = JOB.Model.Group - lenGRP = len(GRP) - noFaces = translate('PathWaterline', - 'Face selection is still under development for Waterline. Ignoring selected faces.') - - # Crete place holders for each base model in Job - for m in range(0, lenGRP): - FACES.append(False) - VOIDS.append(False) - fShapes.append(False) - vShapes.append(False) - - checkBase = False - if obj.Base: - if len(obj.Base) > 0: - checkBase = True - if obj.Algorithm in ['OCL Dropcutter', 'Experimental']: - checkBase = False - PathLog.warning(noFaces) - - # The user has selected subobjects from the base. Pre-Process each. - if checkBase: - PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') - - (FACES, VOIDS) = self._identifyFacesAndVoids(JOB, obj, FACES, VOIDS) - - # Cycle through each base model, processing faces for each - for m in range(0, lenGRP): - base = GRP[m] - (mFS, mVS, mPS) = self._preProcessFacesAndVoids(obj, base, m, FACES, VOIDS) - fShapes[m] = mFS - vShapes[m] = mVS - self.profileShapes[m] = mPS - else: - PathLog.debug(' -No obj.Base data.') - for m in range(0, lenGRP): - self.modelSTLs[m] = True - - # Process each model base, as a whole, as needed - # PathLog.debug(' -Pre-processing all models in Job.') - for m in range(0, lenGRP): - if fShapes[m] is False: - PathLog.debug(' -Pre-processing {} as a whole.'.format(GRP[m].Label)) - if obj.BoundBox == 'BaseBoundBox': - base = GRP[m] - elif obj.BoundBox == 'Stock': - base = JOB.Stock - - pPEB = self._preProcessEntireBase(obj, base, m) - if pPEB is False: - PathLog.error(' -Failed to pre-process base as a whole.') - else: - (fcShp, prflShp) = pPEB - if fcShp is not False: - if fcShp is True: - PathLog.debug(' -fcShp is True.') - fShapes[m] = True - else: - fShapes[m] = [fcShp] - if prflShp is not False: - if fcShp is not False: - PathLog.debug('vShapes[{}]: {}'.format(m, vShapes[m])) - if vShapes[m] is not False: - PathLog.debug(' -Cutting void from base profile shape.') - adjPS = prflShp.cut(vShapes[m][0]) - self.profileShapes[m] = [adjPS] - else: - PathLog.debug(' -vShapes[m] is False.') - self.profileShapes[m] = [prflShp] - else: - PathLog.debug(' -Saving base profile shape.') - self.profileShapes[m] = [prflShp] - PathLog.debug('self.profileShapes[{}]: {}'.format(m, self.profileShapes[m])) - # Efor - - return (fShapes, vShapes) - - def _identifyFacesAndVoids(self, JOB, obj, F, V): - TUPS = list() - GRP = JOB.Model.Group - lenGRP = len(GRP) - - # Separate selected faces into (base, face) tuples and flag model(s) for STL creation - for (bs, SBS) in obj.Base: - for sb in SBS: - # Flag model for STL creation - mdlIdx = None - for m in range(0, lenGRP): - if bs is GRP[m]: - self.modelSTLs[m] = True - mdlIdx = m - break - TUPS.append((mdlIdx, bs, sb)) # (model idx, base, sub) - - # Apply `AvoidXFaces` value - faceCnt = len(TUPS) - add = faceCnt - obj.AvoidLastX_Faces - for bst in range(0, faceCnt): - (m, base, sub) = TUPS[bst] - shape = getattr(base.Shape, sub) - if isinstance(shape, Part.Face): - faceIdx = int(sub[4:]) - 1 - if bst < add: - if F[m] is False: - F[m] = list() - F[m].append((shape, faceIdx)) - else: - if V[m] is False: - V[m] = list() - V[m].append((shape, faceIdx)) - return (F, V) - - def _preProcessFacesAndVoids(self, obj, base, m, FACES, VOIDS): - mFS = False - mVS = False - mPS = False - mIFS = list() - - if FACES[m] is not False: - isHole = False - if obj.HandleMultipleFeatures == 'Collectively': - cont = True - fsL = list() # face shape list - ifL = list() # avoid shape list - outFCS = list() - - # Get collective envelope slice of selected faces - for (fcshp, fcIdx) in FACES[m]: - fNum = fcIdx + 1 - fsL.append(fcshp) - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - PathLog.debug('Attempting to get cross-section of collective faces.') - if len(outFCS) == 0: - PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) - cont = False - else: - cfsL = Part.makeCompound(outFCS) - - # Handle profile edges request - if cont is True and obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(cfsL, ofstVal) - if psOfst is not False: - mPS = [psOfst] - if obj.ProfileEdges == 'Only': - mFS = True - cont = False - else: - PathLog.error(' -Failed to create profile geometry for selected faces.') - cont = False - - if cont: - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCollectiveShape') - T.Shape = cfsL - T.purgeTouched() - self.tempGroup.addObject(T) - - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(cfsL, ofstVal) - if faceOfstShp is False: - PathLog.error(' -Failed to create offset face.') - cont = False - - if cont: - lenIfL = len(ifL) - if obj.InternalFeaturesCut is False: - if lenIfL == 0: - PathLog.debug(' -No internal features saved.') - else: - if lenIfL == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - if self.showDebugObjects is True: - C = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCompoundIntFeat') - C.Shape = casL - C.purgeTouched() - self.tempGroup.addObject(C) - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - mFS = [faceOfstShp] - # Eif - - elif obj.HandleMultipleFeatures == 'Individually': - for (fcshp, fcIdx) in FACES[m]: - cont = True - ifL = list() # avoid shape list - fNum = fcIdx + 1 - outerFace = False - - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - cont = False - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - cont = False - outerFace = False - else: - ((otrFace, raised), intWires) = gFW - outerFace = otrFace - if obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) - - if outerFace is not False: - PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) - - if obj.ProfileEdges != 'None': - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(outerFace, ofstVal) - if psOfst is not False: - if mPS is False: - mPS = list() - mPS.append(psOfst) - if obj.ProfileEdges == 'Only': - if mFS is False: - mFS = list() - mFS.append(True) - cont = False - else: - PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) - cont = False - - if cont: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOfstShp = self._extractFaceOffset(outerFace, ofstVal) - - lenIfl = len(ifL) - if obj.InternalFeaturesCut is False and lenIfl > 0: - if lenIfl == 1: - casL = ifL[0] - else: - casL = Part.makeCompound(ifL) - - ofstVal = self._calculateOffsetValue(obj, isHole=True) - intOfstShp = self._extractFaceOffset(casL, ofstVal) - mIFS.append(intOfstShp) - # faceOfstShp = faceOfstShp.cut(intOfstShp) - - if mFS is False: - mFS = list() - mFS.append(faceOfstShp) - # Eif - # Efor - # Eif - # Eif - - if len(mIFS) > 0: - if mVS is False: - mVS = list() - for ifs in mIFS: - mVS.append(ifs) - - if VOIDS[m] is not False: - PathLog.debug('Processing avoid faces.') - cont = True - isHole = False - outFCS = list() - intFEAT = list() - - for (fcshp, fcIdx) in VOIDS[m]: - fNum = fcIdx + 1 - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) - cont = False - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if obj.AvoidLastX_InternalFeatures is False: - if intWires is not False: - for (iFace, rsd) in intWires: - intFEAT.append(iFace) - - lenOtFcs = len(outFCS) - if lenOtFcs == 0: - cont = False - else: - if lenOtFcs == 1: - avoid = outFCS[0] - else: - avoid = Part.makeCompound(outFCS) - - if self.showDebugObjects is True: - PathLog.debug('*** tmpAvoidArea') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidEnvelope') - P.Shape = avoid - P.purgeTouched() - self.tempGroup.addObject(P) - - if cont: - if self.showDebugObjects is True: - PathLog.debug('*** tmpVoidCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpVoidCompound') - P.Shape = avoid - P.purgeTouched() - self.tempGroup.addObject(P) - ofstVal = self._calculateOffsetValue(obj, isHole, isVoid=True) - avdOfstShp = self._extractFaceOffset(avoid, ofstVal) - if avdOfstShp is False: - PathLog.error('Failed to create collective offset avoid face.') - cont = False - - if cont: - avdShp = avdOfstShp - - if obj.AvoidLastX_InternalFeatures is False and len(intFEAT) > 0: - if len(intFEAT) > 1: - ifc = Part.makeCompound(intFEAT) - else: - ifc = intFEAT[0] - ofstVal = self._calculateOffsetValue(obj, isHole=True) - ifOfstShp = self._extractFaceOffset(ifc, ofstVal) - if ifOfstShp is False: - PathLog.error('Failed to create collective offset avoid internal features.') - else: - avdShp = avdOfstShp.cut(ifOfstShp) - - if mVS is False: - mVS = list() - mVS.append(avdShp) - - - return (mFS, mVS, mPS) - - def _getFaceWires(self, base, fcshp, fcIdx): - outFace = False - INTFCS = list() - fNum = fcIdx + 1 - # preProcEr = translate('PathWaterline', 'Error pre-processing Face') - warnFinDep = translate('PathWaterline', 'Final Depth might need to be lower. Internal features detected in Face') - - PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) - WIRES = self._extractWiresFromFace(base, fcshp) - if WIRES is False: - PathLog.error('Failed to extract wires from Face{}'.format(fNum)) - return False - - # Process remaining internal features, adding to FCS list - lenW = len(WIRES) - for w in range(0, lenW): - (wire, rsd) = WIRES[w] - PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) - if wire.isClosed() is False: - PathLog.debug(' -wire is not closed.') - else: - slc = self._flattenWireToFace(wire) - if slc is False: - PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) - else: - if w == 0: - outFace = (slc, rsd) - else: - # add to VOIDS so cutter avoids area. - PathLog.warning(warnFinDep + str(fNum) + '.') - INTFCS.append((slc, rsd)) - if len(INTFCS) == 0: - return (outFace, False) - else: - return (outFace, INTFCS) - - def _preProcessEntireBase(self, obj, base, m): - cont = True - isHole = False - prflShp = False - # Create envelope, extract cross-section and make offset co-planar shape - # baseEnv = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthParams) - - try: - baseEnv = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as ee: - PathLog.error(str(ee)) - shell = base.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - baseEnv = PathUtils.getEnvelope(partshape=solid, subshape=None, depthparams=self.depthParams) # Produces .Shape - except Exception as eee: - PathLog.error(str(eee)) - cont = False - - if cont: - csFaceShape = self._getShapeSlice(baseEnv) - if csFaceShape is False: - PathLog.debug('_getShapeSlice(baseEnv) failed') - csFaceShape = self._getCrossSection(baseEnv) - if csFaceShape is False: - PathLog.debug('_getCrossSection(baseEnv) failed') - csFaceShape = self._getSliceFromEnvelope(baseEnv) - if csFaceShape is False: - PathLog.error('Failed to slice baseEnv shape.') - cont = False - - if cont is True and obj.ProfileEdges != 'None': - PathLog.debug(' -Attempting profile geometry for model base.') - ofstVal = self._calculateOffsetValue(obj, isHole) - psOfst = self._extractFaceOffset(csFaceShape, ofstVal) - if psOfst is not False: - if obj.ProfileEdges == 'Only': - return (True, psOfst) - prflShp = psOfst - else: - PathLog.error(' -Failed to create profile geometry.') - cont = False - - if cont: - ofstVal = self._calculateOffsetValue(obj, isHole) - faceOffsetShape = self._extractFaceOffset(csFaceShape, ofstVal) - if faceOffsetShape is False: - PathLog.error('_extractFaceOffset() failed.') - else: - faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) - return (faceOffsetShape, prflShp) - return False - - def _extractWiresFromFace(self, base, fc): - '''_extractWiresFromFace(base, fc) ... - Attempts to return all closed wires within a parent face, including the outer most wire of the parent. - The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). - ''' - PathLog.debug('_extractWiresFromFace()') - - WIRES = list() - lenWrs = len(fc.Wires) - PathLog.debug(' -Wire count: {}'.format(lenWrs)) - - def index0(tup): - return tup[0] - - # Cycle through wires in face - for w in range(0, lenWrs): - PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) - wire = fc.Wires[w] - checkEdges = False - cont = True - - # Check for closed edges (circles, ellipses, etc...) - for E in wire.Edges: - if E.isClosed() is True: - checkEdges = True - break - - if checkEdges is True: - PathLog.debug(' -checkEdges is True') - for e in range(0, len(wire.Edges)): - edge = wire.Edges[e] - if edge.isClosed() is True and edge.Mass > 0.01: - PathLog.debug(' -Found closed edge') - raised = False - ip = self._isPocket(base, fc, edge) - if ip is False: - raised = True - ebb = edge.BoundBox - eArea = ebb.XLength * ebb.YLength - F = Part.Face(Part.Wire([edge])) - WIRES.append((eArea, F.Wires[0], raised)) - cont = False - - if cont: - PathLog.debug(' -cont is True') - # If only one wire and not checkEdges, return first wire - if lenWrs == 1: - return [(wire, False)] - - raised = False - wbb = wire.BoundBox - wArea = wbb.XLength * wbb.YLength - if w > 0: - ip = self._isPocket(base, fc, wire) - if ip is False: - raised = True - WIRES.append((wArea, Part.Wire(wire.Edges), raised)) - - nf = len(WIRES) - if nf > 0: - PathLog.debug(' -number of wires found is {}'.format(nf)) - if nf == 1: - (area, W, raised) = WIRES[0] - owLen = fc.OuterWire.Length - wLen = W.Length - if abs(owLen - wLen) > 0.0000001: - OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) - return [(OW, False), (W, raised)] - else: - return [(W, raised)] - else: - sortedWIRES = sorted(WIRES, key=index0, reverse=True) - WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size - # Check if OuterWire is larger than largest in WRS list - (W, raised) = WRS[0] - owLen = fc.OuterWire.Length - wLen = W.Length - if abs(owLen - wLen) > 0.0000001: - OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) - WRS.insert(0, (OW, False)) - return WRS - - return False - - def _calculateOffsetValue(self, obj, isHole, isVoid=False): - '''_calculateOffsetValue(obj, isHole, isVoid) ... internal function. - Calculate the offset for the Path.Area() function.''' - JOB = PathUtils.findParentJob(obj) - tolrnc = JOB.GeometryTolerance.Value - - if isVoid is False: - if isHole is True: - offset = -1 * obj.InternalFeaturesAdjustment.Value - offset += self.radius + (tolrnc / 10.0) - else: - offset = -1 * obj.BoundaryAdjustment.Value - if obj.BoundaryEnforcement is True: - offset += self.radius + (tolrnc / 10.0) - else: - offset -= self.radius + (tolrnc / 10.0) - offset = 0.0 - offset - else: - offset = -1 * obj.BoundaryAdjustment.Value - offset += self.radius + (tolrnc / 10.0) - - return offset - - def _extractFaceOffset(self, fcShape, offset, makeComp=True): - '''_extractFaceOffset(fcShape, offset) ... internal function. - Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. - Adjustments made based on notes by @sliptonic at this webpage: https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' - PathLog.debug('_extractFaceOffset()') - - if fcShape.BoundBox.ZMin != 0.0: - fcShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - fcShape.BoundBox.ZMin)) - - areaParams = {} - areaParams['Offset'] = offset - areaParams['Fill'] = 1 # 1 - areaParams['Coplanar'] = 0 - areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections - areaParams['Reorient'] = True - areaParams['OpenMode'] = 0 - areaParams['MaxArcPoints'] = 400 # 400 - areaParams['Project'] = True - - area = Path.Area() # Create instance of Area() class object - # area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane - area.setPlane(PathUtils.makeWorkplane(self.wpc)) # Set working plane to normal at Z=1 - area.add(fcShape) - area.setParams(**areaParams) # set parameters - - offsetShape = area.getShape() - wCnt = len(offsetShape.Wires) - if wCnt == 0: - return False - elif wCnt == 1: - ofstFace = Part.Face(offsetShape.Wires[0]) - if not makeComp: - ofstFace = [ofstFace] - else: - W = list() - for wr in offsetShape.Wires: - W.append(Part.Face(wr)) - if makeComp: - ofstFace = Part.makeCompound(W) - else: - ofstFace = W - - return ofstFace # offsetShape - - def _isPocket(self, b, f, w): - '''_isPocket(b, f, w)... - Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. - Returns True if pocket, False if raised protrusion.''' - e = w.Edges[0] - for fi in range(0, len(b.Shape.Faces)): - face = b.Shape.Faces[fi] - for ei in range(0, len(face.Edges)): - edge = face.Edges[ei] - if e.isSame(edge) is True: - if f is face: - # Alternative: run loop to see if all edges are same - pass # same source face, look for another - else: - if face.CenterOfMass.z < f.CenterOfMass.z: - return True - return False - - def _flattenWireToFace(self, wire): - PathLog.debug('_flattenWireToFace()') - if wire.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - - # If wire is planar horizontal, convert to a face and return - if wire.BoundBox.ZLength == 0.0: - slc = Part.Face(wire) - return slc - - # Attempt to create a new wire for manipulation, if not, use original - newWire = Part.Wire(wire.Edges) - if newWire.isClosed() is True: - nWire = newWire - else: - PathLog.debug(' -newWire.isClosed() is False') - nWire = wire - - # Attempt extrusion, and then try a manual slice and then cross-section - ext = self._getExtrudedShape(nWire) - if ext is False: - PathLog.debug('_getExtrudedShape() failed') - else: - slc = self._getShapeSlice(ext) - if slc is not False: - return slc - cs = self._getCrossSection(ext, True) - if cs is not False: - return cs - - # Attempt creating an envelope, and then try a manual slice and then cross-section - env = self._getShapeEnvelope(nWire) - if env is False: - PathLog.debug('_getShapeEnvelope() failed') - else: - slc = self._getShapeSlice(env) - if slc is not False: - return slc - cs = self._getCrossSection(env, True) - if cs is not False: - return cs - - # Attempt creating a projection - slc = self._getProjectedFace(nWire) - if slc is False: - PathLog.debug('_getProjectedFace() failed') - else: - return slc - - return False - - def _getExtrudedShape(self, wire): - PathLog.debug('_getExtrudedShape()') - wBB = wire.BoundBox - extFwd = math.floor(2.0 * wBB.ZLength) + 10.0 - - try: - # slower, but renders collective faces correctly. Method 5 in TESTING - shell = wire.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) - except Exception as ee: - PathLog.error(' -extrude wire failed: \n{}'.format(ee)) - return False - - SHP = Part.makeSolid(shell) - return SHP - - def _getShapeSlice(self, shape): - PathLog.debug('_getShapeSlice()') - - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - xmin = bb.XMin - 1.0 - xmax = bb.XMax + 1.0 - ymin = bb.YMin - 1.0 - ymax = bb.YMax + 1.0 - p1 = FreeCAD.Vector(xmin, ymin, mid) - p2 = FreeCAD.Vector(xmax, ymin, mid) - p3 = FreeCAD.Vector(xmax, ymax, mid) - p4 = FreeCAD.Vector(xmin, ymax, mid) - - e1 = Part.makeLine(p1, p2) - e2 = Part.makeLine(p2, p3) - e3 = Part.makeLine(p3, p4) - e4 = Part.makeLine(p4, p1) - face = Part.Face(Part.Wire([e1, e2, e3, e4])) - fArea = face.BoundBox.XLength * face.BoundBox.YLength # face.Wires[0].Area - sArea = shape.BoundBox.XLength * shape.BoundBox.YLength - midArea = (fArea + sArea) / 2.0 - - slcShp = shape.common(face) - slcArea = slcShp.BoundBox.XLength * slcShp.BoundBox.YLength - - if slcArea < midArea: - for W in slcShp.Wires: - if W.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - if len(slcShp.Wires) == 1: - wire = slcShp.Wires[0] - slc = Part.Face(wire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - else: - fL = list() - for W in slcShp.Wires: - slc = Part.Face(W) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - fL.append(slc) - comp = Part.makeCompound(fL) - if self.showDebugObjects is True: - PathLog.debug('*** tmpSliceCompound') - P = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpSliceCompound') - P.Shape = comp - P.purgeTouched() - self.tempGroup.addObject(P) - return comp - - PathLog.debug(' -slcArea !< midArea') - PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) - return False - - def _getProjectedFace(self, wire): - import Draft - PathLog.debug('_getProjectedFace()') - F = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpProjectionWire') - F.Shape = wire - F.purgeTouched() - self.tempGroup.addObject(F) - try: - prj = Draft.makeShape2DView(F, FreeCAD.Vector(0, 0, 1)) - prj.recompute() - prj.purgeTouched() - self.tempGroup.addObject(prj) - except Exception as ee: - PathLog.error(str(ee)) - return False - else: - pWire = Part.Wire(prj.Shape.Edges) - if pWire.isClosed() is False: - # PathLog.debug(' -pWire.isClosed() is False') - return False - slc = Part.Face(pWire) - slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) - return slc - - def _getCrossSection(self, shape, withExtrude=False): - PathLog.debug('_getCrossSection()') - wires = list() - bb = shape.BoundBox - mid = (bb.ZMin + bb.ZMax) / 2.0 - - for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): - wires.append(i) - - if len(wires) > 0: - comp = Part.Compound(wires) # produces correct cross-section wire ! - comp.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - comp.BoundBox.ZMin)) - csWire = comp.Wires[0] - if csWire.isClosed() is False: - PathLog.debug(' -comp.Wires[0] is not closed') - return False - if withExtrude is True: - ext = self._getExtrudedShape(csWire) - CS = self._getShapeSlice(ext) - if CS is False: - return False - else: - CS = Part.Face(csWire) - CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) - return CS - else: - PathLog.debug(' -No wires from .slice() method') - - return False - - def _getShapeEnvelope(self, shape): - PathLog.debug('_getShapeEnvelope()') - - wBB = shape.BoundBox - extFwd = wBB.ZLength + 10.0 - minz = wBB.ZMin - maxz = wBB.ZMin + extFwd - stpDwn = (maxz - minz) / 4.0 - dep_par = PathUtils.depth_params(maxz + 5.0, maxz + 3.0, maxz, stpDwn, 0.0, minz) - - try: - env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape - except Exception as ee: - PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) - return False - else: - return env - - def _getSliceFromEnvelope(self, env): - PathLog.debug('_getSliceFromEnvelope()') - eBB = env.BoundBox - extFwd = eBB.ZLength + 10.0 - maxz = eBB.ZMin + extFwd - - emax = math.floor(maxz - 1.0) - E = list() - for e in range(0, len(env.Edges)): - emin = env.Edges[e].BoundBox.ZMin - if emin > emax: - E.append(env.Edges[e]) - tf = Part.Face(Part.Wire(Part.__sortEdges__(E))) - tf.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - tf.BoundBox.ZMin)) - - return tf - def _prepareModelSTLs(self, JOB, obj): PathLog.debug('_prepareModelSTLs()') for m in range(0, len(JOB.Model.Group)): M = JOB.Model.Group[m] + # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") if self.modelTypes[m] == 'M': + # TODO: test if this works facets = M.Mesh.Facets.Points else: facets = Part.getFacets(M.Shape) @@ -1461,18 +643,18 @@ class ObjectWaterline(PathOp.ObjectOp): if self.modelSTLs[m] is True: stl = ocl.STLSurf() - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - self.modelSTLs[m] = stl + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + self.modelSTLs[m] = stl return def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... Creates and OCL.stl object with combined data with waste stock, - model, and avoided faces. Travel lines can be checked against this + model, and avoided faces. Travel lines can be checked against this STL object to determine minimum travel height to clear stock and model.''' PathLog.debug('_makeSafeSTL()') @@ -1491,7 +673,7 @@ class ObjectWaterline(PathOp.ObjectOp): zmax = mBB.ZMin + extFwd stpDwn = (zmax - zmin) / 4.0 dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) - + try: envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape cont = True @@ -1606,480 +788,6 @@ class ObjectWaterline(PathOp.ObjectOp): return final # Methods for creating path geometry - def _pathGeomToLinesPointSet(self, obj, compGeoShp): - '''_pathGeomToLinesPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings.''' - PathLog.debug('_pathGeomToLinesPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - chkGap = False - lnCnt = 0 - ec = len(compGeoShp.Edges) - cutClimb = self.CutClimb - toolDiam = 2.0 * self.radius - cpa = obj.CutPatternAngle - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if cutClimb is True: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - else: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - inLine.append(tup) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - - for ei in range(1, ec): - chkGap = False - edg = compGeoShp.Edges[ei] # Get edge for vertexes - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) # vertex 0 - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) # vertex 1 - - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (first / middle point) - # iC = sp.isOnLineSegment(ep, cp) - iC = cp.isOnLineSegment(sp, ep) - if iC is True: - inLine.append('BRK') - chkGap = True - else: - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - lnCnt += 1 - inLine = list() # reset collinear container - if cutClimb is True: - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - else: - sp = ep - - if cutClimb is True: - tup = (v2, v1) - if chkGap is True: - gap = abs(toolDiam - lst.sub(ep).Length) - lst = cp - else: - tup = (v1, v2) - if chkGap is True: - gap = abs(toolDiam - lst.sub(cp).Length) - lst = ep - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - tup = (vA, tup[1]) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - if cutClimb is True: - inLine.reverse() - LINES.append(inLine) # Save inLine segments - - # Handle last inLine set, reversing it. - if obj.CutPatternReversed is True: - if cpa != 0.0 and cpa % 90.0 == 0.0: - F = LINES.pop(0) - rev = list() - for iL in F: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - rev.reverse() - LINES.insert(0, rev) - - isEven = lnCnt % 2 - if isEven == 0: - PathLog.debug('Line count is ODD.') - else: - PathLog.debug('Line count is even.') - - return LINES - - def _pathGeomToZigzagPointSet(self, obj, compGeoShp): - '''_pathGeomToZigzagPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directionally-oriented collinear groupings - with a ZigZag directional indicator included for each collinear group.''' - PathLog.debug('_pathGeomToZigzagPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - lnCnt = 0 - chkGap = False - ec = len(compGeoShp.Edges) - toolDiam = 2.0 * self.radius - - if self.CutClimb is True: - dirFlg = -1 - else: - dirFlg = 1 - - edg0 = compGeoShp.Edges[0] - p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) - p2 = (edg0.Vertexes[1].X, edg0.Vertexes[1].Y) - if dirFlg == 1: - tup = (p1, p2) - lst = FreeCAD.Vector(p2[0], p2[1], 0.0) - sp = FreeCAD.Vector(p1[0], p1[1], 0.0) # start point - else: - tup = (p2, p1) - lst = FreeCAD.Vector(p1[0], p1[1], 0.0) - sp = FreeCAD.Vector(p2[0], p2[1], 0.0) # start point - inLine.append(tup) - - for ei in range(1, ec): - edg = compGeoShp.Edges[ei] - v1 = (edg.Vertexes[0].X, edg.Vertexes[0].Y) - v2 = (edg.Vertexes[1].X, edg.Vertexes[1].Y) - - cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) - ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - # iC = sp.isOnLineSegment(ep, cp) - iC = cp.isOnLineSegment(sp, ep) - if iC is True: - inLine.append('BRK') - chkGap = True - gap = abs(toolDiam - lst.sub(cp).Length) - else: - chkGap = False - if dirFlg == -1: - inLine.reverse() - LINES.append((dirFlg, inLine)) - lnCnt += 1 - dirFlg = -1 * dirFlg # Change zig to zag - inLine = list() # reset collinear container - sp = cp # FreeCAD.Vector(v1[0], v1[1], 0.0) - - lst = ep - if dirFlg == 1: - tup = (v1, v2) - else: - tup = (v2, v1) - - if chkGap is True: - if gap < obj.GapThreshold.Value: - b = inLine.pop() # pop off 'BRK' marker - (vA, vB) = inLine.pop() # pop off previous line segment for combining with current - if dirFlg == 1: - tup = (vA, tup[1]) - else: - #tup = (vA, tup[1]) - #tup = (tup[1], vA) - tup = (tup[0], vB) - self.closedGap = True - else: - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - inLine.append(tup) - # Efor - lnCnt += 1 - - # Fix directional issue with LAST line when line count is even - isEven = lnCnt % 2 - if isEven == 0: # Changed to != with 90 degree CutPatternAngle - PathLog.debug('Line count is even.') - else: - PathLog.debug('Line count is ODD.') - dirFlg = -1 * dirFlg - if obj.CutPatternReversed is False: - if self.CutClimb is True: - dirFlg = -1 * dirFlg - - if obj.CutPatternReversed is True: - dirFlg = -1 * dirFlg - - # Handle last inLine list - if dirFlg == 1: - rev = list() - for iL in inLine: - if iL == 'BRK': - rev.append(iL) - else: - (p1, p2) = iL - rev.append((p2, p1)) - - if obj.CutPatternReversed is False: - rev.reverse() - else: - rev2 = list() - for iL in rev: - if iL == 'BRK': - rev2.append(iL) - else: - (p1, p2) = iL - rev2.append((p2, p1)) - rev2.reverse() - rev = rev2 - - LINES.append((dirFlg, rev)) - else: - LINES.append((dirFlg, inLine)) - - return LINES - - def _pathGeomToArcPointSet(self, obj, compGeoShp): - '''_pathGeomToArcPointSet(obj, compGeoShp)... - Convert a compound set of arcs/circles to a set of directionally-oriented arc end points - and the corresponding center point.''' - # Extract intersection line segments for return value as list() - PathLog.debug('_pathGeomToArcPointSet()') - ARCS = list() - stpOvrEI = list() - segEI = list() - isSame = False - sameRad = None - COM = self.tmpCOM - toolDiam = 2.0 * self.radius - ec = len(compGeoShp.Edges) - - def gapDist(sp, ep): - X = (ep[0] - sp[0])**2 - Y = (ep[1] - sp[1])**2 - # Z = (ep[2] - sp[2])**2 - # return math.sqrt(X + Y + Z) - return math.sqrt(X + Y) # the 'z' value is zero in both points - - # Separate arc data into Loops and Arcs - for ei in range(0, ec): - edg = compGeoShp.Edges[ei] - if edg.Closed is True: - stpOvrEI.append(('L', ei, False)) - else: - if isSame is False: - segEI.append(ei) - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - else: - # Check if arc is co-radial to current SEGS - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - if abs(sameRad - pnt.sub(COM).Length) > 0.00001: - isSame = False - - if isSame is True: - segEI.append(ei) - else: - # Move co-radial arc segments - stpOvrEI.append(['A', segEI, False]) - # Start new list of arc segments - segEI = [ei] - isSame = True - pnt = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) - sameRad = pnt.sub(COM).Length - # Process trailing `segEI` data, if available - if isSame is True: - stpOvrEI.append(['A', segEI, False]) - - # Identify adjacent arcs with y=0 start/end points that connect - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'A': - startOnAxis = list() - endOnAxis = list() - EI = SO[1] # list of corresponding compGeoShp.Edges indexes - - # Identify startOnAxis and endOnAxis arcs - for i in range(0, len(EI)): - ei = EI[i] # edge index - E = compGeoShp.Edges[ei] # edge object - if abs(COM.y - E.Vertexes[0].Y) < 0.00001: - startOnAxis.append((i, ei, E.Vertexes[0])) - elif abs(COM.y - E.Vertexes[1].Y) < 0.00001: - endOnAxis.append((i, ei, E.Vertexes[1])) - - # Look for connections between startOnAxis and endOnAxis arcs. Consolidate data when connected - lenSOA = len(startOnAxis) - lenEOA = len(endOnAxis) - if lenSOA > 0 and lenEOA > 0: - for soa in range(0, lenSOA): - (iS, eiS, vS) = startOnAxis[soa] - for eoa in range(0, len(endOnAxis)): - (iE, eiE, vE) = endOnAxis[eoa] - dist = vE.X - vS.X - if abs(dist) < 0.00001: # They connect on axis at same radius - SO[2] = (eiE, eiS) - break - elif dist > 0: - break # stop searching - # Eif - # Eif - # Efor - - # Construct arc data tuples for OCL - dirFlg = 1 - # cutPat = obj.CutPattern - if self.CutClimb is False: # True yields Climb when set to Conventional - dirFlg = -1 - - # Cycle through stepOver data - for so in range(0, len(stpOvrEI)): - SO = stpOvrEI[so] - if SO[0] == 'L': # L = Loop/Ring/Circle - # PathLog.debug("SO[0] == 'Loop'") - lei = SO[1] # loop Edges index - v1 = compGeoShp.Edges[lei].Vertexes[0] - - # space = obj.SampleInterval.Value / 2.0 - space = 0.0000001 - - # p1 = FreeCAD.Vector(v1.X, v1.Y, v1.Z) - p1 = FreeCAD.Vector(v1.X, v1.Y, 0.0) # z=0.0 for waterline; z=v1.Z for 3D Surface - rad = p1.sub(COM).Length - spcRadRatio = space/rad - if spcRadRatio < 1.0: - tolrncAng = math.asin(spcRadRatio) - else: - tolrncAng = 0.9999998 * math.pi - EX = COM.x + (rad * math.cos(tolrncAng)) - EY = v1.Y - space # rad * math.sin(tolrncAng) - - sp = (v1.X, v1.Y, 0.0) - ep = (EX, EY, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - ARCS.append(('L', dirFlg, [arc])) - else: # SO[0] == 'A' A = Arc - # PathLog.debug("SO[0] == 'Arc'") - PRTS = list() - EI = SO[1] # list of corresponding Edges indexes - CONN = SO[2] # list of corresponding connected edges tuples (iE, iS) - chkGap = False - lst = None - - if CONN is not False: - (iE, iS) = CONN - v1 = compGeoShp.Edges[iE].Vertexes[0] - v2 = compGeoShp.Edges[iS].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - lst = sp - PRTS.append(arc) - # Pop connected edge index values from arc segments index list - iEi = EI.index(iE) - iSi = EI.index(iS) - if iEi > iSi: - EI.pop(iEi) - EI.pop(iSi) - else: - EI.pop(iSi) - EI.pop(iEi) - if len(EI) > 0: - PRTS.append('BRK') - chkGap = True - cnt = 0 - for ei in EI: - if cnt > 0: - PRTS.append('BRK') - chkGap = True - v1 = compGeoShp.Edges[ei].Vertexes[0] - v2 = compGeoShp.Edges[ei].Vertexes[1] - sp = (v1.X, v1.Y, 0.0) - ep = (v2.X, v2.Y, 0.0) - cp = (COM.x, COM.y, 0.0) - if dirFlg == 1: - arc = (sp, ep, cp) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, sp)) # abs(toolDiam - lst.sub(sp).Length) - lst = ep - else: - arc = (ep, sp, cp) # OCL.Arc(firstPnt, lastPnt, centerPnt, dir=True(CCW direction)) - if chkGap is True: - gap = abs(toolDiam - gapDist(lst, ep)) # abs(toolDiam - lst.sub(ep).Length) - lst = sp - if chkGap is True: - if gap < obj.GapThreshold.Value: - PRTS.pop() # pop off 'BRK' marker - (vA, vB, vC) = PRTS.pop() # pop off previous arc segment for combining with current - arc = (vA, arc[1], vC) - self.closedGap = True - else: - # PathLog.debug('---- Gap: {} mm'.format(gap)) - gap = round(gap, 6) - if gap < self.gaps[0]: - self.gaps.insert(0, gap) - self.gaps.pop() - PRTS.append(arc) - cnt += 1 - - if dirFlg == -1: - PRTS.reverse() - - ARCS.append(('A', dirFlg, PRTS)) - # Eif - if obj.CutPattern == 'CircularZigZag': - dirFlg = -1 * dirFlg - # Efor - - return ARCS - - def _pathGeomToSpiralPointSet(self, obj, compGeoShp): - '''_pathGeomToSpiralPointSet(obj, compGeoShp)... - Convert a compound set of sequential line segments to directional, connected groupings.''' - PathLog.debug('_pathGeomToSpiralPointSet()') - # Extract intersection line segments for return value as list() - LINES = list() - inLine = list() - lnCnt = 0 - ec = len(compGeoShp.Edges) - start = 2 - - if obj.CutPatternReversed: - edg1 = compGeoShp.Edges[0] # Skip first edge, as it is the closing edge: center to outer tail - ec -= 1 - start = 1 - else: - edg1 = compGeoShp.Edges[1] # Skip first edge, as it is the closing edge: center to outer tail - p1 = FreeCAD.Vector(edg1.Vertexes[0].X, edg1.Vertexes[0].Y, 0.0) - p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0) - tup = ((p1.x, p1.y), (p2.x, p2.y)) - inLine.append(tup) - lst = p2 - - for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1 - edg = compGeoShp.Edges[ei] # Get edge for vertexes - sp = FreeCAD.Vector(edg.Vertexes[0].X, edg.Vertexes[0].Y, 0.0) # check point (first / middle point) - ep = FreeCAD.Vector(edg.Vertexes[1].X, edg.Vertexes[1].Y, 0.0) # end point - tup = ((sp.x, sp.y), (ep.x, ep.y)) - - if sp.sub(p2).Length < 0.000001: - inLine.append(tup) - else: - LINES.append(inLine) # Save inLine segments - lnCnt += 1 - inLine = list() # reset container - inLine.append(tup) - p1 = sp - p2 = ep - # Efor - - lnCnt += 1 - LINES.append(inLine) # Save inLine segments - - return LINES - def _getExperimentalWaterlinePaths(self, PNTSET, csHght, cutPattern): '''_getExperimentalWaterlinePaths(PNTSET, csHght, cutPattern)... Switching function for calling the appropriate path-geometry to OCL points conversion function @@ -2217,13 +925,10 @@ class ObjectWaterline(PathOp.ObjectOp): return cmds - def _planarGetPDC(self, stl, finalDep, SampleInterval, useSafeCutter=False): + def _planarGetPDC(self, stl, finalDep, SampleInterval, cutter): pdc = ocl.PathDropCutter() # create a pdc [PathDropCutter] object pdc.setSTL(stl) # add stl model - if useSafeCutter is True: - pdc.setCutter(self.safeCutter) # add safeCutter - else: - pdc.setCutter(self.cutter) # add cutter + pdc.setCutter(cutter) # add cutter pdc.setZ(finalDep) # set minimumZ (final / target depth value) pdc.setSampling(SampleInterval) # set sampling size return pdc @@ -2613,18 +1318,18 @@ class ObjectWaterline(PathOp.ObjectOp): PathLog.debug('Experimental Waterline depthparams:\n{}'.format(depthparams)) # Prepare PathDropCutter objects with STL data - # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, useSafeCutter=False) + # safePDC = self._planarGetPDC(safeSTL, depthparams[lenDP - 1], obj.SampleInterval.Value, self.cutter) buffer = self.cutter.getDiameter() * 10.0 borderFace = Part.Face(self._makeExtendedBoundBox(JOB.Stock.Shape.BoundBox, buffer, 0.0)) # Get correct boundbox if obj.BoundBox == 'Stock': - stockEnv = self._getShapeEnvelope(JOB.Stock.Shape) - bbFace = self._getCrossSection(stockEnv) # returned at Z=0.0 + stockEnv = PathSurfaceSupport.getShapeEnvelope(JOB.Stock.Shape) + bbFace = PathSurfaceSupport.getCrossSection(stockEnv) # returned at Z=0.0 elif obj.BoundBox == 'BaseBoundBox': - baseEnv = self._getShapeEnvelope(base.Shape) - bbFace = self._getCrossSection(baseEnv) # returned at Z=0.0 + baseEnv = PathSurfaceSupport.getShapeEnvelope(base.Shape) + bbFace = PathSurfaceSupport.getCrossSection(baseEnv) # returned at Z=0.0 trimFace = borderFace.cut(bbFace) if self.showDebugObjects is True: @@ -2675,7 +1380,7 @@ class ObjectWaterline(PathOp.ObjectOp): CA.Shape = activeArea CA.purgeTouched() self.tempGroup.addObject(CA) - ofstArea = self._extractFaceOffset(activeArea, ofst, makeComp=False) + ofstArea = PathSurfaceSupport.extractFaceOffset(activeArea, ofst, self.wpc, makeComp=False) if not ofstArea: data = FreeCAD.Units.Quantity(csHght, FreeCAD.Units.Length).UserString PathLog.debug('No offset area returned for cut area depth at {}.'.format(data)) @@ -2824,8 +1529,8 @@ class ObjectWaterline(PathOp.ObjectOp): PGG = PathSurfaceSupport.PathGeometryGenerator(obj, clrAreaShp, cutPattern) if self.showDebugObjects: PGG.setDebugObjectsGroup(self.tempGroup) - self.tmpCOM = PGG.getCenterOfMass() - pathGeom = PGG.getPathGeometryGenerator() + self.tmpCOM = PGG.getCenterOfPattern() + pathGeom = PGG.generatePathGeometry() if not pathGeom: PathLog.warning('No path geometry generated.') return commands @@ -2838,13 +1543,13 @@ class ObjectWaterline(PathOp.ObjectOp): self.tempGroup.addObject(OA) if cutPattern == 'Line': - pntSet = self._pathGeomToLinesPointSet(obj, pathGeom) + pntSet = PathSurfaceSupport.pathGeomToLinesPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps) elif cutPattern == 'ZigZag': - pntSet = self._pathGeomToZigzagPointSet(obj, pathGeom) + pntSet = PathSurfaceSupport.pathGeomToZigzagPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps) elif cutPattern in ['Circular', 'CircularZigZag']: - pntSet = self._pathGeomToArcPointSet(obj, pathGeom) + pntSet = PathSurfaceSupport.pathGeomToCircularPointSet(obj, pathGeom, self.CutClimb, self.toolDiam, self.closedGap, self.gaps, self.tmpCOM) elif cutPattern == 'Spiral': - pntSet = self._pathGeomToSpiralPointSet(obj, pathGeom) + pntSet = PathSurfaceSupport.pathGeomToSpiralPointSet(obj, pathGeom) stpOVRS = self._getExperimentalWaterlinePaths(pntSet, csHght, cutPattern) safePDC = False @@ -2861,7 +1566,7 @@ class ObjectWaterline(PathOp.ObjectOp): cont = True cnt = 0 while cont: - ofstArea = self._extractFaceOffset(shape, ofst, makeComp=True) + ofstArea = PathSurfaceSupport.extractFaceOffset(shape, ofst, self.wpc, makeComp=True) if not ofstArea: break for F in ofstArea.Faces: @@ -2878,6 +1583,8 @@ class ObjectWaterline(PathOp.ObjectOp): GCODE = [Path.Command('N (Beginning of Single-pass layer.)', {})] tolrnc = JOB.GeometryTolerance.Value lenstpOVRS = len(stpOVRS) + lstSO = lenstpOVRS - 1 + lstStpOvr = False gDIR = ['G3', 'G2'] if self.CutClimb is True: @@ -2897,10 +1604,12 @@ class ObjectWaterline(PathOp.ObjectOp): first = PRTS[0][0] # first point of arc/line stepover group last = None cmds.append(Path.Command('N (Begin step {}.)'.format(so), {})) + if so == lstSO: + lstStpOvr = True if so > 0: if cutPattern == 'CircularZigZag': - if odd is True: + if odd: odd = False else: odd = True @@ -2926,8 +1635,10 @@ class ObjectWaterline(PathOp.ObjectOp): cmds.append(Path.Command('G1', {'X': start.x, 'Y': start.y, 'Z': start.z, 'F': self.horizFeed})) cmds.append(Path.Command('G1', {'X': last.x, 'Y': last.y, 'F': self.horizFeed})) elif cutPattern in ['Circular', 'CircularZigZag']: - start, last, centPnt, cMode = prt - gcode = self._makeGcodeArc(start, last, odd, gDIR, tolrnc) + # isCircle = True if lenPRTS == 1 else False + isZigZag = True if cutPattern == 'CircularZigZag' else False + PathLog.debug('so, isZigZag, odd, cMode: {}, {}, {}, {}'.format(so, isZigZag, odd, prt[3])) + gcode = self._makeGcodeArc(prt, gDIR, odd, isZigZag) cmds.extend(gcode) cmds.append(Path.Command('N (End of step {}.)'.format(so), {})) GCODE.extend(cmds) # save line commands @@ -3009,7 +1720,7 @@ class ObjectWaterline(PathOp.ObjectOp): return False def _getModelCrossSection(self, shape, csHght): - PathLog.debug('_getCrossSection()') + PathLog.debug('getCrossSection()') wires = list() def byArea(fc): @@ -3097,48 +1808,26 @@ class ObjectWaterline(PathOp.ObjectOp): return bb - def _makeGcodeArc(self, strtPnt, endPnt, odd, gDIR, tolrnc): + def _makeGcodeArc(self, prt, gDIR, odd, isZigZag): cmds = list() - isCircle = False + strtPnt, endPnt, cntrPnt, cMode = prt gdi = 0 - if odd is True: + if odd: gdi = 1 - - # Test if pnt set is circle - if abs(strtPnt.x - endPnt.x) < tolrnc: - if abs(strtPnt.y - endPnt.y) < tolrnc: - isCircle = True - isCircle = False - - if isCircle is True: - # convert LN to G2/G3 arc, consolidating GCode - # https://wiki.shapeoko.com/index.php/G-Code#G2_-_clockwise_arc - # https://www.cnccookbook.com/cnc-g-code-arc-circle-g02-g03/ - # Dividing circle into two arcs allows for G2/G3 on inclined surfaces - - # ijk = self.tmpCOM - strtPnt # vector from start to center - ijk = self.tmpCOM - strtPnt # vector from start to center - xyz = self.tmpCOM.add(ijk) # end point - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, 'F': self.horizFeed})) - ijk = self.tmpCOM - xyz # vector from start to center - rst = strtPnt # end point - cmds.append(Path.Command(gDIR[gdi], {'X': rst.x, 'Y': rst.y, 'Z': rst.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) else: - # ijk = self.tmpCOM - strtPnt - ijk = self.tmpCOM.sub(strtPnt) # vector from start to center - xyz = endPnt - cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) - cmds.append(Path.Command(gDIR[gdi], {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, - 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height - 'F': self.horizFeed})) - cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) + if not cMode and isZigZag: + gdi = 1 + gCmd = gDIR[gdi] + + # ijk = self.tmpCOM - strtPnt + # ijk = self.tmpCOM.sub(strtPnt) # vector from start to center + ijk = cntrPnt.sub(strtPnt) # vector from start to center + xyz = endPnt + cmds.append(Path.Command('G1', {'X': strtPnt.x, 'Y': strtPnt.y, 'Z': strtPnt.z, 'F': self.horizFeed})) + cmds.append(Path.Command(gCmd, {'X': xyz.x, 'Y': xyz.y, 'Z': xyz.z, + 'I': ijk.x, 'J': ijk.y, 'K': ijk.z, # leave same xyz.z height + 'F': self.horizFeed})) + cmds.append(Path.Command('G1', {'X': endPnt.x, 'Y': endPnt.y, 'Z': endPnt.z, 'F': self.horizFeed})) return cmds @@ -3264,13 +1953,12 @@ class ObjectWaterline(PathOp.ObjectOp): def SetupProperties(): ''' SetupProperties() ... Return list of properties required for operation.''' setup = ['Algorithm', 'AvoidLastX_Faces', 'AvoidLastX_InternalFeatures', 'BoundBox'] - setup.extend(['BoundaryAdjustment', 'CircularCenterAt', 'CircularCenterCustom']) + setup.extend(['BoundaryAdjustment', 'PatternCenterAt', 'PatternCenterCustom']) setup.extend(['ClearLastLayer', 'InternalFeaturesCut', 'InternalFeaturesAdjustment']) setup.extend(['CutMode', 'CutPattern', 'CutPatternAngle', 'CutPatternReversed']) - setup.extend(['DepthOffset', 'GapSizes', 'GapThreshold']) + setup.extend(['DepthOffset', 'GapSizes', 'GapThreshold', 'StepOver']) setup.extend(['HandleMultipleFeatures', 'LayerMode', 'OptimizeStepOverTransitions']) - setup.extend(['ProfileEdges', 'BoundaryEnforcement', 'SampleInterval']) - setup.extend(['StartPoint', 'StepOver', 'IgnoreOuterAbove']) + setup.extend(['BoundaryEnforcement', 'SampleInterval', 'StartPoint', 'IgnoreOuterAbove']) setup.extend(['UseStartPoint', 'AngularDeflection', 'LinearDeflection', 'ShowTempObjects']) return setup From e65c61036b73fd63eaae53dcd24efba28b0ff944 Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 29 Feb 2020 11:32:21 +0100 Subject: [PATCH 074/142] [Draft] Annotation scale support and statusbar widget Initial commit Preliminary support for Draft Dimensions [Draft] Statusbar Widget code cleanup --- src/Mod/Draft/Draft.py | 14 +- src/Mod/Draft/InitGui.py | 4 + .../Draft/draftutils/init_draft_statusbar.py | 182 ++++++++++++++++++ 3 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 src/Mod/Draft/draftutils/init_draft_statusbar.py diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 5d0c5d140a..943691fc1e 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -3472,15 +3472,17 @@ class _ViewProviderDimension(_ViewProviderDraft): obj.addProperty("App::PropertyVectorDistance","TextPosition","Draft",QT_TRANSLATE_NOOP("App::Property","The position of the text. Leave (0,0,0) for automatic position")) obj.addProperty("App::PropertyString","Override","Draft",QT_TRANSLATE_NOOP("App::Property","Text override. Use $dim to insert the dimension length")) obj.addProperty("App::PropertyString","UnitOverride","Draft",QT_TRANSLATE_NOOP("App::Property","A unit to express the measurement. Leave blank for system default")) - obj.FontSize = getParam("textheight",0.20) - obj.TextSpacing = getParam("dimspacing",0.05) + param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + obj.FontSize = getParam("textheight",0.20) * annotation_scale + obj.TextSpacing = getParam("dimspacing",0.05) * annotation_scale obj.FontName = getParam("textfont","") - obj.ArrowSize = getParam("arrowsize",0.1) + obj.ArrowSize = getParam("arrowsize",0.1) * annotation_scale obj.ArrowType = arrowtypes obj.ArrowType = arrowtypes[getParam("dimsymbol",0)] - obj.ExtLines = getParam("extlines",0.3) - obj.DimOvershoot = getParam("dimovershoot",0) - obj.ExtOvershoot = getParam("extovershoot",0) + obj.ExtLines = getParam("extlines",0.3) * annotation_scale + obj.DimOvershoot = getParam("dimovershoot",0) * annotation_scale + obj.ExtOvershoot = getParam("extovershoot",0) * annotation_scale obj.Decimals = getParam("dimPrecision",2) obj.ShowUnit = getParam("showUnit",True) obj.ShowLine = True diff --git a/src/Mod/Draft/InitGui.py b/src/Mod/Draft/InitGui.py index 1141e911dd..0178ec4978 100644 --- a/src/Mod/Draft/InitGui.py +++ b/src/Mod/Draft/InitGui.py @@ -131,6 +131,8 @@ class DraftWorkbench(FreeCADGui.Workbench): FreeCADGui.draftToolBar.Activated() if hasattr(FreeCADGui, "Snapper"): FreeCADGui.Snapper.show() + import draftutils.init_draft_statusbar as dsb + dsb.show_draft_statusbar() FreeCAD.Console.PrintLog("Draft workbench activated.\n") def Deactivated(self): @@ -139,6 +141,8 @@ class DraftWorkbench(FreeCADGui.Workbench): FreeCADGui.draftToolBar.Deactivated() if hasattr(FreeCADGui, "Snapper"): FreeCADGui.Snapper.hide() + import draftutils.init_draft_statusbar as dsb + dsb.hide_draft_statusbar() FreeCAD.Console.PrintLog("Draft workbench deactivated.\n") def ContextMenu(self, recipient): diff --git a/src/Mod/Draft/draftutils/init_draft_statusbar.py b/src/Mod/Draft/draftutils/init_draft_statusbar.py new file mode 100644 index 0000000000..0b1babcb70 --- /dev/null +++ b/src/Mod/Draft/draftutils/init_draft_statusbar.py @@ -0,0 +1,182 @@ +"""Draft Statusbar commands. + +This module provide the code for the Draft Statusbar, activated by initGui +""" +## @package init_tools +# \ingroup DRAFT +# \brief This module provides the code for the Draft Statusbar. + +# *************************************************************************** +# * * +# * Copyright (c) 2020 Carlo Pavan * +# * * +# * 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 * +# * * +# *************************************************************************** + +import FreeCAD as App +import FreeCADGui as Gui +from PySide import QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP + + +#---------------------------------------------------------------------------- +# SCALE WIDGET FUNCTIONS +#---------------------------------------------------------------------------- + +def scale_to_label(scale): + """ + transform a float number into a 1:X or X:1 scale and return it as label + """ + f = scale.as_integer_ratio() + if f[0] == 1 or f[0] == 1: + label = str(f[0]) + ":" + str(f[1]) + return label + else: + return str(scale) + +def label_to_scale(label): + """ + transform a scale string into scale factor as float + """ + try : + scale = float(label) + return scale + except : + err = QT_TRANSLATE_NOOP("draft", + "Unable to convert input into a scale factor") + if ":" in label: + f = label.split(":") + try: + scale = float(f[0])/float(f[1]) + return scale + except: + App.Console.PrintWarning(err) + return None + if "/" in label: + f = label.split("/") + try: + scale = float(f[0])/float(f[1]) + return scale + except: + App.Console.PrintWarning(err) + return None + +def _set_scale(action): + """ + triggered by scale pushbutton, set DraftAnnotationScale in preferences + """ + # set the label of the scale button + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + + mw = Gui.getMainWindow() + sb = mw.statusBar() + statuswidget = sb.findChild(QtGui.QToolBar,"draft_status_widget") + if action.text() == QT_TRANSLATE_NOOP("draft","custom"): + custom_scale = QtGui.QInputDialog.getText(None, "Custom scale", "") + if custom_scale[1]: + print(custom_scale[0]) + scale = label_to_scale(custom_scale[0]) + if scale is None: return + param.SetFloat("DraftAnnotationScale", scale) + cs = scale_to_label(scale) + statuswidget.scaleLabel.setText(cs) + else: + text_scale = action.text() + statuswidget.scaleLabel.setText(text_scale) + scale = label_to_scale(text_scale) + param.SetFloat("DraftAnnotationScale", scale) + +#---------------------------------------------------------------------------- +# MAIN DRAFT STATUSBAR FUNCTIONS +#---------------------------------------------------------------------------- + +def init_draft_statusbar(sb): + """ + this function initializes draft statusbar + """ + + draft_scales = ["1:1000", "1:500", "1:250", "1:200", "1:100", + "1:50", "1:25","1:20", "1:10", "1:5","1:2", + "1:1", + "2:1", "5:1", "10:1", "20:1", + QT_TRANSLATE_NOOP("draft","custom"), + ] + + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + draft_annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + + statuswidget = QtGui.QToolBar() + statuswidget.setObjectName("draft_status_widget") + + # SCALE TOOL ------------------------------------------------------------- + statuswidget.draft_scales = draft_scales + scaleLabel = QtGui.QPushButton("Scale") + scaleLabel.setObjectName("ScaleLabel") + scaleLabel.setFlat(True) + menu = QtGui.QMenu(scaleLabel) + gUnits = QtGui.QActionGroup(menu) + for u in draft_scales: + a = QtGui.QAction(gUnits) + a.setText(u) + menu.addAction(a) + scaleLabel.setMenu(menu) + gUnits.triggered.connect(_set_scale) + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + draft_annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + scale_label = scale_to_label(draft_annotation_scale) + scaleLabel.setText(scale_label) + tooltip = "Set the scale used by draft annotation tools" + scaleLabel.setToolTip(QT_TRANSLATE_NOOP("draft",tooltip)) + statuswidget.addWidget(scaleLabel) + statuswidget.scaleLabel = scaleLabel + + # ADD TOOLS TO STATUS BAR ------------------------------------------------ + sb.addPermanentWidget(statuswidget) + statuswidget.show() + +def show_draft_statusbar(): + """ + shows draft statusbar if present or initializes it + """ + mw = Gui.getMainWindow() + if mw: + sb = mw.statusBar() + statuswidget = sb.findChild(QtGui.QToolBar,"draft_status_widget") + if statuswidget: + statuswidget.show() + else: + init_draft_statusbar(sb) + +def hide_draft_statusbar(): + """ + hides draft statusbar if present + """ + mw = Gui.getMainWindow() + if mw: + sb = mw.statusBar() + statuswidget = sb.findChild(QtGui.QToolBar,"draft_status_widget") + if statuswidget: + statuswidget.hide() + else: + # when switching workbenches, the toolbar sometimes "jumps" + # out of the status bar to any other dock area... + statuswidget = mw.findChild(QtGui.QToolBar,"draft_status_widget") + if statuswidget: + statuswidget.hide() \ No newline at end of file From 1d7b62e6ee5737e36890e0885a9e58ab4d8c889a Mon Sep 17 00:00:00 2001 From: carlopav Date: Sun, 1 Mar 2020 12:51:45 +0100 Subject: [PATCH 075/142] [Draft] First implementation of annotation scale The changes are minimal, mainly is just cleanup of existing code and documentation. [Draft] Annotation scale for ViewProviderDraftText Adding support for scale factor according to changes in dimensions. [Draft] Annotation scale for ViewProviderDraftLabel completed annotations scale [Draft] Annotation scale for ViewProviderAngularDimension --- src/Mod/Draft/Draft.py | 648 ++++++++++++++++++++++++++++------------- 1 file changed, 439 insertions(+), 209 deletions(-) diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 943691fc1e..130f1ecaf3 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -3452,37 +3452,121 @@ class _Dimension(_DraftObject): class _ViewProviderDimension(_ViewProviderDraft): - """A View Provider for the Draft Dimension object""" - def __init__(self, obj): - obj.addProperty("App::PropertyLength","FontSize","Draft",QT_TRANSLATE_NOOP("App::Property","Font size")) - obj.addProperty("App::PropertyInteger","Decimals","Draft",QT_TRANSLATE_NOOP("App::Property","The number of decimals to show")) - obj.addProperty("App::PropertyLength","ArrowSize","Draft",QT_TRANSLATE_NOOP("App::Property","Arrow size")) - obj.addProperty("App::PropertyLength","TextSpacing","Draft",QT_TRANSLATE_NOOP("App::Property","The spacing between the text and the dimension line")) - obj.addProperty("App::PropertyEnumeration","ArrowType","Draft",QT_TRANSLATE_NOOP("App::Property","Arrow type")) - obj.addProperty("App::PropertyFont","FontName","Draft",QT_TRANSLATE_NOOP("App::Property","Font name")) - obj.addProperty("App::PropertyFloat","LineWidth","Draft",QT_TRANSLATE_NOOP("App::Property","Line width")) - obj.addProperty("App::PropertyColor","LineColor","Draft",QT_TRANSLATE_NOOP("App::Property","Line color")) - obj.addProperty("App::PropertyDistance","ExtLines","Draft",QT_TRANSLATE_NOOP("App::Property","Length of the extension lines")) - obj.addProperty("App::PropertyDistance","DimOvershoot","Draft",QT_TRANSLATE_NOOP("App::Property","The distance the dimension line is extended past the extension lines")) - obj.addProperty("App::PropertyDistance","ExtOvershoot","Draft",QT_TRANSLATE_NOOP("App::Property","Length of the extension line above the dimension line")) - obj.addProperty("App::PropertyBool","FlipArrows","Draft",QT_TRANSLATE_NOOP("App::Property","Rotate the dimension arrows 180 degrees")) - obj.addProperty("App::PropertyBool","FlipText","Draft",QT_TRANSLATE_NOOP("App::Property","Rotate the dimension text 180 degrees")) - obj.addProperty("App::PropertyBool","ShowUnit","Draft",QT_TRANSLATE_NOOP("App::Property","Show the unit suffix")) - obj.addProperty("App::PropertyBool","ShowLine","Draft",QT_TRANSLATE_NOOP("App::Property","Shows the dimension line and arrows")) - obj.addProperty("App::PropertyVectorDistance","TextPosition","Draft",QT_TRANSLATE_NOOP("App::Property","The position of the text. Leave (0,0,0) for automatic position")) - obj.addProperty("App::PropertyString","Override","Draft",QT_TRANSLATE_NOOP("App::Property","Text override. Use $dim to insert the dimension length")) - obj.addProperty("App::PropertyString","UnitOverride","Draft",QT_TRANSLATE_NOOP("App::Property","A unit to express the measurement. Leave blank for system default")) + """ + A View Provider for the Draft Dimension object + + DIMENSION VIEW PROVIDER: + + | txt | e + ----o--------------------------------o----- + | | + | | d + | | + + a b c b a + + a = DimOvershoot (vobj) + b = Arrows (vobj) + c = Dimline (obj) + d = ExtLines (vobj) + e = ExtOvershoot (vobj) + txt = label (vobj) + + STRUCTURE: + vobj.node.color + .drawstyle + .lineswitch1.coords + .line + .marks + .marksDimOvershoot + .marksExtOvershoot + .label.textpos + .color + .font + .text + + vobj.node3d.color + .drawstyle + .lineswitch3.coords + .line + .marks + .marksDimOvershoot + .marksExtOvershoot + .label3d.textpos + .color + .font3d + .text3d + + """ + def __init__(self, obj): + # general properties + obj.addProperty("App::PropertyFloat","ScaleMultiplier","Draft", + QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) + obj.addProperty("App::PropertyFloat","LineWidth", + "Draft",QT_TRANSLATE_NOOP("App::Property","Line width")) + obj.addProperty("App::PropertyColor","LineColor", + "Draft",QT_TRANSLATE_NOOP("App::Property","Line color")) + # text properties + obj.addProperty("App::PropertyFont","FontName", + "Draft",QT_TRANSLATE_NOOP("App::Property","Font name")) + obj.addProperty("App::PropertyLength","FontSize", + "Draft",QT_TRANSLATE_NOOP("App::Property","Font size")) + obj.addProperty("App::PropertyLength","TextSpacing", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "The spacing between the text and the dimension line")) + obj.addProperty("App::PropertyBool","FlipText", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension text 180 degrees")) + obj.addProperty("App::PropertyVectorDistance","TextPosition", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "The position of the text. Leave (0,0,0) for automatic position")) + obj.addProperty("App::PropertyString","Override", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Text override. Use $dim to insert the dimension length")) + # units properties + obj.addProperty("App::PropertyInteger","Decimals", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "The number of decimals to show")) + obj.addProperty("App::PropertyBool","ShowUnit", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Show the unit suffix")) + obj.addProperty("App::PropertyString","UnitOverride", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "A unit to express the measurement. Leave blank for system default")) + # graphics properties + obj.addProperty("App::PropertyLength","ArrowSize", + "Draft",QT_TRANSLATE_NOOP("App::Property","Arrow size")) + obj.addProperty("App::PropertyEnumeration","ArrowType", + "Draft",QT_TRANSLATE_NOOP("App::Property","Arrow type")) + obj.addProperty("App::PropertyBool","FlipArrows", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension arrows 180 degrees")) + obj.addProperty("App::PropertyDistance","DimOvershoot", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "The distance the dimension line is extended past the extension lines")) + obj.addProperty("App::PropertyDistance","ExtLines", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Length of the extension lines")) + obj.addProperty("App::PropertyDistance","ExtOvershoot", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Length of the extension line above the dimension line")) + obj.addProperty("App::PropertyBool","ShowLine", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Shows the dimension line and arrows")) + param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) - obj.FontSize = getParam("textheight",0.20) * annotation_scale - obj.TextSpacing = getParam("dimspacing",0.05) * annotation_scale + obj.ScaleMultiplier = 1 / annotation_scale + obj.FontSize = getParam("textheight",0.20) + obj.TextSpacing = getParam("dimspacing",0.05) obj.FontName = getParam("textfont","") - obj.ArrowSize = getParam("arrowsize",0.1) * annotation_scale + obj.ArrowSize = getParam("arrowsize",0.1) obj.ArrowType = arrowtypes obj.ArrowType = arrowtypes[getParam("dimsymbol",0)] - obj.ExtLines = getParam("extlines",0.3) * annotation_scale - obj.DimOvershoot = getParam("dimovershoot",0) * annotation_scale - obj.ExtOvershoot = getParam("extovershoot",0) * annotation_scale + obj.ExtLines = getParam("extlines",0.3) + obj.DimOvershoot = getParam("dimovershoot",0) + obj.ExtOvershoot = getParam("extovershoot",0) obj.Decimals = getParam("dimPrecision",2) obj.ShowUnit = getParam("showUnit",True) obj.ShowLine = True @@ -3608,7 +3692,7 @@ class _ViewProviderDimension(_ViewProviderDraft): self.p3 = self.p4 if proj: if hasattr(obj.ViewObject,"ExtLines"): - dmax = obj.ViewObject.ExtLines.Value + dmax = obj.ViewObject.ExtLines.Value * obj.ViewObject.ScaleMultiplier if dmax and (proj.Length > dmax): if (dmax > 0): self.p1 = self.p2.add(DraftVecUtils.scaleTo(proj,dmax)) @@ -3669,7 +3753,8 @@ class _ViewProviderDimension(_ViewProviderDraft): self.transExtOvershoot1.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) self.transExtOvershoot2.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) if hasattr(obj.ViewObject,"TextSpacing"): - offset = DraftVecUtils.scaleTo(v1,obj.ViewObject.TextSpacing.Value) + ts = obj.ViewObject.TextSpacing.Value * obj.ViewObject.ScaleMultiplier + offset = DraftVecUtils.scaleTo(v1,ts) else: offset = DraftVecUtils.scaleTo(v1,0.05) rott = rot1 @@ -3738,120 +3823,61 @@ class _ViewProviderDimension(_ViewProviderDraft): def onChanged(self, vobj, prop): """called when a view property has changed""" - - if (prop == "FontSize") and hasattr(vobj,"FontSize"): + if prop == "ScaleMultiplier" and hasattr(vobj,"ScaleMultiplier"): + # update all dimension values if hasattr(self,"font"): - self.font.size = vobj.FontSize.Value + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier if hasattr(self,"font3d"): - self.font3d.size = vobj.FontSize.Value*100 + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + if hasattr(self,"node") and hasattr(self,"p2") and hasattr(vobj,"ArrowSize"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + if hasattr(vobj,"DimOvershoot"): + self.remove_dim_overshoot() + self.draw_dim_overshoot(vobj) + if hasattr(vobj,"ExtOvershoot"): + self.remove_ext_overshoot() + self.draw_ext_overshoot(vobj) + self.updateData(vobj.Object,"Start") vobj.Object.touch() + + elif (prop == "FontSize") and hasattr(vobj,"FontSize"): + if hasattr(self,"font"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + vobj.Object.touch() + elif (prop == "FontName") and hasattr(vobj,"FontName"): if hasattr(self,"font") and hasattr(self,"font3d"): self.font.name = self.font3d.name = str(vobj.FontName) vobj.Object.touch() + elif (prop == "LineColor") and hasattr(vobj,"LineColor"): if hasattr(self,"color"): c = vobj.LineColor self.color.rgb.setValue(c[0],c[1],c[2]) + elif (prop == "LineWidth") and hasattr(vobj,"LineWidth"): if hasattr(self,"drawstyle"): self.drawstyle.lineWidth = vobj.LineWidth + elif (prop in ["ArrowSize","ArrowType"]) and hasattr(vobj,"ArrowSize"): if hasattr(self,"node") and hasattr(self,"p2"): - from pivy import coin - - if not hasattr(vobj,"ArrowType"): - return - - if self.p3.x < self.p2.x: - inv = False - else: - inv = True - - # set scale - symbol = arrowtypes.index(vobj.ArrowType) - s = vobj.ArrowSize.Value - self.trans1.scaleFactor.setValue((s,s,s)) - self.trans2.scaleFactor.setValue((s,s,s)) - - # remove existing nodes - self.node.removeChild(self.marks) - self.node3d.removeChild(self.marks) - - # set new nodes - self.marks = coin.SoSeparator() - self.marks.addChild(self.color) - s1 = coin.SoSeparator() - if symbol == "Circle": - s1.addChild(self.coord1) - else: - s1.addChild(self.trans1) - s1.addChild(dimSymbol(symbol,invert=not(inv))) - self.marks.addChild(s1) - s2 = coin.SoSeparator() - if symbol == "Circle": - s2.addChild(self.coord2) - else: - s2.addChild(self.trans2) - s2.addChild(dimSymbol(symbol,invert=inv)) - self.marks.addChild(s2) - self.node.insertChild(self.marks,2) - self.node3d.insertChild(self.marks,2) + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) vobj.Object.touch() + elif (prop == "DimOvershoot") and hasattr(vobj,"DimOvershoot"): - from pivy import coin - - # set scale - s = vobj.DimOvershoot.Value - self.transDimOvershoot1.scaleFactor.setValue((s,s,s)) - self.transDimOvershoot2.scaleFactor.setValue((s,s,s)) - - # remove existing nodes - self.node.removeChild(self.marksDimOvershoot) - self.node3d.removeChild(self.marksDimOvershoot) - - # set new nodes - self.marksDimOvershoot = coin.SoSeparator() - if vobj.DimOvershoot.Value: - self.marksDimOvershoot.addChild(self.color) - s1 = coin.SoSeparator() - s1.addChild(self.transDimOvershoot1) - s1.addChild(dimDash((-1,0,0),(0,0,0))) - self.marksDimOvershoot.addChild(s1) - s2 = coin.SoSeparator() - s2.addChild(self.transDimOvershoot2) - s2.addChild(dimDash((0,0,0),(1,0,0))) - self.marksDimOvershoot.addChild(s2) - self.node.insertChild(self.marksDimOvershoot,2) - self.node3d.insertChild(self.marksDimOvershoot,2) + self.remove_dim_overshoot() + self.draw_dim_overshoot(vobj) vobj.Object.touch() + elif (prop == "ExtOvershoot") and hasattr(vobj,"ExtOvershoot"): - from pivy import coin - - # set scale - s = vobj.ExtOvershoot.Value - self.transExtOvershoot1.scaleFactor.setValue((s,s,s)) - self.transExtOvershoot2.scaleFactor.setValue((s,s,s)) - - # remove existing nodes - self.node.removeChild(self.marksExtOvershoot) - self.node3d.removeChild(self.marksExtOvershoot) - - # set new nodes - self.marksExtOvershoot = coin.SoSeparator() - if vobj.ExtOvershoot.Value: - self.marksExtOvershoot.addChild(self.color) - s1 = coin.SoSeparator() - s1.addChild(self.transExtOvershoot1) - s1.addChild(dimDash((0,0,0),(-1,0,0))) - self.marksExtOvershoot.addChild(s1) - s2 = coin.SoSeparator() - s2.addChild(self.transExtOvershoot2) - s2.addChild(dimDash((0,0,0),(-1,0,0))) - self.marksExtOvershoot.addChild(s2) - self.node.insertChild(self.marksExtOvershoot,2) - self.node3d.insertChild(self.marksExtOvershoot,2) + self.remove_ext_overshoot() + self.draw_ext_overshoot(vobj) vobj.Object.touch() + elif (prop == "ShowLine") and hasattr(vobj,"ShowLine"): if vobj.ShowLine: self.lineswitch2.whichChild = -3 @@ -3861,6 +3887,109 @@ class _ViewProviderDimension(_ViewProviderDraft): self.lineswitch3.whichChild = -1 else: self.updateData(vobj.Object,"Start") + + def remove_dim_arrows(self): + # remove existing nodes + self.node.removeChild(self.marks) + self.node3d.removeChild(self.marks) + + def draw_dim_arrows(self, vobj): + from pivy import coin + + if not hasattr(vobj,"ArrowType"): + return + + if self.p3.x < self.p2.x: + inv = False + else: + inv = True + + # set scale + symbol = arrowtypes.index(vobj.ArrowType) + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + self.trans1.scaleFactor.setValue((s,s,s)) + self.trans2.scaleFactor.setValue((s,s,s)) + + + # set new nodes + self.marks = coin.SoSeparator() + self.marks.addChild(self.color) + s1 = coin.SoSeparator() + if symbol == "Circle": + s1.addChild(self.coord1) + else: + s1.addChild(self.trans1) + s1.addChild(dimSymbol(symbol,invert=not(inv))) + self.marks.addChild(s1) + s2 = coin.SoSeparator() + if symbol == "Circle": + s2.addChild(self.coord2) + else: + s2.addChild(self.trans2) + s2.addChild(dimSymbol(symbol,invert=inv)) + self.marks.addChild(s2) + self.node.insertChild(self.marks,2) + self.node3d.insertChild(self.marks,2) + + def remove_dim_overshoot(self): + self.node.removeChild(self.marksDimOvershoot) + self.node3d.removeChild(self.marksDimOvershoot) + + + def draw_dim_overshoot(self, vobj): + from pivy import coin + + # set scale + s = vobj.DimOvershoot.Value * vobj.ScaleMultiplier + self.transDimOvershoot1.scaleFactor.setValue((s,s,s)) + self.transDimOvershoot2.scaleFactor.setValue((s,s,s)) + + # remove existing nodes + + # set new nodes + self.marksDimOvershoot = coin.SoSeparator() + if vobj.DimOvershoot.Value: + self.marksDimOvershoot.addChild(self.color) + s1 = coin.SoSeparator() + s1.addChild(self.transDimOvershoot1) + s1.addChild(dimDash((-1,0,0),(0,0,0))) + self.marksDimOvershoot.addChild(s1) + s2 = coin.SoSeparator() + s2.addChild(self.transDimOvershoot2) + s2.addChild(dimDash((0,0,0),(1,0,0))) + self.marksDimOvershoot.addChild(s2) + self.node.insertChild(self.marksDimOvershoot,2) + self.node3d.insertChild(self.marksDimOvershoot,2) + + + def remove_ext_overshoot(self): + self.node.removeChild(self.marksExtOvershoot) + self.node3d.removeChild(self.marksExtOvershoot) + + + def draw_ext_overshoot(self, vobj): + from pivy import coin + + # set scale + s = vobj.ExtOvershoot.Value * vobj.ScaleMultiplier + self.transExtOvershoot1.scaleFactor.setValue((s,s,s)) + self.transExtOvershoot2.scaleFactor.setValue((s,s,s)) + + # set new nodes + self.marksExtOvershoot = coin.SoSeparator() + if vobj.ExtOvershoot.Value: + self.marksExtOvershoot.addChild(self.color) + s1 = coin.SoSeparator() + s1.addChild(self.transExtOvershoot1) + s1.addChild(dimDash((0,0,0),(-1,0,0))) + self.marksExtOvershoot.addChild(s1) + s2 = coin.SoSeparator() + s2.addChild(self.transExtOvershoot2) + s2.addChild(dimDash((0,0,0),(-1,0,0))) + self.marksExtOvershoot.addChild(s2) + self.node.insertChild(self.marksExtOvershoot,2) + self.node3d.insertChild(self.marksExtOvershoot,2) + def doubleClicked(self,vobj): self.setEdit(vobj) @@ -3936,18 +4065,43 @@ class _AngularDimension(_DraftObject): class _ViewProviderAngularDimension(_ViewProviderDraft): """A View Provider for the Draft Angular Dimension object""" def __init__(self, obj): - obj.addProperty("App::PropertyLength","FontSize","Draft",QT_TRANSLATE_NOOP("App::Property","Font size")) - obj.addProperty("App::PropertyInteger","Decimals","Draft",QT_TRANSLATE_NOOP("App::Property","The number of decimals to show")) - obj.addProperty("App::PropertyFont","FontName","Draft",QT_TRANSLATE_NOOP("App::Property","Font name")) - obj.addProperty("App::PropertyLength","ArrowSize","Draft",QT_TRANSLATE_NOOP("App::Property","Arrow size")) - obj.addProperty("App::PropertyLength","TextSpacing","Draft",QT_TRANSLATE_NOOP("App::Property","The spacing between the text and the dimension line")) - obj.addProperty("App::PropertyEnumeration","ArrowType","Draft",QT_TRANSLATE_NOOP("App::Property","Arrow type")) - obj.addProperty("App::PropertyFloat","LineWidth","Draft",QT_TRANSLATE_NOOP("App::Property","Line width")) - obj.addProperty("App::PropertyColor","LineColor","Draft",QT_TRANSLATE_NOOP("App::Property","Line color")) - obj.addProperty("App::PropertyBool","FlipArrows","Draft",QT_TRANSLATE_NOOP("App::Property","Rotate the dimension arrows 180 degrees")) - obj.addProperty("App::PropertyBool","ShowUnit","Draft",QT_TRANSLATE_NOOP("App::Property","Show the unit suffix")) - obj.addProperty("App::PropertyVectorDistance","TextPosition","Draft",QT_TRANSLATE_NOOP("App::Property","The position of the text. Leave (0,0,0) for automatic position")) - obj.addProperty("App::PropertyString","Override","Draft",QT_TRANSLATE_NOOP("App::Property","Text override. Use 'dim' to insert the dimension length")) + obj.addProperty("App::PropertyFloat","ScaleMultiplier","Draft", + QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) + obj.addProperty("App::PropertyLength","FontSize", + "Draft",QT_TRANSLATE_NOOP("App::Property","Font size")) + obj.addProperty("App::PropertyInteger","Decimals", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "The number of decimals to show")) + obj.addProperty("App::PropertyFont","FontName", + "Draft",QT_TRANSLATE_NOOP("App::Property","Font name")) + obj.addProperty("App::PropertyLength","ArrowSize", + "Draft",QT_TRANSLATE_NOOP("App::Property","Arrow size")) + obj.addProperty("App::PropertyLength","TextSpacing", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "The spacing between the text and the dimension line")) + obj.addProperty("App::PropertyEnumeration","ArrowType", + "Draft",QT_TRANSLATE_NOOP("App::Property","Arrow type")) + obj.addProperty("App::PropertyFloat","LineWidth", + "Draft",QT_TRANSLATE_NOOP("App::Property","Line width")) + obj.addProperty("App::PropertyColor","LineColor", + "Draft",QT_TRANSLATE_NOOP("App::Property","Line color")) + obj.addProperty("App::PropertyBool","FlipArrows", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension arrows 180 degrees")) + obj.addProperty("App::PropertyBool","ShowUnit", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Show the unit suffix")) + obj.addProperty("App::PropertyVectorDistance","TextPosition", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "The position of the text. Leave (0,0,0) for automatic position")) + obj.addProperty("App::PropertyString","Override", + "Draft",QT_TRANSLATE_NOOP("App::Property", + "Text override. Use 'dim' to insert the dimension length")) + + param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + obj.ScaleMultiplier = 1 / annotation_scale obj.FontSize = getParam("textheight",0.20) obj.FontName = getParam("textfont","") obj.TextSpacing = getParam("dimspacing",0.05) @@ -4017,8 +4171,6 @@ class _ViewProviderAngularDimension(_ViewProviderDraft): from pivy import coin import Part, DraftGeomUtils import DraftGui - text = None - ivob = None arcsegs = 24 # calculate the arc data @@ -4137,11 +4289,22 @@ class _ViewProviderAngularDimension(_ViewProviderDraft): obj.Angle = a def onChanged(self, vobj, prop): - if prop == "FontSize": + if prop == "ScaleMultiplier" and hasattr(vobj,"ScaleMultiplier"): + # update all dimension values if hasattr(self,"font"): - self.font.size = vobj.FontSize.Value + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier if hasattr(self,"font3d"): - self.font3d.size = vobj.FontSize.Value*100 + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + if hasattr(self,"node") and hasattr(self,"p2") and hasattr(vobj,"ArrowSize"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + self.updateData(vobj.Object,"Start") + vobj.Object.touch() + elif prop == "FontSize": + if hasattr(self,"font"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier vobj.Object.touch() elif prop == "FontName": if hasattr(self,"font") and hasattr(self,"font3d"): @@ -4156,44 +4319,50 @@ class _ViewProviderAngularDimension(_ViewProviderDraft): self.drawstyle.lineWidth = vobj.LineWidth elif prop in ["ArrowSize","ArrowType"]: if hasattr(self,"node") and hasattr(self,"p2"): - from pivy import coin - - if not hasattr(vobj,"ArrowType"): - return - - # set scale - symbol = arrowtypes.index(vobj.ArrowType) - s = vobj.ArrowSize.Value - self.trans1.scaleFactor.setValue((s,s,s)) - self.trans2.scaleFactor.setValue((s,s,s)) - - # remove existing nodes - self.node.removeChild(self.marks) - self.node3d.removeChild(self.marks) - - # set new nodes - self.marks = coin.SoSeparator() - self.marks.addChild(self.color) - s1 = coin.SoSeparator() - if symbol == "Circle": - s1.addChild(self.coord1) - else: - s1.addChild(self.trans1) - s1.addChild(dimSymbol(symbol,invert=False)) - self.marks.addChild(s1) - s2 = coin.SoSeparator() - if symbol == "Circle": - s2.addChild(self.coord2) - else: - s2.addChild(self.trans2) - s2.addChild(dimSymbol(symbol,invert=True)) - self.marks.addChild(s2) - self.node.insertChild(self.marks,2) - self.node3d.insertChild(self.marks,2) + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) vobj.Object.touch() else: self.updateData(vobj.Object, None) + def remove_dim_arrows(self): + # remove existing nodes + self.node.removeChild(self.marks) + self.node3d.removeChild(self.marks) + + def draw_dim_arrows(self, vobj): + from pivy import coin + + if not hasattr(vobj,"ArrowType"): + return + + # set scale + symbol = arrowtypes.index(vobj.ArrowType) + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + self.trans1.scaleFactor.setValue((s,s,s)) + self.trans2.scaleFactor.setValue((s,s,s)) + + # set new nodes + self.marks = coin.SoSeparator() + self.marks.addChild(self.color) + s1 = coin.SoSeparator() + if symbol == "Circle": + s1.addChild(self.coord1) + else: + s1.addChild(self.trans1) + s1.addChild(dimSymbol(symbol,invert=False)) + self.marks.addChild(s1) + s2 = coin.SoSeparator() + if symbol == "Circle": + s2.addChild(self.coord2) + else: + s2.addChild(self.trans2) + s2.addChild(dimSymbol(symbol,invert=True)) + self.marks.addChild(s2) + self.node.insertChild(self.marks,2) + self.node3d.insertChild(self.marks,2) + + def doubleClicked(self,vobj): self.setEdit(vobj) @@ -6357,17 +6526,42 @@ class ViewProviderDraftLabel: """A View Provider for the Draft Label""" def __init__(self,vobj): - vobj.addProperty("App::PropertyLength","TextSize","Base",QT_TRANSLATE_NOOP("App::Property","The size of the text")) - vobj.addProperty("App::PropertyFont","TextFont","Base",QT_TRANSLATE_NOOP("App::Property","The font of the text")) - vobj.addProperty("App::PropertyLength","ArrowSize","Base",QT_TRANSLATE_NOOP("App::Property","The size of the arrow")) - vobj.addProperty("App::PropertyEnumeration","TextAlignment","Base",QT_TRANSLATE_NOOP("App::Property","The vertical alignment of the text")) - vobj.addProperty("App::PropertyEnumeration","ArrowType","Base",QT_TRANSLATE_NOOP("App::Property","The type of arrow of this label")) - vobj.addProperty("App::PropertyEnumeration","Frame","Base",QT_TRANSLATE_NOOP("App::Property","The type of frame around the text of this object")) - vobj.addProperty("App::PropertyBool","Line","Base",QT_TRANSLATE_NOOP("App::Property","Display a leader line or not")) - vobj.addProperty("App::PropertyFloat","LineWidth","Base",QT_TRANSLATE_NOOP("App::Property","Line width")) - vobj.addProperty("App::PropertyColor","LineColor","Base",QT_TRANSLATE_NOOP("App::Property","Line color")) - vobj.addProperty("App::PropertyColor","TextColor","Base",QT_TRANSLATE_NOOP("App::Property","Text color")) - vobj.addProperty("App::PropertyInteger","MaxChars","Base",QT_TRANSLATE_NOOP("App::Property","The maximum number of characters on each line of the text box")) + vobj.addProperty("App::PropertyFloat","ScaleMultiplier", + "Base",QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) + vobj.addProperty("App::PropertyLength","TextSize", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The size of the text")) + vobj.addProperty("App::PropertyFont","TextFont", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The font of the text")) + vobj.addProperty("App::PropertyLength","ArrowSize", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The size of the arrow")) + vobj.addProperty("App::PropertyEnumeration","TextAlignment", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The vertical alignment of the text")) + vobj.addProperty("App::PropertyEnumeration","ArrowType", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The type of arrow of this label")) + vobj.addProperty("App::PropertyEnumeration","Frame", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The type of frame around the text of this object")) + vobj.addProperty("App::PropertyBool","Line", + "Base",QT_TRANSLATE_NOOP("App::Property", + "Display a leader line or not")) + vobj.addProperty("App::PropertyFloat","LineWidth", + "Base",QT_TRANSLATE_NOOP("App::Property", + "Line width")) + vobj.addProperty("App::PropertyColor","LineColor", + "Base",QT_TRANSLATE_NOOP("App::Property", + "Line color")) + vobj.addProperty("App::PropertyColor","TextColor", + "Base",QT_TRANSLATE_NOOP("App::Property", + "Text color")) + vobj.addProperty("App::PropertyInteger","MaxChars", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The maximum number of characters on each line of the text box")) vobj.Proxy = self self.Object = vobj.Object vobj.TextAlignment = ["Top","Middle","Bottom"] @@ -6380,6 +6574,10 @@ class ViewProviderDraftLabel: vobj.ArrowType = arrowtypes[getParam("dimsymbol")] vobj.Frame = ["None","Rectangle"] vobj.Line = True + param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + vobj.ScaleMultiplier = 1 / annotation_scale + def getIcon(self): import Draft_rc @@ -6495,7 +6693,16 @@ class ViewProviderDraftLabel: return b.getBoundingBox().getSize().getValue() def onChanged(self,vobj,prop): - if prop == "LineColor": + if prop == "ScaleMultiplier": + if not hasattr(vobj,"ScaleMultiplier"): + return + if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): + self.update_label(vobj) + if hasattr(vobj,"ArrowSize"): + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + if s: + self.arrowpos.scaleFactor.setValue((s,s,s)) + elif prop == "LineColor": if hasattr(vobj,"LineColor"): l = vobj.LineColor self.matline.diffuseColor.setValue([l[0],l[1],l[2]]) @@ -6511,22 +6718,7 @@ class ViewProviderDraftLabel: self.font.name = vobj.TextFont.encode("utf8") elif prop in ["TextSize","TextAlignment"]: if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): - self.font.size = vobj.TextSize.Value - v = Vector(1,0,0) - if vobj.Object.StraightDistance > 0: - v = v.negative() - v.multiply(vobj.TextSize/10) - tsize = self.getTextSize(vobj) - if len(vobj.Object.Text) > 1: - v = v.add(Vector(0,(tsize[1]-1)*2,0)) - if vobj.TextAlignment == "Top": - v = v.add(Vector(0,-tsize[1]*2,0)) - elif vobj.TextAlignment == "Middle": - v = v.add(Vector(0,-tsize[1],0)) - v = vobj.Object.Placement.Rotation.multVec(v) - pos = vobj.Object.Placement.Base.add(v) - self.textpos.translation.setValue(pos) - self.textpos.rotation.setValue(vobj.Object.Placement.Rotation.Q) + self.update_label(vobj) elif prop == "Line": if hasattr(vobj,"Line"): if vobj.Line: @@ -6546,7 +6738,6 @@ class ViewProviderDraftLabel: v1 = vobj.Object.Points[-2].sub(vobj.Object.Points[-1]) if not DraftVecUtils.isNull(v1): v1.normalize() - import DraftGeomUtils v2 = Vector(0,0,1) if round(v2.getAngle(v1),4) in [0,round(math.pi,4)]: v2 = Vector(0,1,0) @@ -6555,7 +6746,7 @@ class ViewProviderDraftLabel: self.arrowpos.rotation.setValue((q[0],q[1],q[2],q[3])) elif prop == "ArrowSize": if hasattr(vobj,"ArrowSize"): - s = vobj.ArrowSize.Value + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier if s: self.arrowpos.scaleFactor.setValue((s,s,s)) elif prop == "Frame": @@ -6573,6 +6764,25 @@ class ViewProviderDraftLabel: self.fcoords.point.setValues(pts) self.frame.coordIndex.setValues(0,len(pts),range(len(pts))) + + def update_label(self, vobj): + self.font.size = vobj.TextSize.Value * vobj.ScaleMultiplier + v = Vector(1,0,0) + if vobj.Object.StraightDistance > 0: + v = v.negative() + v.multiply(vobj.TextSize/10) + tsize = self.getTextSize(vobj) + if (tsize is not None) and (len(vobj.Object.Text) > 1): + v = v.add(Vector(0,(tsize[1]-1)*2,0)) + if vobj.TextAlignment == "Top": + v = v.add(Vector(0,-tsize[1]*2,0)) + elif vobj.TextAlignment == "Middle": + v = v.add(Vector(0,-tsize[1],0)) + v = vobj.Object.Placement.Rotation.multVec(v) + pos = vobj.Object.Placement.Base.add(v) + self.textpos.translation.setValue(pos) + self.textpos.rotation.setValue(vobj.Object.Placement.Rotation.Q) + def __getstate__(self): return None @@ -6597,13 +6807,31 @@ class ViewProviderDraftText: """A View Provider for the Draft Label""" def __init__(self,vobj): - vobj.addProperty("App::PropertyLength","FontSize","Base",QT_TRANSLATE_NOOP("App::Property","The size of the text")) - vobj.addProperty("App::PropertyFont","FontName","Base",QT_TRANSLATE_NOOP("App::Property","The font of the text")) - vobj.addProperty("App::PropertyEnumeration","Justification","Base",QT_TRANSLATE_NOOP("App::Property","The vertical alignment of the text")) - vobj.addProperty("App::PropertyColor","TextColor","Base",QT_TRANSLATE_NOOP("App::Property","Text color")) - vobj.addProperty("App::PropertyFloat","LineSpacing","Base",QT_TRANSLATE_NOOP("App::Property","Line spacing (relative to font size)")) + vobj.addProperty("App::PropertyFloat","ScaleMultiplier", + "Base",QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) + vobj.addProperty("App::PropertyLength","FontSize", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The size of the text")) + vobj.addProperty("App::PropertyFont","FontName", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The font of the text")) + vobj.addProperty("App::PropertyEnumeration","Justification", + "Base",QT_TRANSLATE_NOOP("App::Property", + "The vertical alignment of the text")) + vobj.addProperty("App::PropertyColor","TextColor", + "Base",QT_TRANSLATE_NOOP("App::Property", + "Text color")) + vobj.addProperty("App::PropertyFloat","LineSpacing", + "Base",QT_TRANSLATE_NOOP("App::Property", + "Line spacing (relative to font size)")) vobj.Proxy = self self.Object = vobj.Object + + param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + vobj.ScaleMultiplier = 1 / annotation_scale + vobj.Justification = ["Left","Center","Right"] vobj.FontName = getParam("textfont","sans") vobj.FontSize = getParam("textheight",1) @@ -6667,7 +6895,9 @@ class ViewProviderDraftText: self.trans.rotation.setValue(obj.Placement.Rotation.Q) def onChanged(self,vobj,prop): - if prop == "TextColor": + if prop == "ScaleMultiplier": + self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier + elif prop == "TextColor": if "TextColor" in vobj.PropertiesList: l = vobj.TextColor self.mattext.diffuseColor.setValue([l[0],l[1],l[2]]) @@ -6676,7 +6906,7 @@ class ViewProviderDraftText: self.font.name = vobj.FontName.encode("utf8") elif prop == "FontSize": if "FontSize" in vobj.PropertiesList: - self.font.size = vobj.FontSize.Value + self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier elif prop == "Justification": from pivy import coin try: From ff3bdf86bc1df7db075140e5108e7ad1c88c8baf Mon Sep 17 00:00:00 2001 From: carlopav Date: Sun, 1 Mar 2020 17:14:11 +0100 Subject: [PATCH 076/142] [Draft] Improved Autogroup to handle Draft Annotations Draft Dimension, Label and Text are now correctly auto-added to Part container --- src/Mod/Draft/DraftTools.py | 1 + src/Mod/Draft/draftutils/gui_utils.py | 105 +++++++++++++++----------- 2 files changed, 60 insertions(+), 46 deletions(-) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 415ea413c8..3f9b08caed 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -5274,6 +5274,7 @@ class Draft_Label(Creator): FreeCAD.ActiveDocument.openTransaction("Create Label") FreeCADGui.addModule("Draft") FreeCADGui.doCommand("l = Draft.makeLabel("+tp+sel+"direction='"+direction+"',distance="+str(dist)+",labeltype='"+self.labeltype+"',"+pl+")") + FreeCADGui.doCommand("Draft.autogroup(l)") FreeCAD.ActiveDocument.recompute() FreeCAD.ActiveDocument.commitTransaction() self.finish() diff --git a/src/Mod/Draft/draftutils/gui_utils.py b/src/Mod/Draft/draftutils/gui_utils.py index 37b4c899d9..b9ef6c8a58 100644 --- a/src/Mod/Draft/draftutils/gui_utils.py +++ b/src/Mod/Draft/draftutils/gui_utils.py @@ -98,55 +98,68 @@ def autogroup(obj): obj: App::DocumentObject Any type of object that will be stored in the group. """ + + # check for required conditions for autogroup to work if not App.GuiUp: return + if not hasattr(Gui,"draftToolBar"): + return + if not hasattr(Gui.draftToolBar,"autogroup"): + return + if Gui.draftToolBar.isConstructionMode(): + return + + # autogroup code + if Gui.draftToolBar.autogroup is not None: + active_group = App.ActiveDocument.getObject(Gui.draftToolBar.autogroup) + if active_group: + found = False + for o in active_group.Group: + if o.Name == obj.Name: + found = True + if not found: + gr = active_group.Group + gr.append(obj) + active_group.Group = gr + + else: - doc = App.ActiveDocument - view = Gui.ActiveDocument.ActiveView - - # Look for active Arch container - active_arch_obj = Gui.ActiveDocument.ActiveView.getActiveObject("Arch") - if hasattr(Gui, "draftToolBar"): - if (hasattr(Gui.draftToolBar, "autogroup") - and not Gui.draftToolBar.isConstructionMode()): - if Gui.draftToolBar.autogroup is not None: - active_group = doc.getObject(Gui.draftToolBar.autogroup) - if active_group: - found = False - for o in active_group.Group: - if o.Name == obj.Name: - found = True - if not found: - gr = active_group.Group - gr.append(obj) - active_group.Group = gr - elif active_arch_obj: - active_arch_obj.addObject(obj) - elif view.getActiveObject("part", False) is not None: - # Add object to active part and change its placement - # accordingly so the object does not jump - # to a different position, works with App::Link if not scaled. - # Modified accordingly to realthunder suggestions - p, parent, sub = view.getActiveObject("part", False) - matrix = parent.getSubObject(sub, retType=4) - if matrix.hasScale() == 1: - _msg(translate("Draft", - "Unable to insert new object into " - "a scaled part")) - return - inverse_placement = App.Placement(matrix.inverse()) - if get_type(obj) == 'Point': - # point vector have a kind of placement, so should be - # processed before generic object with placement - point_vector = App.Vector(obj.X, obj.Y, obj.Z) - real_point = inverse_placement.multVec(point_vector) - obj.X = real_point.x - obj.Y = real_point.y - obj.Z = real_point.z - elif hasattr(obj, "Placement"): - place = inverse_placement.multiply(obj.Placement) - obj.Placement = App.Placement(place) - p.addObject(obj) + if Gui.ActiveDocument.ActiveView.getActiveObject("Arch"): + # add object to active Arch Container + Gui.ActiveDocument.ActiveView.getActiveObject("Arch").addObject(obj) + + elif Gui.ActiveDocument.ActiveView.getActiveObject("part", False) is not None: + # add object to active part and change it's placement accordingly + # so object does not jump to different position, works with App::Link + # if not scaled. Modified accordingly to realthunder suggestions + p, parent, sub = Gui.ActiveDocument.ActiveView.getActiveObject("part", False) + matrix = parent.getSubObject(sub, retType=4) + if matrix.hasScale() == 1: + err = translate("Draft", + "Unable to insert new object into " + "a scaled part") + App.Console.PrintMessage(err) + return + inverse_placement = App.Placement(matrix.inverse()) + if get_type(obj) == 'Point': + point_vector = App.Vector(obj.X, obj.Y, obj.Z) + real_point = inverse_placement.multVec(point_vector) + obj.X = real_point.x + obj.Y = real_point.y + obj.Z = real_point.z + elif get_type(obj) in ["Dimension"]: + obj.Start = inverse_placement.multVec(obj.Start) + obj.End = inverse_placement.multVec(obj.End) + obj.Dimline = inverse_placement.multVec(obj.Dimline) + obj.Normal = inverse_placement.Rotation.multVec(obj.Normal) + obj.Direction = inverse_placement.Rotation.multVec(obj.Direction) + elif get_type(obj) in ["Label"]: + obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) + obj.TargetPoint = inverse_placement.multVec(obj.TargetPoint) + elif hasattr(obj,"Placement"): + # every object that have a placement is processed here + obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) + p.addObject(obj) def dim_symbol(symbol=None, invert=False): From 116052cff03b05005a4cf078b88e7862f620cbc6 Mon Sep 17 00:00:00 2001 From: carlopav Date: Tue, 3 Mar 2020 00:54:22 +0100 Subject: [PATCH 077/142] [Draft] Annotation Scale bugfix Now property ScaleMultiplier is checked for existence before the use. --- src/Mod/Draft/Draft.py | 57 ++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 130f1ecaf3..8e0dd1ef41 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -3500,9 +3500,9 @@ class _ViewProviderDimension(_ViewProviderDraft): """ def __init__(self, obj): # general properties - obj.addProperty("App::PropertyFloat","ScaleMultiplier","Draft", - QT_TRANSLATE_NOOP("App::Property", - "Dimension size overall multiplier")) + obj.addProperty("App::PropertyFloat","ScaleMultiplier", + "Annotation",QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) obj.addProperty("App::PropertyFloat","LineWidth", "Draft",QT_TRANSLATE_NOOP("App::Property","Line width")) obj.addProperty("App::PropertyColor","LineColor", @@ -3691,7 +3691,7 @@ class _ViewProviderDimension(_ViewProviderDraft): self.p2 = self.p1 self.p3 = self.p4 if proj: - if hasattr(obj.ViewObject,"ExtLines"): + if hasattr(obj.ViewObject,"ExtLines") and hasattr(obj.ViewObject,"ScaleMultiplier"): dmax = obj.ViewObject.ExtLines.Value * obj.ViewObject.ScaleMultiplier if dmax and (proj.Length > dmax): if (dmax > 0): @@ -3752,7 +3752,7 @@ class _ViewProviderDimension(_ViewProviderDraft): rot3 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u3,v3,norm)).Rotation.Q self.transExtOvershoot1.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) self.transExtOvershoot2.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) - if hasattr(obj.ViewObject,"TextSpacing"): + if hasattr(obj.ViewObject,"TextSpacing") and hasattr(obj.ViewObject,"ScaleMultiplier"): ts = obj.ViewObject.TextSpacing.Value * obj.ViewObject.ScaleMultiplier offset = DraftVecUtils.scaleTo(v1,ts) else: @@ -3842,9 +3842,9 @@ class _ViewProviderDimension(_ViewProviderDraft): vobj.Object.touch() elif (prop == "FontSize") and hasattr(vobj,"FontSize"): - if hasattr(self,"font"): + if hasattr(self,"font") and hasattr(vobj,"ScaleMultiplier"): self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier - if hasattr(self,"font3d"): + if hasattr(self,"font3d") and hasattr(vobj,"ScaleMultiplier"): self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier vobj.Object.touch() @@ -3864,19 +3864,22 @@ class _ViewProviderDimension(_ViewProviderDraft): elif (prop in ["ArrowSize","ArrowType"]) and hasattr(vobj,"ArrowSize"): if hasattr(self,"node") and hasattr(self,"p2"): - self.remove_dim_arrows() - self.draw_dim_arrows(vobj) - vobj.Object.touch() + if hasattr(vobj,"ScaleMultiplier"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + vobj.Object.touch() elif (prop == "DimOvershoot") and hasattr(vobj,"DimOvershoot"): - self.remove_dim_overshoot() - self.draw_dim_overshoot(vobj) - vobj.Object.touch() + if hasattr(vobj,"ScaleMultiplier"): + self.remove_dim_overshoot() + self.draw_dim_overshoot(vobj) + vobj.Object.touch() elif (prop == "ExtOvershoot") and hasattr(vobj,"ExtOvershoot"): - self.remove_ext_overshoot() - self.draw_ext_overshoot(vobj) - vobj.Object.touch() + if hasattr(vobj,"ScaleMultiplier"): + self.remove_ext_overshoot() + self.draw_ext_overshoot(vobj) + vobj.Object.touch() elif (prop == "ShowLine") and hasattr(vobj,"ShowLine"): if vobj.ShowLine: @@ -4065,8 +4068,8 @@ class _AngularDimension(_DraftObject): class _ViewProviderAngularDimension(_ViewProviderDraft): """A View Provider for the Draft Angular Dimension object""" def __init__(self, obj): - obj.addProperty("App::PropertyFloat","ScaleMultiplier","Draft", - QT_TRANSLATE_NOOP("App::Property", + obj.addProperty("App::PropertyFloat","ScaleMultiplier", + "Annotation",QT_TRANSLATE_NOOP("App::Property", "Dimension size overall multiplier")) obj.addProperty("App::PropertyLength","FontSize", "Draft",QT_TRANSLATE_NOOP("App::Property","Font size")) @@ -4300,7 +4303,7 @@ class _ViewProviderAngularDimension(_ViewProviderDraft): self.draw_dim_arrows(vobj) self.updateData(vobj.Object,"Start") vobj.Object.touch() - elif prop == "FontSize": + elif prop == "FontSize" and hasattr(vobj,"ScaleMultiplier"): if hasattr(self,"font"): self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier if hasattr(self,"font3d"): @@ -4317,7 +4320,7 @@ class _ViewProviderAngularDimension(_ViewProviderDraft): elif prop == "LineWidth": if hasattr(self,"drawstyle"): self.drawstyle.lineWidth = vobj.LineWidth - elif prop in ["ArrowSize","ArrowType"]: + elif prop in ["ArrowSize","ArrowType"] and hasattr(vobj,"ScaleMultiplier"): if hasattr(self,"node") and hasattr(self,"p2"): self.remove_dim_arrows() self.draw_dim_arrows(vobj) @@ -6436,7 +6439,6 @@ def makeLabel(targetpoint=None,target=None,direction=None,distance=None,labeltyp obj.LabelType = labeltype if placement: obj.Placement = placement - return obj @@ -6527,7 +6529,7 @@ class ViewProviderDraftLabel: def __init__(self,vobj): vobj.addProperty("App::PropertyFloat","ScaleMultiplier", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Annotation",QT_TRANSLATE_NOOP("App::Property", "Dimension size overall multiplier")) vobj.addProperty("App::PropertyLength","TextSize", "Base",QT_TRANSLATE_NOOP("App::Property", @@ -6716,7 +6718,7 @@ class ViewProviderDraftLabel: elif (prop == "TextFont"): if hasattr(vobj,"TextFont"): self.font.name = vobj.TextFont.encode("utf8") - elif prop in ["TextSize","TextAlignment"]: + elif prop in ["TextSize","TextAlignment"] and hasattr(vobj,"ScaleMultiplier"): if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): self.update_label(vobj) elif prop == "Line": @@ -6745,7 +6747,7 @@ class ViewProviderDraftLabel: q = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(v1,v3,v2)).Rotation.Q self.arrowpos.rotation.setValue((q[0],q[1],q[2],q[3])) elif prop == "ArrowSize": - if hasattr(vobj,"ArrowSize"): + if hasattr(vobj,"ArrowSize") and hasattr(vobj,"ScaleMultiplier"): s = vobj.ArrowSize.Value * vobj.ScaleMultiplier if s: self.arrowpos.scaleFactor.setValue((s,s,s)) @@ -6808,7 +6810,7 @@ class ViewProviderDraftText: def __init__(self,vobj): vobj.addProperty("App::PropertyFloat","ScaleMultiplier", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Annotation",QT_TRANSLATE_NOOP("App::Property", "Dimension size overall multiplier")) vobj.addProperty("App::PropertyLength","FontSize", "Base",QT_TRANSLATE_NOOP("App::Property", @@ -6896,7 +6898,8 @@ class ViewProviderDraftText: def onChanged(self,vobj,prop): if prop == "ScaleMultiplier": - self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier + if "ScaleMultiplier" in vobj.PropertiesList: + self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier elif prop == "TextColor": if "TextColor" in vobj.PropertiesList: l = vobj.TextColor @@ -6905,7 +6908,7 @@ class ViewProviderDraftText: if "FontName" in vobj.PropertiesList: self.font.name = vobj.FontName.encode("utf8") elif prop == "FontSize": - if "FontSize" in vobj.PropertiesList: + if "FontSize" in vobj.PropertiesList and "ScaleMultiplier" in vobj.PropertiesList: self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier elif prop == "Justification": from pivy import coin From 08d949b0883b3f220ce69951980a42e4cf93a5c0 Mon Sep 17 00:00:00 2001 From: carlopav Date: Thu, 5 Mar 2020 18:04:52 +0100 Subject: [PATCH 078/142] [Draft] Rearrange annotation properties groups ref. https://forum.freecadweb.org/viewtopic.php?f=23&t=43795&p=373731#p373731 bugfix --- src/Mod/Draft/Draft.py | 127 +++++++++--------- .../Draft/draftutils/init_draft_statusbar.py | 9 +- 2 files changed, 71 insertions(+), 65 deletions(-) diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 8e0dd1ef41..23aea008eb 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -3499,60 +3499,60 @@ class _ViewProviderDimension(_ViewProviderDraft): """ def __init__(self, obj): - # general properties + # annotation properties obj.addProperty("App::PropertyFloat","ScaleMultiplier", "Annotation",QT_TRANSLATE_NOOP("App::Property", "Dimension size overall multiplier")) - obj.addProperty("App::PropertyFloat","LineWidth", - "Draft",QT_TRANSLATE_NOOP("App::Property","Line width")) - obj.addProperty("App::PropertyColor","LineColor", - "Draft",QT_TRANSLATE_NOOP("App::Property","Line color")) # text properties obj.addProperty("App::PropertyFont","FontName", - "Draft",QT_TRANSLATE_NOOP("App::Property","Font name")) + "Text",QT_TRANSLATE_NOOP("App::Property","Font name")) obj.addProperty("App::PropertyLength","FontSize", - "Draft",QT_TRANSLATE_NOOP("App::Property","Font size")) + "Text",QT_TRANSLATE_NOOP("App::Property","Font size")) obj.addProperty("App::PropertyLength","TextSpacing", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The spacing between the text and the dimension line")) obj.addProperty("App::PropertyBool","FlipText", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "Rotate the dimension text 180 degrees")) obj.addProperty("App::PropertyVectorDistance","TextPosition", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The position of the text. Leave (0,0,0) for automatic position")) obj.addProperty("App::PropertyString","Override", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "Text override. Use $dim to insert the dimension length")) # units properties obj.addProperty("App::PropertyInteger","Decimals", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Units",QT_TRANSLATE_NOOP("App::Property", "The number of decimals to show")) obj.addProperty("App::PropertyBool","ShowUnit", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Units",QT_TRANSLATE_NOOP("App::Property", "Show the unit suffix")) obj.addProperty("App::PropertyString","UnitOverride", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Units",QT_TRANSLATE_NOOP("App::Property", "A unit to express the measurement. Leave blank for system default")) # graphics properties + obj.addProperty("App::PropertyFloat","LineWidth", + "Graphics",QT_TRANSLATE_NOOP("App::Property","Line width")) + obj.addProperty("App::PropertyColor","LineColor", + "Graphics",QT_TRANSLATE_NOOP("App::Property","Line color")) obj.addProperty("App::PropertyLength","ArrowSize", - "Draft",QT_TRANSLATE_NOOP("App::Property","Arrow size")) + "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow size")) obj.addProperty("App::PropertyEnumeration","ArrowType", - "Draft",QT_TRANSLATE_NOOP("App::Property","Arrow type")) + "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow type")) obj.addProperty("App::PropertyBool","FlipArrows", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Graphics",QT_TRANSLATE_NOOP("App::Property", "Rotate the dimension arrows 180 degrees")) obj.addProperty("App::PropertyDistance","DimOvershoot", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Graphics",QT_TRANSLATE_NOOP("App::Property", "The distance the dimension line is extended past the extension lines")) obj.addProperty("App::PropertyDistance","ExtLines", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Graphics",QT_TRANSLATE_NOOP("App::Property", "Length of the extension lines")) obj.addProperty("App::PropertyDistance","ExtOvershoot", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Graphics",QT_TRANSLATE_NOOP("App::Property", "Length of the extension line above the dimension line")) obj.addProperty("App::PropertyBool","ShowLine", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Graphics",QT_TRANSLATE_NOOP("App::Property", "Shows the dimension line and arrows")) param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") @@ -4072,34 +4072,34 @@ class _ViewProviderAngularDimension(_ViewProviderDraft): "Annotation",QT_TRANSLATE_NOOP("App::Property", "Dimension size overall multiplier")) obj.addProperty("App::PropertyLength","FontSize", - "Draft",QT_TRANSLATE_NOOP("App::Property","Font size")) + "Text",QT_TRANSLATE_NOOP("App::Property","Font size")) obj.addProperty("App::PropertyInteger","Decimals", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Units",QT_TRANSLATE_NOOP("App::Property", "The number of decimals to show")) obj.addProperty("App::PropertyFont","FontName", - "Draft",QT_TRANSLATE_NOOP("App::Property","Font name")) + "Text",QT_TRANSLATE_NOOP("App::Property","Font name")) obj.addProperty("App::PropertyLength","ArrowSize", - "Draft",QT_TRANSLATE_NOOP("App::Property","Arrow size")) + "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow size")) obj.addProperty("App::PropertyLength","TextSpacing", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The spacing between the text and the dimension line")) obj.addProperty("App::PropertyEnumeration","ArrowType", - "Draft",QT_TRANSLATE_NOOP("App::Property","Arrow type")) + "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow type")) obj.addProperty("App::PropertyFloat","LineWidth", - "Draft",QT_TRANSLATE_NOOP("App::Property","Line width")) + "Graphics",QT_TRANSLATE_NOOP("App::Property","Line width")) obj.addProperty("App::PropertyColor","LineColor", - "Draft",QT_TRANSLATE_NOOP("App::Property","Line color")) + "Graphics",QT_TRANSLATE_NOOP("App::Property","Line color")) obj.addProperty("App::PropertyBool","FlipArrows", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Graphics",QT_TRANSLATE_NOOP("App::Property", "Rotate the dimension arrows 180 degrees")) obj.addProperty("App::PropertyBool","ShowUnit", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Units",QT_TRANSLATE_NOOP("App::Property", "Show the unit suffix")) obj.addProperty("App::PropertyVectorDistance","TextPosition", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The position of the text. Leave (0,0,0) for automatic position")) obj.addProperty("App::PropertyString","Override", - "Draft",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "Text override. Use 'dim' to insert the dimension length")) param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") @@ -6528,42 +6528,47 @@ class ViewProviderDraftLabel: """A View Provider for the Draft Label""" def __init__(self,vobj): + # Annotation properties vobj.addProperty("App::PropertyFloat","ScaleMultiplier", "Annotation",QT_TRANSLATE_NOOP("App::Property", "Dimension size overall multiplier")) + # Text properties vobj.addProperty("App::PropertyLength","TextSize", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The size of the text")) vobj.addProperty("App::PropertyFont","TextFont", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The font of the text")) - vobj.addProperty("App::PropertyLength","ArrowSize", - "Base",QT_TRANSLATE_NOOP("App::Property", - "The size of the arrow")) vobj.addProperty("App::PropertyEnumeration","TextAlignment", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The vertical alignment of the text")) - vobj.addProperty("App::PropertyEnumeration","ArrowType", - "Base",QT_TRANSLATE_NOOP("App::Property", - "The type of arrow of this label")) - vobj.addProperty("App::PropertyEnumeration","Frame", - "Base",QT_TRANSLATE_NOOP("App::Property", - "The type of frame around the text of this object")) - vobj.addProperty("App::PropertyBool","Line", - "Base",QT_TRANSLATE_NOOP("App::Property", - "Display a leader line or not")) - vobj.addProperty("App::PropertyFloat","LineWidth", - "Base",QT_TRANSLATE_NOOP("App::Property", - "Line width")) - vobj.addProperty("App::PropertyColor","LineColor", - "Base",QT_TRANSLATE_NOOP("App::Property", - "Line color")) vobj.addProperty("App::PropertyColor","TextColor", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "Text color")) vobj.addProperty("App::PropertyInteger","MaxChars", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The maximum number of characters on each line of the text box")) + # Graphics properties + vobj.addProperty("App::PropertyLength","ArrowSize", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The size of the arrow")) + vobj.addProperty("App::PropertyEnumeration","ArrowType", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The type of arrow of this label")) + vobj.addProperty("App::PropertyEnumeration","Frame", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The type of frame around the text of this object")) + vobj.addProperty("App::PropertyBool","Line", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Display a leader line or not")) + vobj.addProperty("App::PropertyFloat","LineWidth", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Line width")) + vobj.addProperty("App::PropertyColor","LineColor", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Line color") + ) + vobj.Proxy = self self.Object = vobj.Object vobj.TextAlignment = ["Top","Middle","Bottom"] @@ -6813,19 +6818,19 @@ class ViewProviderDraftText: "Annotation",QT_TRANSLATE_NOOP("App::Property", "Dimension size overall multiplier")) vobj.addProperty("App::PropertyLength","FontSize", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The size of the text")) vobj.addProperty("App::PropertyFont","FontName", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The font of the text")) vobj.addProperty("App::PropertyEnumeration","Justification", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "The vertical alignment of the text")) vobj.addProperty("App::PropertyColor","TextColor", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "Text color")) vobj.addProperty("App::PropertyFloat","LineSpacing", - "Base",QT_TRANSLATE_NOOP("App::Property", + "Text",QT_TRANSLATE_NOOP("App::Property", "Line spacing (relative to font size)")) vobj.Proxy = self self.Object = vobj.Object diff --git a/src/Mod/Draft/draftutils/init_draft_statusbar.py b/src/Mod/Draft/draftutils/init_draft_statusbar.py index 0b1babcb70..510f133459 100644 --- a/src/Mod/Draft/draftutils/init_draft_statusbar.py +++ b/src/Mod/Draft/draftutils/init_draft_statusbar.py @@ -2,7 +2,7 @@ This module provide the code for the Draft Statusbar, activated by initGui """ -## @package init_tools +## @package init_draft_statusbar # \ingroup DRAFT # \brief This module provides the code for the Draft Statusbar. @@ -44,9 +44,10 @@ def scale_to_label(scale): """ transform a float number into a 1:X or X:1 scale and return it as label """ - f = scale.as_integer_ratio() - if f[0] == 1 or f[0] == 1: - label = str(f[0]) + ":" + str(f[1]) + f = 1/scale + f = f.as_integer_ratio() + if f[1] == 1 or f[0] == 1: + label = str(f[1]) + ":" + str(f[0]) return label else: return str(scale) From e3ea589d5ce3f00a2c5b9368bec2acde876542ce Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 7 Mar 2020 18:04:10 +0100 Subject: [PATCH 079/142] [Draft] Annotation scale, support for imperial custom scale input Added support for different scale list --- .../Draft/draftutils/init_draft_statusbar.py | 97 ++++++++++++++----- 1 file changed, 73 insertions(+), 24 deletions(-) diff --git a/src/Mod/Draft/draftutils/init_draft_statusbar.py b/src/Mod/Draft/draftutils/init_draft_statusbar.py index 510f133459..1d55d611fd 100644 --- a/src/Mod/Draft/draftutils/init_draft_statusbar.py +++ b/src/Mod/Draft/draftutils/init_draft_statusbar.py @@ -40,11 +40,61 @@ from PySide.QtCore import QT_TRANSLATE_NOOP # SCALE WIDGET FUNCTIONS #---------------------------------------------------------------------------- +draft_scales_metrics = ["1:1000", "1:500", "1:250", "1:200", "1:100", + "1:50", "1:25","1:20", "1:10", "1:5","1:2", + "1:1", + "2:1", "5:1", "10:1", "20:1", + QT_TRANSLATE_NOOP("draft","custom"), + ] + +draft_scales_arch_imperial = ["1/16in=1ft", "3/32in=1ft", "1/8in=1ft", + "3/16in=1ft", "1/4in=1ft","3/8in=1ft", + "1/2in=1ft", "3/4in=1ft", "1in=1ft", + "1.5in=1ft", "3in=1ft", + QT_TRANSLATE_NOOP("draft","custom"), + ] + +draft_scales_eng_imperial = ["1in=10ft", "1in=20ft", "1in=30ft", + "1in=40ft", "1in=50ft", "1in=60ft", + "1in=70ft", "1in=80ft", "1in=90ft", + "1in=100ft", + QT_TRANSLATE_NOOP("draft","custom"), + ] + +def get_scales(unit_system = 0): + """ + returns the list of preset scales accordin to unit system. + + Parameters: + unit_system = 0 : default from user preferences + 1 : metrics + 2 : imperial architectural + 3 : imperial engineering + """ + + if unit_system == 0: + param = App.ParamGet("User parameter:BaseApp/Preferences/Units") + scale_units_system = param.GetInt("UserSchema", 0) + if scale_units_system in [0, 1, 4, 6]: + return draft_scales_metrics + elif scale_units_system in [2, 3, 5]: + return draft_scales_arch_imperial + elif scale_units_system in [7]: + return draft_scales_eng_imperial + elif unit_system == 1: + return draft_scales_metrics + elif unit_system == 2: + return draft_scales_arch_imperial + elif unit_system == 3: + return draft_scales_eng_imperial + + def scale_to_label(scale): """ transform a float number into a 1:X or X:1 scale and return it as label """ f = 1/scale + f = round(f,2) f = f.as_integer_ratio() if f[1] == 1 or f[0] == 1: label = str(f[1]) + ":" + str(f[0]) @@ -60,22 +110,21 @@ def label_to_scale(label): scale = float(label) return scale except : - err = QT_TRANSLATE_NOOP("draft", - "Unable to convert input into a scale factor") if ":" in label: f = label.split(":") + elif "=" in label: + f = label.split("=") + else: + return + if len(f) == 2: try: - scale = float(f[0])/float(f[1]) - return scale - except: - App.Console.PrintWarning(err) - return None - if "/" in label: - f = label.split("/") - try: - scale = float(f[0])/float(f[1]) + num = App.Units.Quantity(f[0]).Value + den = App.Units.Quantity(f[1]).Value + scale = num/den return scale except: + err = QT_TRANSLATE_NOOP("draft", + "Unable to convert input into a scale factor") App.Console.PrintWarning(err) return None @@ -90,7 +139,10 @@ def _set_scale(action): sb = mw.statusBar() statuswidget = sb.findChild(QtGui.QToolBar,"draft_status_widget") if action.text() == QT_TRANSLATE_NOOP("draft","custom"): - custom_scale = QtGui.QInputDialog.getText(None, "Custom scale", "") + dialog_text = QT_TRANSLATE_NOOP("draft", + "Set custom annotation scale in format x:x, x=x" + ) + custom_scale = QtGui.QInputDialog.getText(None, "Set custom scale", dialog_text) if custom_scale[1]: print(custom_scale[0]) scale = label_to_scale(custom_scale[0]) @@ -113,20 +165,19 @@ def init_draft_statusbar(sb): this function initializes draft statusbar """ - draft_scales = ["1:1000", "1:500", "1:250", "1:200", "1:100", - "1:50", "1:25","1:20", "1:10", "1:5","1:2", - "1:1", - "2:1", "5:1", "10:1", "20:1", - QT_TRANSLATE_NOOP("draft","custom"), - ] - - param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") - draft_annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) - statuswidget = QtGui.QToolBar() statuswidget.setObjectName("draft_status_widget") # SCALE TOOL ------------------------------------------------------------- + + # get scales list according to system units + draft_scales = get_scales() + + # get draft annotation scale + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + draft_annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + + # initializes scale widget statuswidget.draft_scales = draft_scales scaleLabel = QtGui.QPushButton("Scale") scaleLabel.setObjectName("ScaleLabel") @@ -139,8 +190,6 @@ def init_draft_statusbar(sb): menu.addAction(a) scaleLabel.setMenu(menu) gUnits.triggered.connect(_set_scale) - param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") - draft_annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) scale_label = scale_to_label(draft_annotation_scale) scaleLabel.setText(scale_label) tooltip = "Set the scale used by draft annotation tools" From 0e097ae7f9f4c868ac9ff72434bff70fadf19784 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sat, 7 Mar 2020 17:02:06 -0600 Subject: [PATCH 080/142] Draft: add init_draft_statusbar to CMakeLists --- src/Mod/Draft/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index c32795e1c3..845d39529f 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -49,6 +49,7 @@ SET(Draft_tests SET(Draft_utilities draftutils/__init__.py draftutils/init_tools.py + draftutils/init_draft_statusbar.py draftutils/utils.py draftutils/gui_utils.py draftutils/todo.py From 34cf98102cc3d91828071df8d9fd3119a0413121 Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 14 Mar 2020 12:24:10 +0100 Subject: [PATCH 081/142] [Draft] Dimension style object still have to split viewprovider from object. [Draft] Dimension Style code cleanup thx @vocx-fc for reviewing further cleanup [Draft] Dimension Style improvements Added a property to the dimension object to link the dimension style. Improved the update of dimensions when style changes. This can be done in 2 different ways: by setting AutoUpdate property to True or by activating Update function from the viewprovider context menu. --- src/Mod/Draft/Draft.py | 77 ++++-- src/Mod/Draft/InitGui.py | 1 + .../draftguitools/gui_style_dimension.py | 68 ++++++ src/Mod/Draft/draftobjects/style_dimension.py | 54 ++++ .../view_style_dimension.py | 230 ++++++++++++++++++ 5 files changed, 416 insertions(+), 14 deletions(-) create mode 100644 src/Mod/Draft/draftguitools/gui_style_dimension.py create mode 100644 src/Mod/Draft/draftobjects/style_dimension.py create mode 100644 src/Mod/Draft/draftviewproviders/view_style_dimension.py diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 23aea008eb..9e9e22fa8d 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -3372,27 +3372,75 @@ class _Dimension(_DraftObject): """The Draft Dimension object""" def __init__(self, obj): _DraftObject.__init__(self,obj,"Dimension") - obj.addProperty("App::PropertyVectorDistance","Start","Draft",QT_TRANSLATE_NOOP("App::Property","Startpoint of dimension")) - obj.addProperty("App::PropertyVectorDistance","End","Draft",QT_TRANSLATE_NOOP("App::Property","Endpoint of dimension")) - obj.addProperty("App::PropertyVector","Normal","Draft",QT_TRANSLATE_NOOP("App::Property","The normal direction of this dimension")) - obj.addProperty("App::PropertyVector","Direction","Draft",QT_TRANSLATE_NOOP("App::Property","The normal direction of this dimension")) - obj.addProperty("App::PropertyVectorDistance","Dimline","Draft",QT_TRANSLATE_NOOP("App::Property","Point through which the dimension line passes")) - obj.addProperty("App::PropertyLink","Support","Draft",QT_TRANSLATE_NOOP("App::Property","The object measured by this dimension")) - obj.addProperty("App::PropertyLinkSubList","LinkedGeometry","Draft",QT_TRANSLATE_NOOP("App::Property","The geometry this dimension is linked to")) - obj.addProperty("App::PropertyLength","Distance","Draft",QT_TRANSLATE_NOOP("App::Property","The measurement of this dimension")) - obj.addProperty("App::PropertyBool","Diameter","Draft",QT_TRANSLATE_NOOP("App::Property","For arc/circle measurements, false = radius, true = diameter")) + + # Annotation + obj.addProperty("App::PropertyLink","DimensionStyle", + "Annotation", + QT_TRANSLATE_NOOP("App::Property", + "Link dimension style")) + + # Draft + obj.addProperty("App::PropertyVectorDistance","Start", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Startpoint of dimension")) + + obj.addProperty("App::PropertyVectorDistance","End", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Endpoint of dimension")) + + obj.addProperty("App::PropertyVector","Normal", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The normal direction of this dimension")) + + obj.addProperty("App::PropertyVector","Direction", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The normal direction of this dimension")) + + obj.addProperty("App::PropertyVectorDistance","Dimline", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Point through which the dimension line passes")) + + obj.addProperty("App::PropertyLink","Support", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The object measured by this dimension")) + + obj.addProperty("App::PropertyLinkSubList","LinkedGeometry", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The geometry this dimension is linked to")) + + obj.addProperty("App::PropertyLength","Distance", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The measurement of this dimension")) + + obj.addProperty("App::PropertyBool","Diameter", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "For arc/circle measurements, false = radius, true = diameter")) obj.Start = FreeCAD.Vector(0,0,0) obj.End = FreeCAD.Vector(1,0,0) obj.Dimline = FreeCAD.Vector(0,1,0) obj.Normal = FreeCAD.Vector(0,0,1) def onChanged(self,obj,prop): - if hasattr(obj,"Distance"): - obj.setEditorMode('Distance',1) + if hasattr(obj, "Distance"): + obj.setEditorMode('Distance', 1) #if hasattr(obj,"Normal"): - # obj.setEditorMode('Normal',2) - if hasattr(obj,"Support"): - obj.setEditorMode('Support',2) + # obj.setEditorMode('Normal', 2) + if hasattr(obj, "Support"): + obj.setEditorMode('Support', 2) + if prop == "DimensionStyle": + if hasattr(obj, "DimensionStyle"): + from draftutils import gui_utils + gui_utils.format_object(target = obj, origin = obj.DimensionStyle) + def execute(self, obj): import DraftGeomUtils @@ -3503,6 +3551,7 @@ class _ViewProviderDimension(_ViewProviderDraft): obj.addProperty("App::PropertyFloat","ScaleMultiplier", "Annotation",QT_TRANSLATE_NOOP("App::Property", "Dimension size overall multiplier")) + # text properties obj.addProperty("App::PropertyFont","FontName", "Text",QT_TRANSLATE_NOOP("App::Property","Font name")) diff --git a/src/Mod/Draft/InitGui.py b/src/Mod/Draft/InitGui.py index 0178ec4978..7513c3d3fe 100644 --- a/src/Mod/Draft/InitGui.py +++ b/src/Mod/Draft/InitGui.py @@ -86,6 +86,7 @@ class DraftWorkbench(FreeCADGui.Workbench): from draftguitools import gui_polararray from draftguitools import gui_orthoarray from draftguitools import gui_arrays + from draftguitools import gui_style_dimension FreeCADGui.addLanguagePath(":/translations") FreeCADGui.addIconPath(":/icons") except Exception as exc: diff --git a/src/Mod/Draft/draftguitools/gui_style_dimension.py b/src/Mod/Draft/draftguitools/gui_style_dimension.py new file mode 100644 index 0000000000..fcba6fa0f1 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_style_dimension.py @@ -0,0 +1,68 @@ +# *************************************************************************** +# * (c) 2020 Carlo Pavan * +# * * +# * 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 * +# * * +# *************************************************************************** + +"""This module provides the Draft Dimension Style tool. +""" +## @package gui_style_dimension +# \ingroup DRAFT +# \brief This module provides the Draft Dimension Style tool. + +import FreeCAD as App +import FreeCADGui as Gui +from PySide import QtCore +from . import gui_base +from draftutils import utils +from draftobjects.style_dimension import make_dimension_style + +class GuiCommandDimensionStyle(gui_base.GuiCommandBase): + """ + The command creates a dimension style object + """ + + def GetResources(self): + _msg = ("Creates a new dimension style.\n" + "The object stores dimension preferences into the document." + ) + return {'Pixmap' : 'Draft_AutoGroup', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft", "Dimension Style"), + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft", _msg)} + + def IsActive(self): + if Gui.ActiveDocument: + return True + else: + return False + + def Activated(self): + sel = Gui.Selection.getSelection() + + if len(sel) == 1: + if utils.get_type(sel[0]) == 'Dimension': + make_dimension_style(sel[0]) + return self.finish() + + make_dimension_style() + return self.finish() + + +Gui.addCommand('Draft_DimensionStyle', GuiCommandDimensionStyle()) diff --git a/src/Mod/Draft/draftobjects/style_dimension.py b/src/Mod/Draft/draftobjects/style_dimension.py new file mode 100644 index 0000000000..c4f75d6778 --- /dev/null +++ b/src/Mod/Draft/draftobjects/style_dimension.py @@ -0,0 +1,54 @@ +# *************************************************************************** +# * (c) 2020 Carlo Pavan * +# * * +# * 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 * +# * * +# *************************************************************************** + +"""This module provides the object code for Draft DimensionStyle. +""" +## @package style_dimension +# \ingroup DRAFT +# \brief This module provides the object code for Draft DimensionStyle. + +import FreeCAD as App +import Draft +from Draft import _DraftObject +from PySide.QtCore import QT_TRANSLATE_NOOP + +if App.GuiUp: + import FreeCADGui as Gui + from draftviewproviders.view_style_dimension import ViewProviderDraftDimensionStyle + +def make_dimension_style(existing_dimension = None): + """ + Make dimension style + """ + if not App.ActiveDocument: + App.Console.PrintError("No active document. Aborting\n") + return + obj = App.ActiveDocument.addObject("App::FeaturePython","DimensionStyle") + DimensionStyle(obj) + if App.GuiUp: + ViewProviderDraftDimensionStyle(obj.ViewObject, existing_dimension) + return obj + +class DimensionStyle(_DraftObject): + def __init__(self, obj): + _DraftObject.__init__(self, obj, "DimensionStyle") \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_style_dimension.py b/src/Mod/Draft/draftviewproviders/view_style_dimension.py new file mode 100644 index 0000000000..aaae6fe778 --- /dev/null +++ b/src/Mod/Draft/draftviewproviders/view_style_dimension.py @@ -0,0 +1,230 @@ +# *************************************************************************** +# * (c) 2020 Carlo Pavan * +# * * +# * 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 * +# * * +# *************************************************************************** + +"""This module provides the view provider code for Draft DimensionStyle. +""" +## @package polararray +# \ingroup DRAFT +# \brief This module provides the view provider code for Draft DimensionStyle. + +import FreeCAD as App +import Draft +from Draft import _ViewProviderDraft +from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.utils as utils + +class ViewProviderDraftDimensionStyle(_ViewProviderDraft): + """ + Dimension style dont have a proper object but just a viewprovider. + It stores inside a document object dimension settings and restore them on demand. + """ + def __init__(self, vobj, existing_dimension = None): + """ + vobj properties type parameter type + ---------------------------------------------------------------------------------- + vobj.ScaleMultiplier" App::PropertyFloat "DraftAnnotationScale" Float + + vobj.FontName App::PropertyFont "textfont" Text + vobj.FontSize App::PropertyLength "textheight" Float + vobj.TextSpacing App::PropertyLength "dimspacing" Float + + vobj.Decimals App::PropertyInteger "dimPrecision" Integer + vobj.ShowUnit App::PropertyBool + vobj.UnitOverride App::PropertyString + + vobj.LineWidth App::PropertyFloat + vobj.LineColor App::PropertyColor + vobj.ArrowSize App::PropertyLength "arrowsize" Float + vobj.ArrowType App::PropertyEnumeration "dimsymbol" Integer + vobj.FlipArrows App::PropertyBool + vobj.DimOvershoot App::PropertyDistance "dimovershoot" Float + vobj.ExtLines App::PropertyDistance "extlines" Float + vobj.ExtOvershoot App::PropertyDistance "extovershoot" Float + vobj.ShowLine App::PropertyBool + """ + + # annotation properties + vobj.addProperty("App::PropertyFloat","ScaleMultiplier", + "Annotation", + QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) + + vobj.addProperty("App::PropertyBool","AutoUpdate", + "Annotation", + QT_TRANSLATE_NOOP("App::Property", + "Auto update associated dimensions")) + + # text properties + vobj.addProperty("App::PropertyFont","FontName", + "Text", + QT_TRANSLATE_NOOP("App::Property","Font name")) + + vobj.addProperty("App::PropertyLength","FontSize", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "Font size")) + + vobj.addProperty("App::PropertyLength","TextSpacing", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "The spacing between the text and " + "the dimension line")) + + # units properties + vobj.addProperty("App::PropertyInteger","Decimals", + "Units", + QT_TRANSLATE_NOOP("App::Property", + "The number of decimals to show")) + + vobj.addProperty("App::PropertyBool","ShowUnit", + "Units", + QT_TRANSLATE_NOOP("App::Property", + "Show the unit suffix")) + + vobj.addProperty("App::PropertyString","UnitOverride", + "Units", + QT_TRANSLATE_NOOP("App::Property", + "A unit to express the measurement. " + "Leave blank for system default")) + + # graphics properties + vobj.addProperty("App::PropertyFloat","LineWidth", + "Graphics", + QT_TRANSLATE_NOOP("App::Property","Line width")) + + vobj.addProperty("App::PropertyColor","LineColor", + "Graphics", + QT_TRANSLATE_NOOP("App::Property","Line color")) + + vobj.addProperty("App::PropertyLength","ArrowSize", + "Graphics", + QT_TRANSLATE_NOOP("App::Property","Arrow size")) + + vobj.addProperty("App::PropertyEnumeration","ArrowType", + "Graphics", + QT_TRANSLATE_NOOP("App::Property","Arrow type")) + + vobj.addProperty("App::PropertyBool","FlipArrows", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension arrows 180 degrees")) + + vobj.addProperty("App::PropertyDistance","DimOvershoot", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "The distance the dimension line is " + "extended past the extension lines")) + + vobj.addProperty("App::PropertyDistance","ExtLines", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Length of the extension lines")) + + vobj.addProperty("App::PropertyDistance","ExtOvershoot", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Length of the extension line above " + "the dimension line")) + + vobj.addProperty("App::PropertyBool","ShowLine", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Shows dimension line and arrows")) + + self.init_properties(vobj, existing_dimension) + + _ViewProviderDraft.__init__(self,vobj) + + def init_properties(self, vobj, existing_dimension): + """ + Initializes Dimension Style properties + """ + # get the style from FreeCAD Draft Parameters + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + + vobj.ScaleMultiplier = 1 / annotation_scale + vobj.AutoUpdate = True + + vobj.FontName = utils.get_param("textfont","") + vobj.FontSize = utils.get_param("textheight",0.20) + vobj.TextSpacing = utils.get_param("dimspacing",0.05) + + vobj.Decimals = utils.get_param("dimPrecision",2) + vobj.ShowUnit = utils.get_param("showUnit",True) + + vobj.ArrowSize = utils.get_param("arrowsize",0.1) + vobj.ArrowType = utils.ARROW_TYPES + vobj.ArrowType = utils.ARROW_TYPES[utils.get_param("dimsymbol",0)] + vobj.DimOvershoot = utils.get_param("dimovershoot",0) + vobj.ExtLines = utils.get_param("extlines",0.3) + vobj.ExtOvershoot = utils.get_param("extovershoot",0) + vobj.ShowLine = True + + if existing_dimension and hasattr(existing_dimension, "ViewObject"): + # get the style from given dimension + from draftutils import gui_utils + gui_utils.format_object(target = vobj.Object, origin = existing_dimension) + + def onChanged(self, vobj, prop): + if hasattr(vobj, "AutoUpdate"): + if vobj.AutoUpdate: + self.update_related_dimensions(vobj) + + def doubleClicked(self,vobj): + self.set_current(vobj) + + def setupContextMenu(self,vobj,menu): + action1 = menu.addAction("Set current") + action1.triggered.connect(lambda f=self.set_current, arg=vobj:f(arg)) + action2 = menu.addAction("Update dimensions") + action2.triggered.connect(lambda f=self.update_related_dimensions, arg=vobj:f(arg)) + + def set_current(self, vobj): + """ + Sets the current dimension style as default for new created dimensions + """ + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + param.SetFloat("DraftAnnotationScale", 1 / vobj.ScaleMultiplier) + + param.SetString("textfont", vobj.FontName) + param.SetFloat("textheight", vobj.FontSize) + param.SetFloat("dimspacing", vobj.TextSpacing) + + param.SetInt("dimPrecision", vobj.Decimals) + + param.SetFloat("arrowsize", vobj.ArrowSize) + param.SetInt("dimsymbol", utils.ARROW_TYPES.index(vobj.ArrowType)) + param.SetFloat("dimovershoot", vobj.DimOvershoot) + param.SetFloat("extlines", vobj.ExtLines) + param.SetFloat("extovershoot", vobj.ExtOvershoot) + + App.Console.PrintMessage("Current dimension style set to " + str(vobj.Object.Label) + "\n") + + def update_related_dimensions(self, vobj): + """ + Apply the style to the related dimensions + """ + from draftutils import gui_utils + for dim in vobj.Object.InList: + gui_utils.format_object(target = dim, origin = vobj.Object) From 1b7058fa35d330c55a37eeaf0a3404dca65da95d Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 21 Mar 2020 12:50:54 +0100 Subject: [PATCH 082/142] [Draft] New Icons for Annotation Style --- .../icons/Draft_Annotation_Style.svg | 139 +++++ .../Resources/icons/Draft_Dimension_Style.svg | 404 +++++++++++++ .../icons/Draft_Dimension_Style_Tree.svg | 542 ++++++++++++++++++ 3 files changed, 1085 insertions(+) create mode 100644 src/Mod/Draft/Resources/icons/Draft_Annotation_Style.svg create mode 100644 src/Mod/Draft/Resources/icons/Draft_Dimension_Style.svg create mode 100644 src/Mod/Draft/Resources/icons/Draft_Dimension_Style_Tree.svg diff --git a/src/Mod/Draft/Resources/icons/Draft_Annotation_Style.svg b/src/Mod/Draft/Resources/icons/Draft_Annotation_Style.svg new file mode 100644 index 0000000000..7e85ad57fd --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_Annotation_Style.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Mon Oct 10 13:44:52 2011 +0000 + + + [wmayer] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_Text.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson + + + The capital letter A, slightly italicized + + + A + letter + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Draft/Resources/icons/Draft_Dimension_Style.svg b/src/Mod/Draft/Resources/icons/Draft_Dimension_Style.svg new file mode 100644 index 0000000000..5e6f485bf6 --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_Dimension_Style.svg @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Mon Oct 10 13:44:52 2011 +0000 + + + [wmayer] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_Dimension.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson + + + + + line + dot + number + + + A number floating above a line corresponding to the upper three sides of a rectangle with a dot at each endpoint and corner + + + + diff --git a/src/Mod/Draft/Resources/icons/Draft_Dimension_Style_Tree.svg b/src/Mod/Draft/Resources/icons/Draft_Dimension_Style_Tree.svg new file mode 100644 index 0000000000..0b13bf93db --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_Dimension_Style_Tree.svg @@ -0,0 +1,542 @@ + + + Draft_Dimension_Tree + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Draft_Dimension_Tree + + Wed Oct 6 12:19:00 2019 -0600 + + + [vocx] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_Dimenstion_Tree + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson, [yorikvanhavre] + + + + + triangle + arrows + + + Two triangles, one pointing left, the other right, with a small line between the two + + + + + + + + + + + + + + + + From 530e8b6c58d92f258953da8c503944cb45b89e47 Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 21 Mar 2020 19:36:33 +0100 Subject: [PATCH 083/142] [Draft] Split Dimension object and reorganize of annotation objects part 1 --- src/Mod/Draft/Draft.py | 797 +----------------- src/Mod/Draft/draftguitools/gui_base.py | 93 ++ ...yle_dimension.py => gui_dimensionstyle.py} | 68 +- src/Mod/Draft/draftobjects/dimension.py | 235 ++++++ .../{style_dimension.py => dimensionstyle.py} | 9 +- .../Draft/draftobjects/draft_annotation.py | 78 ++ .../draftviewproviders/view_dimension.py | 549 ++++++++++++ ...le_dimension.py => view_dimensionstyle.py} | 21 +- .../view_draft_annotation.py | 263 ++++++ 9 files changed, 1313 insertions(+), 800 deletions(-) rename src/Mod/Draft/draftguitools/{gui_style_dimension.py => gui_dimensionstyle.py} (70%) create mode 100644 src/Mod/Draft/draftobjects/dimension.py rename src/Mod/Draft/draftobjects/{style_dimension.py => dimensionstyle.py} (90%) create mode 100644 src/Mod/Draft/draftobjects/draft_annotation.py create mode 100644 src/Mod/Draft/draftviewproviders/view_dimension.py rename src/Mod/Draft/draftviewproviders/{view_style_dimension.py => view_dimensionstyle.py} (94%) create mode 100644 src/Mod/Draft/draftviewproviders/view_draft_annotation.py diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 9e9e22fa8d..c85f37a7ec 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -251,79 +251,14 @@ def makeRectangle(length, height, placement=None, face=None, support=None): return obj -def makeDimension(p1,p2,p3=None,p4=None): - """makeDimension(p1,p2,[p3]) or makeDimension(object,i1,i2,p3) - or makeDimension(objlist,indices,p3): Creates a Dimension object with - the dimension line passign through p3.The current line width and color - will be used. There are multiple ways to create a dimension, depending on - the arguments you pass to it: - - (p1,p2,p3): creates a standard dimension from p1 to p2 - - (object,i1,i2,p3): creates a linked dimension to the given object, - measuring the distance between its vertices indexed i1 and i2 - - (object,i1,mode,p3): creates a linked dimension - to the given object, i1 is the index of the (curved) edge to measure, - and mode is either "radius" or "diameter". - """ - if not FreeCAD.ActiveDocument: - FreeCAD.Console.PrintError("No active document. Aborting\n") - return - obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","Dimension") - _Dimension(obj) - if gui: - _ViewProviderDimension(obj.ViewObject) - if isinstance(p1,Vector) and isinstance(p2,Vector): - obj.Start = p1 - obj.End = p2 - if not p3: - p3 = p2.sub(p1) - p3.multiply(0.5) - p3 = p1.add(p3) - elif isinstance(p2,int) and isinstance(p3,int): - l = [] - idx = (p2,p3) - l.append((p1,"Vertex"+str(p2+1))) - l.append((p1,"Vertex"+str(p3+1))) - obj.LinkedGeometry = l - obj.Support = p1 - p3 = p4 - if not p3: - v1 = obj.Base.Shape.Vertexes[idx[0]].Point - v2 = obj.Base.Shape.Vertexes[idx[1]].Point - p3 = v2.sub(v1) - p3.multiply(0.5) - p3 = v1.add(p3) - elif isinstance(p3,str): - l = [] - l.append((p1,"Edge"+str(p2+1))) - if p3 == "radius": - #l.append((p1,"Center")) - if FreeCAD.GuiUp: - obj.ViewObject.Override = "R $dim" - obj.Diameter = False - elif p3 == "diameter": - #l.append((p1,"Diameter")) - if FreeCAD.GuiUp: - obj.ViewObject.Override = "Ø $dim" - obj.Diameter = True - obj.LinkedGeometry = l - obj.Support = p1 - p3 = p4 - if not p3: - p3 = p1.Shape.Edges[p2].Curve.Center.add(Vector(1,0,0)) - obj.Dimline = p3 - if hasattr(FreeCAD,"DraftWorkingPlane"): - normal = FreeCAD.DraftWorkingPlane.axis - else: - normal = FreeCAD.Vector(0,0,1) - if gui: - # invert the normal if we are viewing it from the back - vnorm = get3DView().getViewDirection() - if vnorm.getAngle(normal) < math.pi/2: - normal = normal.negative() - obj.Normal = normal - if gui: - formatObject(obj) - select(obj) +from draftobjects.dimension import make_dimension +makeDimension = make_dimension + +from draftobjects.dimension import LinearDimension +_Dimension = LinearDimension + +from draftviewproviders.view_dimension import ViewProviderLinearDimension +_ViewProviderDimension = ViewProviderLinearDimension return obj @@ -3368,722 +3303,6 @@ class _ViewProviderDraftLink: else: return obj.ElementList -class _Dimension(_DraftObject): - """The Draft Dimension object""" - def __init__(self, obj): - _DraftObject.__init__(self,obj,"Dimension") - - # Annotation - obj.addProperty("App::PropertyLink","DimensionStyle", - "Annotation", - QT_TRANSLATE_NOOP("App::Property", - "Link dimension style")) - - # Draft - obj.addProperty("App::PropertyVectorDistance","Start", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "Startpoint of dimension")) - - obj.addProperty("App::PropertyVectorDistance","End", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "Endpoint of dimension")) - - obj.addProperty("App::PropertyVector","Normal", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "The normal direction of this dimension")) - - obj.addProperty("App::PropertyVector","Direction", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "The normal direction of this dimension")) - - obj.addProperty("App::PropertyVectorDistance","Dimline", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "Point through which the dimension line passes")) - - obj.addProperty("App::PropertyLink","Support", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "The object measured by this dimension")) - - obj.addProperty("App::PropertyLinkSubList","LinkedGeometry", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "The geometry this dimension is linked to")) - - obj.addProperty("App::PropertyLength","Distance", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "The measurement of this dimension")) - - obj.addProperty("App::PropertyBool","Diameter", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "For arc/circle measurements, false = radius, true = diameter")) - obj.Start = FreeCAD.Vector(0,0,0) - obj.End = FreeCAD.Vector(1,0,0) - obj.Dimline = FreeCAD.Vector(0,1,0) - obj.Normal = FreeCAD.Vector(0,0,1) - - def onChanged(self,obj,prop): - if hasattr(obj, "Distance"): - obj.setEditorMode('Distance', 1) - #if hasattr(obj,"Normal"): - # obj.setEditorMode('Normal', 2) - if hasattr(obj, "Support"): - obj.setEditorMode('Support', 2) - if prop == "DimensionStyle": - if hasattr(obj, "DimensionStyle"): - from draftutils import gui_utils - gui_utils.format_object(target = obj, origin = obj.DimensionStyle) - - - def execute(self, obj): - import DraftGeomUtils - # set start point and end point according to the linked geometry - if obj.LinkedGeometry: - if len(obj.LinkedGeometry) == 1: - lobj = obj.LinkedGeometry[0][0] - lsub = obj.LinkedGeometry[0][1] - if len(lsub) == 1: - if "Edge" in lsub[0]: - n = int(lsub[0][4:])-1 - edge = lobj.Shape.Edges[n] - if DraftGeomUtils.geomType(edge) == "Line": - obj.Start = edge.Vertexes[0].Point - obj.End = edge.Vertexes[-1].Point - elif DraftGeomUtils.geomType(edge) == "Circle": - c = edge.Curve.Center - r = edge.Curve.Radius - a = edge.Curve.Axis - ray = obj.Dimline.sub(c).projectToPlane(Vector(0,0,0),a) - if (ray.Length == 0): - ray = a.cross(Vector(1,0,0)) - if (ray.Length == 0): - ray = a.cross(Vector(0,1,0)) - ray = DraftVecUtils.scaleTo(ray,r) - if hasattr(obj,"Diameter"): - if obj.Diameter: - obj.Start = c.add(ray.negative()) - obj.End = c.add(ray) - else: - obj.Start = c - obj.End = c.add(ray) - elif len(lsub) == 2: - if ("Vertex" in lsub[0]) and ("Vertex" in lsub[1]): - n1 = int(lsub[0][6:])-1 - n2 = int(lsub[1][6:])-1 - obj.Start = lobj.Shape.Vertexes[n1].Point - obj.End = lobj.Shape.Vertexes[n2].Point - elif len(obj.LinkedGeometry) == 2: - lobj1 = obj.LinkedGeometry[0][0] - lobj2 = obj.LinkedGeometry[1][0] - lsub1 = obj.LinkedGeometry[0][1] - lsub2 = obj.LinkedGeometry[1][1] - if (len(lsub1) == 1) and (len(lsub2) == 1): - if ("Vertex" in lsub1[0]) and ("Vertex" in lsub2[1]): - n1 = int(lsub1[0][6:])-1 - n2 = int(lsub2[0][6:])-1 - obj.Start = lobj1.Shape.Vertexes[n1].Point - obj.End = lobj2.Shape.Vertexes[n2].Point - # set the distance property - total_len = (obj.Start.sub(obj.End)).Length - if round(obj.Distance.Value, precision()) != round(total_len, precision()): - obj.Distance = total_len - if gui: - if obj.ViewObject: - obj.ViewObject.update() - - -class _ViewProviderDimension(_ViewProviderDraft): - """ - A View Provider for the Draft Dimension object - - DIMENSION VIEW PROVIDER: - - | txt | e - ----o--------------------------------o----- - | | - | | d - | | - - a b c b a - - a = DimOvershoot (vobj) - b = Arrows (vobj) - c = Dimline (obj) - d = ExtLines (vobj) - e = ExtOvershoot (vobj) - txt = label (vobj) - - STRUCTURE: - vobj.node.color - .drawstyle - .lineswitch1.coords - .line - .marks - .marksDimOvershoot - .marksExtOvershoot - .label.textpos - .color - .font - .text - - vobj.node3d.color - .drawstyle - .lineswitch3.coords - .line - .marks - .marksDimOvershoot - .marksExtOvershoot - .label3d.textpos - .color - .font3d - .text3d - - """ - def __init__(self, obj): - # annotation properties - obj.addProperty("App::PropertyFloat","ScaleMultiplier", - "Annotation",QT_TRANSLATE_NOOP("App::Property", - "Dimension size overall multiplier")) - - # text properties - obj.addProperty("App::PropertyFont","FontName", - "Text",QT_TRANSLATE_NOOP("App::Property","Font name")) - obj.addProperty("App::PropertyLength","FontSize", - "Text",QT_TRANSLATE_NOOP("App::Property","Font size")) - obj.addProperty("App::PropertyLength","TextSpacing", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The spacing between the text and the dimension line")) - obj.addProperty("App::PropertyBool","FlipText", - "Text",QT_TRANSLATE_NOOP("App::Property", - "Rotate the dimension text 180 degrees")) - obj.addProperty("App::PropertyVectorDistance","TextPosition", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The position of the text. Leave (0,0,0) for automatic position")) - obj.addProperty("App::PropertyString","Override", - "Text",QT_TRANSLATE_NOOP("App::Property", - "Text override. Use $dim to insert the dimension length")) - # units properties - obj.addProperty("App::PropertyInteger","Decimals", - "Units",QT_TRANSLATE_NOOP("App::Property", - "The number of decimals to show")) - obj.addProperty("App::PropertyBool","ShowUnit", - "Units",QT_TRANSLATE_NOOP("App::Property", - "Show the unit suffix")) - obj.addProperty("App::PropertyString","UnitOverride", - "Units",QT_TRANSLATE_NOOP("App::Property", - "A unit to express the measurement. Leave blank for system default")) - # graphics properties - obj.addProperty("App::PropertyFloat","LineWidth", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Line width")) - obj.addProperty("App::PropertyColor","LineColor", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Line color")) - obj.addProperty("App::PropertyLength","ArrowSize", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow size")) - obj.addProperty("App::PropertyEnumeration","ArrowType", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow type")) - obj.addProperty("App::PropertyBool","FlipArrows", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Rotate the dimension arrows 180 degrees")) - obj.addProperty("App::PropertyDistance","DimOvershoot", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "The distance the dimension line is extended past the extension lines")) - obj.addProperty("App::PropertyDistance","ExtLines", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Length of the extension lines")) - obj.addProperty("App::PropertyDistance","ExtOvershoot", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Length of the extension line above the dimension line")) - obj.addProperty("App::PropertyBool","ShowLine", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Shows the dimension line and arrows")) - - param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") - annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) - obj.ScaleMultiplier = 1 / annotation_scale - obj.FontSize = getParam("textheight",0.20) - obj.TextSpacing = getParam("dimspacing",0.05) - obj.FontName = getParam("textfont","") - obj.ArrowSize = getParam("arrowsize",0.1) - obj.ArrowType = arrowtypes - obj.ArrowType = arrowtypes[getParam("dimsymbol",0)] - obj.ExtLines = getParam("extlines",0.3) - obj.DimOvershoot = getParam("dimovershoot",0) - obj.ExtOvershoot = getParam("extovershoot",0) - obj.Decimals = getParam("dimPrecision",2) - obj.ShowUnit = getParam("showUnit",True) - obj.ShowLine = True - _ViewProviderDraft.__init__(self,obj) - - def attach(self, vobj): - """called on object creation""" - from pivy import coin - self.Object = vobj.Object - self.color = coin.SoBaseColor() - self.font = coin.SoFont() - self.font3d = coin.SoFont() - self.text = coin.SoAsciiText() - self.text3d = coin.SoText2() - self.text.string = "d" # some versions of coin crash if string is not set - self.text3d.string = "d" - self.textpos = coin.SoTransform() - self.text.justification = self.text3d.justification = coin.SoAsciiText.CENTER - label = coin.SoSeparator() - label.addChild(self.textpos) - label.addChild(self.color) - label.addChild(self.font) - label.addChild(self.text) - label3d = coin.SoSeparator() - label3d.addChild(self.textpos) - label3d.addChild(self.color) - label3d.addChild(self.font3d) - label3d.addChild(self.text3d) - self.coord1 = coin.SoCoordinate3() - self.trans1 = coin.SoTransform() - self.coord2 = coin.SoCoordinate3() - self.trans2 = coin.SoTransform() - self.transDimOvershoot1 = coin.SoTransform() - self.transDimOvershoot2 = coin.SoTransform() - self.transExtOvershoot1 = coin.SoTransform() - self.transExtOvershoot2 = coin.SoTransform() - self.marks = coin.SoSeparator() - self.marksDimOvershoot = coin.SoSeparator() - self.marksExtOvershoot = coin.SoSeparator() - self.drawstyle = coin.SoDrawStyle() - self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance() - self.coords = coin.SoCoordinate3() - self.node = coin.SoGroup() - self.node.addChild(self.color) - self.node.addChild(self.drawstyle) - self.lineswitch2 = coin.SoSwitch() - self.lineswitch2.whichChild = -3 - self.node.addChild(self.lineswitch2) - self.lineswitch2.addChild(self.coords) - self.lineswitch2.addChild(self.line) - self.lineswitch2.addChild(self.marks) - self.lineswitch2.addChild(self.marksDimOvershoot) - self.lineswitch2.addChild(self.marksExtOvershoot) - self.node.addChild(label) - self.node3d = coin.SoGroup() - self.node3d.addChild(self.color) - self.node3d.addChild(self.drawstyle) - self.lineswitch3 = coin.SoSwitch() - self.lineswitch3.whichChild = -3 - self.node3d.addChild(self.lineswitch3) - self.lineswitch3.addChild(self.coords) - self.lineswitch3.addChild(self.line) - self.lineswitch3.addChild(self.marks) - self.lineswitch3.addChild(self.marksDimOvershoot) - self.lineswitch3.addChild(self.marksExtOvershoot) - self.node3d.addChild(label3d) - vobj.addDisplayMode(self.node,"2D") - vobj.addDisplayMode(self.node3d,"3D") - self.updateData(vobj.Object,"Start") - self.onChanged(vobj,"FontSize") - self.onChanged(vobj,"FontName") - self.onChanged(vobj,"ArrowType") - self.onChanged(vobj,"LineColor") - self.onChanged(vobj,"DimOvershoot") - self.onChanged(vobj,"ExtOvershoot") - - def updateData(self, obj, prop): - """called when the base object is changed""" - import DraftGui - if prop in ["Start","End","Dimline","Direction"]: - - if obj.Start == obj.End: - return - - if not hasattr(self,"node"): - return - - import Part, DraftGeomUtils - from pivy import coin - - # calculate the 4 points - self.p1 = obj.Start - self.p4 = obj.End - base = None - if hasattr(obj,"Direction"): - if not DraftVecUtils.isNull(obj.Direction): - v2 = self.p1.sub(obj.Dimline) - v3 = self.p4.sub(obj.Dimline) - v2 = DraftVecUtils.project(v2,obj.Direction) - v3 = DraftVecUtils.project(v3,obj.Direction) - self.p2 = obj.Dimline.add(v2) - self.p3 = obj.Dimline.add(v3) - if DraftVecUtils.equals(self.p2,self.p3): - base = None - proj = None - else: - base = Part.LineSegment(self.p2,self.p3).toShape() - proj = DraftGeomUtils.findDistance(self.p1,base) - if proj: - proj = proj.negative() - if not base: - if DraftVecUtils.equals(self.p1,self.p4): - base = None - proj = None - else: - base = Part.LineSegment(self.p1,self.p4).toShape() - proj = DraftGeomUtils.findDistance(obj.Dimline,base) - if proj: - self.p2 = self.p1.add(proj.negative()) - self.p3 = self.p4.add(proj.negative()) - else: - self.p2 = self.p1 - self.p3 = self.p4 - if proj: - if hasattr(obj.ViewObject,"ExtLines") and hasattr(obj.ViewObject,"ScaleMultiplier"): - dmax = obj.ViewObject.ExtLines.Value * obj.ViewObject.ScaleMultiplier - if dmax and (proj.Length > dmax): - if (dmax > 0): - self.p1 = self.p2.add(DraftVecUtils.scaleTo(proj,dmax)) - self.p4 = self.p3.add(DraftVecUtils.scaleTo(proj,dmax)) - else: - rest = proj.Length + dmax - self.p1 = self.p2.add(DraftVecUtils.scaleTo(proj,rest)) - self.p4 = self.p3.add(DraftVecUtils.scaleTo(proj,rest)) - else: - proj = (self.p3.sub(self.p2)).cross(Vector(0,0,1)) - - # calculate the arrows positions - self.trans1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.coord1.point.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.trans2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) - self.coord2.point.setValue((self.p3.x,self.p3.y,self.p3.z)) - - # calculate dimension and extension lines overshoots positions - self.transDimOvershoot1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.transDimOvershoot2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) - self.transExtOvershoot1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.transExtOvershoot2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) - - # calculate the text position and orientation - if hasattr(obj,"Normal"): - if DraftVecUtils.isNull(obj.Normal): - if proj: - norm = (self.p3.sub(self.p2).cross(proj)).negative() - else: - norm = Vector(0,0,1) - else: - norm = FreeCAD.Vector(obj.Normal) - else: - if proj: - norm = (self.p3.sub(self.p2).cross(proj)).negative() - else: - norm = Vector(0,0,1) - if not DraftVecUtils.isNull(norm): - norm.normalize() - u = self.p3.sub(self.p2) - u.normalize() - v1 = norm.cross(u) - rot1 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u,v1,norm)).Rotation.Q - self.transDimOvershoot1.rotation.setValue((rot1[0],rot1[1],rot1[2],rot1[3])) - self.transDimOvershoot2.rotation.setValue((rot1[0],rot1[1],rot1[2],rot1[3])) - if hasattr(obj.ViewObject,"FlipArrows"): - if obj.ViewObject.FlipArrows: - u = u.negative() - v2 = norm.cross(u) - rot2 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u,v2,norm)).Rotation.Q - self.trans1.rotation.setValue((rot2[0],rot2[1],rot2[2],rot2[3])) - self.trans2.rotation.setValue((rot2[0],rot2[1],rot2[2],rot2[3])) - if self.p1 != self.p2: - u3 = self.p1.sub(self.p2) - u3.normalize() - v3 = norm.cross(u3) - rot3 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u3,v3,norm)).Rotation.Q - self.transExtOvershoot1.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) - self.transExtOvershoot2.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) - if hasattr(obj.ViewObject,"TextSpacing") and hasattr(obj.ViewObject,"ScaleMultiplier"): - ts = obj.ViewObject.TextSpacing.Value * obj.ViewObject.ScaleMultiplier - offset = DraftVecUtils.scaleTo(v1,ts) - else: - offset = DraftVecUtils.scaleTo(v1,0.05) - rott = rot1 - if hasattr(obj.ViewObject,"FlipText"): - if obj.ViewObject.FlipText: - rott = FreeCAD.Rotation(*rott).multiply(FreeCAD.Rotation(norm,180)).Q - offset = offset.negative() - # setting text - try: - m = obj.ViewObject.DisplayMode - except: # swallow all exceptions here since it always fails on first run (Displaymode enum no set yet) - m = ["2D","3D"][getParam("dimstyle",0)] - if m == "3D": - offset = offset.negative() - self.tbase = (self.p2.add((self.p3.sub(self.p2).multiply(0.5)))).add(offset) - if hasattr(obj.ViewObject,"TextPosition"): - if not DraftVecUtils.isNull(obj.ViewObject.TextPosition): - self.tbase = obj.ViewObject.TextPosition - self.textpos.translation.setValue([self.tbase.x,self.tbase.y,self.tbase.z]) - self.textpos.rotation = coin.SbRotation(rott[0],rott[1],rott[2],rott[3]) - su = True - if hasattr(obj.ViewObject,"ShowUnit"): - su = obj.ViewObject.ShowUnit - # set text value - l = self.p3.sub(self.p2).Length - unit = None - if hasattr(obj.ViewObject,"UnitOverride"): - unit = obj.ViewObject.UnitOverride - # special representation if "Building US" scheme - if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt("UserSchema",0) == 5: - s = FreeCAD.Units.Quantity(l,FreeCAD.Units.Length).UserString - self.string = s.replace("' ","'- ") - self.string = s.replace("+"," ") - elif hasattr(obj.ViewObject,"Decimals"): - self.string = DraftGui.displayExternal(l,obj.ViewObject.Decimals,'Length',su,unit) - else: - self.string = DraftGui.displayExternal(l,None,'Length',su,unit) - if hasattr(obj.ViewObject,"Override"): - if obj.ViewObject.Override: - self.string = obj.ViewObject.Override.replace("$dim",\ - self.string) - self.text.string = self.text3d.string = stringencodecoin(self.string) - - # set the lines - if m == "3D": - # calculate the spacing of the text - textsize = (len(self.string)*obj.ViewObject.FontSize.Value)/4.0 - spacing = ((self.p3.sub(self.p2)).Length/2.0) - textsize - self.p2a = self.p2.add(DraftVecUtils.scaleTo(self.p3.sub(self.p2),spacing)) - self.p2b = self.p3.add(DraftVecUtils.scaleTo(self.p2.sub(self.p3),spacing)) - self.coords.point.setValues([[self.p1.x,self.p1.y,self.p1.z], - [self.p2.x,self.p2.y,self.p2.z], - [self.p2a.x,self.p2a.y,self.p2a.z], - [self.p2b.x,self.p2b.y,self.p2b.z], - [self.p3.x,self.p3.y,self.p3.z], - [self.p4.x,self.p4.y,self.p4.z]]) - #self.line.numVertices.setValues([3,3]) - self.line.coordIndex.setValues(0,7,(0,1,2,-1,3,4,5)) - else: - self.coords.point.setValues([[self.p1.x,self.p1.y,self.p1.z], - [self.p2.x,self.p2.y,self.p2.z], - [self.p3.x,self.p3.y,self.p3.z], - [self.p4.x,self.p4.y,self.p4.z]]) - #self.line.numVertices.setValue(4) - self.line.coordIndex.setValues(0,4,(0,1,2,3)) - - def onChanged(self, vobj, prop): - """called when a view property has changed""" - if prop == "ScaleMultiplier" and hasattr(vobj,"ScaleMultiplier"): - # update all dimension values - if hasattr(self,"font"): - self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier - if hasattr(self,"font3d"): - self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier - if hasattr(self,"node") and hasattr(self,"p2") and hasattr(vobj,"ArrowSize"): - self.remove_dim_arrows() - self.draw_dim_arrows(vobj) - if hasattr(vobj,"DimOvershoot"): - self.remove_dim_overshoot() - self.draw_dim_overshoot(vobj) - if hasattr(vobj,"ExtOvershoot"): - self.remove_ext_overshoot() - self.draw_ext_overshoot(vobj) - self.updateData(vobj.Object,"Start") - vobj.Object.touch() - - elif (prop == "FontSize") and hasattr(vobj,"FontSize"): - if hasattr(self,"font") and hasattr(vobj,"ScaleMultiplier"): - self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier - if hasattr(self,"font3d") and hasattr(vobj,"ScaleMultiplier"): - self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier - vobj.Object.touch() - - elif (prop == "FontName") and hasattr(vobj,"FontName"): - if hasattr(self,"font") and hasattr(self,"font3d"): - self.font.name = self.font3d.name = str(vobj.FontName) - vobj.Object.touch() - - elif (prop == "LineColor") and hasattr(vobj,"LineColor"): - if hasattr(self,"color"): - c = vobj.LineColor - self.color.rgb.setValue(c[0],c[1],c[2]) - - elif (prop == "LineWidth") and hasattr(vobj,"LineWidth"): - if hasattr(self,"drawstyle"): - self.drawstyle.lineWidth = vobj.LineWidth - - elif (prop in ["ArrowSize","ArrowType"]) and hasattr(vobj,"ArrowSize"): - if hasattr(self,"node") and hasattr(self,"p2"): - if hasattr(vobj,"ScaleMultiplier"): - self.remove_dim_arrows() - self.draw_dim_arrows(vobj) - vobj.Object.touch() - - elif (prop == "DimOvershoot") and hasattr(vobj,"DimOvershoot"): - if hasattr(vobj,"ScaleMultiplier"): - self.remove_dim_overshoot() - self.draw_dim_overshoot(vobj) - vobj.Object.touch() - - elif (prop == "ExtOvershoot") and hasattr(vobj,"ExtOvershoot"): - if hasattr(vobj,"ScaleMultiplier"): - self.remove_ext_overshoot() - self.draw_ext_overshoot(vobj) - vobj.Object.touch() - - elif (prop == "ShowLine") and hasattr(vobj,"ShowLine"): - if vobj.ShowLine: - self.lineswitch2.whichChild = -3 - self.lineswitch3.whichChild = -3 - else: - self.lineswitch2.whichChild = -1 - self.lineswitch3.whichChild = -1 - else: - self.updateData(vobj.Object,"Start") - - def remove_dim_arrows(self): - # remove existing nodes - self.node.removeChild(self.marks) - self.node3d.removeChild(self.marks) - - def draw_dim_arrows(self, vobj): - from pivy import coin - - if not hasattr(vobj,"ArrowType"): - return - - if self.p3.x < self.p2.x: - inv = False - else: - inv = True - - # set scale - symbol = arrowtypes.index(vobj.ArrowType) - s = vobj.ArrowSize.Value * vobj.ScaleMultiplier - self.trans1.scaleFactor.setValue((s,s,s)) - self.trans2.scaleFactor.setValue((s,s,s)) - - - # set new nodes - self.marks = coin.SoSeparator() - self.marks.addChild(self.color) - s1 = coin.SoSeparator() - if symbol == "Circle": - s1.addChild(self.coord1) - else: - s1.addChild(self.trans1) - s1.addChild(dimSymbol(symbol,invert=not(inv))) - self.marks.addChild(s1) - s2 = coin.SoSeparator() - if symbol == "Circle": - s2.addChild(self.coord2) - else: - s2.addChild(self.trans2) - s2.addChild(dimSymbol(symbol,invert=inv)) - self.marks.addChild(s2) - self.node.insertChild(self.marks,2) - self.node3d.insertChild(self.marks,2) - - def remove_dim_overshoot(self): - self.node.removeChild(self.marksDimOvershoot) - self.node3d.removeChild(self.marksDimOvershoot) - - - def draw_dim_overshoot(self, vobj): - from pivy import coin - - # set scale - s = vobj.DimOvershoot.Value * vobj.ScaleMultiplier - self.transDimOvershoot1.scaleFactor.setValue((s,s,s)) - self.transDimOvershoot2.scaleFactor.setValue((s,s,s)) - - # remove existing nodes - - # set new nodes - self.marksDimOvershoot = coin.SoSeparator() - if vobj.DimOvershoot.Value: - self.marksDimOvershoot.addChild(self.color) - s1 = coin.SoSeparator() - s1.addChild(self.transDimOvershoot1) - s1.addChild(dimDash((-1,0,0),(0,0,0))) - self.marksDimOvershoot.addChild(s1) - s2 = coin.SoSeparator() - s2.addChild(self.transDimOvershoot2) - s2.addChild(dimDash((0,0,0),(1,0,0))) - self.marksDimOvershoot.addChild(s2) - self.node.insertChild(self.marksDimOvershoot,2) - self.node3d.insertChild(self.marksDimOvershoot,2) - - - def remove_ext_overshoot(self): - self.node.removeChild(self.marksExtOvershoot) - self.node3d.removeChild(self.marksExtOvershoot) - - - def draw_ext_overshoot(self, vobj): - from pivy import coin - - # set scale - s = vobj.ExtOvershoot.Value * vobj.ScaleMultiplier - self.transExtOvershoot1.scaleFactor.setValue((s,s,s)) - self.transExtOvershoot2.scaleFactor.setValue((s,s,s)) - - # set new nodes - self.marksExtOvershoot = coin.SoSeparator() - if vobj.ExtOvershoot.Value: - self.marksExtOvershoot.addChild(self.color) - s1 = coin.SoSeparator() - s1.addChild(self.transExtOvershoot1) - s1.addChild(dimDash((0,0,0),(-1,0,0))) - self.marksExtOvershoot.addChild(s1) - s2 = coin.SoSeparator() - s2.addChild(self.transExtOvershoot2) - s2.addChild(dimDash((0,0,0),(-1,0,0))) - self.marksExtOvershoot.addChild(s2) - self.node.insertChild(self.marksExtOvershoot,2) - self.node3d.insertChild(self.marksExtOvershoot,2) - - - def doubleClicked(self,vobj): - self.setEdit(vobj) - - def getDisplayModes(self,vobj): - return ["2D","3D"] - - def getDefaultDisplayMode(self): - if hasattr(self,"defaultmode"): - return self.defaultmode - else: - return ["2D","3D"][getParam("dimstyle",0)] - - def setDisplayMode(self,mode): - return mode - - def is_linked_to_circle(self): - import DraftGeomUtils - _obj = self.Object - if _obj.LinkedGeometry and len(_obj.LinkedGeometry) == 1: - lobj = _obj.LinkedGeometry[0][0] - lsub = _obj.LinkedGeometry[0][1] - if len(lsub) == 1 and "Edge" in lsub[0]: - n = int(lsub[0][4:]) - 1 - edge = lobj.Shape.Edges[n] - if DraftGeomUtils.geomType(edge) == "Circle": - return True - return False - - def getIcon(self): - if self.is_linked_to_circle(): - return ":/icons/Draft_DimensionRadius.svg" - return ":/icons/Draft_Dimension_Tree.svg" - - def __getstate__(self): - return self.Object.ViewObject.DisplayMode - - def __setstate__(self,state): - if state: - self.defaultmode = state - self.setDisplayMode(state) - class _AngularDimension(_DraftObject): """The Draft AngularDimension object""" def __init__(self, obj): diff --git a/src/Mod/Draft/draftguitools/gui_base.py b/src/Mod/Draft/draftguitools/gui_base.py index 1a149cc8cc..40b0660ccc 100644 --- a/src/Mod/Draft/draftguitools/gui_base.py +++ b/src/Mod/Draft/draftguitools/gui_base.py @@ -32,6 +32,99 @@ import FreeCADGui as Gui import draftutils.todo as todo +class GuiCommandSimplest: + """Simplest base class for GuiCommands. + + This class only sets up the command name and the document object + to use for the command. + When it is executed, it logs the command name to the log file, + and prints the command name to the console. + + It implements the `IsActive` method, which must return `True` + when the command should be available. + It should return `True` when there is an active document, + otherwise the command (button or menu) should be disabled. + + This class is meant to be inherited by other GuiCommand classes + to quickly log the command name, and set the correct document object. + + Parameter + --------- + name: str, optional + It defaults to `'None'`. + The name of the action that is being run, + for example, `'Heal'`, `'Flip dimensions'`, + `'Line'`, `'Circle'`, etc. + + doc: App::Document, optional + It defaults to the value of `App.activeDocument()`. + The document object itself, which indicates where the actions + of the command will be executed. + + Attributes + ---------- + command_name: str + This is the command name, which is assigned by `name`. + + doc: App::Document + This is the document object itself, which is assigned by `doc`. + + This attribute should be used by functions to make sure + that the operations are performed in the correct document + and not in other documents. + To set the active document we can use + + >>> App.setActiveDocument(self.doc.Name) + """ + + def __init__(self, name="None", doc=App.activeDocument()): + self.command_name = name + self.doc = doc + + def IsActive(self): + """Return True when this command should be available. + + It is `True` when there is a document. + """ + if App.activeDocument(): + return True + else: + return False + + def Activated(self): + """Execute when the command is called. + + Log the command name to the log file and console. + Also update the `doc` attribute. + """ + self.doc = App.activeDocument() + _log("Document: {}".format(self.doc.Label)) + _log("GuiCommand: {}".format(self.command_name)) + _msg("{}".format(16*"-")) + _msg("GuiCommand: {}".format(self.command_name)) + + +class GuiCommandNeedsSelection(GuiCommandSimplest): + """Base class for GuiCommands that need a selection to be available. + + It re-implements the `IsActive` method to return `True` + when there is both an active document and an active selection. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def IsActive(self): + """Return True when this command should be available. + + It is `True` when there is a selection. + """ + if App.activeDocument() and Gui.Selection.getSelection(): + return True + else: + return False + + class GuiCommandBase: """Generic class that is the basis of all Gui commands. diff --git a/src/Mod/Draft/draftguitools/gui_style_dimension.py b/src/Mod/Draft/draftguitools/gui_dimensionstyle.py similarity index 70% rename from src/Mod/Draft/draftguitools/gui_style_dimension.py rename to src/Mod/Draft/draftguitools/gui_dimensionstyle.py index fcba6fa0f1..ff3fec839f 100644 --- a/src/Mod/Draft/draftguitools/gui_style_dimension.py +++ b/src/Mod/Draft/draftguitools/gui_dimensionstyle.py @@ -32,18 +32,78 @@ import FreeCADGui as Gui from PySide import QtCore from . import gui_base from draftutils import utils -from draftobjects.style_dimension import make_dimension_style +from draftobjects.dimensionstyle import make_dimension_style -class GuiCommandDimensionStyle(gui_base.GuiCommandBase): + +''' +class AnnotationStylesContainer: + """The Layer Container""" + + def __init__(self, obj): + + self.Type = "AnnotationContainer" + obj.Proxy = self + + def execute(self, obj): + + g = obj.Group + g.sort(key=lambda o: o.Label) + obj.Group = g + + def __getstate__(self): + + if hasattr(self, "Type"): + return self.Type + + def __setstate__(self, state): + + if state: + self.Type = state + + +class ViewProviderAnnotationStylesContainer: + """A View Provider for the Layer Container""" + + def __init__(self, vobj): + + vobj.Proxy = self + + def getIcon(self): + + return ":/icons/Draft_Annotation_Style.svg" + + def attach(self, vobj): + + self.Object = vobj.Object + + def __getstate__(self): + + return None + + def __setstate__(self, state): + + return None + + +''' + + + +# XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + +class GuiCommandDimensionStyle(gui_base.GuiCommandSimplest): """ The command creates a dimension style object """ + def __init__(self): + super().__init__() + self.command_name = "DimensionStyle" def GetResources(self): _msg = ("Creates a new dimension style.\n" "The object stores dimension preferences into the document." ) - return {'Pixmap' : 'Draft_AutoGroup', + return {'Pixmap' : 'Draft_Annotation_Style', 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft", "Dimension Style"), 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft", _msg)} @@ -59,10 +119,8 @@ class GuiCommandDimensionStyle(gui_base.GuiCommandBase): if len(sel) == 1: if utils.get_type(sel[0]) == 'Dimension': make_dimension_style(sel[0]) - return self.finish() make_dimension_style() - return self.finish() Gui.addCommand('Draft_DimensionStyle', GuiCommandDimensionStyle()) diff --git a/src/Mod/Draft/draftobjects/dimension.py b/src/Mod/Draft/draftobjects/dimension.py new file mode 100644 index 0000000000..e4e194aea4 --- /dev/null +++ b/src/Mod/Draft/draftobjects/dimension.py @@ -0,0 +1,235 @@ +# *************************************************************************** +# * (c) 2020 Carlo Pavan * +# * * +# * 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 * +# * * +# *************************************************************************** + +"""This module provides the object code for Draft DimensionStyle. +""" +## @package style_dimension +# \ingroup DRAFT +# \brief This module provides the object code for Draft DimensionStyle. + +import FreeCAD as App +import math +from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.gui_utils as gui_utils +import draftutils.utils as utils +from draftobjects.draft_annotation import DimensionBase +from draftviewproviders.view_dimension import ViewProviderLinearDimension + +def make_dimension(p1,p2,p3=None,p4=None): + """makeDimension(p1,p2,[p3]) or makeDimension(object,i1,i2,p3) + or makeDimension(objlist,indices,p3): Creates a Dimension object with + the dimension line passign through p3.The current line width and color + will be used. There are multiple ways to create a dimension, depending on + the arguments you pass to it: + - (p1,p2,p3): creates a standard dimension from p1 to p2 + - (object,i1,i2,p3): creates a linked dimension to the given object, + measuring the distance between its vertices indexed i1 and i2 + - (object,i1,mode,p3): creates a linked dimension + to the given object, i1 is the index of the (curved) edge to measure, + and mode is either "radius" or "diameter". + """ + if not App.ActiveDocument: + App.Console.PrintError("No active document. Aborting\n") + return + obj = App.ActiveDocument.addObject("App::FeaturePython","Dimension") + LinearDimension(obj) + if App.GuiUp: + ViewProviderLinearDimension(obj.ViewObject) + if isinstance(p1,App.Vector) and isinstance(p2,App.Vector): + obj.Start = p1 + obj.End = p2 + if not p3: + p3 = p2.sub(p1) + p3.multiply(0.5) + p3 = p1.add(p3) + elif isinstance(p2,int) and isinstance(p3,int): + l = [] + idx = (p2,p3) + l.append((p1,"Vertex"+str(p2+1))) + l.append((p1,"Vertex"+str(p3+1))) + obj.LinkedGeometry = l + obj.Support = p1 + p3 = p4 + if not p3: + v1 = obj.Base.Shape.Vertexes[idx[0]].Point + v2 = obj.Base.Shape.Vertexes[idx[1]].Point + p3 = v2.sub(v1) + p3.multiply(0.5) + p3 = v1.add(p3) + elif isinstance(p3,str): + l = [] + l.append((p1,"Edge"+str(p2+1))) + if p3 == "radius": + #l.append((p1,"Center")) + if App.GuiUp: + obj.ViewObject.Override = "R $dim" + obj.Diameter = False + elif p3 == "diameter": + #l.append((p1,"Diameter")) + if App.GuiUp: + obj.ViewObject.Override = "Ø $dim" + obj.Diameter = True + obj.LinkedGeometry = l + obj.Support = p1 + p3 = p4 + if not p3: + p3 = p1.Shape.Edges[p2].Curve.Center.add(App.Vector(1,0,0)) + obj.Dimline = p3 + if hasattr(App,"DraftWorkingPlane"): + normal = App.DraftWorkingPlane.axis + else: + normal = App.Vector(0,0,1) + if App.GuiUp: + # invert the normal if we are viewing it from the back + vnorm = gui_utils.get3DView().getViewDirection() + if vnorm.getAngle(normal) < math.pi/2: + normal = normal.negative() + obj.Normal = normal + if App.GuiUp: + gui_utils.format_object(obj) + gui_utils.select(obj) + + return obj + +class LinearDimension(DimensionBase): + """The Draft Dimension object""" + def __init__(self, obj): + DimensionBase.__init__(self,obj,"Dimension") + + # Draft + obj.addProperty("App::PropertyVectorDistance","Start", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Startpoint of dimension")) + + obj.addProperty("App::PropertyVectorDistance","End", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Endpoint of dimension")) + + obj.addProperty("App::PropertyVector","Normal", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The normal direction of this dimension")) + + obj.addProperty("App::PropertyVector","Direction", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The normal direction of this dimension")) + + obj.addProperty("App::PropertyVectorDistance","Dimline", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Point through which the dimension line passes")) + + obj.addProperty("App::PropertyLink","Support", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The object measured by this dimension")) + + obj.addProperty("App::PropertyLinkSubList","LinkedGeometry", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The geometry this dimension is linked to")) + + obj.addProperty("App::PropertyLength","Distance", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The measurement of this dimension")) + + obj.addProperty("App::PropertyBool","Diameter", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "For arc/circle measurements, false = radius, true = diameter")) + obj.Start = App.Vector(0,0,0) + obj.End = App.Vector(1,0,0) + obj.Dimline = App.Vector(0,1,0) + obj.Normal = App.Vector(0,0,1) + + def onChanged(self,obj,prop): + if hasattr(obj, "Distance"): + obj.setEditorMode('Distance', 1) + #if hasattr(obj,"Normal"): + # obj.setEditorMode('Normal', 2) + if hasattr(obj, "Support"): + obj.setEditorMode('Support', 2) + if prop == "DimensionStyle": + if hasattr(obj, "DimensionStyle"): + gui_utils.format_object(target = obj, origin = obj.DimensionStyle) + + + def execute(self, obj): + import DraftGeomUtils + # set start point and end point according to the linked geometry + if obj.LinkedGeometry: + if len(obj.LinkedGeometry) == 1: + lobj = obj.LinkedGeometry[0][0] + lsub = obj.LinkedGeometry[0][1] + if len(lsub) == 1: + if "Edge" in lsub[0]: + n = int(lsub[0][4:])-1 + edge = lobj.Shape.Edges[n] + if DraftGeomUtils.geomType(edge) == "Line": + obj.Start = edge.Vertexes[0].Point + obj.End = edge.Vertexes[-1].Point + elif DraftGeomUtils.geomType(edge) == "Circle": + c = edge.Curve.Center + r = edge.Curve.Radius + a = edge.Curve.Axis + ray = obj.Dimline.sub(c).projectToPlane(App.Vector(0,0,0),a) + if (ray.Length == 0): + ray = a.cross(App.Vector(1,0,0)) + if (ray.Length == 0): + ray = a.cross(App.Vector(0,1,0)) + ray = DraftVecUtils.scaleTo(ray,r) + if hasattr(obj,"Diameter"): + if obj.Diameter: + obj.Start = c.add(ray.negative()) + obj.End = c.add(ray) + else: + obj.Start = c + obj.End = c.add(ray) + elif len(lsub) == 2: + if ("Vertex" in lsub[0]) and ("Vertex" in lsub[1]): + n1 = int(lsub[0][6:])-1 + n2 = int(lsub[1][6:])-1 + obj.Start = lobj.Shape.Vertexes[n1].Point + obj.End = lobj.Shape.Vertexes[n2].Point + elif len(obj.LinkedGeometry) == 2: + lobj1 = obj.LinkedGeometry[0][0] + lobj2 = obj.LinkedGeometry[1][0] + lsub1 = obj.LinkedGeometry[0][1] + lsub2 = obj.LinkedGeometry[1][1] + if (len(lsub1) == 1) and (len(lsub2) == 1): + if ("Vertex" in lsub1[0]) and ("Vertex" in lsub2[1]): + n1 = int(lsub1[0][6:])-1 + n2 = int(lsub2[0][6:])-1 + obj.Start = lobj1.Shape.Vertexes[n1].Point + obj.End = lobj2.Shape.Vertexes[n2].Point + # set the distance property + total_len = (obj.Start.sub(obj.End)).Length + if round(obj.Distance.Value, utils.precision()) != round(total_len, utils.precision()): + obj.Distance = total_len + if App.GuiUp: + if obj.ViewObject: + obj.ViewObject.update() \ No newline at end of file diff --git a/src/Mod/Draft/draftobjects/style_dimension.py b/src/Mod/Draft/draftobjects/dimensionstyle.py similarity index 90% rename from src/Mod/Draft/draftobjects/style_dimension.py rename to src/Mod/Draft/draftobjects/dimensionstyle.py index c4f75d6778..8f86251725 100644 --- a/src/Mod/Draft/draftobjects/style_dimension.py +++ b/src/Mod/Draft/draftobjects/dimensionstyle.py @@ -28,13 +28,12 @@ # \brief This module provides the object code for Draft DimensionStyle. import FreeCAD as App -import Draft -from Draft import _DraftObject +from draftobjects.draft_annotation import DraftAnnotation from PySide.QtCore import QT_TRANSLATE_NOOP if App.GuiUp: import FreeCADGui as Gui - from draftviewproviders.view_style_dimension import ViewProviderDraftDimensionStyle + from draftviewproviders.view_dimensionstyle import ViewProviderDraftDimensionStyle def make_dimension_style(existing_dimension = None): """ @@ -49,6 +48,6 @@ def make_dimension_style(existing_dimension = None): ViewProviderDraftDimensionStyle(obj.ViewObject, existing_dimension) return obj -class DimensionStyle(_DraftObject): +class DimensionStyle(DraftAnnotation): def __init__(self, obj): - _DraftObject.__init__(self, obj, "DimensionStyle") \ No newline at end of file + DraftAnnotation.__init__(self, obj, "DimensionStyle") \ No newline at end of file diff --git a/src/Mod/Draft/draftobjects/draft_annotation.py b/src/Mod/Draft/draftobjects/draft_annotation.py new file mode 100644 index 0000000000..fd28c9e527 --- /dev/null +++ b/src/Mod/Draft/draftobjects/draft_annotation.py @@ -0,0 +1,78 @@ +# *************************************************************************** +# * (c) 2020 Carlo Pavan * +# * * +# * 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 * +# * * +# *************************************************************************** + +"""This module provides the object code for Draft DimensionStyle. +""" +## @package style_dimension +# \ingroup DRAFT +# \brief This module provides the object code for Draft DimensionStyle. + +import FreeCAD as App +from PySide.QtCore import QT_TRANSLATE_NOOP +from draftutils import gui_utils + +class DraftAnnotation: + """The Draft Annotation Base object""" + def __init__(self,obj,tp="Unknown"): + if obj: + obj.Proxy = self + self.Type = tp + + def __getstate__(self): + return self.Type + + def __setstate__(self,state): + if state: + self.Type = state + + def execute(self,obj): + pass + + def onChanged(self, obj, prop): + pass + + + +class DimensionBase(DraftAnnotation): + """The Draft Dimension Base object""" + + def __init__(self, obj, tp = "Dimension"): + "Initialize common properties for dimension objects" + DraftAnnotation.__init__(self,obj, tp) + + # Annotation + obj.addProperty("App::PropertyLink","DimensionStyle", + "Annotation", + QT_TRANSLATE_NOOP("App::Property", + "Link dimension style")) + + def onChanged(self,obj,prop): + + if prop == "DimensionStyle": + if hasattr(obj, "DimensionStyle"): + gui_utils.format_object(target = obj, origin = obj.DimensionStyle) + + + def execute(self, obj): + + return diff --git a/src/Mod/Draft/draftviewproviders/view_dimension.py b/src/Mod/Draft/draftviewproviders/view_dimension.py new file mode 100644 index 0000000000..d09b240440 --- /dev/null +++ b/src/Mod/Draft/draftviewproviders/view_dimension.py @@ -0,0 +1,549 @@ +# *************************************************************************** +# * (c) 2019 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 * +# * * +# *************************************************************************** +"""This module provides the Draft Annotations view provider base classes +""" +## @package polararray +# \ingroup DRAFT +# \brief This module provides the view provider code for Draft PolarArray. + + +import FreeCAD as App +import FreeCADGui as Gui +import DraftVecUtils +from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.utils as utils +import draftutils.gui_utils as gui_utils +from draftviewproviders.view_draft_annotation import ViewProviderDimensionBase + +class ViewProviderLinearDimension(ViewProviderDimensionBase): + """ + A View Provider for the Draft Dimension object + + DIMENSION VIEW PROVIDER: + + | txt | e + ----o--------------------------------o----- + | | + | | d + | | + + a b c b a + + a = DimOvershoot (vobj) + b = Arrows (vobj) + c = Dimline (obj) + d = ExtLines (vobj) + e = ExtOvershoot (vobj) + txt = label (vobj) + + STRUCTURE: + vobj.node.color + .drawstyle + .lineswitch1.coords + .line + .marks + .marksDimOvershoot + .marksExtOvershoot + .label.textpos + .color + .font + .text + + vobj.node3d.color + .drawstyle + .lineswitch3.coords + .line + .marks + .marksDimOvershoot + .marksExtOvershoot + .label3d.textpos + .color + .font3d + .text3d + + """ + def __init__(self, vobj): + ViewProviderDimensionBase.__init__(self,vobj) + + def attach(self, vobj): + """called on object creation""" + from pivy import coin + self.Object = vobj.Object + self.color = coin.SoBaseColor() + self.font = coin.SoFont() + self.font3d = coin.SoFont() + self.text = coin.SoAsciiText() + self.text3d = coin.SoText2() + self.text.string = "d" # some versions of coin crash if string is not set + self.text3d.string = "d" + self.textpos = coin.SoTransform() + self.text.justification = self.text3d.justification = coin.SoAsciiText.CENTER + label = coin.SoSeparator() + label.addChild(self.textpos) + label.addChild(self.color) + label.addChild(self.font) + label.addChild(self.text) + label3d = coin.SoSeparator() + label3d.addChild(self.textpos) + label3d.addChild(self.color) + label3d.addChild(self.font3d) + label3d.addChild(self.text3d) + self.coord1 = coin.SoCoordinate3() + self.trans1 = coin.SoTransform() + self.coord2 = coin.SoCoordinate3() + self.trans2 = coin.SoTransform() + self.transDimOvershoot1 = coin.SoTransform() + self.transDimOvershoot2 = coin.SoTransform() + self.transExtOvershoot1 = coin.SoTransform() + self.transExtOvershoot2 = coin.SoTransform() + self.marks = coin.SoSeparator() + self.marksDimOvershoot = coin.SoSeparator() + self.marksExtOvershoot = coin.SoSeparator() + self.drawstyle = coin.SoDrawStyle() + self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance() + self.coords = coin.SoCoordinate3() + self.node = coin.SoGroup() + self.node.addChild(self.color) + self.node.addChild(self.drawstyle) + self.lineswitch2 = coin.SoSwitch() + self.lineswitch2.whichChild = -3 + self.node.addChild(self.lineswitch2) + self.lineswitch2.addChild(self.coords) + self.lineswitch2.addChild(self.line) + self.lineswitch2.addChild(self.marks) + self.lineswitch2.addChild(self.marksDimOvershoot) + self.lineswitch2.addChild(self.marksExtOvershoot) + self.node.addChild(label) + self.node3d = coin.SoGroup() + self.node3d.addChild(self.color) + self.node3d.addChild(self.drawstyle) + self.lineswitch3 = coin.SoSwitch() + self.lineswitch3.whichChild = -3 + self.node3d.addChild(self.lineswitch3) + self.lineswitch3.addChild(self.coords) + self.lineswitch3.addChild(self.line) + self.lineswitch3.addChild(self.marks) + self.lineswitch3.addChild(self.marksDimOvershoot) + self.lineswitch3.addChild(self.marksExtOvershoot) + self.node3d.addChild(label3d) + vobj.addDisplayMode(self.node,"2D") + vobj.addDisplayMode(self.node3d,"3D") + self.updateData(vobj.Object,"Start") + self.onChanged(vobj,"FontSize") + self.onChanged(vobj,"FontName") + self.onChanged(vobj,"ArrowType") + self.onChanged(vobj,"LineColor") + self.onChanged(vobj,"DimOvershoot") + self.onChanged(vobj,"ExtOvershoot") + + def updateData(self, obj, prop): + """called when the base object is changed""" + import DraftGui + if prop in ["Start","End","Dimline","Direction"]: + + if obj.Start == obj.End: + return + + if not hasattr(self,"node"): + return + + import Part, DraftGeomUtils + from pivy import coin + + # calculate the 4 points + self.p1 = obj.Start + self.p4 = obj.End + base = None + if hasattr(obj,"Direction"): + if not DraftVecUtils.isNull(obj.Direction): + v2 = self.p1.sub(obj.Dimline) + v3 = self.p4.sub(obj.Dimline) + v2 = DraftVecUtils.project(v2,obj.Direction) + v3 = DraftVecUtils.project(v3,obj.Direction) + self.p2 = obj.Dimline.add(v2) + self.p3 = obj.Dimline.add(v3) + if DraftVecUtils.equals(self.p2,self.p3): + base = None + proj = None + else: + base = Part.LineSegment(self.p2,self.p3).toShape() + proj = DraftGeomUtils.findDistance(self.p1,base) + if proj: + proj = proj.negative() + if not base: + if DraftVecUtils.equals(self.p1,self.p4): + base = None + proj = None + else: + base = Part.LineSegment(self.p1,self.p4).toShape() + proj = DraftGeomUtils.findDistance(obj.Dimline,base) + if proj: + self.p2 = self.p1.add(proj.negative()) + self.p3 = self.p4.add(proj.negative()) + else: + self.p2 = self.p1 + self.p3 = self.p4 + if proj: + if hasattr(obj.ViewObject,"ExtLines") and hasattr(obj.ViewObject,"ScaleMultiplier"): + dmax = obj.ViewObject.ExtLines.Value * obj.ViewObject.ScaleMultiplier + if dmax and (proj.Length > dmax): + if (dmax > 0): + self.p1 = self.p2.add(DraftVecUtils.scaleTo(proj,dmax)) + self.p4 = self.p3.add(DraftVecUtils.scaleTo(proj,dmax)) + else: + rest = proj.Length + dmax + self.p1 = self.p2.add(DraftVecUtils.scaleTo(proj,rest)) + self.p4 = self.p3.add(DraftVecUtils.scaleTo(proj,rest)) + else: + proj = (self.p3.sub(self.p2)).cross(App.Vector(0,0,1)) + + # calculate the arrows positions + self.trans1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.coord1.point.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.trans2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) + self.coord2.point.setValue((self.p3.x,self.p3.y,self.p3.z)) + + # calculate dimension and extension lines overshoots positions + self.transDimOvershoot1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.transDimOvershoot2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) + self.transExtOvershoot1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.transExtOvershoot2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) + + # calculate the text position and orientation + if hasattr(obj,"Normal"): + if DraftVecUtils.isNull(obj.Normal): + if proj: + norm = (self.p3.sub(self.p2).cross(proj)).negative() + else: + norm = App.Vector(0,0,1) + else: + norm = App.Vector(obj.Normal) + else: + if proj: + norm = (self.p3.sub(self.p2).cross(proj)).negative() + else: + norm = App.Vector(0,0,1) + if not DraftVecUtils.isNull(norm): + norm.normalize() + u = self.p3.sub(self.p2) + u.normalize() + v1 = norm.cross(u) + rot1 = App.Placement(DraftVecUtils.getPlaneRotation(u,v1,norm)).Rotation.Q + self.transDimOvershoot1.rotation.setValue((rot1[0],rot1[1],rot1[2],rot1[3])) + self.transDimOvershoot2.rotation.setValue((rot1[0],rot1[1],rot1[2],rot1[3])) + if hasattr(obj.ViewObject,"FlipArrows"): + if obj.ViewObject.FlipArrows: + u = u.negative() + v2 = norm.cross(u) + rot2 = App.Placement(DraftVecUtils.getPlaneRotation(u,v2,norm)).Rotation.Q + self.trans1.rotation.setValue((rot2[0],rot2[1],rot2[2],rot2[3])) + self.trans2.rotation.setValue((rot2[0],rot2[1],rot2[2],rot2[3])) + if self.p1 != self.p2: + u3 = self.p1.sub(self.p2) + u3.normalize() + v3 = norm.cross(u3) + rot3 = App.Placement(DraftVecUtils.getPlaneRotation(u3,v3,norm)).Rotation.Q + self.transExtOvershoot1.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) + self.transExtOvershoot2.rotation.setValue((rot3[0],rot3[1],rot3[2],rot3[3])) + if hasattr(obj.ViewObject,"TextSpacing") and hasattr(obj.ViewObject,"ScaleMultiplier"): + ts = obj.ViewObject.TextSpacing.Value * obj.ViewObject.ScaleMultiplier + offset = DraftVecUtils.scaleTo(v1,ts) + else: + offset = DraftVecUtils.scaleTo(v1,0.05) + rott = rot1 + if hasattr(obj.ViewObject,"FlipText"): + if obj.ViewObject.FlipText: + rott = App.Rotation(*rott).multiply(App.Rotation(norm,180)).Q + offset = offset.negative() + # setting text + try: + m = obj.ViewObject.DisplayMode + except: # swallow all exceptions here since it always fails on first run (Displaymode enum no set yet) + m = ["2D","3D"][utils.get_param("dimstyle",0)] + if m == "3D": + offset = offset.negative() + self.tbase = (self.p2.add((self.p3.sub(self.p2).multiply(0.5)))).add(offset) + if hasattr(obj.ViewObject,"TextPosition"): + if not DraftVecUtils.isNull(obj.ViewObject.TextPosition): + self.tbase = obj.ViewObject.TextPosition + self.textpos.translation.setValue([self.tbase.x,self.tbase.y,self.tbase.z]) + self.textpos.rotation = coin.SbRotation(rott[0],rott[1],rott[2],rott[3]) + su = True + if hasattr(obj.ViewObject,"ShowUnit"): + su = obj.ViewObject.ShowUnit + # set text value + l = self.p3.sub(self.p2).Length + unit = None + if hasattr(obj.ViewObject,"UnitOverride"): + unit = obj.ViewObject.UnitOverride + # special representation if "Building US" scheme + if App.ParamGet("User parameter:BaseApp/Preferences/Units").GetInt("UserSchema",0) == 5: + s = App.Units.Quantity(l,App.Units.Length).UserString + self.string = s.replace("' ","'- ") + self.string = s.replace("+"," ") + elif hasattr(obj.ViewObject,"Decimals"): + self.string = DraftGui.displayExternal(l,obj.ViewObject.Decimals,'Length',su,unit) + else: + self.string = DraftGui.displayExternal(l,None,'Length',su,unit) + if hasattr(obj.ViewObject,"Override"): + if obj.ViewObject.Override: + self.string = obj.ViewObject.Override.replace("$dim",\ + self.string) + self.text.string = self.text3d.string = utils.string_encode_coin(self.string) + + # set the lines + if m == "3D": + # calculate the spacing of the text + textsize = (len(self.string)*obj.ViewObject.FontSize.Value)/4.0 + spacing = ((self.p3.sub(self.p2)).Length/2.0) - textsize + self.p2a = self.p2.add(DraftVecUtils.scaleTo(self.p3.sub(self.p2),spacing)) + self.p2b = self.p3.add(DraftVecUtils.scaleTo(self.p2.sub(self.p3),spacing)) + self.coords.point.setValues([[self.p1.x,self.p1.y,self.p1.z], + [self.p2.x,self.p2.y,self.p2.z], + [self.p2a.x,self.p2a.y,self.p2a.z], + [self.p2b.x,self.p2b.y,self.p2b.z], + [self.p3.x,self.p3.y,self.p3.z], + [self.p4.x,self.p4.y,self.p4.z]]) + #self.line.numVertices.setValues([3,3]) + self.line.coordIndex.setValues(0,7,(0,1,2,-1,3,4,5)) + else: + self.coords.point.setValues([[self.p1.x,self.p1.y,self.p1.z], + [self.p2.x,self.p2.y,self.p2.z], + [self.p3.x,self.p3.y,self.p3.z], + [self.p4.x,self.p4.y,self.p4.z]]) + #self.line.numVertices.setValue(4) + self.line.coordIndex.setValues(0,4,(0,1,2,3)) + + def onChanged(self, vobj, prop): + """called when a view property has changed""" + if prop == "ScaleMultiplier" and hasattr(vobj,"ScaleMultiplier"): + # update all dimension values + if hasattr(self,"font"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + if hasattr(self,"node") and hasattr(self,"p2") and hasattr(vobj,"ArrowSize"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + if hasattr(vobj,"DimOvershoot"): + self.remove_dim_overshoot() + self.draw_dim_overshoot(vobj) + if hasattr(vobj,"ExtOvershoot"): + self.remove_ext_overshoot() + self.draw_ext_overshoot(vobj) + self.updateData(vobj.Object,"Start") + vobj.Object.touch() + + elif (prop == "FontSize") and hasattr(vobj,"FontSize"): + if hasattr(self,"font") and hasattr(vobj,"ScaleMultiplier"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d") and hasattr(vobj,"ScaleMultiplier"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + vobj.Object.touch() + + elif (prop == "FontName") and hasattr(vobj,"FontName"): + if hasattr(self,"font") and hasattr(self,"font3d"): + self.font.name = self.font3d.name = str(vobj.FontName) + vobj.Object.touch() + + elif (prop == "LineColor") and hasattr(vobj,"LineColor"): + if hasattr(self,"color"): + c = vobj.LineColor + self.color.rgb.setValue(c[0],c[1],c[2]) + + elif (prop == "LineWidth") and hasattr(vobj,"LineWidth"): + if hasattr(self,"drawstyle"): + self.drawstyle.lineWidth = vobj.LineWidth + + elif (prop in ["ArrowSize","ArrowType"]) and hasattr(vobj,"ArrowSize"): + if hasattr(self,"node") and hasattr(self,"p2"): + if hasattr(vobj,"ScaleMultiplier"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + vobj.Object.touch() + + elif (prop == "DimOvershoot") and hasattr(vobj,"DimOvershoot"): + if hasattr(vobj,"ScaleMultiplier"): + self.remove_dim_overshoot() + self.draw_dim_overshoot(vobj) + vobj.Object.touch() + + elif (prop == "ExtOvershoot") and hasattr(vobj,"ExtOvershoot"): + if hasattr(vobj,"ScaleMultiplier"): + self.remove_ext_overshoot() + self.draw_ext_overshoot(vobj) + vobj.Object.touch() + + elif (prop == "ShowLine") and hasattr(vobj,"ShowLine"): + if vobj.ShowLine: + self.lineswitch2.whichChild = -3 + self.lineswitch3.whichChild = -3 + else: + self.lineswitch2.whichChild = -1 + self.lineswitch3.whichChild = -1 + else: + self.updateData(vobj.Object,"Start") + + def remove_dim_arrows(self): + # remove existing nodes + self.node.removeChild(self.marks) + self.node3d.removeChild(self.marks) + + def draw_dim_arrows(self, vobj): + from pivy import coin + + if not hasattr(vobj,"ArrowType"): + return + + if self.p3.x < self.p2.x: + inv = False + else: + inv = True + + # set scale + symbol = utils.ARROW_TYPES.index(vobj.ArrowType) + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + self.trans1.scaleFactor.setValue((s,s,s)) + self.trans2.scaleFactor.setValue((s,s,s)) + + + # set new nodes + self.marks = coin.SoSeparator() + self.marks.addChild(self.color) + s1 = coin.SoSeparator() + if symbol == "Circle": + s1.addChild(self.coord1) + else: + s1.addChild(self.trans1) + s1.addChild(gui_utils.dim_symbol(symbol,invert=not(inv))) + self.marks.addChild(s1) + s2 = coin.SoSeparator() + if symbol == "Circle": + s2.addChild(self.coord2) + else: + s2.addChild(self.trans2) + s2.addChild(gui_utils.dim_symbol(symbol,invert=inv)) + self.marks.addChild(s2) + self.node.insertChild(self.marks,2) + self.node3d.insertChild(self.marks,2) + + def remove_dim_overshoot(self): + self.node.removeChild(self.marksDimOvershoot) + self.node3d.removeChild(self.marksDimOvershoot) + + + def draw_dim_overshoot(self, vobj): + from pivy import coin + + # set scale + s = vobj.DimOvershoot.Value * vobj.ScaleMultiplier + self.transDimOvershoot1.scaleFactor.setValue((s,s,s)) + self.transDimOvershoot2.scaleFactor.setValue((s,s,s)) + + # remove existing nodes + + # set new nodes + self.marksDimOvershoot = coin.SoSeparator() + if vobj.DimOvershoot.Value: + self.marksDimOvershoot.addChild(self.color) + s1 = coin.SoSeparator() + s1.addChild(self.transDimOvershoot1) + s1.addChild(gui_utils.dimDash((-1,0,0),(0,0,0))) + self.marksDimOvershoot.addChild(s1) + s2 = coin.SoSeparator() + s2.addChild(self.transDimOvershoot2) + s2.addChild(gui_utils.dimDash((0,0,0),(1,0,0))) + self.marksDimOvershoot.addChild(s2) + self.node.insertChild(self.marksDimOvershoot,2) + self.node3d.insertChild(self.marksDimOvershoot,2) + + + def remove_ext_overshoot(self): + self.node.removeChild(self.marksExtOvershoot) + self.node3d.removeChild(self.marksExtOvershoot) + + + def draw_ext_overshoot(self, vobj): + from pivy import coin + + # set scale + s = vobj.ExtOvershoot.Value * vobj.ScaleMultiplier + self.transExtOvershoot1.scaleFactor.setValue((s,s,s)) + self.transExtOvershoot2.scaleFactor.setValue((s,s,s)) + + # set new nodes + self.marksExtOvershoot = coin.SoSeparator() + if vobj.ExtOvershoot.Value: + self.marksExtOvershoot.addChild(self.color) + s1 = coin.SoSeparator() + s1.addChild(self.transExtOvershoot1) + s1.addChild(gui_utils.dimDash((0,0,0),(-1,0,0))) + self.marksExtOvershoot.addChild(s1) + s2 = coin.SoSeparator() + s2.addChild(self.transExtOvershoot2) + s2.addChild(gui_utils.dimDash((0,0,0),(-1,0,0))) + self.marksExtOvershoot.addChild(s2) + self.node.insertChild(self.marksExtOvershoot,2) + self.node3d.insertChild(self.marksExtOvershoot,2) + + + def doubleClicked(self,vobj): + self.setEdit(vobj) + + def getDisplayModes(self,vobj): + return ["2D","3D"] + + def getDefaultDisplayMode(self): + if hasattr(self,"defaultmode"): + return self.defaultmode + else: + return ["2D","3D"][utils.get_param("dimstyle",0)] + + def setDisplayMode(self,mode): + return mode + + def is_linked_to_circle(self): + _obj = self.Object + if _obj.LinkedGeometry and len(_obj.LinkedGeometry) == 1: + lobj = _obj.LinkedGeometry[0][0] + lsub = _obj.LinkedGeometry[0][1] + if len(lsub) == 1 and "Edge" in lsub[0]: + n = int(lsub[0][4:]) - 1 + edge = lobj.Shape.Edges[n] + if DraftGeomUtils.geomType(edge) == "Circle": + return True + return False + + def getIcon(self): + if self.is_linked_to_circle(): + return ":/icons/Draft_DimensionRadius.svg" + return ":/icons/Draft_Dimension_Tree.svg" + + def __getstate__(self): + return self.Object.ViewObject.DisplayMode + + def __setstate__(self,state): + if state: + self.defaultmode = state + self.setDisplayMode(state) + diff --git a/src/Mod/Draft/draftviewproviders/view_style_dimension.py b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py similarity index 94% rename from src/Mod/Draft/draftviewproviders/view_style_dimension.py rename to src/Mod/Draft/draftviewproviders/view_dimensionstyle.py index aaae6fe778..9e2fb24f13 100644 --- a/src/Mod/Draft/draftviewproviders/view_style_dimension.py +++ b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py @@ -28,10 +28,10 @@ # \brief This module provides the view provider code for Draft DimensionStyle. import FreeCAD as App -import Draft from Draft import _ViewProviderDraft from PySide.QtCore import QT_TRANSLATE_NOOP import draftutils.utils as utils +from pivy import coin class ViewProviderDraftDimensionStyle(_ViewProviderDraft): """ @@ -190,6 +190,9 @@ class ViewProviderDraftDimensionStyle(_ViewProviderDraft): if hasattr(vobj, "AutoUpdate"): if vobj.AutoUpdate: self.update_related_dimensions(vobj) + if hasattr(vobj, "Visibility"): + if prop == "Visibility": + print(vobj.Visibility) def doubleClicked(self,vobj): self.set_current(vobj) @@ -228,3 +231,19 @@ class ViewProviderDraftDimensionStyle(_ViewProviderDraft): from draftutils import gui_utils for dim in vobj.Object.InList: gui_utils.format_object(target = dim, origin = vobj.Object) + + def getIcon(self): + import Draft_rc + return ":/icons/Draft_Dimension_Tree_Style.svg" + + def attach(self, vobj): + self.standard = coin.SoGroup() + vobj.addDisplayMode(self.standard,"Standard") + + def getDisplayModes(self,obj): + "'''Return a list of display modes.'''" + return ["Standard"] + + def getDefaultDisplayMode(self): + "'''Return the name of the default display mode. It must be defined in getDisplayModes.'''" + return "Standard" \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py new file mode 100644 index 0000000000..9c24b5bcd7 --- /dev/null +++ b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py @@ -0,0 +1,263 @@ +# *************************************************************************** +# * (c) 2019 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 * +# * * +# *************************************************************************** +"""This module provides the Draft Annotations view provider base classes +""" +## @package polararray +# \ingroup DRAFT +# \brief This module provides the view provider code for Draft PolarArray. + + +import FreeCAD as App +import FreeCADGui as Gui +from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.utils as utils + + +class ViewProviderDraftAnnotation: + """The base class for Draft Annotation Viewproviders""" + + def __init__(self, vobj): + vobj.Proxy = self + self.Object = vobj.Object + + # annotation properties + vobj.addProperty("App::PropertyFloat","ScaleMultiplier", + "Annotation",QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) + + # graphics properties + vobj.addProperty("App::PropertyFloat","LineWidth", + "Graphics",QT_TRANSLATE_NOOP("App::Property","Line width")) + vobj.addProperty("App::PropertyColor","LineColor", + "Graphics",QT_TRANSLATE_NOOP("App::Property","Line color")) + + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + vobj.ScaleMultiplier = 1 / annotation_scale + + + def __getstate__(self): + return None + + def __setstate__(self, state): + return None + + def attach(self,vobj): + self.Object = vobj.Object + return + + def updateData(self, obj, prop): + return + + def getDisplayModes(self, vobj): + modes=[] + return modes + + def setDisplayMode(self, mode): + return mode + + def onChanged(self, vobj, prop): + return + + def execute(self,vobj): + return + + def setEdit(self,vobj,mode=0): + if mode == 0: + Gui.runCommand("Draft_Edit") + return True + return False + + def unsetEdit(self,vobj,mode=0): + if App.activeDraftCommand: + App.activeDraftCommand.finish() + Gui.Control.closeDialog() + return False + + def getIcon(self): + return ":/icons/Draft_Draft.svg" + + def claimChildren(self): + """perhaps this is not useful???""" + objs = [] + if hasattr(self.Object,"Base"): + objs.append(self.Object.Base) + if hasattr(self.Object,"Objects"): + objs.extend(self.Object.Objects) + if hasattr(self.Object,"Components"): + objs.extend(self.Object.Components) + if hasattr(self.Object,"Group"): + objs.extend(self.Object.Group) + return objs + + +class ViewProviderDimensionBase(ViewProviderDraftAnnotation): + """ + A View Provider for the Draft Dimension object + + DIMENSION VIEW PROVIDER: + + | txt | e + ----o--------------------------------o----- + | | + | | d + | | + + a b c b a + + a = DimOvershoot (vobj) + b = Arrows (vobj) + c = Dimline (obj) + d = ExtLines (vobj) + e = ExtOvershoot (vobj) + txt = label (vobj) + + STRUCTURE: + vobj.node.color + .drawstyle + .lineswitch1.coords + .line + .marks + .marksDimOvershoot + .marksExtOvershoot + .label.textpos + .color + .font + .text + + vobj.node3d.color + .drawstyle + .lineswitch3.coords + .line + .marks + .marksDimOvershoot + .marksExtOvershoot + .label3d.textpos + .color + .font3d + .text3d + + """ + def __init__(self, vobj): + # text properties + vobj.addProperty("App::PropertyFont","FontName", + "Text",QT_TRANSLATE_NOOP("App::Property","Font name")) + vobj.addProperty("App::PropertyLength","FontSize", + "Text",QT_TRANSLATE_NOOP("App::Property","Font size")) + vobj.addProperty("App::PropertyLength","TextSpacing", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The spacing between the text and the dimension line")) + vobj.addProperty("App::PropertyBool","FlipText", + "Text",QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension text 180 degrees")) + vobj.addProperty("App::PropertyVectorDistance","TextPosition", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The position of the text. Leave (0,0,0) for automatic position")) + vobj.addProperty("App::PropertyString","Override", + "Text",QT_TRANSLATE_NOOP("App::Property", + "Text override. Use $dim to insert the dimension length")) + # units properties + vobj.addProperty("App::PropertyInteger","Decimals", + "Units",QT_TRANSLATE_NOOP("App::Property", + "The number of decimals to show")) + vobj.addProperty("App::PropertyBool","ShowUnit", + "Units",QT_TRANSLATE_NOOP("App::Property", + "Show the unit suffix")) + vobj.addProperty("App::PropertyString","UnitOverride", + "Units",QT_TRANSLATE_NOOP("App::Property", + "A unit to express the measurement. Leave blank for system default")) + # graphics properties + vobj.addProperty("App::PropertyLength","ArrowSize", + "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow size")) + vobj.addProperty("App::PropertyEnumeration","ArrowType", + "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow type")) + vobj.addProperty("App::PropertyBool","FlipArrows", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension arrows 180 degrees")) + vobj.addProperty("App::PropertyDistance","DimOvershoot", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The distance the dimension line is extended past the extension lines")) + vobj.addProperty("App::PropertyDistance","ExtLines", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Length of the extension lines")) + vobj.addProperty("App::PropertyDistance","ExtOvershoot", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Length of the extension line above the dimension line")) + vobj.addProperty("App::PropertyBool","ShowLine", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Shows the dimension line and arrows")) + + vobj.FontSize = utils.get_param("textheight",0.20) + vobj.TextSpacing = utils.get_param("dimspacing",0.05) + vobj.FontName = utils.get_param("textfont","") + vobj.ArrowSize = utils.get_param("arrowsize",0.1) + vobj.ArrowType = utils.ARROW_TYPES + vobj.ArrowType = utils.ARROW_TYPES[utils.get_param("dimsymbol",0)] + vobj.ExtLines = utils.get_param("extlines",0.3) + vobj.DimOvershoot = utils.get_param("dimovershoot",0) + vobj.ExtOvershoot = utils.get_param("extovershoot",0) + vobj.Decimals = utils.get_param("dimPrecision",2) + vobj.ShowUnit = utils.get_param("showUnit",True) + vobj.ShowLine = True + ViewProviderDraftAnnotation.__init__(self,vobj) + + def attach(self, vobj): + """called on object creation""" + return + + def updateData(self, obj, prop): + """called when the base object is changed""" + return + + def onChanged(self, vobj, prop): + """called when a view property has changed""" + return + + def doubleClicked(self,vobj): + self.setEdit(vobj) + + def getDisplayModes(self,vobj): + return ["2D","3D"] + + def getDefaultDisplayMode(self): + if hasattr(self,"defaultmode"): + return self.defaultmode + else: + return ["2D","3D"][utils.get_param("dimstyle",0)] + + def setDisplayMode(self,mode): + return mode + + def getIcon(self): + if self.is_linked_to_circle(): + return ":/icons/Draft_DimensionRadius.svg" + return ":/icons/Draft_Dimension_Tree.svg" + + def __getstate__(self): + return self.Object.ViewObject.DisplayMode + + def __setstate__(self,state): + if state: + self.defaultmode = state + self.setDisplayMode(state) + From 6d8fe0731242f8562a30350d49b18be07ecd1aba Mon Sep 17 00:00:00 2001 From: carlopav Date: Sun, 22 Mar 2020 09:40:33 +0100 Subject: [PATCH 084/142] [Draft] Annotation refactor and Cleanup . . --- src/Mod/Draft/Draft.py | 1 - .../Draft/draftguitools/gui_dimensionstyle.py | 3 +- src/Mod/Draft/draftobjects/dimension.py | 39 +++- src/Mod/Draft/draftobjects/dimensionstyle.py | 4 +- .../Draft/draftobjects/draft_annotation.py | 35 +--- .../draftviewproviders/view_dimension.py | 175 ++++++++++++++---- .../draftviewproviders/view_dimensionstyle.py | 2 +- .../view_draft_annotation.py | 161 +--------------- 8 files changed, 193 insertions(+), 227 deletions(-) diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index c85f37a7ec..3e418fcf83 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -260,7 +260,6 @@ _Dimension = LinearDimension from draftviewproviders.view_dimension import ViewProviderLinearDimension _ViewProviderDimension = ViewProviderLinearDimension - return obj def makeAngularDimension(center,angles,p3,normal=None): """makeAngularDimension(center,angle1,angle2,p3,[normal]): creates an angular Dimension diff --git a/src/Mod/Draft/draftguitools/gui_dimensionstyle.py b/src/Mod/Draft/draftguitools/gui_dimensionstyle.py index ff3fec839f..d5906e9cfe 100644 --- a/src/Mod/Draft/draftguitools/gui_dimensionstyle.py +++ b/src/Mod/Draft/draftguitools/gui_dimensionstyle.py @@ -96,8 +96,7 @@ class GuiCommandDimensionStyle(gui_base.GuiCommandSimplest): The command creates a dimension style object """ def __init__(self): - super().__init__() - self.command_name = "DimensionStyle" + super().__init__(name="Dimension style") def GetResources(self): _msg = ("Creates a new dimension style.\n" diff --git a/src/Mod/Draft/draftobjects/dimension.py b/src/Mod/Draft/draftobjects/dimension.py index e4e194aea4..9f08f2661b 100644 --- a/src/Mod/Draft/draftobjects/dimension.py +++ b/src/Mod/Draft/draftobjects/dimension.py @@ -30,9 +30,11 @@ import FreeCAD as App import math from PySide.QtCore import QT_TRANSLATE_NOOP +import DraftGeomUtils import draftutils.gui_utils as gui_utils import draftutils.utils as utils -from draftobjects.draft_annotation import DimensionBase +from draftobjects.draft_annotation import DraftAnnotation +from draftviewproviders.view_dimension import ViewProviderDimensionBase from draftviewproviders.view_dimension import ViewProviderLinearDimension def make_dimension(p1,p2,p3=None,p4=None): @@ -111,10 +113,40 @@ def make_dimension(p1,p2,p3=None,p4=None): return obj + +class DimensionBase(DraftAnnotation): + """ + The Draft Dimension Base object + This class is not used directly, but inherited by all dimension + objects. + """ + + def __init__(self, obj, tp = "Dimension"): + "Initialize common properties for dimension objects" + DraftAnnotation.__init__(self,obj, tp) + + # Annotation + obj.addProperty("App::PropertyLink","DimensionStyle", + "Annotation", + QT_TRANSLATE_NOOP("App::Property", + "Link dimension style")) + + def onChanged(self,obj,prop): + + if prop == "DimensionStyle": + if hasattr(obj, "DimensionStyle"): + gui_utils.format_object(target = obj, origin = obj.DimensionStyle) + + + def execute(self, obj): + + return + + class LinearDimension(DimensionBase): - """The Draft Dimension object""" + """The Draft Linear Dimension object""" def __init__(self, obj): - DimensionBase.__init__(self,obj,"Dimension") + super().__init__(obj, "Dimension") # Draft obj.addProperty("App::PropertyVectorDistance","Start", @@ -179,7 +211,6 @@ class LinearDimension(DimensionBase): def execute(self, obj): - import DraftGeomUtils # set start point and end point according to the linked geometry if obj.LinkedGeometry: if len(obj.LinkedGeometry) == 1: diff --git a/src/Mod/Draft/draftobjects/dimensionstyle.py b/src/Mod/Draft/draftobjects/dimensionstyle.py index 8f86251725..2ebe8bebea 100644 --- a/src/Mod/Draft/draftobjects/dimensionstyle.py +++ b/src/Mod/Draft/draftobjects/dimensionstyle.py @@ -30,10 +30,10 @@ import FreeCAD as App from draftobjects.draft_annotation import DraftAnnotation from PySide.QtCore import QT_TRANSLATE_NOOP +from draftviewproviders.view_dimensionstyle import ViewProviderDraftDimensionStyle if App.GuiUp: import FreeCADGui as Gui - from draftviewproviders.view_dimensionstyle import ViewProviderDraftDimensionStyle def make_dimension_style(existing_dimension = None): """ @@ -50,4 +50,4 @@ def make_dimension_style(existing_dimension = None): class DimensionStyle(DraftAnnotation): def __init__(self, obj): - DraftAnnotation.__init__(self, obj, "DimensionStyle") \ No newline at end of file + super().__init__(obj, "DimensionStyle") \ No newline at end of file diff --git a/src/Mod/Draft/draftobjects/draft_annotation.py b/src/Mod/Draft/draftobjects/draft_annotation.py index fd28c9e527..c662b3c237 100644 --- a/src/Mod/Draft/draftobjects/draft_annotation.py +++ b/src/Mod/Draft/draftobjects/draft_annotation.py @@ -32,8 +32,11 @@ from PySide.QtCore import QT_TRANSLATE_NOOP from draftutils import gui_utils class DraftAnnotation: - """The Draft Annotation Base object""" - def __init__(self,obj,tp="Unknown"): + """The Draft Annotation Base object + This class is not used directly, but inherited by all annotation + objects. + """ + def __init__(self, obj, tp="Unknown"): if obj: obj.Proxy = self self.Type = tp @@ -49,30 +52,4 @@ class DraftAnnotation: pass def onChanged(self, obj, prop): - pass - - - -class DimensionBase(DraftAnnotation): - """The Draft Dimension Base object""" - - def __init__(self, obj, tp = "Dimension"): - "Initialize common properties for dimension objects" - DraftAnnotation.__init__(self,obj, tp) - - # Annotation - obj.addProperty("App::PropertyLink","DimensionStyle", - "Annotation", - QT_TRANSLATE_NOOP("App::Property", - "Link dimension style")) - - def onChanged(self,obj,prop): - - if prop == "DimensionStyle": - if hasattr(obj, "DimensionStyle"): - gui_utils.format_object(target = obj, origin = obj.DimensionStyle) - - - def execute(self, obj): - - return + pass \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_dimension.py b/src/Mod/Draft/draftviewproviders/view_dimension.py index d09b240440..537c00554f 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimension.py +++ b/src/Mod/Draft/draftviewproviders/view_dimension.py @@ -30,16 +30,19 @@ import FreeCAD as App import FreeCADGui as Gui import DraftVecUtils +from pivy import coin from PySide.QtCore import QT_TRANSLATE_NOOP import draftutils.utils as utils import draftutils.gui_utils as gui_utils -from draftviewproviders.view_draft_annotation import ViewProviderDimensionBase +from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation -class ViewProviderLinearDimension(ViewProviderDimensionBase): +class ViewProviderDimensionBase(ViewProviderDraftAnnotation): """ A View Provider for the Draft Dimension object - - DIMENSION VIEW PROVIDER: + This class is not used directly, but inherited by all dimension + view providers. + + DIMENSION VIEW PROVIDER NOMENCLATURE: | txt | e ----o--------------------------------o----- @@ -56,7 +59,7 @@ class ViewProviderLinearDimension(ViewProviderDimensionBase): e = ExtOvershoot (vobj) txt = label (vobj) - STRUCTURE: + COIN OBJECT STRUCTURE: vobj.node.color .drawstyle .lineswitch1.coords @@ -82,12 +85,141 @@ class ViewProviderLinearDimension(ViewProviderDimensionBase): .text3d """ - def __init__(self, vobj): - ViewProviderDimensionBase.__init__(self,vobj) + def __init__(self, vobj): + # text properties + vobj.addProperty("App::PropertyFont","FontName", + "Text", + QT_TRANSLATE_NOOP("App::Property","Font name")) + vobj.addProperty("App::PropertyLength","FontSize", + "Text", + QT_TRANSLATE_NOOP("App::Property","Font size")) + vobj.addProperty("App::PropertyLength","TextSpacing", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "Spacing between text and dimension line")) + vobj.addProperty("App::PropertyBool","FlipText", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension text 180 degrees")) + vobj.addProperty("App::PropertyVectorDistance","TextPosition", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "Text Position. \n" + "Leave (0,0,0) for automatic position")) + vobj.addProperty("App::PropertyString","Override", + "Text", + QT_TRANSLATE_NOOP("App::Property", + "Text override. \n" + "Use $dim to insert the dimension length")) + # units properties + vobj.addProperty("App::PropertyInteger","Decimals", + "Units", + QT_TRANSLATE_NOOP("App::Property", + "The number of decimals to show")) + vobj.addProperty("App::PropertyBool","ShowUnit", + "Units", + QT_TRANSLATE_NOOP("App::Property", + "Show the unit suffix")) + vobj.addProperty("App::PropertyString","UnitOverride", + "Units", + QT_TRANSLATE_NOOP("App::Property", + "A unit to express the measurement. \n" + "Leave blank for system default")) + # graphics properties + vobj.addProperty("App::PropertyLength","ArrowSize", + "Graphics", + QT_TRANSLATE_NOOP("App::Property","Arrow size")) + vobj.addProperty("App::PropertyEnumeration","ArrowType", + "Graphics", + QT_TRANSLATE_NOOP("App::Property","Arrow type")) + vobj.addProperty("App::PropertyBool","FlipArrows", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension arrows 180 degrees")) + vobj.addProperty("App::PropertyDistance","DimOvershoot", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "The distance the dimension line is extended\n" + "past the extension lines")) + vobj.addProperty("App::PropertyDistance","ExtLines", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Length of the extension lines")) + vobj.addProperty("App::PropertyDistance","ExtOvershoot", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Length of the extension line \n" + "above the dimension line")) + vobj.addProperty("App::PropertyBool","ShowLine", + "Graphics", + QT_TRANSLATE_NOOP("App::Property", + "Shows the dimension line and arrows")) + + vobj.FontSize = utils.get_param("textheight",0.20) + vobj.TextSpacing = utils.get_param("dimspacing",0.05) + vobj.FontName = utils.get_param("textfont","") + vobj.ArrowSize = utils.get_param("arrowsize",0.1) + vobj.ArrowType = utils.ARROW_TYPES + vobj.ArrowType = utils.ARROW_TYPES[utils.get_param("dimsymbol",0)] + vobj.ExtLines = utils.get_param("extlines",0.3) + vobj.DimOvershoot = utils.get_param("dimovershoot",0) + vobj.ExtOvershoot = utils.get_param("extovershoot",0) + vobj.Decimals = utils.get_param("dimPrecision",2) + vobj.ShowUnit = utils.get_param("showUnit",True) + vobj.ShowLine = True + super().__init__(vobj) + + def attach(self, vobj): + """called on object creation""" + return + + def updateData(self, obj, prop): + """called when the base object is changed""" + return + + def onChanged(self, vobj, prop): + """called when a view property has changed""" + return + + def doubleClicked(self,vobj): + self.setEdit(vobj) + + def getDisplayModes(self,vobj): + return ["2D","3D"] + + def getDefaultDisplayMode(self): + if hasattr(self,"defaultmode"): + return self.defaultmode + else: + return ["2D","3D"][utils.get_param("dimstyle",0)] + + def setDisplayMode(self,mode): + return mode + + def getIcon(self): + if self.is_linked_to_circle(): + return ":/icons/Draft_DimensionRadius.svg" + return ":/icons/Draft_Dimension_Tree.svg" + + def __getstate__(self): + return self.Object.ViewObject.DisplayMode + + def __setstate__(self,state): + if state: + self.defaultmode = state + self.setDisplayMode(state) + + + +class ViewProviderLinearDimension(ViewProviderDimensionBase): + """ + A View Provider for the Draft Linear Dimension object + """ + def __init__(self, vobj): + super().__init__(vobj) def attach(self, vobj): """called on object creation""" - from pivy import coin self.Object = vobj.Object self.color = coin.SoBaseColor() self.font = coin.SoFont() @@ -506,22 +638,6 @@ class ViewProviderLinearDimension(ViewProviderDimensionBase): self.node.insertChild(self.marksExtOvershoot,2) self.node3d.insertChild(self.marksExtOvershoot,2) - - def doubleClicked(self,vobj): - self.setEdit(vobj) - - def getDisplayModes(self,vobj): - return ["2D","3D"] - - def getDefaultDisplayMode(self): - if hasattr(self,"defaultmode"): - return self.defaultmode - else: - return ["2D","3D"][utils.get_param("dimstyle",0)] - - def setDisplayMode(self,mode): - return mode - def is_linked_to_circle(self): _obj = self.Object if _obj.LinkedGeometry and len(_obj.LinkedGeometry) == 1: @@ -537,13 +653,4 @@ class ViewProviderLinearDimension(ViewProviderDimensionBase): def getIcon(self): if self.is_linked_to_circle(): return ":/icons/Draft_DimensionRadius.svg" - return ":/icons/Draft_Dimension_Tree.svg" - - def __getstate__(self): - return self.Object.ViewObject.DisplayMode - - def __setstate__(self,state): - if state: - self.defaultmode = state - self.setDisplayMode(state) - + return ":/icons/Draft_Dimension_Tree.svg" \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py index 9e2fb24f13..19ce732f3f 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py +++ b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py @@ -153,7 +153,7 @@ class ViewProviderDraftDimensionStyle(_ViewProviderDraft): self.init_properties(vobj, existing_dimension) - _ViewProviderDraft.__init__(self,vobj) + super().__init__(vobj) def init_properties(self, vobj, existing_dimension): """ diff --git a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py index 9c24b5bcd7..1973516b84 100644 --- a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py +++ b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py @@ -20,21 +20,24 @@ # * USA * # * * # *************************************************************************** -"""This module provides the Draft Annotations view provider base classes +"""This module provides the Draft Annotations view provider base class """ ## @package polararray # \ingroup DRAFT -# \brief This module provides the view provider code for Draft PolarArray. +# \brief This module provides the Draft Annotations view provider base class import FreeCAD as App import FreeCADGui as Gui from PySide.QtCore import QT_TRANSLATE_NOOP -import draftutils.utils as utils class ViewProviderDraftAnnotation: - """The base class for Draft Annotation Viewproviders""" + """ + The base class for Draft Annotation Viewproviders + This class is not used directly, but inherited by all annotation + view providers. + """ def __init__(self, vobj): vobj.Proxy = self @@ -111,153 +114,3 @@ class ViewProviderDraftAnnotation: return objs -class ViewProviderDimensionBase(ViewProviderDraftAnnotation): - """ - A View Provider for the Draft Dimension object - - DIMENSION VIEW PROVIDER: - - | txt | e - ----o--------------------------------o----- - | | - | | d - | | - - a b c b a - - a = DimOvershoot (vobj) - b = Arrows (vobj) - c = Dimline (obj) - d = ExtLines (vobj) - e = ExtOvershoot (vobj) - txt = label (vobj) - - STRUCTURE: - vobj.node.color - .drawstyle - .lineswitch1.coords - .line - .marks - .marksDimOvershoot - .marksExtOvershoot - .label.textpos - .color - .font - .text - - vobj.node3d.color - .drawstyle - .lineswitch3.coords - .line - .marks - .marksDimOvershoot - .marksExtOvershoot - .label3d.textpos - .color - .font3d - .text3d - - """ - def __init__(self, vobj): - # text properties - vobj.addProperty("App::PropertyFont","FontName", - "Text",QT_TRANSLATE_NOOP("App::Property","Font name")) - vobj.addProperty("App::PropertyLength","FontSize", - "Text",QT_TRANSLATE_NOOP("App::Property","Font size")) - vobj.addProperty("App::PropertyLength","TextSpacing", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The spacing between the text and the dimension line")) - vobj.addProperty("App::PropertyBool","FlipText", - "Text",QT_TRANSLATE_NOOP("App::Property", - "Rotate the dimension text 180 degrees")) - vobj.addProperty("App::PropertyVectorDistance","TextPosition", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The position of the text. Leave (0,0,0) for automatic position")) - vobj.addProperty("App::PropertyString","Override", - "Text",QT_TRANSLATE_NOOP("App::Property", - "Text override. Use $dim to insert the dimension length")) - # units properties - vobj.addProperty("App::PropertyInteger","Decimals", - "Units",QT_TRANSLATE_NOOP("App::Property", - "The number of decimals to show")) - vobj.addProperty("App::PropertyBool","ShowUnit", - "Units",QT_TRANSLATE_NOOP("App::Property", - "Show the unit suffix")) - vobj.addProperty("App::PropertyString","UnitOverride", - "Units",QT_TRANSLATE_NOOP("App::Property", - "A unit to express the measurement. Leave blank for system default")) - # graphics properties - vobj.addProperty("App::PropertyLength","ArrowSize", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow size")) - vobj.addProperty("App::PropertyEnumeration","ArrowType", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow type")) - vobj.addProperty("App::PropertyBool","FlipArrows", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Rotate the dimension arrows 180 degrees")) - vobj.addProperty("App::PropertyDistance","DimOvershoot", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "The distance the dimension line is extended past the extension lines")) - vobj.addProperty("App::PropertyDistance","ExtLines", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Length of the extension lines")) - vobj.addProperty("App::PropertyDistance","ExtOvershoot", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Length of the extension line above the dimension line")) - vobj.addProperty("App::PropertyBool","ShowLine", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Shows the dimension line and arrows")) - - vobj.FontSize = utils.get_param("textheight",0.20) - vobj.TextSpacing = utils.get_param("dimspacing",0.05) - vobj.FontName = utils.get_param("textfont","") - vobj.ArrowSize = utils.get_param("arrowsize",0.1) - vobj.ArrowType = utils.ARROW_TYPES - vobj.ArrowType = utils.ARROW_TYPES[utils.get_param("dimsymbol",0)] - vobj.ExtLines = utils.get_param("extlines",0.3) - vobj.DimOvershoot = utils.get_param("dimovershoot",0) - vobj.ExtOvershoot = utils.get_param("extovershoot",0) - vobj.Decimals = utils.get_param("dimPrecision",2) - vobj.ShowUnit = utils.get_param("showUnit",True) - vobj.ShowLine = True - ViewProviderDraftAnnotation.__init__(self,vobj) - - def attach(self, vobj): - """called on object creation""" - return - - def updateData(self, obj, prop): - """called when the base object is changed""" - return - - def onChanged(self, vobj, prop): - """called when a view property has changed""" - return - - def doubleClicked(self,vobj): - self.setEdit(vobj) - - def getDisplayModes(self,vobj): - return ["2D","3D"] - - def getDefaultDisplayMode(self): - if hasattr(self,"defaultmode"): - return self.defaultmode - else: - return ["2D","3D"][utils.get_param("dimstyle",0)] - - def setDisplayMode(self,mode): - return mode - - def getIcon(self): - if self.is_linked_to_circle(): - return ":/icons/Draft_DimensionRadius.svg" - return ":/icons/Draft_Dimension_Tree.svg" - - def __getstate__(self): - return self.Object.ViewObject.DisplayMode - - def __setstate__(self,state): - if state: - self.defaultmode = state - self.setDisplayMode(state) - From 33f6d01192c2b878f5fe9361fb57ea4b86dafa96 Mon Sep 17 00:00:00 2001 From: carlopav Date: Sun, 22 Mar 2020 11:20:28 +0100 Subject: [PATCH 085/142] [Draft] Split angular dimension and made it derived from DimensionBase <- Draft Annotation --- src/Mod/Draft/draftobjects/dimension.py | 161 ++++++++-- .../draftviewproviders/view_dimension.py | 275 +++++++++++++++++- 2 files changed, 405 insertions(+), 31 deletions(-) diff --git a/src/Mod/Draft/draftobjects/dimension.py b/src/Mod/Draft/draftobjects/dimension.py index 9f08f2661b..f26998461f 100644 --- a/src/Mod/Draft/draftobjects/dimension.py +++ b/src/Mod/Draft/draftobjects/dimension.py @@ -36,6 +36,7 @@ import draftutils.utils as utils from draftobjects.draft_annotation import DraftAnnotation from draftviewproviders.view_dimension import ViewProviderDimensionBase from draftviewproviders.view_dimension import ViewProviderLinearDimension +from draftviewproviders.view_dimension import ViewProviderAngularDimension def make_dimension(p1,p2,p3=None,p4=None): """makeDimension(p1,p2,[p3]) or makeDimension(object,i1,i2,p3) @@ -114,6 +115,43 @@ def make_dimension(p1,p2,p3=None,p4=None): return obj + +def make_angular_dimension(center,angles,p3,normal=None): + """makeAngularDimension(center,angle1,angle2,p3,[normal]): creates an angular Dimension + from the given center, with the given list of angles, passing through p3. + """ + if not App.ActiveDocument: + App.Console.PrintError("No active document. Aborting\n") + return + obj = App.ActiveDocument.addObject("App::FeaturePython","Dimension") + AngularDimension(obj) + obj.Center = center + for a in range(len(angles)): + if angles[a] > 2*math.pi: + angles[a] = angles[a]-(2*math.pi) + obj.FirstAngle = math.degrees(angles[1]) + obj.LastAngle = math.degrees(angles[0]) + obj.Dimline = p3 + if not normal: + if hasattr(App,"DraftWorkingPlane"): + normal = App.DraftWorkingPlane.axis + else: + normal = App.Vector(0,0,1) + if App.GuiUp: + # invert the normal if we are viewing it from the back + vnorm = gui_utils.get3DView().getViewDirection() + if vnorm.getAngle(normal) < math.pi/2: + normal = normal.negative() + obj.Normal = normal + if App.GuiUp: + ViewProviderAngularDimension(obj.ViewObject) + gui_utils.format_object(obj) + gui_utils.select(obj) + + return obj + + + class DimensionBase(DraftAnnotation): """ The Draft Dimension Base object @@ -131,6 +169,32 @@ class DimensionBase(DraftAnnotation): QT_TRANSLATE_NOOP("App::Property", "Link dimension style")) + # Draft + obj.addProperty("App::PropertyVector", + "Normal", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The normal direction of this dimension")) + + obj.addProperty("App::PropertyLink", + "Support", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The object measured by this dimension")) + + obj.addProperty("App::PropertyLinkSubList", + "LinkedGeometry", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The geometry this dimension is linked to")) + + obj.addProperty("App::PropertyVectorDistance", + "Dimline", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Point on which the dimension \n" + "line is placed.")) + def onChanged(self,obj,prop): if prop == "DimensionStyle": @@ -143,56 +207,46 @@ class DimensionBase(DraftAnnotation): return + class LinearDimension(DimensionBase): - """The Draft Linear Dimension object""" + """ + The Draft Linear Dimension object + """ + def __init__(self, obj): super().__init__(obj, "Dimension") # Draft - obj.addProperty("App::PropertyVectorDistance","Start", + obj.addProperty("App::PropertyVectorDistance", + "Start", "Draft", QT_TRANSLATE_NOOP("App::Property", "Startpoint of dimension")) - obj.addProperty("App::PropertyVectorDistance","End", + obj.addProperty("App::PropertyVectorDistance", + "End", "Draft", QT_TRANSLATE_NOOP("App::Property", "Endpoint of dimension")) - obj.addProperty("App::PropertyVector","Normal", + obj.addProperty("App::PropertyVector", + "Direction", "Draft", QT_TRANSLATE_NOOP("App::Property", "The normal direction of this dimension")) - obj.addProperty("App::PropertyVector","Direction", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "The normal direction of this dimension")) - - obj.addProperty("App::PropertyVectorDistance","Dimline", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "Point through which the dimension line passes")) - - obj.addProperty("App::PropertyLink","Support", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "The object measured by this dimension")) - - obj.addProperty("App::PropertyLinkSubList","LinkedGeometry", - "Draft", - QT_TRANSLATE_NOOP("App::Property", - "The geometry this dimension is linked to")) - - obj.addProperty("App::PropertyLength","Distance", + obj.addProperty("App::PropertyLength", + "Distance", "Draft", QT_TRANSLATE_NOOP("App::Property", "The measurement of this dimension")) - obj.addProperty("App::PropertyBool","Diameter", + obj.addProperty("App::PropertyBool", + "Diameter", "Draft", QT_TRANSLATE_NOOP("App::Property", "For arc/circle measurements, false = radius, true = diameter")) + obj.Start = App.Vector(0,0,0) obj.End = App.Vector(1,0,0) obj.Dimline = App.Vector(0,1,0) @@ -263,4 +317,57 @@ class LinearDimension(DimensionBase): obj.Distance = total_len if App.GuiUp: if obj.ViewObject: - obj.ViewObject.update() \ No newline at end of file + obj.ViewObject.update() + + + +class AngularDimension(DimensionBase): + """ + The Draft AngularDimension object + """ + + def __init__(self, obj): + + super().__init__(obj,"AngularDimension") + + obj.addProperty("App::PropertyAngle", + "FirstAngle", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "Start angle of the dimension")) + + obj.addProperty("App::PropertyAngle", + "LastAngle", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "End angle of the dimension")) + + obj.addProperty("App::PropertyVectorDistance", + "Center", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The center point of this dimension")) + + obj.addProperty("App::PropertyAngle", + "Angle", + "Draft", + QT_TRANSLATE_NOOP("App::Property", + "The measurement of this dimension")) + + obj.FirstAngle = 0 + obj.LastAngle = 90 + obj.Dimline = App.Vector(0,1,0) + obj.Center = App.Vector(0,0,0) + obj.Normal = App.Vector(0,0,1) + + def onChanged(self,obj,prop): + if hasattr(obj,"Angle"): + obj.setEditorMode('Angle',1) + if hasattr(obj,"Normal"): + obj.setEditorMode('Normal',2) + if hasattr(obj,"Support"): + obj.setEditorMode('Support',2) + + def execute(self, fp): + if fp.ViewObject: + fp.ViewObject.update() diff --git a/src/Mod/Draft/draftviewproviders/view_dimension.py b/src/Mod/Draft/draftviewproviders/view_dimension.py index 537c00554f..0b58a1253a 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimension.py +++ b/src/Mod/Draft/draftviewproviders/view_dimension.py @@ -28,8 +28,7 @@ import FreeCAD as App -import FreeCADGui as Gui -import DraftVecUtils +import DraftVecUtils, DraftGeomUtils from pivy import coin from PySide.QtCore import QT_TRANSLATE_NOOP import draftutils.utils as utils @@ -85,7 +84,8 @@ class ViewProviderDimensionBase(ViewProviderDraftAnnotation): .text3d """ - def __init__(self, vobj): + def __init__(self, vobj): + # text properties vobj.addProperty("App::PropertyFont","FontName", "Text", @@ -111,6 +111,7 @@ class ViewProviderDimensionBase(ViewProviderDraftAnnotation): QT_TRANSLATE_NOOP("App::Property", "Text override. \n" "Use $dim to insert the dimension length")) + # units properties vobj.addProperty("App::PropertyInteger","Decimals", "Units", @@ -125,6 +126,7 @@ class ViewProviderDimensionBase(ViewProviderDraftAnnotation): QT_TRANSLATE_NOOP("App::Property", "A unit to express the measurement. \n" "Leave blank for system default")) + # graphics properties vobj.addProperty("App::PropertyLength","ArrowSize", "Graphics", @@ -167,6 +169,7 @@ class ViewProviderDimensionBase(ViewProviderDraftAnnotation): vobj.Decimals = utils.get_param("dimPrecision",2) vobj.ShowUnit = utils.get_param("showUnit",True) vobj.ShowLine = True + super().__init__(vobj) def attach(self, vobj): @@ -653,4 +656,268 @@ class ViewProviderLinearDimension(ViewProviderDimensionBase): def getIcon(self): if self.is_linked_to_circle(): return ":/icons/Draft_DimensionRadius.svg" - return ":/icons/Draft_Dimension_Tree.svg" \ No newline at end of file + return ":/icons/Draft_Dimension_Tree.svg" + + +class ViewProviderAngularDimension(ViewProviderDimensionBase): + """A View Provider for the Draft Angular Dimension object""" + def __init__(self, vobj): + + vobj.addProperty("App::PropertyBool","FlipArrows", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Rotate the dimension arrows 180 degrees")) + + super().__init__(vobj) + + def attach(self, vobj): + from pivy import coin + self.Object = vobj.Object + self.color = coin.SoBaseColor() + if hasattr(vobj,"LineColor"): + self.color.rgb.setValue(vobj.LineColor[0],vobj.LineColor[1],vobj.LineColor[2]) + self.font = coin.SoFont() + self.font3d = coin.SoFont() + self.text = coin.SoAsciiText() + self.text3d = coin.SoText2() + self.text.string = "d" # some versions of coin crash if string is not set + self.text3d.string = "d" + self.text.justification = self.text3d.justification = coin.SoAsciiText.CENTER + self.textpos = coin.SoTransform() + label = coin.SoSeparator() + label.addChild(self.textpos) + label.addChild(self.color) + label.addChild(self.font) + label.addChild(self.text) + label3d = coin.SoSeparator() + label3d.addChild(self.textpos) + label3d.addChild(self.color) + label3d.addChild(self.font3d) + label3d.addChild(self.text3d) + self.coord1 = coin.SoCoordinate3() + self.trans1 = coin.SoTransform() + self.coord2 = coin.SoCoordinate3() + self.trans2 = coin.SoTransform() + self.marks = coin.SoSeparator() + self.drawstyle = coin.SoDrawStyle() + self.coords = coin.SoCoordinate3() + self.arc = coin.SoType.fromName("SoBrepEdgeSet").createInstance() + self.node = coin.SoGroup() + self.node.addChild(self.color) + self.node.addChild(self.drawstyle) + self.node.addChild(self.coords) + self.node.addChild(self.arc) + self.node.addChild(self.marks) + self.node.addChild(label) + self.node3d = coin.SoGroup() + self.node3d.addChild(self.color) + self.node3d.addChild(self.drawstyle) + self.node3d.addChild(self.coords) + self.node3d.addChild(self.arc) + self.node3d.addChild(self.marks) + self.node3d.addChild(label3d) + vobj.addDisplayMode(self.node,"2D") + vobj.addDisplayMode(self.node3d,"3D") + self.updateData(vobj.Object,None) + self.onChanged(vobj,"FontSize") + self.onChanged(vobj,"FontName") + self.onChanged(vobj,"ArrowType") + self.onChanged(vobj,"LineColor") + + def updateData(self, obj, prop): + if hasattr(self,"arc"): + from pivy import coin + import Part, DraftGeomUtils + import DraftGui + arcsegs = 24 + + # calculate the arc data + if DraftVecUtils.isNull(obj.Normal): + norm = App.Vector(0,0,1) + else: + norm = obj.Normal + radius = (obj.Dimline.sub(obj.Center)).Length + self.circle = Part.makeCircle(radius,obj.Center,norm,obj.FirstAngle.Value,obj.LastAngle.Value) + self.p2 = self.circle.Vertexes[0].Point + self.p3 = self.circle.Vertexes[-1].Point + mp = DraftGeomUtils.findMidpoint(self.circle.Edges[0]) + ray = mp.sub(obj.Center) + + # set text value + if obj.LastAngle.Value > obj.FirstAngle.Value: + a = obj.LastAngle.Value - obj.FirstAngle.Value + else: + a = (360 - obj.FirstAngle.Value) + obj.LastAngle.Value + su = True + if hasattr(obj.ViewObject,"ShowUnit"): + su = obj.ViewObject.ShowUnit + if hasattr(obj.ViewObject,"Decimals"): + self.string = DraftGui.displayExternal(a,obj.ViewObject.Decimals,'Angle',su) + else: + self.string = DraftGui.displayExternal(a,None,'Angle',su) + if obj.ViewObject.Override: + self.string = obj.ViewObject.Override.replace("$dim",\ + self.string) + self.text.string = self.text3d.string = utils.string_encode_coin(self.string) + + # check display mode + try: + m = obj.ViewObject.DisplayMode + except: # swallow all exceptions here since it always fails on first run (Displaymode enum no set yet) + m = ["2D","3D"][utils.get_param("dimstyle",0)] + + # set the arc + if m == "3D": + # calculate the spacing of the text + spacing = (len(self.string)*obj.ViewObject.FontSize.Value)/8.0 + pts1 = [] + cut = None + pts2 = [] + for i in range(arcsegs+1): + p = self.circle.valueAt(self.circle.FirstParameter+((self.circle.LastParameter-self.circle.FirstParameter)/arcsegs)*i) + if (p.sub(mp)).Length <= spacing: + if cut is None: + cut = i + else: + if cut is None: + pts1.append([p.x,p.y,p.z]) + else: + pts2.append([p.x,p.y,p.z]) + self.coords.point.setValues(pts1+pts2) + i1 = len(pts1) + i2 = i1+len(pts2) + self.arc.coordIndex.setValues(0,len(pts1)+len(pts2)+1,list(range(len(pts1)))+[-1]+list(range(i1,i2))) + if (len(pts1) >= 3) and (len(pts2) >= 3): + self.circle1 = Part.Arc(App.Vector(pts1[0][0],pts1[0][1],pts1[0][2]),App.Vector(pts1[1][0],pts1[1][1],pts1[1][2]),App.Vector(pts1[-1][0],pts1[-1][1],pts1[-1][2])).toShape() + self.circle2 = Part.Arc(App.Vector(pts2[0][0],pts2[0][1],pts2[0][2]),App.Vector(pts2[1][0],pts2[1][1],pts2[1][2]),App.Vector(pts2[-1][0],pts2[-1][1],pts2[-1][2])).toShape() + else: + pts = [] + for i in range(arcsegs+1): + p = self.circle.valueAt(self.circle.FirstParameter+((self.circle.LastParameter-self.circle.FirstParameter)/arcsegs)*i) + pts.append([p.x,p.y,p.z]) + self.coords.point.setValues(pts) + self.arc.coordIndex.setValues(0,arcsegs+1,list(range(arcsegs+1))) + + # set the arrow coords and rotation + self.trans1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.coord1.point.setValue((self.p2.x,self.p2.y,self.p2.z)) + self.trans2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) + self.coord2.point.setValue((self.p3.x,self.p3.y,self.p3.z)) + # calculate small chords to make arrows look better + arrowlength = 4*obj.ViewObject.ArrowSize.Value + u1 = (self.circle.valueAt(self.circle.FirstParameter+arrowlength)).sub(self.circle.valueAt(self.circle.FirstParameter)).normalize() + u2 = (self.circle.valueAt(self.circle.LastParameter)).sub(self.circle.valueAt(self.circle.LastParameter-arrowlength)).normalize() + if hasattr(obj.ViewObject,"FlipArrows"): + if obj.ViewObject.FlipArrows: + u1 = u1.negative() + u2 = u2.negative() + w2 = self.circle.Curve.Axis + w1 = w2.negative() + v1 = w1.cross(u1) + v2 = w2.cross(u2) + q1 = App.Placement(DraftVecUtils.getPlaneRotation(u1,v1,w1)).Rotation.Q + q2 = App.Placement(DraftVecUtils.getPlaneRotation(u2,v2,w2)).Rotation.Q + self.trans1.rotation.setValue((q1[0],q1[1],q1[2],q1[3])) + self.trans2.rotation.setValue((q2[0],q2[1],q2[2],q2[3])) + + # setting text pos & rot + self.tbase = mp + if hasattr(obj.ViewObject,"TextPosition"): + if not DraftVecUtils.isNull(obj.ViewObject.TextPosition): + self.tbase = obj.ViewObject.TextPosition + + u3 = ray.cross(norm).normalize() + v3 = norm.cross(u3) + r = App.Placement(DraftVecUtils.getPlaneRotation(u3,v3,norm)).Rotation + offset = r.multVec(App.Vector(0,1,0)) + + if hasattr(obj.ViewObject,"TextSpacing"): + offset = DraftVecUtils.scaleTo(offset,obj.ViewObject.TextSpacing.Value) + else: + offset = DraftVecUtils.scaleTo(offset,0.05) + if m == "3D": + offset = offset.negative() + self.tbase = self.tbase.add(offset) + q = r.Q + self.textpos.translation.setValue([self.tbase.x,self.tbase.y,self.tbase.z]) + self.textpos.rotation = coin.SbRotation(q[0],q[1],q[2],q[3]) + + # set the angle property + if round(obj.Angle,utils.precision()) != round(a,utils.precision()): + obj.Angle = a + + def onChanged(self, vobj, prop): + if prop == "ScaleMultiplier" and hasattr(vobj,"ScaleMultiplier"): + # update all dimension values + if hasattr(self,"font"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + if hasattr(self,"node") and hasattr(self,"p2") and hasattr(vobj,"ArrowSize"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + self.updateData(vobj.Object,"Start") + vobj.Object.touch() + elif prop == "FontSize" and hasattr(vobj,"ScaleMultiplier"): + if hasattr(self,"font"): + self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier + if hasattr(self,"font3d"): + self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier + vobj.Object.touch() + elif prop == "FontName": + if hasattr(self,"font") and hasattr(self,"font3d"): + self.font.name = self.font3d.name = str(vobj.FontName) + vobj.Object.touch() + elif prop == "LineColor": + if hasattr(self,"color") and hasattr(vobj,"LineColor"): + c = vobj.LineColor + self.color.rgb.setValue(c[0],c[1],c[2]) + elif prop == "LineWidth": + if hasattr(self,"drawstyle"): + self.drawstyle.lineWidth = vobj.LineWidth + elif prop in ["ArrowSize","ArrowType"] and hasattr(vobj,"ScaleMultiplier"): + if hasattr(self,"node") and hasattr(self,"p2"): + self.remove_dim_arrows() + self.draw_dim_arrows(vobj) + vobj.Object.touch() + else: + self.updateData(vobj.Object, None) + + def remove_dim_arrows(self): + # remove existing nodes + self.node.removeChild(self.marks) + self.node3d.removeChild(self.marks) + + def draw_dim_arrows(self, vobj): + from pivy import coin + + if not hasattr(vobj,"ArrowType"): + return + + # set scale + symbol = utils.ARROW_TYPES.index(vobj.ArrowType) + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + self.trans1.scaleFactor.setValue((s,s,s)) + self.trans2.scaleFactor.setValue((s,s,s)) + + # set new nodes + self.marks = coin.SoSeparator() + self.marks.addChild(self.color) + s1 = coin.SoSeparator() + if symbol == "Circle": + s1.addChild(self.coord1) + else: + s1.addChild(self.trans1) + s1.addChild(gui_utils.dim_symbol(symbol,invert=False)) + self.marks.addChild(s1) + s2 = coin.SoSeparator() + if symbol == "Circle": + s2.addChild(self.coord2) + else: + s2.addChild(self.trans2) + s2.addChild(gui_utils.dim_symbol(symbol,invert=True)) + self.marks.addChild(s2) + self.node.insertChild(self.marks,2) + self.node3d.insertChild(self.marks,2) + + def getIcon(self): + return ":/icons/Draft_DimensionAngular.svg" \ No newline at end of file From 4855564bb8d04f8f39c2f6b0395e0cb9f7748d7b Mon Sep 17 00:00:00 2001 From: carlopav Date: Sun, 22 Mar 2020 14:34:53 +0100 Subject: [PATCH 086/142] [Draft] Group object for dimension styles --- src/Mod/Draft/Draft.py | 402 +----------------- .../Draft/draftguitools/gui_dimensionstyle.py | 55 --- src/Mod/Draft/draftobjects/dimensionstyle.py | 31 ++ .../Draft/draftobjects/draft_annotation.py | 26 +- .../draftviewproviders/view_dimensionstyle.py | 15 + .../view_draft_annotation.py | 26 ++ 6 files changed, 106 insertions(+), 449 deletions(-) diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index 3e418fcf83..b4dd770d57 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -251,50 +251,22 @@ def makeRectangle(length, height, placement=None, face=None, support=None): return obj -from draftobjects.dimension import make_dimension -makeDimension = make_dimension +# Backward compatibility for annotation objects. -from draftobjects.dimension import LinearDimension +from draftobjects.dimension import make_dimension, make_angular_dimension +makeDimension = make_dimension +makeAngularDimension = make_angular_dimension + +from draftobjects.dimension import LinearDimension, AngularDimension _Dimension = LinearDimension +_AngularDimension = AngularDimension from draftviewproviders.view_dimension import ViewProviderLinearDimension +from draftviewproviders.view_dimension import ViewProviderAngularDimension _ViewProviderDimension = ViewProviderLinearDimension +_ViewProviderAngularDimension = ViewProviderAngularDimension -def makeAngularDimension(center,angles,p3,normal=None): - """makeAngularDimension(center,angle1,angle2,p3,[normal]): creates an angular Dimension - from the given center, with the given list of angles, passing through p3. - """ - if not FreeCAD.ActiveDocument: - FreeCAD.Console.PrintError("No active document. Aborting\n") - return - obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","Dimension") - _AngularDimension(obj) - obj.Center = center - for a in range(len(angles)): - if angles[a] > 2*math.pi: - angles[a] = angles[a]-(2*math.pi) - obj.FirstAngle = math.degrees(angles[1]) - obj.LastAngle = math.degrees(angles[0]) - obj.Dimline = p3 - if not normal: - if hasattr(FreeCAD,"DraftWorkingPlane"): - normal = FreeCAD.DraftWorkingPlane.axis - else: - normal = Vector(0,0,1) - if gui: - # invert the normal if we are viewing it from the back - vnorm = get3DView().getViewDirection() - if vnorm.getAngle(normal) < math.pi/2: - normal = normal.negative() - obj.Normal = normal - if gui: - _ViewProviderAngularDimension(obj.ViewObject) - formatObject(obj) - select(obj) - - return obj - def makeWire(pointslist,closed=False,placement=None,face=None,support=None,bs2wire=False): """makeWire(pointslist,[closed],[placement]): Creates a Wire object from the given list of vectors. If closed is True or first @@ -3302,362 +3274,6 @@ class _ViewProviderDraftLink: else: return obj.ElementList -class _AngularDimension(_DraftObject): - """The Draft AngularDimension object""" - def __init__(self, obj): - _DraftObject.__init__(self,obj,"AngularDimension") - obj.addProperty("App::PropertyAngle","FirstAngle","Draft",QT_TRANSLATE_NOOP("App::Property","Start angle of the dimension")) - obj.addProperty("App::PropertyAngle","LastAngle","Draft",QT_TRANSLATE_NOOP("App::Property","End angle of the dimension")) - obj.addProperty("App::PropertyVectorDistance","Dimline","Draft",QT_TRANSLATE_NOOP("App::Property","Point through which the dimension line passes")) - obj.addProperty("App::PropertyVectorDistance","Center","Draft",QT_TRANSLATE_NOOP("App::Property","The center point of this dimension")) - obj.addProperty("App::PropertyVector","Normal","Draft",QT_TRANSLATE_NOOP("App::Property","The normal direction of this dimension")) - obj.addProperty("App::PropertyLink","Support","Draft",QT_TRANSLATE_NOOP("App::Property","The object measured by this dimension")) - obj.addProperty("App::PropertyLinkSubList","LinkedGeometry","Draft",QT_TRANSLATE_NOOP("App::Property","The geometry this dimension is linked to")) - obj.addProperty("App::PropertyAngle","Angle","Draft",QT_TRANSLATE_NOOP("App::Property","The measurement of this dimension")) - obj.FirstAngle = 0 - obj.LastAngle = 90 - obj.Dimline = FreeCAD.Vector(0,1,0) - obj.Center = FreeCAD.Vector(0,0,0) - obj.Normal = FreeCAD.Vector(0,0,1) - - def onChanged(self,obj,prop): - if hasattr(obj,"Angle"): - obj.setEditorMode('Angle',1) - if hasattr(obj,"Normal"): - obj.setEditorMode('Normal',2) - if hasattr(obj,"Support"): - obj.setEditorMode('Support',2) - - def execute(self, fp): - if fp.ViewObject: - fp.ViewObject.update() - -class _ViewProviderAngularDimension(_ViewProviderDraft): - """A View Provider for the Draft Angular Dimension object""" - def __init__(self, obj): - obj.addProperty("App::PropertyFloat","ScaleMultiplier", - "Annotation",QT_TRANSLATE_NOOP("App::Property", - "Dimension size overall multiplier")) - obj.addProperty("App::PropertyLength","FontSize", - "Text",QT_TRANSLATE_NOOP("App::Property","Font size")) - obj.addProperty("App::PropertyInteger","Decimals", - "Units",QT_TRANSLATE_NOOP("App::Property", - "The number of decimals to show")) - obj.addProperty("App::PropertyFont","FontName", - "Text",QT_TRANSLATE_NOOP("App::Property","Font name")) - obj.addProperty("App::PropertyLength","ArrowSize", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow size")) - obj.addProperty("App::PropertyLength","TextSpacing", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The spacing between the text and the dimension line")) - obj.addProperty("App::PropertyEnumeration","ArrowType", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Arrow type")) - obj.addProperty("App::PropertyFloat","LineWidth", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Line width")) - obj.addProperty("App::PropertyColor","LineColor", - "Graphics",QT_TRANSLATE_NOOP("App::Property","Line color")) - obj.addProperty("App::PropertyBool","FlipArrows", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Rotate the dimension arrows 180 degrees")) - obj.addProperty("App::PropertyBool","ShowUnit", - "Units",QT_TRANSLATE_NOOP("App::Property", - "Show the unit suffix")) - obj.addProperty("App::PropertyVectorDistance","TextPosition", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The position of the text. Leave (0,0,0) for automatic position")) - obj.addProperty("App::PropertyString","Override", - "Text",QT_TRANSLATE_NOOP("App::Property", - "Text override. Use 'dim' to insert the dimension length")) - - param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") - annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) - obj.ScaleMultiplier = 1 / annotation_scale - obj.FontSize = getParam("textheight",0.20) - obj.FontName = getParam("textfont","") - obj.TextSpacing = getParam("dimspacing",0.05) - obj.ArrowSize = getParam("arrowsize",0.1) - obj.ArrowType = arrowtypes - obj.ArrowType = arrowtypes[getParam("dimsymbol",0)] - obj.Override = '' - obj.Decimals = getParam("dimPrecision",2) - obj.ShowUnit = getParam("showUnit",True) - _ViewProviderDraft.__init__(self,obj) - - def attach(self, vobj): - from pivy import coin - self.Object = vobj.Object - self.color = coin.SoBaseColor() - self.color.rgb.setValue(vobj.LineColor[0],vobj.LineColor[1],vobj.LineColor[2]) - self.font = coin.SoFont() - self.font3d = coin.SoFont() - self.text = coin.SoAsciiText() - self.text3d = coin.SoText2() - self.text.string = "d" # some versions of coin crash if string is not set - self.text3d.string = "d" - self.text.justification = self.text3d.justification = coin.SoAsciiText.CENTER - self.textpos = coin.SoTransform() - label = coin.SoSeparator() - label.addChild(self.textpos) - label.addChild(self.color) - label.addChild(self.font) - label.addChild(self.text) - label3d = coin.SoSeparator() - label3d.addChild(self.textpos) - label3d.addChild(self.color) - label3d.addChild(self.font3d) - label3d.addChild(self.text3d) - self.coord1 = coin.SoCoordinate3() - self.trans1 = coin.SoTransform() - self.coord2 = coin.SoCoordinate3() - self.trans2 = coin.SoTransform() - self.marks = coin.SoSeparator() - self.drawstyle = coin.SoDrawStyle() - self.coords = coin.SoCoordinate3() - self.arc = coin.SoType.fromName("SoBrepEdgeSet").createInstance() - self.node = coin.SoGroup() - self.node.addChild(self.color) - self.node.addChild(self.drawstyle) - self.node.addChild(self.coords) - self.node.addChild(self.arc) - self.node.addChild(self.marks) - self.node.addChild(label) - self.node3d = coin.SoGroup() - self.node3d.addChild(self.color) - self.node3d.addChild(self.drawstyle) - self.node3d.addChild(self.coords) - self.node3d.addChild(self.arc) - self.node3d.addChild(self.marks) - self.node3d.addChild(label3d) - vobj.addDisplayMode(self.node,"2D") - vobj.addDisplayMode(self.node3d,"3D") - self.updateData(vobj.Object,None) - self.onChanged(vobj,"FontSize") - self.onChanged(vobj,"FontName") - self.onChanged(vobj,"ArrowType") - self.onChanged(vobj,"LineColor") - - def updateData(self, obj, prop): - if hasattr(self,"arc"): - from pivy import coin - import Part, DraftGeomUtils - import DraftGui - arcsegs = 24 - - # calculate the arc data - if DraftVecUtils.isNull(obj.Normal): - norm = Vector(0,0,1) - else: - norm = obj.Normal - radius = (obj.Dimline.sub(obj.Center)).Length - self.circle = Part.makeCircle(radius,obj.Center,norm,obj.FirstAngle.Value,obj.LastAngle.Value) - self.p2 = self.circle.Vertexes[0].Point - self.p3 = self.circle.Vertexes[-1].Point - mp = DraftGeomUtils.findMidpoint(self.circle.Edges[0]) - ray = mp.sub(obj.Center) - - # set text value - if obj.LastAngle.Value > obj.FirstAngle.Value: - a = obj.LastAngle.Value - obj.FirstAngle.Value - else: - a = (360 - obj.FirstAngle.Value) + obj.LastAngle.Value - su = True - if hasattr(obj.ViewObject,"ShowUnit"): - su = obj.ViewObject.ShowUnit - if hasattr(obj.ViewObject,"Decimals"): - self.string = DraftGui.displayExternal(a,obj.ViewObject.Decimals,'Angle',su) - else: - self.string = DraftGui.displayExternal(a,None,'Angle',su) - if obj.ViewObject.Override: - self.string = obj.ViewObject.Override.replace("$dim",\ - self.string) - self.text.string = self.text3d.string = stringencodecoin(self.string) - - # check display mode - try: - m = obj.ViewObject.DisplayMode - except: # swallow all exceptions here since it always fails on first run (Displaymode enum no set yet) - m = ["2D","3D"][getParam("dimstyle",0)] - - # set the arc - if m == "3D": - # calculate the spacing of the text - spacing = (len(self.string)*obj.ViewObject.FontSize.Value)/8.0 - pts1 = [] - cut = None - pts2 = [] - for i in range(arcsegs+1): - p = self.circle.valueAt(self.circle.FirstParameter+((self.circle.LastParameter-self.circle.FirstParameter)/arcsegs)*i) - if (p.sub(mp)).Length <= spacing: - if cut is None: - cut = i - else: - if cut is None: - pts1.append([p.x,p.y,p.z]) - else: - pts2.append([p.x,p.y,p.z]) - self.coords.point.setValues(pts1+pts2) - i1 = len(pts1) - i2 = i1+len(pts2) - self.arc.coordIndex.setValues(0,len(pts1)+len(pts2)+1,list(range(len(pts1)))+[-1]+list(range(i1,i2))) - if (len(pts1) >= 3) and (len(pts2) >= 3): - self.circle1 = Part.Arc(Vector(pts1[0][0],pts1[0][1],pts1[0][2]),Vector(pts1[1][0],pts1[1][1],pts1[1][2]),Vector(pts1[-1][0],pts1[-1][1],pts1[-1][2])).toShape() - self.circle2 = Part.Arc(Vector(pts2[0][0],pts2[0][1],pts2[0][2]),Vector(pts2[1][0],pts2[1][1],pts2[1][2]),Vector(pts2[-1][0],pts2[-1][1],pts2[-1][2])).toShape() - else: - pts = [] - for i in range(arcsegs+1): - p = self.circle.valueAt(self.circle.FirstParameter+((self.circle.LastParameter-self.circle.FirstParameter)/arcsegs)*i) - pts.append([p.x,p.y,p.z]) - self.coords.point.setValues(pts) - self.arc.coordIndex.setValues(0,arcsegs+1,list(range(arcsegs+1))) - - # set the arrow coords and rotation - self.trans1.translation.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.coord1.point.setValue((self.p2.x,self.p2.y,self.p2.z)) - self.trans2.translation.setValue((self.p3.x,self.p3.y,self.p3.z)) - self.coord2.point.setValue((self.p3.x,self.p3.y,self.p3.z)) - # calculate small chords to make arrows look better - arrowlength = 4*obj.ViewObject.ArrowSize.Value - u1 = (self.circle.valueAt(self.circle.FirstParameter+arrowlength)).sub(self.circle.valueAt(self.circle.FirstParameter)).normalize() - u2 = (self.circle.valueAt(self.circle.LastParameter)).sub(self.circle.valueAt(self.circle.LastParameter-arrowlength)).normalize() - if hasattr(obj.ViewObject,"FlipArrows"): - if obj.ViewObject.FlipArrows: - u1 = u1.negative() - u2 = u2.negative() - w2 = self.circle.Curve.Axis - w1 = w2.negative() - v1 = w1.cross(u1) - v2 = w2.cross(u2) - q1 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u1,v1,w1)).Rotation.Q - q2 = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u2,v2,w2)).Rotation.Q - self.trans1.rotation.setValue((q1[0],q1[1],q1[2],q1[3])) - self.trans2.rotation.setValue((q2[0],q2[1],q2[2],q2[3])) - - # setting text pos & rot - self.tbase = mp - if hasattr(obj.ViewObject,"TextPosition"): - if not DraftVecUtils.isNull(obj.ViewObject.TextPosition): - self.tbase = obj.ViewObject.TextPosition - - u3 = ray.cross(norm).normalize() - v3 = norm.cross(u3) - r = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(u3,v3,norm)).Rotation - offset = r.multVec(Vector(0,1,0)) - - if hasattr(obj.ViewObject,"TextSpacing"): - offset = DraftVecUtils.scaleTo(offset,obj.ViewObject.TextSpacing.Value) - else: - offset = DraftVecUtils.scaleTo(offset,0.05) - if m == "3D": - offset = offset.negative() - self.tbase = self.tbase.add(offset) - q = r.Q - self.textpos.translation.setValue([self.tbase.x,self.tbase.y,self.tbase.z]) - self.textpos.rotation = coin.SbRotation(q[0],q[1],q[2],q[3]) - - # set the angle property - if round(obj.Angle,precision()) != round(a,precision()): - obj.Angle = a - - def onChanged(self, vobj, prop): - if prop == "ScaleMultiplier" and hasattr(vobj,"ScaleMultiplier"): - # update all dimension values - if hasattr(self,"font"): - self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier - if hasattr(self,"font3d"): - self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier - if hasattr(self,"node") and hasattr(self,"p2") and hasattr(vobj,"ArrowSize"): - self.remove_dim_arrows() - self.draw_dim_arrows(vobj) - self.updateData(vobj.Object,"Start") - vobj.Object.touch() - elif prop == "FontSize" and hasattr(vobj,"ScaleMultiplier"): - if hasattr(self,"font"): - self.font.size = vobj.FontSize.Value*vobj.ScaleMultiplier - if hasattr(self,"font3d"): - self.font3d.size = vobj.FontSize.Value*100*vobj.ScaleMultiplier - vobj.Object.touch() - elif prop == "FontName": - if hasattr(self,"font") and hasattr(self,"font3d"): - self.font.name = self.font3d.name = str(vobj.FontName) - vobj.Object.touch() - elif prop == "LineColor": - if hasattr(self,"color"): - c = vobj.LineColor - self.color.rgb.setValue(c[0],c[1],c[2]) - elif prop == "LineWidth": - if hasattr(self,"drawstyle"): - self.drawstyle.lineWidth = vobj.LineWidth - elif prop in ["ArrowSize","ArrowType"] and hasattr(vobj,"ScaleMultiplier"): - if hasattr(self,"node") and hasattr(self,"p2"): - self.remove_dim_arrows() - self.draw_dim_arrows(vobj) - vobj.Object.touch() - else: - self.updateData(vobj.Object, None) - - def remove_dim_arrows(self): - # remove existing nodes - self.node.removeChild(self.marks) - self.node3d.removeChild(self.marks) - - def draw_dim_arrows(self, vobj): - from pivy import coin - - if not hasattr(vobj,"ArrowType"): - return - - # set scale - symbol = arrowtypes.index(vobj.ArrowType) - s = vobj.ArrowSize.Value * vobj.ScaleMultiplier - self.trans1.scaleFactor.setValue((s,s,s)) - self.trans2.scaleFactor.setValue((s,s,s)) - - # set new nodes - self.marks = coin.SoSeparator() - self.marks.addChild(self.color) - s1 = coin.SoSeparator() - if symbol == "Circle": - s1.addChild(self.coord1) - else: - s1.addChild(self.trans1) - s1.addChild(dimSymbol(symbol,invert=False)) - self.marks.addChild(s1) - s2 = coin.SoSeparator() - if symbol == "Circle": - s2.addChild(self.coord2) - else: - s2.addChild(self.trans2) - s2.addChild(dimSymbol(symbol,invert=True)) - self.marks.addChild(s2) - self.node.insertChild(self.marks,2) - self.node3d.insertChild(self.marks,2) - - - def doubleClicked(self,vobj): - self.setEdit(vobj) - - def getDisplayModes(self,obj): - modes=[] - modes.extend(["2D","3D"]) - return modes - - def getDefaultDisplayMode(self): - if hasattr(self,"defaultmode"): - return self.defaultmode - else: - return ["2D","3D"][getParam("dimstyle",0)] - - def getIcon(self): - return ":/icons/Draft_DimensionAngular.svg" - - def __getstate__(self): - return self.Object.ViewObject.DisplayMode - - def __setstate__(self,state): - if state: - self.defaultmode = state - self.setDisplayMode(state) - class _Rectangle(_DraftObject): """The Rectangle object""" diff --git a/src/Mod/Draft/draftguitools/gui_dimensionstyle.py b/src/Mod/Draft/draftguitools/gui_dimensionstyle.py index d5906e9cfe..146870c43b 100644 --- a/src/Mod/Draft/draftguitools/gui_dimensionstyle.py +++ b/src/Mod/Draft/draftguitools/gui_dimensionstyle.py @@ -35,61 +35,6 @@ from draftutils import utils from draftobjects.dimensionstyle import make_dimension_style -''' -class AnnotationStylesContainer: - """The Layer Container""" - - def __init__(self, obj): - - self.Type = "AnnotationContainer" - obj.Proxy = self - - def execute(self, obj): - - g = obj.Group - g.sort(key=lambda o: o.Label) - obj.Group = g - - def __getstate__(self): - - if hasattr(self, "Type"): - return self.Type - - def __setstate__(self, state): - - if state: - self.Type = state - - -class ViewProviderAnnotationStylesContainer: - """A View Provider for the Layer Container""" - - def __init__(self, vobj): - - vobj.Proxy = self - - def getIcon(self): - - return ":/icons/Draft_Annotation_Style.svg" - - def attach(self, vobj): - - self.Object = vobj.Object - - def __getstate__(self): - - return None - - def __setstate__(self, state): - - return None - - -''' - - - -# XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX class GuiCommandDimensionStyle(gui_base.GuiCommandSimplest): """ diff --git a/src/Mod/Draft/draftobjects/dimensionstyle.py b/src/Mod/Draft/draftobjects/dimensionstyle.py index 2ebe8bebea..56c66dcee3 100644 --- a/src/Mod/Draft/draftobjects/dimensionstyle.py +++ b/src/Mod/Draft/draftobjects/dimensionstyle.py @@ -31,6 +31,8 @@ import FreeCAD as App from draftobjects.draft_annotation import DraftAnnotation from PySide.QtCore import QT_TRANSLATE_NOOP from draftviewproviders.view_dimensionstyle import ViewProviderDraftDimensionStyle +from draftviewproviders.view_dimensionstyle import ViewProviderDimensionStylesContainer +from draftobjects.draft_annotation import AnnotationStylesContainer if App.GuiUp: import FreeCADGui as Gui @@ -46,8 +48,37 @@ def make_dimension_style(existing_dimension = None): DimensionStyle(obj) if App.GuiUp: ViewProviderDraftDimensionStyle(obj.ViewObject, existing_dimension) + get_dimension_style_container().addObject(obj) return obj +def get_dimension_style_container(): + """get_dimension_style_container(): returns a group object to put dimensions in""" + for obj in App.ActiveDocument.Objects: + if obj.Name == "DimensionStyleContainer": + return obj + obj = App.ActiveDocument.addObject("App::DocumentObjectGroupPython", "DimensionStyleContainer") + obj.Label = QT_TRANSLATE_NOOP("draft", "Dimension Styles") + DimensionStylesContainer(obj) + if App.GuiUp: + ViewProviderDimensionStylesContainer(obj.ViewObject) + return obj + + +class DimensionStylesContainer(AnnotationStylesContainer): + """The Dimension Container""" + + def __init__(self, obj): + super().__init__(obj) + self.Type = "DimensionStyleContainer" + obj.Proxy = self + + def execute(self, obj): + + g = obj.Group + g.sort(key=lambda o: o.Label) + obj.Group = g + + class DimensionStyle(DraftAnnotation): def __init__(self, obj): super().__init__(obj, "DimensionStyle") \ No newline at end of file diff --git a/src/Mod/Draft/draftobjects/draft_annotation.py b/src/Mod/Draft/draftobjects/draft_annotation.py index c662b3c237..65c781dedc 100644 --- a/src/Mod/Draft/draftobjects/draft_annotation.py +++ b/src/Mod/Draft/draftobjects/draft_annotation.py @@ -52,4 +52,28 @@ class DraftAnnotation: pass def onChanged(self, obj, prop): - pass \ No newline at end of file + pass + +class AnnotationStylesContainer: + """The Annotation Container""" + + def __init__(self, obj): + + self.Type = "AnnotationContainer" + obj.Proxy = self + + def execute(self, obj): + + g = obj.Group + g.sort(key=lambda o: o.Label) + obj.Group = g + + def __getstate__(self): + + if hasattr(self, "Type"): + return self.Type + + def __setstate__(self, state): + + if state: + self.Type = state \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py index 19ce732f3f..7229f46724 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py +++ b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py @@ -32,6 +32,21 @@ from Draft import _ViewProviderDraft from PySide.QtCore import QT_TRANSLATE_NOOP import draftutils.utils as utils from pivy import coin +from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation +from draftviewproviders.view_draft_annotation import ViewProviderAnnotationStylesContainer + + +class ViewProviderDimensionStylesContainer(ViewProviderAnnotationStylesContainer): + """A View Provider for the Dimension Style Container""" + + def __init__(self, vobj): + super().__init__(vobj) + vobj.Proxy = self + + def getIcon(self): + + return ":/icons/Draft_Annotation_Style.svg" + class ViewProviderDraftDimensionStyle(_ViewProviderDraft): """ diff --git a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py index 1973516b84..ab0c0f15b5 100644 --- a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py +++ b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py @@ -32,6 +32,32 @@ import FreeCADGui as Gui from PySide.QtCore import QT_TRANSLATE_NOOP + +class ViewProviderAnnotationStylesContainer: + """A View Provider for the Layer Container""" + + def __init__(self, vobj): + + vobj.Proxy = self + + def getIcon(self): + + return ":/icons/Draft_Annotation_Style.svg" + + def attach(self, vobj): + + self.Object = vobj.Object + + def __getstate__(self): + + return None + + def __setstate__(self, state): + + return None + + + class ViewProviderDraftAnnotation: """ The base class for Draft Annotation Viewproviders From 88612bee7d62c8fc712357bc2316bee8fdd4459c Mon Sep 17 00:00:00 2001 From: carlopav Date: Sun, 22 Mar 2020 15:20:12 +0100 Subject: [PATCH 087/142] [Draft] Dimension Style cleanup --- src/Mod/Draft/draftobjects/dimensionstyle.py | 10 +- .../draftviewproviders/view_dimensionstyle.py | 120 ++---------------- 2 files changed, 17 insertions(+), 113 deletions(-) diff --git a/src/Mod/Draft/draftobjects/dimensionstyle.py b/src/Mod/Draft/draftobjects/dimensionstyle.py index 56c66dcee3..598071a47d 100644 --- a/src/Mod/Draft/draftobjects/dimensionstyle.py +++ b/src/Mod/Draft/draftobjects/dimensionstyle.py @@ -81,4 +81,12 @@ class DimensionStylesContainer(AnnotationStylesContainer): class DimensionStyle(DraftAnnotation): def __init__(self, obj): - super().__init__(obj, "DimensionStyle") \ No newline at end of file + super().__init__(obj, "DimensionStyle") + + def set_current(self, obj): + "turn non visible all the concurrent styles" + for o in get_dimension_style_container().Group: + if hasattr(o, "Visibility"): + o.Visibility = False + if hasattr(obj, "Visibility"): + obj.Visibility = True diff --git a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py index 7229f46724..c28a9097ce 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py +++ b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py @@ -34,6 +34,7 @@ import draftutils.utils as utils from pivy import coin from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation from draftviewproviders.view_draft_annotation import ViewProviderAnnotationStylesContainer +from draftviewproviders.view_dimension import ViewProviderDimensionBase class ViewProviderDimensionStylesContainer(ViewProviderAnnotationStylesContainer): @@ -48,127 +49,23 @@ class ViewProviderDimensionStylesContainer(ViewProviderAnnotationStylesContainer return ":/icons/Draft_Annotation_Style.svg" -class ViewProviderDraftDimensionStyle(_ViewProviderDraft): +class ViewProviderDraftDimensionStyle(ViewProviderDimensionBase): """ Dimension style dont have a proper object but just a viewprovider. It stores inside a document object dimension settings and restore them on demand. """ def __init__(self, vobj, existing_dimension = None): - """ - vobj properties type parameter type - ---------------------------------------------------------------------------------- - vobj.ScaleMultiplier" App::PropertyFloat "DraftAnnotationScale" Float - - vobj.FontName App::PropertyFont "textfont" Text - vobj.FontSize App::PropertyLength "textheight" Float - vobj.TextSpacing App::PropertyLength "dimspacing" Float - - vobj.Decimals App::PropertyInteger "dimPrecision" Integer - vobj.ShowUnit App::PropertyBool - vobj.UnitOverride App::PropertyString - - vobj.LineWidth App::PropertyFloat - vobj.LineColor App::PropertyColor - vobj.ArrowSize App::PropertyLength "arrowsize" Float - vobj.ArrowType App::PropertyEnumeration "dimsymbol" Integer - vobj.FlipArrows App::PropertyBool - vobj.DimOvershoot App::PropertyDistance "dimovershoot" Float - vobj.ExtLines App::PropertyDistance "extlines" Float - vobj.ExtOvershoot App::PropertyDistance "extovershoot" Float - vobj.ShowLine App::PropertyBool - """ - - # annotation properties - vobj.addProperty("App::PropertyFloat","ScaleMultiplier", - "Annotation", - QT_TRANSLATE_NOOP("App::Property", - "Dimension size overall multiplier")) + super().__init__(vobj) vobj.addProperty("App::PropertyBool","AutoUpdate", "Annotation", QT_TRANSLATE_NOOP("App::Property", "Auto update associated dimensions")) - # text properties - vobj.addProperty("App::PropertyFont","FontName", - "Text", - QT_TRANSLATE_NOOP("App::Property","Font name")) - - vobj.addProperty("App::PropertyLength","FontSize", - "Text", - QT_TRANSLATE_NOOP("App::Property", - "Font size")) - - vobj.addProperty("App::PropertyLength","TextSpacing", - "Text", - QT_TRANSLATE_NOOP("App::Property", - "The spacing between the text and " - "the dimension line")) - - # units properties - vobj.addProperty("App::PropertyInteger","Decimals", - "Units", - QT_TRANSLATE_NOOP("App::Property", - "The number of decimals to show")) - - vobj.addProperty("App::PropertyBool","ShowUnit", - "Units", - QT_TRANSLATE_NOOP("App::Property", - "Show the unit suffix")) - - vobj.addProperty("App::PropertyString","UnitOverride", - "Units", - QT_TRANSLATE_NOOP("App::Property", - "A unit to express the measurement. " - "Leave blank for system default")) - - # graphics properties - vobj.addProperty("App::PropertyFloat","LineWidth", - "Graphics", - QT_TRANSLATE_NOOP("App::Property","Line width")) - - vobj.addProperty("App::PropertyColor","LineColor", - "Graphics", - QT_TRANSLATE_NOOP("App::Property","Line color")) - - vobj.addProperty("App::PropertyLength","ArrowSize", - "Graphics", - QT_TRANSLATE_NOOP("App::Property","Arrow size")) - - vobj.addProperty("App::PropertyEnumeration","ArrowType", - "Graphics", - QT_TRANSLATE_NOOP("App::Property","Arrow type")) - - vobj.addProperty("App::PropertyBool","FlipArrows", - "Graphics", - QT_TRANSLATE_NOOP("App::Property", - "Rotate the dimension arrows 180 degrees")) - - vobj.addProperty("App::PropertyDistance","DimOvershoot", - "Graphics", - QT_TRANSLATE_NOOP("App::Property", - "The distance the dimension line is " - "extended past the extension lines")) - - vobj.addProperty("App::PropertyDistance","ExtLines", - "Graphics", - QT_TRANSLATE_NOOP("App::Property", - "Length of the extension lines")) - - vobj.addProperty("App::PropertyDistance","ExtOvershoot", - "Graphics", - QT_TRANSLATE_NOOP("App::Property", - "Length of the extension line above " - "the dimension line")) - - vobj.addProperty("App::PropertyBool","ShowLine", - "Graphics", - QT_TRANSLATE_NOOP("App::Property", - "Shows dimension line and arrows")) - self.init_properties(vobj, existing_dimension) - - super().__init__(vobj) + + # Visibility is True only if the style is active + vobj.Visibility = False def init_properties(self, vobj, existing_dimension): """ @@ -205,9 +102,6 @@ class ViewProviderDraftDimensionStyle(_ViewProviderDraft): if hasattr(vobj, "AutoUpdate"): if vobj.AutoUpdate: self.update_related_dimensions(vobj) - if hasattr(vobj, "Visibility"): - if prop == "Visibility": - print(vobj.Visibility) def doubleClicked(self,vobj): self.set_current(vobj) @@ -239,6 +133,8 @@ class ViewProviderDraftDimensionStyle(_ViewProviderDraft): App.Console.PrintMessage("Current dimension style set to " + str(vobj.Object.Label) + "\n") + vobj.Object.Proxy.set_current(vobj.Object) + def update_related_dimensions(self, vobj): """ Apply the style to the related dimensions From f45314c9bc263221e9bafa21695fed4d8d73746c Mon Sep 17 00:00:00 2001 From: carlopav Date: Mon, 23 Mar 2020 17:32:37 +0100 Subject: [PATCH 088/142] [Draft] Updated cmake with splitted annotation objects . . --- src/Mod/Draft/CMakeLists.txt | 7 +++++++ src/Mod/Draft/Resources/Draft.qrc | 3 +++ src/Mod/Draft/draftobjects/dimension.py | 8 +++++--- src/Mod/Draft/draftobjects/dimensionstyle.py | 1 - src/Mod/Draft/draftviewproviders/view_dimension.py | 7 ++++--- src/Mod/Draft/draftviewproviders/view_dimensionstyle.py | 1 - src/Mod/Draft/draftviewproviders/view_draft_annotation.py | 1 - 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 845d39529f..0f1a0054b1 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -64,6 +64,9 @@ SET(Draft_objects draftobjects/orthoarray.py draftobjects/polararray.py draftobjects/arc_3points.py + draftobjects/draft_annotation.py + draftobjects/dimension.py + draftobjects/dimensionstyle.py draftobjects/README.md ) @@ -72,6 +75,9 @@ SET(Draft_view_providers draftviewproviders/view_circulararray.py draftviewproviders/view_orthoarray.py draftviewproviders/view_polararray.py + draftviewproviders/view_draft_annotation.py + draftviewproviders/view_dimension.py + draftviewproviders/view_dimensionstyle.py draftviewproviders/README.md ) @@ -79,6 +85,7 @@ SET(Draft_GUI_tools draftguitools/__init__.py draftguitools/gui_base.py draftguitools/gui_circulararray.py + draftguitools/gui_dimensionstyle.py draftguitools/gui_orthoarray.py draftguitools/gui_polararray.py draftguitools/gui_planeproxy.py diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index 0b8b5145ee..06c8ea3b8c 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -3,6 +3,7 @@ icons/Draft_2DShapeView.svg icons/Draft_AddPoint.svg icons/Draft_AddToGroup.svg + icons/Draft_Annotation_Style.svg icons/Draft_Apply.svg icons/Draft_Arc.svg icons/Draft_Arc_3Points.svg @@ -24,6 +25,8 @@ icons/Draft_DelPoint.svg icons/Draft_Dimension.svg icons/Draft_Dimension_Tree.svg + icons/Draft_Dimension_Style.svg + icons/Draft_Dimension_Style_Tree.svg icons/Draft_DimensionAngular.svg icons/Draft_DimensionRadius.svg icons/Draft_Dot.svg diff --git a/src/Mod/Draft/draftobjects/dimension.py b/src/Mod/Draft/draftobjects/dimension.py index f26998461f..b91ef8d456 100644 --- a/src/Mod/Draft/draftobjects/dimension.py +++ b/src/Mod/Draft/draftobjects/dimension.py @@ -1,5 +1,7 @@ +# -*- coding: utf-8 -*- # *************************************************************************** -# * (c) 2020 Carlo Pavan * +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -21,11 +23,11 @@ # * * # *************************************************************************** -"""This module provides the object code for Draft DimensionStyle. +"""This module provides the object code for Draft Dimension. """ ## @package style_dimension # \ingroup DRAFT -# \brief This module provides the object code for Draft DimensionStyle. +# \brief This module provides the object code for Draft Dimension. import FreeCAD as App import math diff --git a/src/Mod/Draft/draftobjects/dimensionstyle.py b/src/Mod/Draft/draftobjects/dimensionstyle.py index 598071a47d..dff2b6250d 100644 --- a/src/Mod/Draft/draftobjects/dimensionstyle.py +++ b/src/Mod/Draft/draftobjects/dimensionstyle.py @@ -1,5 +1,4 @@ # *************************************************************************** -# * (c) 2020 Carlo Pavan * # * * # * This file is part of the FreeCAD CAx development system. * # * * diff --git a/src/Mod/Draft/draftviewproviders/view_dimension.py b/src/Mod/Draft/draftviewproviders/view_dimension.py index 0b58a1253a..eac7a6d046 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimension.py +++ b/src/Mod/Draft/draftviewproviders/view_dimension.py @@ -1,5 +1,6 @@ # *************************************************************************** -# * (c) 2019 Eliud Cabrera Castillo * +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -20,11 +21,11 @@ # * USA * # * * # *************************************************************************** -"""This module provides the Draft Annotations view provider base classes +"""This module provides the Draft Dimensions view provider classes """ ## @package polararray # \ingroup DRAFT -# \brief This module provides the view provider code for Draft PolarArray. +# \brief This module provides the view provider code for Draft Dimensions. import FreeCAD as App diff --git a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py index c28a9097ce..2dd20198ee 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py +++ b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py @@ -1,5 +1,4 @@ # *************************************************************************** -# * (c) 2020 Carlo Pavan * # * * # * This file is part of the FreeCAD CAx development system. * # * * diff --git a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py index ab0c0f15b5..2cc0f38d8c 100644 --- a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py +++ b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py @@ -1,5 +1,4 @@ # *************************************************************************** -# * (c) 2019 Eliud Cabrera Castillo * # * * # * This file is part of the FreeCAD CAx development system. * # * * From 85bf45937eb0e9394ec693e05a9ff11891448721 Mon Sep 17 00:00:00 2001 From: carlopav Date: Mon, 23 Mar 2020 20:32:59 +0100 Subject: [PATCH 089/142] [Draft] Splitted object Text and Label from Draft.py And based them on Draft Annotation object --- src/Mod/Draft/CMakeLists.txt | 4 + src/Mod/Draft/Draft.py | 634 ++---------------- src/Mod/Draft/draftobjects/label.py | 231 +++++++ src/Mod/Draft/draftobjects/text.py | 112 ++++ .../Draft/draftviewproviders/view_label.py | 320 +++++++++ src/Mod/Draft/draftviewproviders/view_text.py | 170 +++++ 6 files changed, 891 insertions(+), 580 deletions(-) create mode 100644 src/Mod/Draft/draftobjects/label.py create mode 100644 src/Mod/Draft/draftobjects/text.py create mode 100644 src/Mod/Draft/draftviewproviders/view_label.py create mode 100644 src/Mod/Draft/draftviewproviders/view_text.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 0f1a0054b1..10ca81f2c4 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -65,8 +65,10 @@ SET(Draft_objects draftobjects/polararray.py draftobjects/arc_3points.py draftobjects/draft_annotation.py + draftobjects/label.py draftobjects/dimension.py draftobjects/dimensionstyle.py + draftobjects/text.py draftobjects/README.md ) @@ -76,8 +78,10 @@ SET(Draft_view_providers draftviewproviders/view_orthoarray.py draftviewproviders/view_polararray.py draftviewproviders/view_draft_annotation.py + draftviewproviders/view_label.py draftviewproviders/view_dimension.py draftviewproviders/view_dimensionstyle.py + draftviewproviders/view_text.py draftviewproviders/README.md ) diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index b4dd770d57..d0d6ca6beb 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -251,21 +251,68 @@ def makeRectangle(length, height, placement=None, face=None, support=None): return obj -# Backward compatibility for annotation objects. +# Backward compatibility for dimension objects. from draftobjects.dimension import make_dimension, make_angular_dimension -makeDimension = make_dimension -makeAngularDimension = make_angular_dimension - from draftobjects.dimension import LinearDimension, AngularDimension -_Dimension = LinearDimension -_AngularDimension = AngularDimension - from draftviewproviders.view_dimension import ViewProviderLinearDimension from draftviewproviders.view_dimension import ViewProviderAngularDimension + +makeDimension = make_dimension +_Dimension = LinearDimension _ViewProviderDimension = ViewProviderLinearDimension + +makeAngularDimension = make_angular_dimension +_AngularDimension = AngularDimension _ViewProviderAngularDimension = ViewProviderAngularDimension +# Backward compatibility for label object. +from draftobjects.label import make_label +from draftobjects.label import Label +from draftviewproviders.view_label import ViewProviderLabel + +makeLabel = make_label +DraftLabel = Label +ViewProviderDraftLabel = ViewProviderLabel + +# Backward compatibility for text object. +# introduced when splitted text module from Draft.py in v 0.19 +from draftobjects.text import make_text +from draftobjects.text import Text +from draftviewproviders.view_text import ViewProviderText + +makeText = make_text +DraftText = Text +ViewProviderDraftText = ViewProviderText + +# already present at splitting time during v 0.19 +def convertDraftTexts(textslist=[]): + """ + converts the given Draft texts (or all that is found + in the active document) to the new object + """ + if not isinstance(textslist,list): + textslist = [textslist] + if not textslist: + for o in FreeCAD.ActiveDocument.Objects: + if o.TypeId == "App::Annotation": + textslist.append(o) + todelete = [] + for o in textslist: + l = o.Label + o.Label = l+".old" + obj = makeText(o.LabelText,point=o.Position) + obj.Label = l + todelete.append(o.Name) + for p in o.InList: + if p.isDerivedFrom("App::DocumentObjectGroup"): + if o in p.Group: + g = p.Group + g.append(obj) + p.Group = g + for n in todelete: + FreeCAD.ActiveDocument.removeObject(n) + def makeWire(pointslist,closed=False,placement=None,face=None,support=None,bs2wire=False): """makeWire(pointslist,[closed],[placement]): Creates a Wire object @@ -441,35 +488,6 @@ def makeBezCurve(pointslist,closed=False,placement=None,face=None,support=None,d return obj -def makeText(stringslist,point=Vector(0,0,0),screen=False): - """makeText(strings,[point],[screen]): Creates a Text object at the given point, - containing the strings given in the strings list, one string by line (strings - can also be one single string). The current color and text height and font - specified in preferences are used. - If screen is True, the text always faces the view direction.""" - if not FreeCAD.ActiveDocument: - FreeCAD.Console.PrintError("No active document. Aborting\n") - return - typecheck([(point,Vector)], "makeText") - if not isinstance(stringslist,list): stringslist = [stringslist] - obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","Text") - DraftText(obj) - obj.Text = stringslist - obj.Placement.Base = point - if FreeCAD.GuiUp: - ViewProviderDraftText(obj.ViewObject) - if screen: - obj.ViewObject.DisplayMode = "3D text" - h = getParam("textheight",0.20) - if screen: - h = h*10 - obj.ViewObject.FontSize = h - obj.ViewObject.FontName = getParam("textfont","") - obj.ViewObject.LineSpacing = 1 - formatObject(obj) - select(obj) - return obj - def makeCopy(obj,force=None,reparent=False): """makeCopy(object): returns an exact copy of an object""" if not FreeCAD.ActiveDocument: @@ -5305,548 +5323,4 @@ class ViewProviderWorkingPlaneProxy: return None -def makeLabel(targetpoint=None,target=None,direction=None,distance=None,labeltype=None,placement=None): - obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython","dLabel") - DraftLabel(obj) - if FreeCAD.GuiUp: - ViewProviderDraftLabel(obj.ViewObject) - if targetpoint: - obj.TargetPoint = targetpoint - if target: - obj.Target = target - if direction: - obj.StraightDirection = direction - if distance: - obj.StraightDistance = distance - if labeltype: - obj.LabelType = labeltype - if placement: - obj.Placement = placement - return obj - - -class DraftLabel: - """The Draft Label object""" - - def __init__(self,obj): - obj.Proxy = self - obj.addProperty("App::PropertyPlacement","Placement","Base",QT_TRANSLATE_NOOP("App::Property","The placement of this object")) - obj.addProperty("App::PropertyDistance","StraightDistance","Base",QT_TRANSLATE_NOOP("App::Property","The length of the straight segment")) - obj.addProperty("App::PropertyVector","TargetPoint","Base",QT_TRANSLATE_NOOP("App::Property","The point indicated by this label")) - obj.addProperty("App::PropertyVectorList","Points","Base",QT_TRANSLATE_NOOP("App::Property","The points defining the label polyline")) - obj.addProperty("App::PropertyEnumeration","StraightDirection","Base",QT_TRANSLATE_NOOP("App::Property","The direction of the straight segment")) - obj.addProperty("App::PropertyEnumeration","LabelType","Base",QT_TRANSLATE_NOOP("App::Property","The type of information shown by this label")) - obj.addProperty("App::PropertyLinkSub","Target","Base",QT_TRANSLATE_NOOP("App::Property","The target object of this label")) - obj.addProperty("App::PropertyStringList","CustomText","Base",QT_TRANSLATE_NOOP("App::Property","The text to display when type is set to custom")) - obj.addProperty("App::PropertyStringList","Text","Base",QT_TRANSLATE_NOOP("App::Property","The text displayed by this label")) - self.Type = "Label" - obj.StraightDirection = ["Horizontal","Vertical","Custom"] - obj.LabelType = ["Custom","Name","Label","Position","Length","Area","Volume","Tag","Material"] - obj.setEditorMode("Text",1) - obj.StraightDistance = 1 - obj.TargetPoint = Vector(2,-1,0) - obj.CustomText = "Label" - - def execute(self,obj): - if obj.StraightDirection != "Custom": - p1 = obj.Placement.Base - if obj.StraightDirection == "Horizontal": - p2 = Vector(obj.StraightDistance.Value,0,0) - else: - p2 = Vector(0,obj.StraightDistance.Value,0) - p2 = obj.Placement.multVec(p2) - # p3 = obj.Placement.multVec(obj.TargetPoint) - p3 = obj.TargetPoint - obj.Points = [p1,p2,p3] - if obj.LabelType == "Custom": - if obj.CustomText: - obj.Text = obj.CustomText - elif obj.Target and obj.Target[0]: - if obj.LabelType == "Name": - obj.Text = [obj.Target[0].Name] - elif obj.LabelType == "Label": - obj.Text = [obj.Target[0].Label] - elif obj.LabelType == "Tag": - if hasattr(obj.Target[0],"Tag"): - obj.Text = [obj.Target[0].Tag] - elif obj.LabelType == "Material": - if hasattr(obj.Target[0],"Material"): - if hasattr(obj.Target[0].Material,"Label"): - obj.Text = [obj.Target[0].Material.Label] - elif obj.LabelType == "Position": - p = obj.Target[0].Placement.Base - if obj.Target[1]: - if "Vertex" in obj.Target[1][0]: - p = obj.Target[0].Shape.Vertexes[int(obj.Target[1][0][6:])-1].Point - obj.Text = [FreeCAD.Units.Quantity(x,FreeCAD.Units.Length).UserString for x in tuple(p)] - elif obj.LabelType == "Length": - if hasattr(obj.Target[0],'Shape'): - if hasattr(obj.Target[0].Shape,"Length"): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Length,FreeCAD.Units.Length).UserString] - if obj.Target[1] and ("Edge" in obj.Target[1][0]): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Edges[int(obj.Target[1][0][4:])-1].Length,FreeCAD.Units.Length).UserString] - elif obj.LabelType == "Area": - if hasattr(obj.Target[0],'Shape'): - if hasattr(obj.Target[0].Shape,"Area"): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Area,FreeCAD.Units.Area).UserString.replace("^2","²")] - if obj.Target[1] and ("Face" in obj.Target[1][0]): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Faces[int(obj.Target[1][0][4:])-1].Area,FreeCAD.Units.Area).UserString] - elif obj.LabelType == "Volume": - if hasattr(obj.Target[0],'Shape'): - if hasattr(obj.Target[0].Shape,"Volume"): - obj.Text = [FreeCAD.Units.Quantity(obj.Target[0].Shape.Volume,FreeCAD.Units.Volume).UserString.replace("^3","³")] - - def onChanged(self,obj,prop): - pass - - def __getstate__(self): - return self.Type - - def __setstate__(self,state): - if state: - self.Type = state - - -class ViewProviderDraftLabel: - """A View Provider for the Draft Label""" - - def __init__(self,vobj): - # Annotation properties - vobj.addProperty("App::PropertyFloat","ScaleMultiplier", - "Annotation",QT_TRANSLATE_NOOP("App::Property", - "Dimension size overall multiplier")) - # Text properties - vobj.addProperty("App::PropertyLength","TextSize", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The size of the text")) - vobj.addProperty("App::PropertyFont","TextFont", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The font of the text")) - vobj.addProperty("App::PropertyEnumeration","TextAlignment", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The vertical alignment of the text")) - vobj.addProperty("App::PropertyColor","TextColor", - "Text",QT_TRANSLATE_NOOP("App::Property", - "Text color")) - vobj.addProperty("App::PropertyInteger","MaxChars", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The maximum number of characters on each line of the text box")) - # Graphics properties - vobj.addProperty("App::PropertyLength","ArrowSize", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "The size of the arrow")) - vobj.addProperty("App::PropertyEnumeration","ArrowType", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "The type of arrow of this label")) - vobj.addProperty("App::PropertyEnumeration","Frame", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "The type of frame around the text of this object")) - vobj.addProperty("App::PropertyBool","Line", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Display a leader line or not")) - vobj.addProperty("App::PropertyFloat","LineWidth", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Line width")) - vobj.addProperty("App::PropertyColor","LineColor", - "Graphics",QT_TRANSLATE_NOOP("App::Property", - "Line color") - ) - - vobj.Proxy = self - self.Object = vobj.Object - vobj.TextAlignment = ["Top","Middle","Bottom"] - vobj.TextAlignment = "Middle" - vobj.LineWidth = getParam("linewidth",1) - vobj.TextFont = getParam("textfont") - vobj.TextSize = getParam("textheight",1) - vobj.ArrowSize = getParam("arrowsize",1) - vobj.ArrowType = arrowtypes - vobj.ArrowType = arrowtypes[getParam("dimsymbol")] - vobj.Frame = ["None","Rectangle"] - vobj.Line = True - param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") - annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) - vobj.ScaleMultiplier = 1 / annotation_scale - - - def getIcon(self): - import Draft_rc - return ":/icons/Draft_Label.svg" - - def claimChildren(self): - return [] - - def attach(self,vobj): - from pivy import coin - self.arrow = coin.SoSeparator() - self.arrowpos = coin.SoTransform() - self.arrow.addChild(self.arrowpos) - self.matline = coin.SoMaterial() - self.drawstyle = coin.SoDrawStyle() - self.drawstyle.style = coin.SoDrawStyle.LINES - self.lcoords = coin.SoCoordinate3() - self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance() - self.mattext = coin.SoMaterial() - textdrawstyle = coin.SoDrawStyle() - textdrawstyle.style = coin.SoDrawStyle.FILLED - self.textpos = coin.SoTransform() - self.font = coin.SoFont() - self.text2d = coin.SoText2() - self.text3d = coin.SoAsciiText() - self.text2d.string = self.text3d.string = "Label" # need to init with something, otherwise, crash! - self.text2d.justification = coin.SoText2.RIGHT - self.text3d.justification = coin.SoAsciiText.RIGHT - self.fcoords = coin.SoCoordinate3() - self.frame = coin.SoType.fromName("SoBrepEdgeSet").createInstance() - self.lineswitch = coin.SoSwitch() - switchnode = coin.SoSeparator() - switchnode.addChild(self.line) - switchnode.addChild(self.arrow) - self.lineswitch.addChild(switchnode) - self.lineswitch.whichChild = 0 - self.node2d = coin.SoGroup() - self.node2d.addChild(self.matline) - self.node2d.addChild(self.arrow) - self.node2d.addChild(self.drawstyle) - self.node2d.addChild(self.lcoords) - self.node2d.addChild(self.lineswitch) - self.node2d.addChild(self.mattext) - self.node2d.addChild(textdrawstyle) - self.node2d.addChild(self.textpos) - self.node2d.addChild(self.font) - self.node2d.addChild(self.text2d) - self.node2d.addChild(self.fcoords) - self.node2d.addChild(self.frame) - self.node3d = coin.SoGroup() - self.node3d.addChild(self.matline) - self.node3d.addChild(self.arrow) - self.node3d.addChild(self.drawstyle) - self.node3d.addChild(self.lcoords) - self.node3d.addChild(self.lineswitch) - self.node3d.addChild(self.mattext) - self.node3d.addChild(textdrawstyle) - self.node3d.addChild(self.textpos) - self.node3d.addChild(self.font) - self.node3d.addChild(self.text3d) - self.node3d.addChild(self.fcoords) - self.node3d.addChild(self.frame) - vobj.addDisplayMode(self.node2d,"2D text") - vobj.addDisplayMode(self.node3d,"3D text") - self.onChanged(vobj,"LineColor") - self.onChanged(vobj,"TextColor") - self.onChanged(vobj,"ArrowSize") - self.onChanged(vobj,"Line") - - def getDisplayModes(self,vobj): - return ["2D text","3D text"] - - def getDefaultDisplayMode(self): - return "3D text" - - def setDisplayMode(self,mode): - return mode - - def updateData(self,obj,prop): - if prop == "Points": - from pivy import coin - if len(obj.Points) >= 2: - self.line.coordIndex.deleteValues(0) - self.lcoords.point.setValues(obj.Points) - self.line.coordIndex.setValues(0,len(obj.Points),range(len(obj.Points))) - self.onChanged(obj.ViewObject,"TextSize") - self.onChanged(obj.ViewObject,"ArrowType") - if obj.StraightDistance > 0: - self.text2d.justification = coin.SoText2.RIGHT - self.text3d.justification = coin.SoAsciiText.RIGHT - else: - self.text2d.justification = coin.SoText2.LEFT - self.text3d.justification = coin.SoAsciiText.LEFT - elif prop == "Text": - if obj.Text: - if sys.version_info.major >= 3: - self.text2d.string.setValues([l for l in obj.Text if l]) - self.text3d.string.setValues([l for l in obj.Text if l]) - else: - self.text2d.string.setValues([l.encode("utf8") for l in obj.Text if l]) - self.text3d.string.setValues([l.encode("utf8") for l in obj.Text if l]) - self.onChanged(obj.ViewObject,"TextAlignment") - - def getTextSize(self,vobj): - from pivy import coin - if vobj.DisplayMode == "3D text": - text = self.text3d - else: - text = self.text2d - v = FreeCADGui.ActiveDocument.ActiveView.getViewer().getSoRenderManager().getViewportRegion() - b = coin.SoGetBoundingBoxAction(v) - text.getBoundingBox(b) - return b.getBoundingBox().getSize().getValue() - - def onChanged(self,vobj,prop): - if prop == "ScaleMultiplier": - if not hasattr(vobj,"ScaleMultiplier"): - return - if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): - self.update_label(vobj) - if hasattr(vobj,"ArrowSize"): - s = vobj.ArrowSize.Value * vobj.ScaleMultiplier - if s: - self.arrowpos.scaleFactor.setValue((s,s,s)) - elif prop == "LineColor": - if hasattr(vobj,"LineColor"): - l = vobj.LineColor - self.matline.diffuseColor.setValue([l[0],l[1],l[2]]) - elif prop == "TextColor": - if hasattr(vobj,"TextColor"): - l = vobj.TextColor - self.mattext.diffuseColor.setValue([l[0],l[1],l[2]]) - elif prop == "LineWidth": - if hasattr(vobj,"LineWidth"): - self.drawstyle.lineWidth = vobj.LineWidth - elif (prop == "TextFont"): - if hasattr(vobj,"TextFont"): - self.font.name = vobj.TextFont.encode("utf8") - elif prop in ["TextSize","TextAlignment"] and hasattr(vobj,"ScaleMultiplier"): - if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): - self.update_label(vobj) - elif prop == "Line": - if hasattr(vobj,"Line"): - if vobj.Line: - self.lineswitch.whichChild = 0 - else: - self.lineswitch.whichChild = -1 - elif prop == "ArrowType": - if hasattr(vobj,"ArrowType"): - if len(vobj.Object.Points) > 1: - if hasattr(self,"symbol"): - if self.arrow.findChild(self.symbol) != -1: - self.arrow.removeChild(self.symbol) - s = arrowtypes.index(vobj.ArrowType) - self.symbol = dimSymbol(s) - self.arrow.addChild(self.symbol) - self.arrowpos.translation.setValue(vobj.Object.Points[-1]) - v1 = vobj.Object.Points[-2].sub(vobj.Object.Points[-1]) - if not DraftVecUtils.isNull(v1): - v1.normalize() - v2 = Vector(0,0,1) - if round(v2.getAngle(v1),4) in [0,round(math.pi,4)]: - v2 = Vector(0,1,0) - v3 = v1.cross(v2).negative() - q = FreeCAD.Placement(DraftVecUtils.getPlaneRotation(v1,v3,v2)).Rotation.Q - self.arrowpos.rotation.setValue((q[0],q[1],q[2],q[3])) - elif prop == "ArrowSize": - if hasattr(vobj,"ArrowSize") and hasattr(vobj,"ScaleMultiplier"): - s = vobj.ArrowSize.Value * vobj.ScaleMultiplier - if s: - self.arrowpos.scaleFactor.setValue((s,s,s)) - elif prop == "Frame": - if hasattr(vobj,"Frame"): - self.frame.coordIndex.deleteValues(0) - if vobj.Frame == "Rectangle": - tsize = self.getTextSize(vobj) - pts = [] - base = vobj.Object.Placement.Base.sub(Vector(self.textpos.translation.getValue().getValue())) - pts.append(base.add(Vector(0,tsize[1]*3,0))) - pts.append(pts[-1].add(Vector(-tsize[0]*6,0,0))) - pts.append(pts[-1].add(Vector(0,-tsize[1]*6,0))) - pts.append(pts[-1].add(Vector(tsize[0]*6,0,0))) - pts.append(pts[0]) - self.fcoords.point.setValues(pts) - self.frame.coordIndex.setValues(0,len(pts),range(len(pts))) - - - def update_label(self, vobj): - self.font.size = vobj.TextSize.Value * vobj.ScaleMultiplier - v = Vector(1,0,0) - if vobj.Object.StraightDistance > 0: - v = v.negative() - v.multiply(vobj.TextSize/10) - tsize = self.getTextSize(vobj) - if (tsize is not None) and (len(vobj.Object.Text) > 1): - v = v.add(Vector(0,(tsize[1]-1)*2,0)) - if vobj.TextAlignment == "Top": - v = v.add(Vector(0,-tsize[1]*2,0)) - elif vobj.TextAlignment == "Middle": - v = v.add(Vector(0,-tsize[1],0)) - v = vobj.Object.Placement.Rotation.multVec(v) - pos = vobj.Object.Placement.Base.add(v) - self.textpos.translation.setValue(pos) - self.textpos.rotation.setValue(vobj.Object.Placement.Rotation.Q) - - def __getstate__(self): - return None - - def __setstate__(self,state): - return None - - -class DraftText: - """The Draft Text object""" - - def __init__(self,obj): - obj.Proxy = self - obj.addProperty("App::PropertyPlacement","Placement","Base",QT_TRANSLATE_NOOP("App::Property","The placement of this object")) - obj.addProperty("App::PropertyStringList","Text","Base",QT_TRANSLATE_NOOP("App::Property","The text displayed by this object")) - self.Type = "DraftText" - - def execute(self,obj): - pass - - -class ViewProviderDraftText: - """A View Provider for the Draft Label""" - - def __init__(self,vobj): - vobj.addProperty("App::PropertyFloat","ScaleMultiplier", - "Annotation",QT_TRANSLATE_NOOP("App::Property", - "Dimension size overall multiplier")) - vobj.addProperty("App::PropertyLength","FontSize", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The size of the text")) - vobj.addProperty("App::PropertyFont","FontName", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The font of the text")) - vobj.addProperty("App::PropertyEnumeration","Justification", - "Text",QT_TRANSLATE_NOOP("App::Property", - "The vertical alignment of the text")) - vobj.addProperty("App::PropertyColor","TextColor", - "Text",QT_TRANSLATE_NOOP("App::Property", - "Text color")) - vobj.addProperty("App::PropertyFloat","LineSpacing", - "Text",QT_TRANSLATE_NOOP("App::Property", - "Line spacing (relative to font size)")) - vobj.Proxy = self - self.Object = vobj.Object - - param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") - annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) - vobj.ScaleMultiplier = 1 / annotation_scale - - vobj.Justification = ["Left","Center","Right"] - vobj.FontName = getParam("textfont","sans") - vobj.FontSize = getParam("textheight",1) - - def getIcon(self): - import Draft_rc - return ":/icons/Draft_Text.svg" - - def claimChildren(self): - return [] - - def attach(self,vobj): - from pivy import coin - self.mattext = coin.SoMaterial() - textdrawstyle = coin.SoDrawStyle() - textdrawstyle.style = coin.SoDrawStyle.FILLED - self.trans = coin.SoTransform() - self.font = coin.SoFont() - self.text2d = coin.SoAsciiText() - self.text3d = coin.SoText2() - self.text2d.string = self.text3d.string = "Label" # need to init with something, otherwise, crash! - self.text2d.justification = coin.SoAsciiText.LEFT - self.text3d.justification = coin.SoText2.LEFT - self.node2d = coin.SoGroup() - self.node2d.addChild(self.trans) - self.node2d.addChild(self.mattext) - self.node2d.addChild(textdrawstyle) - self.node2d.addChild(self.font) - self.node2d.addChild(self.text2d) - self.node3d = coin.SoGroup() - self.node3d.addChild(self.trans) - self.node3d.addChild(self.mattext) - self.node3d.addChild(textdrawstyle) - self.node3d.addChild(self.font) - self.node3d.addChild(self.text3d) - vobj.addDisplayMode(self.node2d,"2D text") - vobj.addDisplayMode(self.node3d,"3D text") - self.onChanged(vobj,"TextColor") - self.onChanged(vobj,"FontSize") - self.onChanged(vobj,"FontName") - self.onChanged(vobj,"Justification") - self.onChanged(vobj,"LineSpacing") - - def getDisplayModes(self,vobj): - return ["2D text","3D text"] - - def setDisplayMode(self,mode): - return mode - - def updateData(self,obj,prop): - if prop == "Text": - if obj.Text: - if sys.version_info.major >= 3: - self.text2d.string.setValues([l for l in obj.Text if l]) - self.text3d.string.setValues([l for l in obj.Text if l]) - else: - self.text2d.string.setValues([l.encode("utf8") for l in obj.Text if l]) - self.text3d.string.setValues([l.encode("utf8") for l in obj.Text if l]) - elif prop == "Placement": - self.trans.translation.setValue(obj.Placement.Base) - self.trans.rotation.setValue(obj.Placement.Rotation.Q) - - def onChanged(self,vobj,prop): - if prop == "ScaleMultiplier": - if "ScaleMultiplier" in vobj.PropertiesList: - self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier - elif prop == "TextColor": - if "TextColor" in vobj.PropertiesList: - l = vobj.TextColor - self.mattext.diffuseColor.setValue([l[0],l[1],l[2]]) - elif (prop == "FontName"): - if "FontName" in vobj.PropertiesList: - self.font.name = vobj.FontName.encode("utf8") - elif prop == "FontSize": - if "FontSize" in vobj.PropertiesList and "ScaleMultiplier" in vobj.PropertiesList: - self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier - elif prop == "Justification": - from pivy import coin - try: - if getattr(vobj, "Justification", None) is not None: - if vobj.Justification == "Left": - self.text2d.justification = coin.SoAsciiText.LEFT - self.text3d.justification = coin.SoText2.LEFT - elif vobj.Justification == "Right": - self.text2d.justification = coin.SoAsciiText.RIGHT - self.text3d.justification = coin.SoText2.RIGHT - else: - self.text2d.justification = coin.SoAsciiText.CENTER - self.text3d.justification = coin.SoText2.CENTER - except AssertionError: - pass # Race condition - Justification enum has not been set yet - elif prop == "LineSpacing": - if "LineSpacing" in vobj.PropertiesList: - self.text2d.spacing = vobj.LineSpacing - self.text3d.spacing = vobj.LineSpacing - - def __getstate__(self): - return None - - def __setstate__(self,state): - return None - - -def convertDraftTexts(textslist=[]): - """converts the given Draft texts (or all that are found in the active document) to the new object""" - if not isinstance(textslist,list): - textslist = [textslist] - if not textslist: - for o in FreeCAD.ActiveDocument.Objects: - if o.TypeId == "App::Annotation": - textslist.append(o) - todelete = [] - for o in textslist: - l = o.Label - o.Label = l+".old" - obj = makeText(o.LabelText,point=o.Position) - obj.Label = l - todelete.append(o.Name) - for p in o.InList: - if p.isDerivedFrom("App::DocumentObjectGroup"): - if o in p.Group: - g = p.Group - g.append(obj) - p.Group = g - for n in todelete: - FreeCAD.ActiveDocument.removeObject(n) - ## @} diff --git a/src/Mod/Draft/draftobjects/label.py b/src/Mod/Draft/draftobjects/label.py new file mode 100644 index 0000000000..9180f24d73 --- /dev/null +++ b/src/Mod/Draft/draftobjects/label.py @@ -0,0 +1,231 @@ +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * 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 * +# * * +# *************************************************************************** + +"""This module provides the object code for Draft Label. +""" +## @package label +# \ingroup DRAFT +# \brief This module provides the object code for Draft Label. + +import FreeCAD as App +import math +from PySide.QtCore import QT_TRANSLATE_NOOP +import DraftGeomUtils +import draftutils.gui_utils as gui_utils +import draftutils.utils as utils +from draftobjects.draft_annotation import DraftAnnotation +from draftviewproviders.view_label import ViewProviderLabel + + + +def make_label(targetpoint=None, target=None, direction=None, + distance=None, labeltype=None, placement=None): + """ + make_label(targetpoint, target, direction, distance, labeltype, placement) + + Function to create a Draft Label annotation object + + Parameters + ---------- + targetpoint : App::Vector + To be completed + + target : LinkSub + To be completed + + direction : String + Straight direction of the label + ["Horizontal","Vertical","Custom"] + + distance : Quantity + Lenght of the straight segment of label leader line + + labeltype : String + Label type in + ["Custom","Name","Label","Position", + "Length","Area","Volume","Tag","Material"] + + placement : Base::Placement + To be completed + + Returns + ------- + obj : App::DocumentObject + Newly created label object + """ + obj = App.ActiveDocument.addObject("App::FeaturePython", + "dLabel") + Label(obj) + if App.GuiUp: + ViewProviderLabel(obj.ViewObject) + if targetpoint: + obj.TargetPoint = targetpoint + if target: + obj.Target = target + if direction: + obj.StraightDirection = direction + if distance: + obj.StraightDistance = distance + if labeltype: + obj.LabelType = labeltype + if placement: + obj.Placement = placement + + if App.GuiUp: + gui_utils.format_object(obj) + gui_utils.select(obj) + + return obj + + + +class Label(DraftAnnotation): + """The Draft Label object""" + + def __init__(self,obj): + + super().__init__(obj, "Label") + + obj.addProperty("App::PropertyPlacement", + "Placement", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The placement of this object")) + + obj.addProperty("App::PropertyDistance", + "StraightDistance", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The length of the straight segment")) + + obj.addProperty("App::PropertyVector", + "TargetPoint", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The point indicated by this label")) + + obj.addProperty("App::PropertyVectorList", + "Points", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The points defining the label polyline")) + + obj.addProperty("App::PropertyEnumeration", + "StraightDirection", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The direction of the straight segment")) + + obj.addProperty("App::PropertyEnumeration", + "LabelType", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The type of information shown by this label")) + + obj.addProperty("App::PropertyLinkSub", + "Target", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The target object of this label")) + + obj.addProperty("App::PropertyStringList", + "CustomText", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The text to display when type is set to custom")) + + obj.addProperty("App::PropertyStringList", + "Text", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The text displayed by this label")) + + obj.StraightDirection = ["Horizontal","Vertical","Custom"] + obj.LabelType = ["Custom","Name","Label","Position", + "Length","Area","Volume","Tag","Material"] + obj.setEditorMode("Text",1) + obj.StraightDistance = 1 + obj.TargetPoint = App.Vector(2,-1,0) + obj.CustomText = "Label" + + + def execute(self,obj): + + if obj.StraightDirection != "Custom": + p1 = obj.Placement.Base + if obj.StraightDirection == "Horizontal": + p2 = App.Vector(obj.StraightDistance.Value,0,0) + else: + p2 = App.Vector(0,obj.StraightDistance.Value,0) + p2 = obj.Placement.multVec(p2) + # p3 = obj.Placement.multVec(obj.TargetPoint) + p3 = obj.TargetPoint + obj.Points = [p1,p2,p3] + if obj.LabelType == "Custom": + if obj.CustomText: + obj.Text = obj.CustomText + elif obj.Target and obj.Target[0]: + if obj.LabelType == "Name": + obj.Text = [obj.Target[0].Name] + elif obj.LabelType == "Label": + obj.Text = [obj.Target[0].Label] + elif obj.LabelType == "Tag": + if hasattr(obj.Target[0],"Tag"): + obj.Text = [obj.Target[0].Tag] + elif obj.LabelType == "Material": + if hasattr(obj.Target[0],"Material"): + if hasattr(obj.Target[0].Material,"Label"): + obj.Text = [obj.Target[0].Material.Label] + elif obj.LabelType == "Position": + p = obj.Target[0].Placement.Base + if obj.Target[1]: + if "Vertex" in obj.Target[1][0]: + p = obj.Target[0].Shape.Vertexes[int(obj.Target[1][0][6:])-1].Point + obj.Text = [App.Units.Quantity(x,App.Units.Length).UserString for x in tuple(p)] + elif obj.LabelType == "Length": + if hasattr(obj.Target[0],'Shape'): + if hasattr(obj.Target[0].Shape,"Length"): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Length,App.Units.Length).UserString] + if obj.Target[1] and ("Edge" in obj.Target[1][0]): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Edges[int(obj.Target[1][0][4:])-1].Length,App.Units.Length).UserString] + elif obj.LabelType == "Area": + if hasattr(obj.Target[0],'Shape'): + if hasattr(obj.Target[0].Shape,"Area"): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Area,App.Units.Area).UserString.replace("^2","²")] + if obj.Target[1] and ("Face" in obj.Target[1][0]): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Faces[int(obj.Target[1][0][4:])-1].Area,App.Units.Area).UserString] + elif obj.LabelType == "Volume": + if hasattr(obj.Target[0],'Shape'): + if hasattr(obj.Target[0].Shape,"Volume"): + obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Volume,App.Units.Volume).UserString.replace("^3","³")] + + def onChanged(self,obj,prop): + pass + + def __getstate__(self): + return self.Type + + def __setstate__(self,state): + if state: + self.Type = state \ No newline at end of file diff --git a/src/Mod/Draft/draftobjects/text.py b/src/Mod/Draft/draftobjects/text.py new file mode 100644 index 0000000000..a26cf3378a --- /dev/null +++ b/src/Mod/Draft/draftobjects/text.py @@ -0,0 +1,112 @@ +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * 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 * +# * * +# *************************************************************************** + +"""This module provides the object code for Draft Label. +""" +## @package label +# \ingroup DRAFT +# \brief This module provides the object code for Draft Label. + +import FreeCAD as App +import math +from PySide.QtCore import QT_TRANSLATE_NOOP +import DraftGeomUtils +import draftutils.gui_utils as gui_utils +import draftutils.utils as utils +from draftobjects.draft_annotation import DraftAnnotation +from draftviewproviders.view_text import ViewProviderText + + + + +def make_text(stringslist, point=App.Vector(0,0,0), screen=False): + """makeText(strings,[point],[screen]) + + Creates a Text object containing the given strings. + The current color and text height and font + specified in preferences are used. + + Parameters + ---------- + stringlist : List + Given list of strings, one string by line (strings can also + be one single string) + + point : App::Vector + + + screen : Bool + If screen is True, the text always faces the view direction. + + """ + + if not App.ActiveDocument: + App.Console.PrintError("No active document. Aborting\n") + return + utils.type_check([(point, App.Vector)], "makeText") + if not isinstance(stringslist,list): stringslist = [stringslist] + obj = App.ActiveDocument.addObject("App::FeaturePython","Text") + Text(obj) + obj.Text = stringslist + obj.Placement.Base = point + + if App.GuiUp: + ViewProviderText(obj.ViewObject) + if screen: + obj.ViewObject.DisplayMode = "3D text" + h = utils.get_param("textheight",0.20) + if screen: + h = h*10 + obj.ViewObject.FontSize = h + obj.ViewObject.FontName = utils.get_param("textfont","") + obj.ViewObject.LineSpacing = 1 + gui_utils.format_object(obj) + gui_utils.select(obj) + + return obj + + + +class Text(DraftAnnotation): + """The Draft Text object""" + + def __init__(self,obj): + + super().__init__(obj, "Text") + + obj.addProperty("App::PropertyPlacement", + "Placement", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The placement of this object")) + + obj.addProperty("App::PropertyStringList", + "Text", + "Base", + QT_TRANSLATE_NOOP("App::Property", + "The text displayed by this object")) + + def execute(self,obj): + + pass \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_label.py b/src/Mod/Draft/draftviewproviders/view_label.py new file mode 100644 index 0000000000..a4a2f611a1 --- /dev/null +++ b/src/Mod/Draft/draftviewproviders/view_label.py @@ -0,0 +1,320 @@ +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * 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 * +# * * +# *************************************************************************** +"""This module provides the Draft Dimensions view provider classes +""" +## @package polararray +# \ingroup DRAFT +# \brief This module provides the view provider code for Draft Dimensions. + + +import FreeCAD as App +import FreeCADGui as Gui +import DraftVecUtils, DraftGeomUtils +import math, sys +from pivy import coin +from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.utils as utils +import draftutils.gui_utils as gui_utils +from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation + +class ViewProviderLabel(ViewProviderDraftAnnotation): + """A View Provider for the Label annotation object""" + + def __init__(self,vobj): + + super().__init__(vobj) + + # Text properties + + vobj.addProperty("App::PropertyLength","TextSize", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The size of the text")) + + vobj.addProperty("App::PropertyFont","TextFont", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The font of the text")) + + vobj.addProperty("App::PropertyEnumeration","TextAlignment", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The vertical alignment of the text")) + + vobj.addProperty("App::PropertyColor","TextColor", + "Text",QT_TRANSLATE_NOOP("App::Property", + "Text color")) + + vobj.addProperty("App::PropertyInteger","MaxChars", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The maximum number of characters on each line of the text box")) + + # Graphics properties + + vobj.addProperty("App::PropertyLength","ArrowSize", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The size of the arrow")) + + vobj.addProperty("App::PropertyEnumeration","ArrowType", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The type of arrow of this label")) + + vobj.addProperty("App::PropertyEnumeration","Frame", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "The type of frame around the text of this object")) + + vobj.addProperty("App::PropertyBool","Line", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Display a leader line or not")) + + vobj.addProperty("App::PropertyFloat","LineWidth", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Line width")) + + vobj.addProperty("App::PropertyColor","LineColor", + "Graphics",QT_TRANSLATE_NOOP("App::Property", + "Line color")) + + vobj.Proxy = self + self.Object = vobj.Object + vobj.TextAlignment = ["Top","Middle","Bottom"] + vobj.TextAlignment = "Middle" + vobj.LineWidth = utils.get_param("linewidth",1) + vobj.TextFont = utils.get_param("textfont") + vobj.TextSize = utils.get_param("textheight",1) + vobj.ArrowSize = utils.get_param("arrowsize",1) + vobj.ArrowType = utils.ARROW_TYPES + vobj.ArrowType = utils.ARROW_TYPES[utils.get_param("dimsymbol")] + vobj.Frame = ["None","Rectangle"] + vobj.Line = True + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + vobj.ScaleMultiplier = 1 / annotation_scale + + + def getIcon(self): + import Draft_rc + return ":/icons/Draft_Label.svg" + + def claimChildren(self): + return [] + + def attach(self,vobj): + self.arrow = coin.SoSeparator() + self.arrowpos = coin.SoTransform() + self.arrow.addChild(self.arrowpos) + self.matline = coin.SoMaterial() + self.drawstyle = coin.SoDrawStyle() + self.drawstyle.style = coin.SoDrawStyle.LINES + self.lcoords = coin.SoCoordinate3() + self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance() + self.mattext = coin.SoMaterial() + textdrawstyle = coin.SoDrawStyle() + textdrawstyle.style = coin.SoDrawStyle.FILLED + self.textpos = coin.SoTransform() + self.font = coin.SoFont() + self.text2d = coin.SoText2() + self.text3d = coin.SoAsciiText() + self.text2d.string = self.text3d.string = "Label" # need to init with something, otherwise, crash! + self.text2d.justification = coin.SoText2.RIGHT + self.text3d.justification = coin.SoAsciiText.RIGHT + self.fcoords = coin.SoCoordinate3() + self.frame = coin.SoType.fromName("SoBrepEdgeSet").createInstance() + self.lineswitch = coin.SoSwitch() + switchnode = coin.SoSeparator() + switchnode.addChild(self.line) + switchnode.addChild(self.arrow) + self.lineswitch.addChild(switchnode) + self.lineswitch.whichChild = 0 + self.node2d = coin.SoGroup() + self.node2d.addChild(self.matline) + self.node2d.addChild(self.arrow) + self.node2d.addChild(self.drawstyle) + self.node2d.addChild(self.lcoords) + self.node2d.addChild(self.lineswitch) + self.node2d.addChild(self.mattext) + self.node2d.addChild(textdrawstyle) + self.node2d.addChild(self.textpos) + self.node2d.addChild(self.font) + self.node2d.addChild(self.text2d) + self.node2d.addChild(self.fcoords) + self.node2d.addChild(self.frame) + self.node3d = coin.SoGroup() + self.node3d.addChild(self.matline) + self.node3d.addChild(self.arrow) + self.node3d.addChild(self.drawstyle) + self.node3d.addChild(self.lcoords) + self.node3d.addChild(self.lineswitch) + self.node3d.addChild(self.mattext) + self.node3d.addChild(textdrawstyle) + self.node3d.addChild(self.textpos) + self.node3d.addChild(self.font) + self.node3d.addChild(self.text3d) + self.node3d.addChild(self.fcoords) + self.node3d.addChild(self.frame) + vobj.addDisplayMode(self.node2d,"2D text") + vobj.addDisplayMode(self.node3d,"3D text") + self.onChanged(vobj,"LineColor") + self.onChanged(vobj,"TextColor") + self.onChanged(vobj,"ArrowSize") + self.onChanged(vobj,"Line") + + def getDisplayModes(self,vobj): + return ["2D text","3D text"] + + def getDefaultDisplayMode(self): + return "3D text" + + def setDisplayMode(self,mode): + return mode + + def updateData(self,obj,prop): + if prop == "Points": + from pivy import coin + if len(obj.Points) >= 2: + self.line.coordIndex.deleteValues(0) + self.lcoords.point.setValues(obj.Points) + self.line.coordIndex.setValues(0,len(obj.Points),range(len(obj.Points))) + self.onChanged(obj.ViewObject,"TextSize") + self.onChanged(obj.ViewObject,"ArrowType") + if obj.StraightDistance > 0: + self.text2d.justification = coin.SoText2.RIGHT + self.text3d.justification = coin.SoAsciiText.RIGHT + else: + self.text2d.justification = coin.SoText2.LEFT + self.text3d.justification = coin.SoAsciiText.LEFT + elif prop == "Text": + if obj.Text: + if sys.version_info.major >= 3: + self.text2d.string.setValues([l for l in obj.Text if l]) + self.text3d.string.setValues([l for l in obj.Text if l]) + else: + self.text2d.string.setValues([l.encode("utf8") for l in obj.Text if l]) + self.text3d.string.setValues([l.encode("utf8") for l in obj.Text if l]) + self.onChanged(obj.ViewObject,"TextAlignment") + + def getTextSize(self,vobj): + from pivy import coin + if vobj.DisplayMode == "3D text": + text = self.text3d + else: + text = self.text2d + v = Gui.ActiveDocument.ActiveView.getViewer().getSoRenderManager().getViewportRegion() + b = coin.SoGetBoundingBoxAction(v) + text.getBoundingBox(b) + return b.getBoundingBox().getSize().getValue() + + def onChanged(self,vobj,prop): + if prop == "ScaleMultiplier": + if not hasattr(vobj,"ScaleMultiplier"): + return + if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): + self.update_label(vobj) + if hasattr(vobj,"ArrowSize"): + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + if s: + self.arrowpos.scaleFactor.setValue((s,s,s)) + elif prop == "LineColor": + if hasattr(vobj,"LineColor"): + l = vobj.LineColor + self.matline.diffuseColor.setValue([l[0],l[1],l[2]]) + elif prop == "TextColor": + if hasattr(vobj,"TextColor"): + l = vobj.TextColor + self.mattext.diffuseColor.setValue([l[0],l[1],l[2]]) + elif prop == "LineWidth": + if hasattr(vobj,"LineWidth"): + self.drawstyle.lineWidth = vobj.LineWidth + elif (prop == "TextFont"): + if hasattr(vobj,"TextFont"): + self.font.name = vobj.TextFont.encode("utf8") + elif prop in ["TextSize","TextAlignment"] and hasattr(vobj,"ScaleMultiplier"): + if hasattr(vobj,"TextSize") and hasattr(vobj,"TextAlignment"): + self.update_label(vobj) + elif prop == "Line": + if hasattr(vobj,"Line"): + if vobj.Line: + self.lineswitch.whichChild = 0 + else: + self.lineswitch.whichChild = -1 + elif prop == "ArrowType": + if hasattr(vobj,"ArrowType"): + if len(vobj.Object.Points) > 1: + if hasattr(self,"symbol"): + if self.arrow.findChild(self.symbol) != -1: + self.arrow.removeChild(self.symbol) + s = utils.ARROW_TYPES.index(vobj.ArrowType) + self.symbol = gui_utils.dim_symbol(s) + self.arrow.addChild(self.symbol) + self.arrowpos.translation.setValue(vobj.Object.Points[-1]) + v1 = vobj.Object.Points[-2].sub(vobj.Object.Points[-1]) + if not DraftVecUtils.isNull(v1): + v1.normalize() + v2 = App.Vector(0,0,1) + if round(v2.getAngle(v1),4) in [0,round(math.pi,4)]: + v2 = App.Vector(0,1,0) + v3 = v1.cross(v2).negative() + q = App.Placement(DraftVecUtils.getPlaneRotation(v1,v3,v2)).Rotation.Q + self.arrowpos.rotation.setValue((q[0],q[1],q[2],q[3])) + elif prop == "ArrowSize": + if hasattr(vobj,"ArrowSize") and hasattr(vobj,"ScaleMultiplier"): + s = vobj.ArrowSize.Value * vobj.ScaleMultiplier + if s: + self.arrowpos.scaleFactor.setValue((s,s,s)) + elif prop == "Frame": + if hasattr(vobj,"Frame"): + self.frame.coordIndex.deleteValues(0) + if vobj.Frame == "Rectangle": + tsize = self.getTextSize(vobj) + pts = [] + base = vobj.Object.Placement.Base.sub(App.Vector(self.textpos.translation.getValue().getValue())) + pts.append(base.add(App.Vector(0,tsize[1]*3,0))) + pts.append(pts[-1].add(App.Vector(-tsize[0]*6,0,0))) + pts.append(pts[-1].add(App.Vector(0,-tsize[1]*6,0))) + pts.append(pts[-1].add(App.Vector(tsize[0]*6,0,0))) + pts.append(pts[0]) + self.fcoords.point.setValues(pts) + self.frame.coordIndex.setValues(0,len(pts),range(len(pts))) + + + def update_label(self, vobj): + self.font.size = vobj.TextSize.Value * vobj.ScaleMultiplier + v = App.Vector(1,0,0) + if vobj.Object.StraightDistance > 0: + v = v.negative() + v.multiply(vobj.TextSize/10) + tsize = self.getTextSize(vobj) + if (tsize is not None) and (len(vobj.Object.Text) > 1): + v = v.add(App.Vector(0,(tsize[1]-1)*2,0)) + if vobj.TextAlignment == "Top": + v = v.add(App.Vector(0,-tsize[1]*2,0)) + elif vobj.TextAlignment == "Middle": + v = v.add(App.Vector(0,-tsize[1],0)) + v = vobj.Object.Placement.Rotation.multVec(v) + pos = vobj.Object.Placement.Base.add(v) + self.textpos.translation.setValue(pos) + self.textpos.rotation.setValue(vobj.Object.Placement.Rotation.Q) + + def __getstate__(self): + return None + + def __setstate__(self,state): + return None \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_text.py b/src/Mod/Draft/draftviewproviders/view_text.py new file mode 100644 index 0000000000..15622be54e --- /dev/null +++ b/src/Mod/Draft/draftviewproviders/view_text.py @@ -0,0 +1,170 @@ +# *************************************************************************** +# * Copyright (c) 2009, 2010 Yorik van Havre * +# * Copyright (c) 2009, 2010 Ken Cline * +# * * +# * 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 * +# * * +# *************************************************************************** +"""This module provides the Draft Dimensions view provider classes +""" +## @package polararray +# \ingroup DRAFT +# \brief This module provides the view provider code for Draft Dimensions. + + +import FreeCAD as App +import FreeCADGui as Gui +import DraftVecUtils, DraftGeomUtils +import math, sys +from pivy import coin +from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.utils as utils +import draftutils.gui_utils as gui_utils +from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation + + +class ViewProviderText(ViewProviderDraftAnnotation): + """A View Provider for the Draft Label""" + + def __init__(self,vobj): + + super().__init__(vobj) + + vobj.addProperty("App::PropertyFloat","ScaleMultiplier", + "Annotation",QT_TRANSLATE_NOOP("App::Property", + "Dimension size overall multiplier")) + + vobj.addProperty("App::PropertyLength","FontSize", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The size of the text")) + vobj.addProperty("App::PropertyFont","FontName", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The font of the text")) + vobj.addProperty("App::PropertyEnumeration","Justification", + "Text",QT_TRANSLATE_NOOP("App::Property", + "The vertical alignment of the text")) + vobj.addProperty("App::PropertyColor","TextColor", + "Text",QT_TRANSLATE_NOOP("App::Property", + "Text color")) + vobj.addProperty("App::PropertyFloat","LineSpacing", + "Text",QT_TRANSLATE_NOOP("App::Property", + "Line spacing (relative to font size)")) + + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) + vobj.ScaleMultiplier = 1 / annotation_scale + + vobj.Justification = ["Left","Center","Right"] + vobj.FontName = utils.get_param("textfont","sans") + vobj.FontSize = utils.get_param("textheight",1) + + def getIcon(self): + return ":/icons/Draft_Text.svg" + + def claimChildren(self): + return [] + + def attach(self,vobj): + self.mattext = coin.SoMaterial() + textdrawstyle = coin.SoDrawStyle() + textdrawstyle.style = coin.SoDrawStyle.FILLED + self.trans = coin.SoTransform() + self.font = coin.SoFont() + self.text2d = coin.SoAsciiText() + self.text3d = coin.SoText2() + self.text2d.string = self.text3d.string = "Label" # need to init with something, otherwise, crash! + self.text2d.justification = coin.SoAsciiText.LEFT + self.text3d.justification = coin.SoText2.LEFT + self.node2d = coin.SoGroup() + self.node2d.addChild(self.trans) + self.node2d.addChild(self.mattext) + self.node2d.addChild(textdrawstyle) + self.node2d.addChild(self.font) + self.node2d.addChild(self.text2d) + self.node3d = coin.SoGroup() + self.node3d.addChild(self.trans) + self.node3d.addChild(self.mattext) + self.node3d.addChild(textdrawstyle) + self.node3d.addChild(self.font) + self.node3d.addChild(self.text3d) + vobj.addDisplayMode(self.node2d,"2D text") + vobj.addDisplayMode(self.node3d,"3D text") + self.onChanged(vobj,"TextColor") + self.onChanged(vobj,"FontSize") + self.onChanged(vobj,"FontName") + self.onChanged(vobj,"Justification") + self.onChanged(vobj,"LineSpacing") + + def getDisplayModes(self,vobj): + return ["2D text","3D text"] + + def setDisplayMode(self,mode): + return mode + + def updateData(self,obj,prop): + if prop == "Text": + if obj.Text: + if sys.version_info.major >= 3: + self.text2d.string.setValues([l for l in obj.Text if l]) + self.text3d.string.setValues([l for l in obj.Text if l]) + else: + self.text2d.string.setValues([l.encode("utf8") for l in obj.Text if l]) + self.text3d.string.setValues([l.encode("utf8") for l in obj.Text if l]) + elif prop == "Placement": + self.trans.translation.setValue(obj.Placement.Base) + self.trans.rotation.setValue(obj.Placement.Rotation.Q) + + def onChanged(self,vobj,prop): + if prop == "ScaleMultiplier": + if "ScaleMultiplier" in vobj.PropertiesList and "FontSize" in vobj.PropertiesList: + self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier + elif prop == "TextColor": + if "TextColor" in vobj.PropertiesList: + l = vobj.TextColor + self.mattext.diffuseColor.setValue([l[0],l[1],l[2]]) + elif (prop == "FontName"): + if "FontName" in vobj.PropertiesList: + self.font.name = vobj.FontName.encode("utf8") + elif prop == "FontSize": + if "FontSize" in vobj.PropertiesList and "ScaleMultiplier" in vobj.PropertiesList: + self.font.size = vobj.FontSize.Value * vobj.ScaleMultiplier + elif prop == "Justification": + try: + if getattr(vobj, "Justification", None) is not None: + if vobj.Justification == "Left": + self.text2d.justification = coin.SoAsciiText.LEFT + self.text3d.justification = coin.SoText2.LEFT + elif vobj.Justification == "Right": + self.text2d.justification = coin.SoAsciiText.RIGHT + self.text3d.justification = coin.SoText2.RIGHT + else: + self.text2d.justification = coin.SoAsciiText.CENTER + self.text3d.justification = coin.SoText2.CENTER + except AssertionError: + pass # Race condition - Justification enum has not been set yet + elif prop == "LineSpacing": + if "LineSpacing" in vobj.PropertiesList: + self.text2d.spacing = vobj.LineSpacing + self.text3d.spacing = vobj.LineSpacing + + def __getstate__(self): + return None + + def __setstate__(self,state): + return None \ No newline at end of file From 164bbabbd8e408f6c2267cd4f8ee8d3024396b97 Mon Sep 17 00:00:00 2001 From: carlopav Date: Thu, 26 Mar 2020 10:20:10 +0100 Subject: [PATCH 090/142] [Draft] Cleanup of Annotation style branch Further cleanup and guarded imports of Gui in modules. . --- src/Mod/Draft/Draft.py | 137 ++++++++++-------- src/Mod/Draft/draftobjects/dimension.py | 10 +- src/Mod/Draft/draftobjects/dimensionstyle.py | 6 +- .../Draft/draftobjects/draft_annotation.py | 6 +- src/Mod/Draft/draftobjects/label.py | 5 +- src/Mod/Draft/draftobjects/text.py | 9 +- .../draftviewproviders/view_dimension.py | 2 +- .../draftviewproviders/view_dimensionstyle.py | 3 +- .../view_draft_annotation.py | 6 +- .../Draft/draftviewproviders/view_label.py | 21 +-- src/Mod/Draft/draftviewproviders/view_text.py | 17 +-- 11 files changed, 113 insertions(+), 109 deletions(-) diff --git a/src/Mod/Draft/Draft.py b/src/Mod/Draft/Draft.py index d0d6ca6beb..9fd870b6f2 100644 --- a/src/Mod/Draft/Draft.py +++ b/src/Mod/Draft/Draft.py @@ -169,6 +169,80 @@ from draftutils.gui_utils import select from draftutils.gui_utils import loadTexture from draftutils.gui_utils import load_texture +#--------------------------------------------------------------------------- +# Draft objects +#--------------------------------------------------------------------------- + + + +#--------------------------------------------------------------------------- +# Draft annotation objects +#--------------------------------------------------------------------------- + +from draftobjects.dimension import make_dimension, make_angular_dimension +from draftobjects.dimension import LinearDimension, AngularDimension + +makeDimension = make_dimension +makeAngularDimension = make_angular_dimension +_Dimension = LinearDimension +_AngularDimension = AngularDimension + +if gui: + from draftviewproviders.view_dimension import ViewProviderLinearDimension + from draftviewproviders.view_dimension import ViewProviderAngularDimension + _ViewProviderDimension = ViewProviderLinearDimension + _ViewProviderAngularDimension = ViewProviderAngularDimension + + +from draftobjects.label import make_label +from draftobjects.label import Label + +makeLabel = make_label +DraftLabel = Label + +if gui: + from draftviewproviders.view_label import ViewProviderLabel + ViewProviderDraftLabel = ViewProviderLabel + + +from draftobjects.text import make_text +from draftobjects.text import Text +makeText = make_text +DraftText = Text + +if gui: + from draftviewproviders.view_text import ViewProviderText + ViewProviderDraftText = ViewProviderText + +def convertDraftTexts(textslist=[]): + """ + converts the given Draft texts (or all that is found + in the active document) to the new object + This function was already present at splitting time during v 0.19 + """ + if not isinstance(textslist,list): + textslist = [textslist] + if not textslist: + for o in FreeCAD.ActiveDocument.Objects: + if o.TypeId == "App::Annotation": + textslist.append(o) + todelete = [] + for o in textslist: + l = o.Label + o.Label = l+".old" + obj = makeText(o.LabelText,point=o.Position) + obj.Label = l + todelete.append(o.Name) + for p in o.InList: + if p.isDerivedFrom("App::DocumentObjectGroup"): + if o in p.Group: + g = p.Group + g.append(obj) + p.Group = g + for n in todelete: + FreeCAD.ActiveDocument.removeObject(n) + + def makeCircle(radius, placement=None, face=None, startangle=None, endangle=None, support=None): """makeCircle(radius,[placement,face,startangle,endangle]) @@ -251,69 +325,6 @@ def makeRectangle(length, height, placement=None, face=None, support=None): return obj -# Backward compatibility for dimension objects. - -from draftobjects.dimension import make_dimension, make_angular_dimension -from draftobjects.dimension import LinearDimension, AngularDimension -from draftviewproviders.view_dimension import ViewProviderLinearDimension -from draftviewproviders.view_dimension import ViewProviderAngularDimension - -makeDimension = make_dimension -_Dimension = LinearDimension -_ViewProviderDimension = ViewProviderLinearDimension - -makeAngularDimension = make_angular_dimension -_AngularDimension = AngularDimension -_ViewProviderAngularDimension = ViewProviderAngularDimension - -# Backward compatibility for label object. -from draftobjects.label import make_label -from draftobjects.label import Label -from draftviewproviders.view_label import ViewProviderLabel - -makeLabel = make_label -DraftLabel = Label -ViewProviderDraftLabel = ViewProviderLabel - -# Backward compatibility for text object. -# introduced when splitted text module from Draft.py in v 0.19 -from draftobjects.text import make_text -from draftobjects.text import Text -from draftviewproviders.view_text import ViewProviderText - -makeText = make_text -DraftText = Text -ViewProviderDraftText = ViewProviderText - -# already present at splitting time during v 0.19 -def convertDraftTexts(textslist=[]): - """ - converts the given Draft texts (or all that is found - in the active document) to the new object - """ - if not isinstance(textslist,list): - textslist = [textslist] - if not textslist: - for o in FreeCAD.ActiveDocument.Objects: - if o.TypeId == "App::Annotation": - textslist.append(o) - todelete = [] - for o in textslist: - l = o.Label - o.Label = l+".old" - obj = makeText(o.LabelText,point=o.Position) - obj.Label = l - todelete.append(o.Name) - for p in o.InList: - if p.isDerivedFrom("App::DocumentObjectGroup"): - if o in p.Group: - g = p.Group - g.append(obj) - p.Group = g - for n in todelete: - FreeCAD.ActiveDocument.removeObject(n) - - def makeWire(pointslist,closed=False,placement=None,face=None,support=None,bs2wire=False): """makeWire(pointslist,[closed],[placement]): Creates a Wire object from the given list of vectors. If closed is True or first diff --git a/src/Mod/Draft/draftobjects/dimension.py b/src/Mod/Draft/draftobjects/dimension.py index b91ef8d456..73d7159607 100644 --- a/src/Mod/Draft/draftobjects/dimension.py +++ b/src/Mod/Draft/draftobjects/dimension.py @@ -25,7 +25,7 @@ """This module provides the object code for Draft Dimension. """ -## @package style_dimension +## @package dimension # \ingroup DRAFT # \brief This module provides the object code for Draft Dimension. @@ -36,9 +36,11 @@ import DraftGeomUtils import draftutils.gui_utils as gui_utils import draftutils.utils as utils from draftobjects.draft_annotation import DraftAnnotation -from draftviewproviders.view_dimension import ViewProviderDimensionBase -from draftviewproviders.view_dimension import ViewProviderLinearDimension -from draftviewproviders.view_dimension import ViewProviderAngularDimension + +if App.GuiUp: + from draftviewproviders.view_dimension import ViewProviderDimensionBase + from draftviewproviders.view_dimension import ViewProviderLinearDimension + from draftviewproviders.view_dimension import ViewProviderAngularDimension def make_dimension(p1,p2,p3=None,p4=None): """makeDimension(p1,p2,[p3]) or makeDimension(object,i1,i2,p3) diff --git a/src/Mod/Draft/draftobjects/dimensionstyle.py b/src/Mod/Draft/draftobjects/dimensionstyle.py index dff2b6250d..c61d628fa9 100644 --- a/src/Mod/Draft/draftobjects/dimensionstyle.py +++ b/src/Mod/Draft/draftobjects/dimensionstyle.py @@ -22,19 +22,19 @@ """This module provides the object code for Draft DimensionStyle. """ -## @package style_dimension +## @package dimensionstyle # \ingroup DRAFT # \brief This module provides the object code for Draft DimensionStyle. import FreeCAD as App from draftobjects.draft_annotation import DraftAnnotation from PySide.QtCore import QT_TRANSLATE_NOOP -from draftviewproviders.view_dimensionstyle import ViewProviderDraftDimensionStyle -from draftviewproviders.view_dimensionstyle import ViewProviderDimensionStylesContainer from draftobjects.draft_annotation import AnnotationStylesContainer if App.GuiUp: import FreeCADGui as Gui + from draftviewproviders.view_dimensionstyle import ViewProviderDraftDimensionStyle + from draftviewproviders.view_dimensionstyle import ViewProviderDimensionStylesContainer def make_dimension_style(existing_dimension = None): """ diff --git a/src/Mod/Draft/draftobjects/draft_annotation.py b/src/Mod/Draft/draftobjects/draft_annotation.py index 65c781dedc..133bfb5658 100644 --- a/src/Mod/Draft/draftobjects/draft_annotation.py +++ b/src/Mod/Draft/draftobjects/draft_annotation.py @@ -21,11 +21,11 @@ # * * # *************************************************************************** -"""This module provides the object code for Draft DimensionStyle. +"""This module provides the object code for Draft Annotation. """ -## @package style_dimension +## @package annotation # \ingroup DRAFT -# \brief This module provides the object code for Draft DimensionStyle. +# \brief This module provides the object code for Draft Annotation. import FreeCAD as App from PySide.QtCore import QT_TRANSLATE_NOOP diff --git a/src/Mod/Draft/draftobjects/label.py b/src/Mod/Draft/draftobjects/label.py index 9180f24d73..31181cc4cd 100644 --- a/src/Mod/Draft/draftobjects/label.py +++ b/src/Mod/Draft/draftobjects/label.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # *************************************************************************** # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * @@ -35,7 +36,9 @@ import DraftGeomUtils import draftutils.gui_utils as gui_utils import draftutils.utils as utils from draftobjects.draft_annotation import DraftAnnotation -from draftviewproviders.view_label import ViewProviderLabel + +if App.GuiUp: + from draftviewproviders.view_label import ViewProviderLabel diff --git a/src/Mod/Draft/draftobjects/text.py b/src/Mod/Draft/draftobjects/text.py index a26cf3378a..ef5278144f 100644 --- a/src/Mod/Draft/draftobjects/text.py +++ b/src/Mod/Draft/draftobjects/text.py @@ -22,11 +22,11 @@ # * * # *************************************************************************** -"""This module provides the object code for Draft Label. +"""This module provides the object code for Draft Text. """ -## @package label +## @package text # \ingroup DRAFT -# \brief This module provides the object code for Draft Label. +# \brief This module provides the object code for Draft Text. import FreeCAD as App import math @@ -35,8 +35,9 @@ import DraftGeomUtils import draftutils.gui_utils as gui_utils import draftutils.utils as utils from draftobjects.draft_annotation import DraftAnnotation -from draftviewproviders.view_text import ViewProviderText +if App.GuiUp: + from draftviewproviders.view_text import ViewProviderText diff --git a/src/Mod/Draft/draftviewproviders/view_dimension.py b/src/Mod/Draft/draftviewproviders/view_dimension.py index eac7a6d046..5f89a0dff9 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimension.py +++ b/src/Mod/Draft/draftviewproviders/view_dimension.py @@ -23,7 +23,7 @@ # *************************************************************************** """This module provides the Draft Dimensions view provider classes """ -## @package polararray +## @package dimension # \ingroup DRAFT # \brief This module provides the view provider code for Draft Dimensions. diff --git a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py index 2dd20198ee..03ed8d647d 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py +++ b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py @@ -22,7 +22,7 @@ """This module provides the view provider code for Draft DimensionStyle. """ -## @package polararray +## @package dimensionstyle # \ingroup DRAFT # \brief This module provides the view provider code for Draft DimensionStyle. @@ -31,7 +31,6 @@ from Draft import _ViewProviderDraft from PySide.QtCore import QT_TRANSLATE_NOOP import draftutils.utils as utils from pivy import coin -from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation from draftviewproviders.view_draft_annotation import ViewProviderAnnotationStylesContainer from draftviewproviders.view_dimension import ViewProviderDimensionBase diff --git a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py index 2cc0f38d8c..9685541970 100644 --- a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py +++ b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py @@ -21,16 +21,16 @@ # *************************************************************************** """This module provides the Draft Annotations view provider base class """ -## @package polararray +## @package annotation # \ingroup DRAFT # \brief This module provides the Draft Annotations view provider base class import FreeCAD as App -import FreeCADGui as Gui from PySide.QtCore import QT_TRANSLATE_NOOP - +if App.GuiUp: + import FreeCADGui as Gui class ViewProviderAnnotationStylesContainer: """A View Provider for the Layer Container""" diff --git a/src/Mod/Draft/draftviewproviders/view_label.py b/src/Mod/Draft/draftviewproviders/view_label.py index a4a2f611a1..1d131b946b 100644 --- a/src/Mod/Draft/draftviewproviders/view_label.py +++ b/src/Mod/Draft/draftviewproviders/view_label.py @@ -21,15 +21,14 @@ # * USA * # * * # *************************************************************************** -"""This module provides the Draft Dimensions view provider classes +"""This module provides the Draft Label view provider classes """ -## @package polararray +## @package label # \ingroup DRAFT -# \brief This module provides the view provider code for Draft Dimensions. +# \brief This module provides the view provider code for Draft Label. import FreeCAD as App -import FreeCADGui as Gui import DraftVecUtils, DraftGeomUtils import math, sys from pivy import coin @@ -38,6 +37,10 @@ import draftutils.utils as utils import draftutils.gui_utils as gui_utils from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation +if App.GuiUp: + import FreeCADGui as Gui + + class ViewProviderLabel(ViewProviderDraftAnnotation): """A View Provider for the Label annotation object""" @@ -111,7 +114,6 @@ class ViewProviderLabel(ViewProviderDraftAnnotation): def getIcon(self): - import Draft_rc return ":/icons/Draft_Label.svg" def claimChildren(self): @@ -212,7 +214,6 @@ class ViewProviderLabel(ViewProviderDraftAnnotation): self.onChanged(obj.ViewObject,"TextAlignment") def getTextSize(self,vobj): - from pivy import coin if vobj.DisplayMode == "3D text": text = self.text3d else: @@ -311,10 +312,4 @@ class ViewProviderLabel(ViewProviderDraftAnnotation): v = vobj.Object.Placement.Rotation.multVec(v) pos = vobj.Object.Placement.Base.add(v) self.textpos.translation.setValue(pos) - self.textpos.rotation.setValue(vobj.Object.Placement.Rotation.Q) - - def __getstate__(self): - return None - - def __setstate__(self,state): - return None \ No newline at end of file + self.textpos.rotation.setValue(vobj.Object.Placement.Rotation.Q) \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_text.py b/src/Mod/Draft/draftviewproviders/view_text.py index 15622be54e..8f08fd7f8b 100644 --- a/src/Mod/Draft/draftviewproviders/view_text.py +++ b/src/Mod/Draft/draftviewproviders/view_text.py @@ -21,15 +21,14 @@ # * USA * # * * # *************************************************************************** -"""This module provides the Draft Dimensions view provider classes +"""This module provides the Draft Text view provider classes """ -## @package polararray +## @package text # \ingroup DRAFT -# \brief This module provides the view provider code for Draft Dimensions. +# \brief This module provides the view provider code for Draft Text. import FreeCAD as App -import FreeCADGui as Gui import DraftVecUtils, DraftGeomUtils import math, sys from pivy import coin @@ -40,7 +39,7 @@ from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation class ViewProviderText(ViewProviderDraftAnnotation): - """A View Provider for the Draft Label""" + """A View Provider for the Draft Text annotation""" def __init__(self,vobj): @@ -161,10 +160,4 @@ class ViewProviderText(ViewProviderDraftAnnotation): elif prop == "LineSpacing": if "LineSpacing" in vobj.PropertiesList: self.text2d.spacing = vobj.LineSpacing - self.text3d.spacing = vobj.LineSpacing - - def __getstate__(self): - return None - - def __setstate__(self,state): - return None \ No newline at end of file + self.text3d.spacing = vobj.LineSpacing \ No newline at end of file From 8e3dfe7c26b70464785d716aae337050ab733caf Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 28 Mar 2020 14:30:09 +0100 Subject: [PATCH 091/142] [Draft] Dimension styles improvements Dimension style property is auto-set on dimension creation. Further improvementes in DimensionStyleContainer. . --- .../Draft/draftguitools/gui_dimensionstyle.py | 1 + src/Mod/Draft/draftobjects/dimension.py | 28 ++++++- src/Mod/Draft/draftobjects/dimensionstyle.py | 80 +++++++++++++------ .../Draft/draftobjects/draft_annotation.py | 28 ++++--- .../draftviewproviders/view_dimensionstyle.py | 18 ++--- .../view_draft_annotation.py | 18 ++++- 6 files changed, 125 insertions(+), 48 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_dimensionstyle.py b/src/Mod/Draft/draftguitools/gui_dimensionstyle.py index 146870c43b..71155f30b8 100644 --- a/src/Mod/Draft/draftguitools/gui_dimensionstyle.py +++ b/src/Mod/Draft/draftguitools/gui_dimensionstyle.py @@ -63,6 +63,7 @@ class GuiCommandDimensionStyle(gui_base.GuiCommandSimplest): if len(sel) == 1: if utils.get_type(sel[0]) == 'Dimension': make_dimension_style(sel[0]) + return make_dimension_style() diff --git a/src/Mod/Draft/draftobjects/dimension.py b/src/Mod/Draft/draftobjects/dimension.py index 73d7159607..e78d949cbd 100644 --- a/src/Mod/Draft/draftobjects/dimension.py +++ b/src/Mod/Draft/draftobjects/dimension.py @@ -112,8 +112,17 @@ def make_dimension(p1,p2,p3=None,p4=None): if vnorm.getAngle(normal) < math.pi/2: normal = normal.negative() obj.Normal = normal + + # format dimension according to ActiveDimensionStyle or user Preferences + _style_applied = False + if hasattr(App.ActiveDocument, "DimensionStyles"): + active_style = App.ActiveDocument.DimensionStyles.ActiveDimensionStyle + if active_style is not None: + obj.DimensionStyle = active_style + _style_applied = True if App.GuiUp: - gui_utils.format_object(obj) + if not _style_applied: + gui_utils.format_object(obj) gui_utils.select(obj) return obj @@ -129,6 +138,8 @@ def make_angular_dimension(center,angles,p3,normal=None): return obj = App.ActiveDocument.addObject("App::FeaturePython","Dimension") AngularDimension(obj) + if App.GuiUp: + ViewProviderAngularDimension(obj.ViewObject) obj.Center = center for a in range(len(angles)): if angles[a] > 2*math.pi: @@ -146,10 +157,20 @@ def make_angular_dimension(center,angles,p3,normal=None): vnorm = gui_utils.get3DView().getViewDirection() if vnorm.getAngle(normal) < math.pi/2: normal = normal.negative() + obj.Normal = normal + + # format dimension according to ActiveDimensionStyle or user Preferences + _style_applied = False + if hasattr(App.ActiveDocument, "DimensionStyles"): + active_style = App.ActiveDocument.DimensionStyles.ActiveDimensionStyle + if active_style is not None: + obj.DimensionStyle = active_style + _style_applied = True + if App.GuiUp: - ViewProviderAngularDimension(obj.ViewObject) - gui_utils.format_object(obj) + if not _style_applied: + gui_utils.format_object(obj) gui_utils.select(obj) return obj @@ -365,6 +386,7 @@ class AngularDimension(DimensionBase): obj.Normal = App.Vector(0,0,1) def onChanged(self,obj,prop): + super().onChanged(obj, prop) if hasattr(obj,"Angle"): obj.setEditorMode('Angle',1) if hasattr(obj,"Normal"): diff --git a/src/Mod/Draft/draftobjects/dimensionstyle.py b/src/Mod/Draft/draftobjects/dimensionstyle.py index c61d628fa9..daa602c913 100644 --- a/src/Mod/Draft/draftobjects/dimensionstyle.py +++ b/src/Mod/Draft/draftobjects/dimensionstyle.py @@ -27,13 +27,13 @@ # \brief This module provides the object code for Draft DimensionStyle. import FreeCAD as App -from draftobjects.draft_annotation import DraftAnnotation from PySide.QtCore import QT_TRANSLATE_NOOP -from draftobjects.draft_annotation import AnnotationStylesContainer +from draftobjects.draft_annotation import DraftAnnotation +from draftobjects.draft_annotation import StylesContainerBase if App.GuiUp: import FreeCADGui as Gui - from draftviewproviders.view_dimensionstyle import ViewProviderDraftDimensionStyle + from draftviewproviders.view_dimensionstyle import ViewProviderDimensionStyle from draftviewproviders.view_dimensionstyle import ViewProviderDimensionStylesContainer def make_dimension_style(existing_dimension = None): @@ -46,16 +46,16 @@ def make_dimension_style(existing_dimension = None): obj = App.ActiveDocument.addObject("App::FeaturePython","DimensionStyle") DimensionStyle(obj) if App.GuiUp: - ViewProviderDraftDimensionStyle(obj.ViewObject, existing_dimension) - get_dimension_style_container().addObject(obj) + ViewProviderDimensionStyle(obj.ViewObject, existing_dimension) + get_dimension_styles_container().addObject(obj) return obj -def get_dimension_style_container(): - """get_dimension_style_container(): returns a group object to put dimensions in""" +def get_dimension_styles_container(): + """get_dimension_styles_container(): returns a group object to put dimensions in""" for obj in App.ActiveDocument.Objects: - if obj.Name == "DimensionStyleContainer": + if obj.Name == "DimensionStyles": return obj - obj = App.ActiveDocument.addObject("App::DocumentObjectGroupPython", "DimensionStyleContainer") + obj = App.ActiveDocument.addObject("App::DocumentObjectGroupPython", "DimensionStyles") obj.Label = QT_TRANSLATE_NOOP("draft", "Dimension Styles") DimensionStylesContainer(obj) if App.GuiUp: @@ -63,29 +63,61 @@ def get_dimension_style_container(): return obj -class DimensionStylesContainer(AnnotationStylesContainer): +class DimensionStylesContainer(StylesContainerBase): """The Dimension Container""" def __init__(self, obj): - super().__init__(obj) - self.Type = "DimensionStyleContainer" - obj.Proxy = self + super().__init__(obj, tp = "DimensionStyles") - def execute(self, obj): + # init properties - g = obj.Group - g.sort(key=lambda o: o.Label) - obj.Group = g + obj.addProperty("App::PropertyLink","ActiveDimensionStyle", + "Annotation", + QT_TRANSLATE_NOOP("App::Property", + "Active dimension style")) + + # sets properties read only + obj.setEditorMode("Visibility", 1) + obj.setEditorMode("ActiveDimensionStyle", 1) + + + def onChanged(self, obj, prop): + if prop == "Visibility" and hasattr(obj, "Visibility"): + if obj.Visibility == False: + obj.Visibility = True + if hasattr(obj, "ActiveDimensionStyle"): + if obj.ActiveDimensionStyle: + super().make_unique_visible(obj, obj.ActiveDimensionStyle) + + if prop == "ActiveDimensionStyle" and hasattr(obj, "ActiveDimensionStyle"): + super().make_unique_visible(obj, obj.ActiveDimensionStyle) class DimensionStyle(DraftAnnotation): def __init__(self, obj): + super().__init__(obj, "DimensionStyle") - + + obj.setEditorMode("Visibility", 1) # sets visibility read only + + + def onChanged(self, obj, prop): + """ visibility property controls setting the activeDimensionStyle + so the only visible style is the current one + """ + if prop == "Visibility" and hasattr(obj, "Visibility"): + if obj.Visibility == True: + self.set_current(obj) + elif obj.Visibility == False: + self.remove_from_current(obj) + + def set_visible(self, obj): + obj.Visibility = True + def set_current(self, obj): - "turn non visible all the concurrent styles" - for o in get_dimension_style_container().Group: - if hasattr(o, "Visibility"): - o.Visibility = False - if hasattr(obj, "Visibility"): - obj.Visibility = True + get_dimension_styles_container().ActiveDimensionStyle = obj + + def remove_from_current(self, obj): + if get_dimension_styles_container().ActiveDimensionStyle: + if get_dimension_styles_container().ActiveDimensionStyle.Name == obj.Name: + get_dimension_styles_container().ActiveDimensionStyle = None diff --git a/src/Mod/Draft/draftobjects/draft_annotation.py b/src/Mod/Draft/draftobjects/draft_annotation.py index 133bfb5658..c45a7041ca 100644 --- a/src/Mod/Draft/draftobjects/draft_annotation.py +++ b/src/Mod/Draft/draftobjects/draft_annotation.py @@ -54,12 +54,12 @@ class DraftAnnotation: def onChanged(self, obj, prop): pass -class AnnotationStylesContainer: - """The Annotation Container""" +class StylesContainerBase: + """The Base class for Annotation Containers""" - def __init__(self, obj): + def __init__(self, obj, tp = "AnnotationContainer"): - self.Type = "AnnotationContainer" + self.Type = tp obj.Proxy = self def execute(self, obj): @@ -68,12 +68,20 @@ class AnnotationStylesContainer: g.sort(key=lambda o: o.Label) obj.Group = g - def __getstate__(self): + def make_unique_visible(self, obj, active_style): + "turn non visible all the concurrent styles" + if hasattr(active_style, "Visibility"): + for o in obj.Group: + if o.Name != active_style.Name: + if hasattr(o, "Visibility"): + o.Visibility = False - if hasattr(self, "Type"): - return self.Type - def __setstate__(self, state): +class AnnotationStylesContainer(StylesContainerBase): + """The Annotation Container""" + + def __init__(self, obj): + + self.Type = "AnnotationContainer" + obj.Proxy = self - if state: - self.Type = state \ No newline at end of file diff --git a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py index 03ed8d647d..f5026bd7ca 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py +++ b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py @@ -29,25 +29,25 @@ import FreeCAD as App from Draft import _ViewProviderDraft from PySide.QtCore import QT_TRANSLATE_NOOP +import draftutils.gui_utils as gui_utils import draftutils.utils as utils from pivy import coin -from draftviewproviders.view_draft_annotation import ViewProviderAnnotationStylesContainer +from draftviewproviders.view_draft_annotation import ViewProviderStylesContainerBase from draftviewproviders.view_dimension import ViewProviderDimensionBase -class ViewProviderDimensionStylesContainer(ViewProviderAnnotationStylesContainer): - """A View Provider for the Dimension Style Container""" +class ViewProviderDimensionStylesContainer(ViewProviderStylesContainerBase): + """A View Provider for the Dimension Styles Container""" def __init__(self, vobj): super().__init__(vobj) - vobj.Proxy = self def getIcon(self): return ":/icons/Draft_Annotation_Style.svg" -class ViewProviderDraftDimensionStyle(ViewProviderDimensionBase): +class ViewProviderDimensionStyle(ViewProviderDimensionBase): """ Dimension style dont have a proper object but just a viewprovider. It stores inside a document object dimension settings and restore them on demand. @@ -93,10 +93,11 @@ class ViewProviderDraftDimensionStyle(ViewProviderDimensionBase): if existing_dimension and hasattr(existing_dimension, "ViewObject"): # get the style from given dimension - from draftutils import gui_utils gui_utils.format_object(target = vobj.Object, origin = existing_dimension) def onChanged(self, vobj, prop): + if prop == "Visibility": + return if hasattr(vobj, "AutoUpdate"): if vobj.AutoUpdate: self.update_related_dimensions(vobj) @@ -131,18 +132,17 @@ class ViewProviderDraftDimensionStyle(ViewProviderDimensionBase): App.Console.PrintMessage("Current dimension style set to " + str(vobj.Object.Label) + "\n") - vobj.Object.Proxy.set_current(vobj.Object) + vobj.Object.Proxy.set_visible(vobj.Object) def update_related_dimensions(self, vobj): """ Apply the style to the related dimensions """ - from draftutils import gui_utils for dim in vobj.Object.InList: gui_utils.format_object(target = dim, origin = vobj.Object) def getIcon(self): - import Draft_rc + return ":/icons/Draft_Dimension_Tree_Style.svg" def attach(self, vobj): diff --git a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py index 9685541970..e3ac148f8d 100644 --- a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py +++ b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py @@ -32,8 +32,8 @@ from PySide.QtCore import QT_TRANSLATE_NOOP if App.GuiUp: import FreeCADGui as Gui -class ViewProviderAnnotationStylesContainer: - """A View Provider for the Layer Container""" +class ViewProviderStylesContainerBase: + """A Base View Provider for Annotation Styles Containers""" def __init__(self, vobj): @@ -56,6 +56,20 @@ class ViewProviderAnnotationStylesContainer: return None +class ViewProviderAnnotationStylesContainer(ViewProviderStylesContainerBase): + """A View Provider for the Annotation Styles Container""" + + def __init__(self, vobj): + super().__init__(vobj) + + def getIcon(self): + + return ":/icons/Draft_Annotation_Style.svg" + + def attach(self, vobj): + + self.Object = vobj.Object + class ViewProviderDraftAnnotation: """ From 0745f760a028651a1dfc5bfc10fad3f494aa1824 Mon Sep 17 00:00:00 2001 From: carlopav Date: Fri, 10 Apr 2020 17:39:32 +0200 Subject: [PATCH 092/142] [Draft] Removed annotation styles objects Removed Annotation styles current implementation. As pointed out by yorik, in https://forum.freecadweb.org/viewtopic.php?f=23&t=44051&p=385710#p385179, this feature will be added using document Meta property. --- src/Mod/Draft/CMakeLists.txt | 3 - src/Mod/Draft/InitGui.py | 1 - src/Mod/Draft/Resources/Draft.qrc | 2 - .../Resources/icons/Draft_Dimension_Style.svg | 404 ------------- .../icons/Draft_Dimension_Style_Tree.svg | 542 ------------------ .../Draft/draftguitools/gui_dimensionstyle.py | 71 --- src/Mod/Draft/draftobjects/dimension.py | 37 +- src/Mod/Draft/draftobjects/dimensionstyle.py | 123 ---- .../draftviewproviders/view_dimensionstyle.py | 158 ----- 9 files changed, 4 insertions(+), 1337 deletions(-) delete mode 100644 src/Mod/Draft/Resources/icons/Draft_Dimension_Style.svg delete mode 100644 src/Mod/Draft/Resources/icons/Draft_Dimension_Style_Tree.svg delete mode 100644 src/Mod/Draft/draftguitools/gui_dimensionstyle.py delete mode 100644 src/Mod/Draft/draftobjects/dimensionstyle.py delete mode 100644 src/Mod/Draft/draftviewproviders/view_dimensionstyle.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 10ca81f2c4..40687f5a31 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -67,7 +67,6 @@ SET(Draft_objects draftobjects/draft_annotation.py draftobjects/label.py draftobjects/dimension.py - draftobjects/dimensionstyle.py draftobjects/text.py draftobjects/README.md ) @@ -80,7 +79,6 @@ SET(Draft_view_providers draftviewproviders/view_draft_annotation.py draftviewproviders/view_label.py draftviewproviders/view_dimension.py - draftviewproviders/view_dimensionstyle.py draftviewproviders/view_text.py draftviewproviders/README.md ) @@ -89,7 +87,6 @@ SET(Draft_GUI_tools draftguitools/__init__.py draftguitools/gui_base.py draftguitools/gui_circulararray.py - draftguitools/gui_dimensionstyle.py draftguitools/gui_orthoarray.py draftguitools/gui_polararray.py draftguitools/gui_planeproxy.py diff --git a/src/Mod/Draft/InitGui.py b/src/Mod/Draft/InitGui.py index 7513c3d3fe..0178ec4978 100644 --- a/src/Mod/Draft/InitGui.py +++ b/src/Mod/Draft/InitGui.py @@ -86,7 +86,6 @@ class DraftWorkbench(FreeCADGui.Workbench): from draftguitools import gui_polararray from draftguitools import gui_orthoarray from draftguitools import gui_arrays - from draftguitools import gui_style_dimension FreeCADGui.addLanguagePath(":/translations") FreeCADGui.addIconPath(":/icons") except Exception as exc: diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index 06c8ea3b8c..5e0c16a03b 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -25,8 +25,6 @@ icons/Draft_DelPoint.svg icons/Draft_Dimension.svg icons/Draft_Dimension_Tree.svg - icons/Draft_Dimension_Style.svg - icons/Draft_Dimension_Style_Tree.svg icons/Draft_DimensionAngular.svg icons/Draft_DimensionRadius.svg icons/Draft_Dot.svg diff --git a/src/Mod/Draft/Resources/icons/Draft_Dimension_Style.svg b/src/Mod/Draft/Resources/icons/Draft_Dimension_Style.svg deleted file mode 100644 index 5e6f485bf6..0000000000 --- a/src/Mod/Draft/Resources/icons/Draft_Dimension_Style.svg +++ /dev/null @@ -1,404 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - Mon Oct 10 13:44:52 2011 +0000 - - - [wmayer] - - - - - FreeCAD LGPL2+ - - - - - FreeCAD - - - FreeCAD/src/Mod/Draft/Resources/icons/Draft_Dimension.svg - http://www.freecadweb.org/wiki/index.php?title=Artwork - - - [agryson] Alexander Gryson - - - - - line - dot - number - - - A number floating above a line corresponding to the upper three sides of a rectangle with a dot at each endpoint and corner - - - - diff --git a/src/Mod/Draft/Resources/icons/Draft_Dimension_Style_Tree.svg b/src/Mod/Draft/Resources/icons/Draft_Dimension_Style_Tree.svg deleted file mode 100644 index 0b13bf93db..0000000000 --- a/src/Mod/Draft/Resources/icons/Draft_Dimension_Style_Tree.svg +++ /dev/null @@ -1,542 +0,0 @@ - - - Draft_Dimension_Tree - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - Draft_Dimension_Tree - - Wed Oct 6 12:19:00 2019 -0600 - - - [vocx] - - - - - FreeCAD LGPL2+ - - - - - FreeCAD - - - FreeCAD/src/Mod/Draft/Resources/icons/Draft_Dimenstion_Tree - http://www.freecadweb.org/wiki/index.php?title=Artwork - - - [agryson] Alexander Gryson, [yorikvanhavre] - - - - - triangle - arrows - - - Two triangles, one pointing left, the other right, with a small line between the two - - - - - - - - - - - - - - - - diff --git a/src/Mod/Draft/draftguitools/gui_dimensionstyle.py b/src/Mod/Draft/draftguitools/gui_dimensionstyle.py deleted file mode 100644 index 71155f30b8..0000000000 --- a/src/Mod/Draft/draftguitools/gui_dimensionstyle.py +++ /dev/null @@ -1,71 +0,0 @@ -# *************************************************************************** -# * (c) 2020 Carlo Pavan * -# * * -# * 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 * -# * * -# *************************************************************************** - -"""This module provides the Draft Dimension Style tool. -""" -## @package gui_style_dimension -# \ingroup DRAFT -# \brief This module provides the Draft Dimension Style tool. - -import FreeCAD as App -import FreeCADGui as Gui -from PySide import QtCore -from . import gui_base -from draftutils import utils -from draftobjects.dimensionstyle import make_dimension_style - - - -class GuiCommandDimensionStyle(gui_base.GuiCommandSimplest): - """ - The command creates a dimension style object - """ - def __init__(self): - super().__init__(name="Dimension style") - - def GetResources(self): - _msg = ("Creates a new dimension style.\n" - "The object stores dimension preferences into the document." - ) - return {'Pixmap' : 'Draft_Annotation_Style', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft", "Dimension Style"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft", _msg)} - - def IsActive(self): - if Gui.ActiveDocument: - return True - else: - return False - - def Activated(self): - sel = Gui.Selection.getSelection() - - if len(sel) == 1: - if utils.get_type(sel[0]) == 'Dimension': - make_dimension_style(sel[0]) - return - - make_dimension_style() - - -Gui.addCommand('Draft_DimensionStyle', GuiCommandDimensionStyle()) diff --git a/src/Mod/Draft/draftobjects/dimension.py b/src/Mod/Draft/draftobjects/dimension.py index e78d949cbd..6ee342c5aa 100644 --- a/src/Mod/Draft/draftobjects/dimension.py +++ b/src/Mod/Draft/draftobjects/dimension.py @@ -32,7 +32,7 @@ import FreeCAD as App import math from PySide.QtCore import QT_TRANSLATE_NOOP -import DraftGeomUtils +import DraftGeomUtils, DraftVecUtils import draftutils.gui_utils as gui_utils import draftutils.utils as utils from draftobjects.draft_annotation import DraftAnnotation @@ -113,22 +113,13 @@ def make_dimension(p1,p2,p3=None,p4=None): normal = normal.negative() obj.Normal = normal - # format dimension according to ActiveDimensionStyle or user Preferences - _style_applied = False - if hasattr(App.ActiveDocument, "DimensionStyles"): - active_style = App.ActiveDocument.DimensionStyles.ActiveDimensionStyle - if active_style is not None: - obj.DimensionStyle = active_style - _style_applied = True if App.GuiUp: - if not _style_applied: - gui_utils.format_object(obj) + gui_utils.format_object(obj) gui_utils.select(obj) return obj - def make_angular_dimension(center,angles,p3,normal=None): """makeAngularDimension(center,angle1,angle2,p3,[normal]): creates an angular Dimension from the given center, with the given list of angles, passing through p3. @@ -159,18 +150,9 @@ def make_angular_dimension(center,angles,p3,normal=None): normal = normal.negative() obj.Normal = normal - - # format dimension according to ActiveDimensionStyle or user Preferences - _style_applied = False - if hasattr(App.ActiveDocument, "DimensionStyles"): - active_style = App.ActiveDocument.DimensionStyles.ActiveDimensionStyle - if active_style is not None: - obj.DimensionStyle = active_style - _style_applied = True if App.GuiUp: - if not _style_applied: - gui_utils.format_object(obj) + gui_utils.format_object(obj) gui_utils.select(obj) return obj @@ -188,12 +170,6 @@ class DimensionBase(DraftAnnotation): "Initialize common properties for dimension objects" DraftAnnotation.__init__(self,obj, tp) - # Annotation - obj.addProperty("App::PropertyLink","DimensionStyle", - "Annotation", - QT_TRANSLATE_NOOP("App::Property", - "Link dimension style")) - # Draft obj.addProperty("App::PropertyVector", "Normal", @@ -222,9 +198,7 @@ class DimensionBase(DraftAnnotation): def onChanged(self,obj,prop): - if prop == "DimensionStyle": - if hasattr(obj, "DimensionStyle"): - gui_utils.format_object(target = obj, origin = obj.DimensionStyle) + return def execute(self, obj): @@ -284,9 +258,6 @@ class LinearDimension(DimensionBase): # obj.setEditorMode('Normal', 2) if hasattr(obj, "Support"): obj.setEditorMode('Support', 2) - if prop == "DimensionStyle": - if hasattr(obj, "DimensionStyle"): - gui_utils.format_object(target = obj, origin = obj.DimensionStyle) def execute(self, obj): diff --git a/src/Mod/Draft/draftobjects/dimensionstyle.py b/src/Mod/Draft/draftobjects/dimensionstyle.py deleted file mode 100644 index daa602c913..0000000000 --- a/src/Mod/Draft/draftobjects/dimensionstyle.py +++ /dev/null @@ -1,123 +0,0 @@ -# *************************************************************************** -# * * -# * 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 * -# * * -# *************************************************************************** - -"""This module provides the object code for Draft DimensionStyle. -""" -## @package dimensionstyle -# \ingroup DRAFT -# \brief This module provides the object code for Draft DimensionStyle. - -import FreeCAD as App -from PySide.QtCore import QT_TRANSLATE_NOOP -from draftobjects.draft_annotation import DraftAnnotation -from draftobjects.draft_annotation import StylesContainerBase - -if App.GuiUp: - import FreeCADGui as Gui - from draftviewproviders.view_dimensionstyle import ViewProviderDimensionStyle - from draftviewproviders.view_dimensionstyle import ViewProviderDimensionStylesContainer - -def make_dimension_style(existing_dimension = None): - """ - Make dimension style - """ - if not App.ActiveDocument: - App.Console.PrintError("No active document. Aborting\n") - return - obj = App.ActiveDocument.addObject("App::FeaturePython","DimensionStyle") - DimensionStyle(obj) - if App.GuiUp: - ViewProviderDimensionStyle(obj.ViewObject, existing_dimension) - get_dimension_styles_container().addObject(obj) - return obj - -def get_dimension_styles_container(): - """get_dimension_styles_container(): returns a group object to put dimensions in""" - for obj in App.ActiveDocument.Objects: - if obj.Name == "DimensionStyles": - return obj - obj = App.ActiveDocument.addObject("App::DocumentObjectGroupPython", "DimensionStyles") - obj.Label = QT_TRANSLATE_NOOP("draft", "Dimension Styles") - DimensionStylesContainer(obj) - if App.GuiUp: - ViewProviderDimensionStylesContainer(obj.ViewObject) - return obj - - -class DimensionStylesContainer(StylesContainerBase): - """The Dimension Container""" - - def __init__(self, obj): - super().__init__(obj, tp = "DimensionStyles") - - # init properties - - obj.addProperty("App::PropertyLink","ActiveDimensionStyle", - "Annotation", - QT_TRANSLATE_NOOP("App::Property", - "Active dimension style")) - - # sets properties read only - obj.setEditorMode("Visibility", 1) - obj.setEditorMode("ActiveDimensionStyle", 1) - - - def onChanged(self, obj, prop): - if prop == "Visibility" and hasattr(obj, "Visibility"): - if obj.Visibility == False: - obj.Visibility = True - if hasattr(obj, "ActiveDimensionStyle"): - if obj.ActiveDimensionStyle: - super().make_unique_visible(obj, obj.ActiveDimensionStyle) - - if prop == "ActiveDimensionStyle" and hasattr(obj, "ActiveDimensionStyle"): - super().make_unique_visible(obj, obj.ActiveDimensionStyle) - - -class DimensionStyle(DraftAnnotation): - def __init__(self, obj): - - super().__init__(obj, "DimensionStyle") - - obj.setEditorMode("Visibility", 1) # sets visibility read only - - - def onChanged(self, obj, prop): - """ visibility property controls setting the activeDimensionStyle - so the only visible style is the current one - """ - if prop == "Visibility" and hasattr(obj, "Visibility"): - if obj.Visibility == True: - self.set_current(obj) - elif obj.Visibility == False: - self.remove_from_current(obj) - - def set_visible(self, obj): - obj.Visibility = True - - def set_current(self, obj): - get_dimension_styles_container().ActiveDimensionStyle = obj - - def remove_from_current(self, obj): - if get_dimension_styles_container().ActiveDimensionStyle: - if get_dimension_styles_container().ActiveDimensionStyle.Name == obj.Name: - get_dimension_styles_container().ActiveDimensionStyle = None diff --git a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py b/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py deleted file mode 100644 index f5026bd7ca..0000000000 --- a/src/Mod/Draft/draftviewproviders/view_dimensionstyle.py +++ /dev/null @@ -1,158 +0,0 @@ -# *************************************************************************** -# * * -# * 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 * -# * * -# *************************************************************************** - -"""This module provides the view provider code for Draft DimensionStyle. -""" -## @package dimensionstyle -# \ingroup DRAFT -# \brief This module provides the view provider code for Draft DimensionStyle. - -import FreeCAD as App -from Draft import _ViewProviderDraft -from PySide.QtCore import QT_TRANSLATE_NOOP -import draftutils.gui_utils as gui_utils -import draftutils.utils as utils -from pivy import coin -from draftviewproviders.view_draft_annotation import ViewProviderStylesContainerBase -from draftviewproviders.view_dimension import ViewProviderDimensionBase - - -class ViewProviderDimensionStylesContainer(ViewProviderStylesContainerBase): - """A View Provider for the Dimension Styles Container""" - - def __init__(self, vobj): - super().__init__(vobj) - - def getIcon(self): - - return ":/icons/Draft_Annotation_Style.svg" - - -class ViewProviderDimensionStyle(ViewProviderDimensionBase): - """ - Dimension style dont have a proper object but just a viewprovider. - It stores inside a document object dimension settings and restore them on demand. - """ - def __init__(self, vobj, existing_dimension = None): - super().__init__(vobj) - - vobj.addProperty("App::PropertyBool","AutoUpdate", - "Annotation", - QT_TRANSLATE_NOOP("App::Property", - "Auto update associated dimensions")) - - self.init_properties(vobj, existing_dimension) - - # Visibility is True only if the style is active - vobj.Visibility = False - - def init_properties(self, vobj, existing_dimension): - """ - Initializes Dimension Style properties - """ - # get the style from FreeCAD Draft Parameters - param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") - annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) - - vobj.ScaleMultiplier = 1 / annotation_scale - vobj.AutoUpdate = True - - vobj.FontName = utils.get_param("textfont","") - vobj.FontSize = utils.get_param("textheight",0.20) - vobj.TextSpacing = utils.get_param("dimspacing",0.05) - - vobj.Decimals = utils.get_param("dimPrecision",2) - vobj.ShowUnit = utils.get_param("showUnit",True) - - vobj.ArrowSize = utils.get_param("arrowsize",0.1) - vobj.ArrowType = utils.ARROW_TYPES - vobj.ArrowType = utils.ARROW_TYPES[utils.get_param("dimsymbol",0)] - vobj.DimOvershoot = utils.get_param("dimovershoot",0) - vobj.ExtLines = utils.get_param("extlines",0.3) - vobj.ExtOvershoot = utils.get_param("extovershoot",0) - vobj.ShowLine = True - - if existing_dimension and hasattr(existing_dimension, "ViewObject"): - # get the style from given dimension - gui_utils.format_object(target = vobj.Object, origin = existing_dimension) - - def onChanged(self, vobj, prop): - if prop == "Visibility": - return - if hasattr(vobj, "AutoUpdate"): - if vobj.AutoUpdate: - self.update_related_dimensions(vobj) - - def doubleClicked(self,vobj): - self.set_current(vobj) - - def setupContextMenu(self,vobj,menu): - action1 = menu.addAction("Set current") - action1.triggered.connect(lambda f=self.set_current, arg=vobj:f(arg)) - action2 = menu.addAction("Update dimensions") - action2.triggered.connect(lambda f=self.update_related_dimensions, arg=vobj:f(arg)) - - def set_current(self, vobj): - """ - Sets the current dimension style as default for new created dimensions - """ - param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") - param.SetFloat("DraftAnnotationScale", 1 / vobj.ScaleMultiplier) - - param.SetString("textfont", vobj.FontName) - param.SetFloat("textheight", vobj.FontSize) - param.SetFloat("dimspacing", vobj.TextSpacing) - - param.SetInt("dimPrecision", vobj.Decimals) - - param.SetFloat("arrowsize", vobj.ArrowSize) - param.SetInt("dimsymbol", utils.ARROW_TYPES.index(vobj.ArrowType)) - param.SetFloat("dimovershoot", vobj.DimOvershoot) - param.SetFloat("extlines", vobj.ExtLines) - param.SetFloat("extovershoot", vobj.ExtOvershoot) - - App.Console.PrintMessage("Current dimension style set to " + str(vobj.Object.Label) + "\n") - - vobj.Object.Proxy.set_visible(vobj.Object) - - def update_related_dimensions(self, vobj): - """ - Apply the style to the related dimensions - """ - for dim in vobj.Object.InList: - gui_utils.format_object(target = dim, origin = vobj.Object) - - def getIcon(self): - - return ":/icons/Draft_Dimension_Tree_Style.svg" - - def attach(self, vobj): - self.standard = coin.SoGroup() - vobj.addDisplayMode(self.standard,"Standard") - - def getDisplayModes(self,obj): - "'''Return a list of display modes.'''" - return ["Standard"] - - def getDefaultDisplayMode(self): - "'''Return the name of the default display mode. It must be defined in getDisplayModes.'''" - return "Standard" \ No newline at end of file From 131961c2a8baafbba41159efe10a227b5a4fd48b Mon Sep 17 00:00:00 2001 From: carlopav Date: Wed, 15 Apr 2020 22:54:35 +0200 Subject: [PATCH 093/142] [Draft] Cleanup splitted annotatation objects - corrected super() methods to be Py2 compatible - further cleanup of the code. further cleanup changed again to avoid super method updated super() functions updated to correct the parent classess targeted by super() --- src/Mod/Draft/draftobjects/dimension.py | 59 ++++++++++++++----- .../Draft/draftobjects/draft_annotation.py | 51 +++++----------- src/Mod/Draft/draftobjects/label.py | 23 +++++--- src/Mod/Draft/draftobjects/text.py | 36 ++++++++--- .../draftviewproviders/view_dimension.py | 25 ++++---- .../view_draft_annotation.py | 47 ++------------- .../Draft/draftviewproviders/view_label.py | 11 +++- src/Mod/Draft/draftviewproviders/view_text.py | 19 +++++- 8 files changed, 145 insertions(+), 126 deletions(-) diff --git a/src/Mod/Draft/draftobjects/dimension.py b/src/Mod/Draft/draftobjects/dimension.py index 6ee342c5aa..79d1262046 100644 --- a/src/Mod/Draft/draftobjects/dimension.py +++ b/src/Mod/Draft/draftobjects/dimension.py @@ -22,7 +22,6 @@ # * USA * # * * # *************************************************************************** - """This module provides the object code for Draft Dimension. """ ## @package dimension @@ -167,9 +166,10 @@ class DimensionBase(DraftAnnotation): """ def __init__(self, obj, tp = "Dimension"): - "Initialize common properties for dimension objects" - DraftAnnotation.__init__(self,obj, tp) + """Add common dimension properties to the object and set them""" + super(DimensionBase, self).__init__(obj, tp) + # Draft obj.addProperty("App::PropertyVector", "Normal", @@ -195,17 +195,22 @@ class DimensionBase(DraftAnnotation): QT_TRANSLATE_NOOP("App::Property", "Point on which the dimension \n" "line is placed.")) - - def onChanged(self,obj,prop): - - return + + obj.Dimline = App.Vector(0,1,0) + obj.Normal = App.Vector(0,0,1) def execute(self, obj): + '''Do something when recompute object''' return + def onChanged(self,obj,prop): + '''Do something when a property has changed''' + + return + class LinearDimension(DimensionBase): """ @@ -213,8 +218,17 @@ class LinearDimension(DimensionBase): """ def __init__(self, obj): - super().__init__(obj, "Dimension") - + + super(LinearDimension, self).__init__(obj, "LinearDimension") + + obj.Proxy = self + + self.init_properties(obj) + + + def init_properties(self, obj): + """Add Linear Dimension specific properties to the object and set them""" + # Draft obj.addProperty("App::PropertyVectorDistance", "Start", @@ -248,10 +262,9 @@ class LinearDimension(DimensionBase): obj.Start = App.Vector(0,0,0) obj.End = App.Vector(1,0,0) - obj.Dimline = App.Vector(0,1,0) - obj.Normal = App.Vector(0,0,1) def onChanged(self,obj,prop): + '''Do something when a property has changed''' if hasattr(obj, "Distance"): obj.setEditorMode('Distance', 1) #if hasattr(obj,"Normal"): @@ -261,7 +274,7 @@ class LinearDimension(DimensionBase): def execute(self, obj): - # set start point and end point according to the linked geometry + """ Set start point and end point according to the linked geometry""" if obj.LinkedGeometry: if len(obj.LinkedGeometry) == 1: lobj = obj.LinkedGeometry[0][0] @@ -324,7 +337,15 @@ class AngularDimension(DimensionBase): def __init__(self, obj): - super().__init__(obj,"AngularDimension") + super(AngularDimension, self).__init__(obj, "AngularDimension") + + self.init_properties(obj) + + obj.Proxy = self + + + def init_properties(self, obj): + """Add Angular Dimension specific properties to the object and set them""" obj.addProperty("App::PropertyAngle", "FirstAngle", @@ -356,7 +377,15 @@ class AngularDimension(DimensionBase): obj.Center = App.Vector(0,0,0) obj.Normal = App.Vector(0,0,1) + + def execute(self, fp): + '''Do something when recompute object''' + if fp.ViewObject: + fp.ViewObject.update() + + def onChanged(self,obj,prop): + '''Do something when a property has changed''' super().onChanged(obj, prop) if hasattr(obj,"Angle"): obj.setEditorMode('Angle',1) @@ -365,6 +394,4 @@ class AngularDimension(DimensionBase): if hasattr(obj,"Support"): obj.setEditorMode('Support',2) - def execute(self, fp): - if fp.ViewObject: - fp.ViewObject.update() + diff --git a/src/Mod/Draft/draftobjects/draft_annotation.py b/src/Mod/Draft/draftobjects/draft_annotation.py index c45a7041ca..1ee3ab5432 100644 --- a/src/Mod/Draft/draftobjects/draft_annotation.py +++ b/src/Mod/Draft/draftobjects/draft_annotation.py @@ -31,57 +31,34 @@ import FreeCAD as App from PySide.QtCore import QT_TRANSLATE_NOOP from draftutils import gui_utils -class DraftAnnotation: +class DraftAnnotation(object): """The Draft Annotation Base object This class is not used directly, but inherited by all annotation objects. """ - def __init__(self, obj, tp="Unknown"): - if obj: - obj.Proxy = self + def __init__(self, obj, tp="Annotation"): + """Add general Annotation properties to the object""" + self.Type = tp + def __getstate__(self): return self.Type + def __setstate__(self,state): if state: self.Type = state + def execute(self,obj): - pass + '''Do something when recompute object''' + + return + def onChanged(self, obj, prop): - pass - -class StylesContainerBase: - """The Base class for Annotation Containers""" - - def __init__(self, obj, tp = "AnnotationContainer"): - - self.Type = tp - obj.Proxy = self - - def execute(self, obj): - - g = obj.Group - g.sort(key=lambda o: o.Label) - obj.Group = g - - def make_unique_visible(self, obj, active_style): - "turn non visible all the concurrent styles" - if hasattr(active_style, "Visibility"): - for o in obj.Group: - if o.Name != active_style.Name: - if hasattr(o, "Visibility"): - o.Visibility = False - - -class AnnotationStylesContainer(StylesContainerBase): - """The Annotation Container""" - - def __init__(self, obj): - - self.Type = "AnnotationContainer" - obj.Proxy = self + '''Do something when a property has changed''' + + return diff --git a/src/Mod/Draft/draftobjects/label.py b/src/Mod/Draft/draftobjects/label.py index 31181cc4cd..6d6af72010 100644 --- a/src/Mod/Draft/draftobjects/label.py +++ b/src/Mod/Draft/draftobjects/label.py @@ -106,9 +106,17 @@ def make_label(targetpoint=None, target=None, direction=None, class Label(DraftAnnotation): """The Draft Label object""" - def __init__(self,obj): + def __init__(self, obj): - super().__init__(obj, "Label") + super(Label, self).__init__(obj, "Label") + + self.init_properties(obj) + + obj.Proxy = self + + + def init_properties(self, obj): + """Add properties to the object and set them""" obj.addProperty("App::PropertyPlacement", "Placement", @@ -174,6 +182,7 @@ class Label(DraftAnnotation): def execute(self,obj): + '''Do something when recompute object''' if obj.StraightDirection != "Custom": p1 = obj.Placement.Base @@ -223,12 +232,8 @@ class Label(DraftAnnotation): if hasattr(obj.Target[0].Shape,"Volume"): obj.Text = [App.Units.Quantity(obj.Target[0].Shape.Volume,App.Units.Volume).UserString.replace("^3","³")] + def onChanged(self,obj,prop): - pass + '''Do something when a property has changed''' - def __getstate__(self): - return self.Type - - def __setstate__(self,state): - if state: - self.Type = state \ No newline at end of file + return \ No newline at end of file diff --git a/src/Mod/Draft/draftobjects/text.py b/src/Mod/Draft/draftobjects/text.py index ef5278144f..4ad02b3488 100644 --- a/src/Mod/Draft/draftobjects/text.py +++ b/src/Mod/Draft/draftobjects/text.py @@ -42,7 +42,7 @@ if App.GuiUp: def make_text(stringslist, point=App.Vector(0,0,0), screen=False): - """makeText(strings,[point],[screen]) + """makeText(strings, point, screen) Creates a Text object containing the given strings. The current color and text height and font @@ -51,22 +51,24 @@ def make_text(stringslist, point=App.Vector(0,0,0), screen=False): Parameters ---------- stringlist : List - Given list of strings, one string by line (strings can also - be one single string) + Given list of strings, one string by line (strings can also + be one single string) point : App::Vector - + insert point of the text screen : Bool - If screen is True, the text always faces the view direction. + If screen is True, the text always faces the view direction. """ if not App.ActiveDocument: App.Console.PrintError("No active document. Aborting\n") return + utils.type_check([(point, App.Vector)], "makeText") if not isinstance(stringslist,list): stringslist = [stringslist] + obj = App.ActiveDocument.addObject("App::FeaturePython","Text") Text(obj) obj.Text = stringslist @@ -92,9 +94,17 @@ def make_text(stringslist, point=App.Vector(0,0,0), screen=False): class Text(DraftAnnotation): """The Draft Text object""" - def __init__(self,obj): + def __init__(self, obj): - super().__init__(obj, "Text") + super(Text, self).__init__(obj, "Text") + + self.init_properties(obj) + + obj.Proxy = self + + + def init_properties(self, obj): + """Add Text specific properties to the object and set them""" obj.addProperty("App::PropertyPlacement", "Placement", @@ -108,6 +118,14 @@ class Text(DraftAnnotation): QT_TRANSLATE_NOOP("App::Property", "The text displayed by this object")) - def execute(self,obj): - pass \ No newline at end of file + def execute(self,obj): + '''Do something when recompute object''' + + return + + + def onChanged(self,obj,prop): + '''Do something when a property has changed''' + + return diff --git a/src/Mod/Draft/draftviewproviders/view_dimension.py b/src/Mod/Draft/draftviewproviders/view_dimension.py index 5f89a0dff9..b73553f21a 100644 --- a/src/Mod/Draft/draftviewproviders/view_dimension.py +++ b/src/Mod/Draft/draftviewproviders/view_dimension.py @@ -87,6 +87,8 @@ class ViewProviderDimensionBase(ViewProviderDraftAnnotation): """ def __init__(self, vobj): + super(ViewProviderDimensionBase, self).__init__(vobj) + # text properties vobj.addProperty("App::PropertyFont","FontName", "Text", @@ -171,12 +173,6 @@ class ViewProviderDimensionBase(ViewProviderDraftAnnotation): vobj.ShowUnit = utils.get_param("showUnit",True) vobj.ShowLine = True - super().__init__(vobj) - - def attach(self, vobj): - """called on object creation""" - return - def updateData(self, obj, prop): """called when the base object is changed""" return @@ -219,11 +215,16 @@ class ViewProviderLinearDimension(ViewProviderDimensionBase): """ A View Provider for the Draft Linear Dimension object """ - def __init__(self, vobj): - super().__init__(vobj) + def __init__(self, vobj): + + super(ViewProviderLinearDimension, self).__init__(vobj) + + self.Object = vobj.Object + vobj.Proxy = self + def attach(self, vobj): - """called on object creation""" + '''Setup the scene sub-graph of the view provider''' self.Object = vobj.Object self.color = coin.SoBaseColor() self.font = coin.SoFont() @@ -664,13 +665,17 @@ class ViewProviderAngularDimension(ViewProviderDimensionBase): """A View Provider for the Draft Angular Dimension object""" def __init__(self, vobj): + super(ViewProviderAngularDimension, self).__init__(vobj) + vobj.addProperty("App::PropertyBool","FlipArrows", "Graphics",QT_TRANSLATE_NOOP("App::Property", "Rotate the dimension arrows 180 degrees")) - super().__init__(vobj) + self.Object = vobj.Object + vobj.Proxy = self def attach(self, vobj): + '''Setup the scene sub-graph of the view provider''' from pivy import coin self.Object = vobj.Object self.color = coin.SoBaseColor() diff --git a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py index e3ac148f8d..4f386a8686 100644 --- a/src/Mod/Draft/draftviewproviders/view_draft_annotation.py +++ b/src/Mod/Draft/draftviewproviders/view_draft_annotation.py @@ -32,46 +32,8 @@ from PySide.QtCore import QT_TRANSLATE_NOOP if App.GuiUp: import FreeCADGui as Gui -class ViewProviderStylesContainerBase: - """A Base View Provider for Annotation Styles Containers""" - def __init__(self, vobj): - - vobj.Proxy = self - - def getIcon(self): - - return ":/icons/Draft_Annotation_Style.svg" - - def attach(self, vobj): - - self.Object = vobj.Object - - def __getstate__(self): - - return None - - def __setstate__(self, state): - - return None - - -class ViewProviderAnnotationStylesContainer(ViewProviderStylesContainerBase): - """A View Provider for the Annotation Styles Container""" - - def __init__(self, vobj): - super().__init__(vobj) - - def getIcon(self): - - return ":/icons/Draft_Annotation_Style.svg" - - def attach(self, vobj): - - self.Object = vobj.Object - - -class ViewProviderDraftAnnotation: +class ViewProviderDraftAnnotation(object): """ The base class for Draft Annotation Viewproviders This class is not used directly, but inherited by all annotation @@ -79,8 +41,8 @@ class ViewProviderDraftAnnotation: """ def __init__(self, vobj): - vobj.Proxy = self - self.Object = vobj.Object + #vobj.Proxy = self + #self.Object = vobj.Object # annotation properties vobj.addProperty("App::PropertyFloat","ScaleMultiplier", @@ -95,7 +57,8 @@ class ViewProviderDraftAnnotation: param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) - vobj.ScaleMultiplier = 1 / annotation_scale + if annotation_scale != 0: + vobj.ScaleMultiplier = 1 / annotation_scale def __getstate__(self): diff --git a/src/Mod/Draft/draftviewproviders/view_label.py b/src/Mod/Draft/draftviewproviders/view_label.py index 1d131b946b..8f48ac063e 100644 --- a/src/Mod/Draft/draftviewproviders/view_label.py +++ b/src/Mod/Draft/draftviewproviders/view_label.py @@ -46,7 +46,14 @@ class ViewProviderLabel(ViewProviderDraftAnnotation): def __init__(self,vobj): - super().__init__(vobj) + super(ViewProviderLabel, self).__init__(vobj) + + self.set_properties(vobj) + + self.Object = vobj.Object + vobj.Proxy = self + + def set_properties(self, vobj): # Text properties @@ -96,7 +103,6 @@ class ViewProviderLabel(ViewProviderDraftAnnotation): "Graphics",QT_TRANSLATE_NOOP("App::Property", "Line color")) - vobj.Proxy = self self.Object = vobj.Object vobj.TextAlignment = ["Top","Middle","Bottom"] vobj.TextAlignment = "Middle" @@ -120,6 +126,7 @@ class ViewProviderLabel(ViewProviderDraftAnnotation): return [] def attach(self,vobj): + '''Setup the scene sub-graph of the view provider''' self.arrow = coin.SoSeparator() self.arrowpos = coin.SoTransform() self.arrow.addChild(self.arrowpos) diff --git a/src/Mod/Draft/draftviewproviders/view_text.py b/src/Mod/Draft/draftviewproviders/view_text.py index 8f08fd7f8b..bbf2636315 100644 --- a/src/Mod/Draft/draftviewproviders/view_text.py +++ b/src/Mod/Draft/draftviewproviders/view_text.py @@ -41,9 +41,18 @@ from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation class ViewProviderText(ViewProviderDraftAnnotation): """A View Provider for the Draft Text annotation""" + def __init__(self,vobj): - super().__init__(vobj) + super(ViewProviderText, self).__init__(vobj) + + self.set_properties(vobj) + + self.Object = vobj.Object + vobj.Proxy = self + + + def set_properties(self, vobj): vobj.addProperty("App::PropertyFloat","ScaleMultiplier", "Annotation",QT_TRANSLATE_NOOP("App::Property", @@ -73,13 +82,17 @@ class ViewProviderText(ViewProviderDraftAnnotation): vobj.FontName = utils.get_param("textfont","sans") vobj.FontSize = utils.get_param("textheight",1) + def getIcon(self): return ":/icons/Draft_Text.svg" + def claimChildren(self): return [] + def attach(self,vobj): + '''Setup the scene sub-graph of the view provider''' self.mattext = coin.SoMaterial() textdrawstyle = coin.SoDrawStyle() textdrawstyle.style = coin.SoDrawStyle.FILLED @@ -110,12 +123,15 @@ class ViewProviderText(ViewProviderDraftAnnotation): self.onChanged(vobj,"Justification") self.onChanged(vobj,"LineSpacing") + def getDisplayModes(self,vobj): return ["2D text","3D text"] + def setDisplayMode(self,mode): return mode + def updateData(self,obj,prop): if prop == "Text": if obj.Text: @@ -129,6 +145,7 @@ class ViewProviderText(ViewProviderDraftAnnotation): self.trans.translation.setValue(obj.Placement.Base) self.trans.rotation.setValue(obj.Placement.Rotation.Q) + def onChanged(self,vobj,prop): if prop == "ScaleMultiplier": if "ScaleMultiplier" in vobj.PropertiesList and "FontSize" in vobj.PropertiesList: From a104ece52e0ce51024cd0464517d5fa9f7abe9be Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Wed, 11 Mar 2020 21:37:48 -0600 Subject: [PATCH 094/142] Draft: update InitGui with context commands The `ContextMenu` method defines commands that will be listed when right clicking and opening the context menu in the 3D view or the tree view. This sets up the line GUI commands when either a line, wire, polyline, spline, or bezier curve is active. However, this currently doesn't work at all for unknown reasons. Maybe some other functionality in the internal C++ code needs to be changed first. --- src/Mod/Draft/InitGui.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Mod/Draft/InitGui.py b/src/Mod/Draft/InitGui.py index 0178ec4978..8c3dba77c1 100644 --- a/src/Mod/Draft/InitGui.py +++ b/src/Mod/Draft/InitGui.py @@ -156,8 +156,15 @@ class DraftWorkbench(FreeCADGui.Workbench): else: self.appendContextMenu("Draft", self.drawing_commands) else: - if FreeCAD.activeDraftCommand.featureName == translate("draft","Line"): - # BUG: line subcommands are not usable while another command is active + if FreeCAD.activeDraftCommand.featureName in (translate("draft", "Line"), + translate("draft", "Wire"), + translate("draft", "Polyline"), + translate("draft", "BSpline"), + translate("draft", "BezCurve"), + translate("draft", "CubicBezCurve")): + # BUG: the line subcommands are in fact listed + # in the context menu, but they are de-activated + # so they don't work. self.appendContextMenu("", self.line_commands) else: if FreeCADGui.Selection.getSelection(): From 017226e8037504a291262862a56e956afde853fb Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 16 Mar 2020 22:45:21 -0600 Subject: [PATCH 095/142] Draft: GuiCommandSimplest to serve as base of simple Gui Commands This class defines the `command_name` of the command, so that it is output to the report view, and is also recorded in the log file. It also stores the current document so it can be used inside the command. The class implements with `IsActive` method so that the command is only active when an active document exists. Also `GuiCommandNeedsSelection`, which subclasses the former class. It reimplements `IsActive` in order to be available only when there is a selection. --- src/Mod/Draft/draftguitools/gui_base.py | 94 +++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/Mod/Draft/draftguitools/gui_base.py b/src/Mod/Draft/draftguitools/gui_base.py index 40b0660ccc..d3f072b7d3 100644 --- a/src/Mod/Draft/draftguitools/gui_base.py +++ b/src/Mod/Draft/draftguitools/gui_base.py @@ -30,6 +30,100 @@ import FreeCAD as App import FreeCADGui as Gui import draftutils.todo as todo +from draftutils.messages import _msg, _log + + +class GuiCommandSimplest: + """Simplest base class for GuiCommands. + + This class only sets up the command name and the document object + to use for the command. + When it is executed, it logs the command name to the log file, + and prints the command name to the console. + + It implements the `IsActive` method, which must return `True` + when the command should be available. + It should return `True` when there is an active document, + otherwise the command (button or menu) should be disabled. + + This class is meant to be inherited by other GuiCommand classes + to quickly log the command name, and set the correct document object. + + Parameter + --------- + name: str, optional + It defaults to `'None'`. + The name of the action that is being run, + for example, `'Heal'`, `'Flip dimensions'`, + `'Line'`, `'Circle'`, etc. + + doc: App::Document, optional + It defaults to the value of `App.activeDocument()`. + The document object itself, which indicates where the actions + of the command will be executed. + + Attributes + ---------- + command_name: str + This is the command name, which is assigned by `name`. + + doc: App::Document + This is the document object itself, which is assigned by `doc`. + + This attribute should be used by functions to make sure + that the operations are performed in the correct document + and not in other documents. + To set the active document we can use + + >>> App.setActiveDocument(self.doc.Name) + """ + + def __init__(self, name="None", doc=App.activeDocument()): + self.command_name = name + self.doc = doc + + def IsActive(self): + """Return True when this command should be available. + + It is `True` when there is a document. + """ + if App.activeDocument(): + return True + else: + return False + + def Activated(self): + """Execute when the command is called. + + Log the command name to the log file and console. + Also update the `doc` attribute. + """ + self.doc = App.activeDocument() + _log("Document: {}".format(self.doc.Label)) + _log("GuiCommand: {}".format(self.command_name)) + _msg("{}".format(16*"-")) + _msg("GuiCommand: {}".format(self.command_name)) + + +class GuiCommandNeedsSelection(GuiCommandSimplest): + """Base class for GuiCommands that need a selection to be available. + + It re-implements the `IsActive` method to return `True` + when there is both an active document and an active selection. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def IsActive(self): + """Return True when this command should be available. + + It is `True` when there is a selection. + """ + if App.activeDocument() and Gui.Selection.getSelection(): + return True + else: + return False class GuiCommandSimplest: From 7189d3de056206097cce3254c8f359b558d30aaf Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Wed, 11 Mar 2020 17:48:19 -0600 Subject: [PATCH 096/142] Draft: move line GuiCommands to gui_lineops module The commands `FinishLine`, `CloseLine`, and `UndoLine` are moved from the huge `DraftTools.py` to `gui_lineops.py` to reduce the complexity of the former file. These GuiCommands aren't actually used presently in the Draft Workbench. They were used in the past particularly from the context menu when editing a Line object. --- src/Mod/Draft/CMakeLists.txt | 1 + src/Mod/Draft/DraftTools.py | 66 +-------- src/Mod/Draft/draftguitools/gui_lineops.py | 163 +++++++++++++++++++++ 3 files changed, 167 insertions(+), 63 deletions(-) create mode 100644 src/Mod/Draft/draftguitools/gui_lineops.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 40687f5a31..5d3a234361 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -96,6 +96,7 @@ SET(Draft_GUI_tools draftguitools/gui_snapper.py draftguitools/gui_trackers.py draftguitools/gui_edit.py + draftguitools/gui_lineops.py draftguitools/README.md ) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 3f9b08caed..e76434bb95 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -77,6 +77,9 @@ if not hasattr(FreeCAD, "DraftWorkingPlane"): import draftguitools.gui_edit import draftguitools.gui_selectplane import draftguitools.gui_planeproxy +from draftguitools.gui_lineops import FinishLine +from draftguitools.gui_lineops import CloseLine +from draftguitools.gui_lineops import UndoLine # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -980,66 +983,6 @@ class CubicBezCurve(Line): self.Activated() -class FinishLine: - """a FreeCAD command to finish any running Line drawing operation""" - - def Activated(self): - if (FreeCAD.activeDraftCommand != None): - if (FreeCAD.activeDraftCommand.featureName == "Line"): - FreeCAD.activeDraftCommand.finish(False) - - def GetResources(self): - return {'Pixmap' : 'Draft_Finish', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_FinishLine", "Finish line"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_FinishLine", "Finishes a line without closing it")} - - def IsActive(self): - if FreeCADGui.ActiveDocument: - return True - else: - return False - - -class CloseLine: - """a FreeCAD command to close any running Line drawing operation""" - - def Activated(self): - if (FreeCAD.activeDraftCommand != None): - if (FreeCAD.activeDraftCommand.featureName == "Line"): - FreeCAD.activeDraftCommand.finish(True) - - def GetResources(self): - return {'Pixmap' : 'Draft_Lock', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_CloseLine", "Close Line"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_CloseLine", "Closes the line being drawn")} - - def IsActive(self): - if FreeCADGui.ActiveDocument: - return True - else: - return False - - -class UndoLine: - """a FreeCAD command to undo last drawn segment of a line""" - - def Activated(self): - if (FreeCAD.activeDraftCommand != None): - if (FreeCAD.activeDraftCommand.featureName == "Line"): - FreeCAD.activeDraftCommand.undolast() - - def GetResources(self): - return {'Pixmap' : 'Draft_Rotate', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_UndoLine", "Undo last segment"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_UndoLine", "Undoes the last drawn segment of the line being drawn")} - - def IsActive(self): - if FreeCADGui.ActiveDocument: - return True - else: - return False - - class Rectangle(Creator): """the Draft_Rectangle FreeCAD command definition""" @@ -5501,9 +5444,6 @@ FreeCADGui.addCommand('Draft_Slope',Draft_Slope()) FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands -FreeCADGui.addCommand('Draft_FinishLine',FinishLine()) -FreeCADGui.addCommand('Draft_CloseLine',CloseLine()) -FreeCADGui.addCommand('Draft_UndoLine',UndoLine()) FreeCADGui.addCommand('Draft_ToggleConstructionMode',ToggleConstructionMode()) FreeCADGui.addCommand('Draft_ToggleContinueMode',ToggleContinueMode()) FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) diff --git a/src/Mod/Draft/draftguitools/gui_lineops.py b/src/Mod/Draft/draftguitools/gui_lineops.py new file mode 100644 index 0000000000..75d0d589fc --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_lineops.py @@ -0,0 +1,163 @@ +# *************************************************************************** +# * (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 certain line operations of the Draft Workbench. + +These GuiCommands aren't really used anymore, as the same actions +are called from the task panel interface by other methods. +""" +## @package gui_lineops +# \ingroup DRAFT +# \brief Provides certain line operations in the Draft Workbench. +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc +import draftguitools.gui_base as gui_base +from draftutils.messages import _msg +from draftutils.translate import _tr + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class LineAction(gui_base.GuiCommandSimplest): + """Base class for Line context GuiCommands. + + This is inherited by the other GuiCommand classes to run + a set of similar actions when editing a line, wire, spline, + or bezier curve. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def Activated(self, action="None"): + """Execute when the command is called. + + Parameters + ---------- + action: str + Indicates the type of action to perform with the line object. + It can be `'finish'`, `'close'`, or `'undo'`. + """ + if hasattr(App, "activeDraftCommand"): + _command = App.activeDraftCommand + else: + _msg(_tr("No active command.")) + return + + if (_command is not None + and _command.featureName in ("Line", "Polyline", + "BSpline", "BezCurve", + "CubicBezCurve")): + if action == "finish": + _command.finish(False) + elif action == "close": + _command.finish(True) + elif action == "undo": + _command.undolast() + + +class FinishLine(LineAction): + """GuiCommand to finish any running line drawing operation.""" + + def __init__(self): + super().__init__(name=_tr("Finish line")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Finishes a line without closing it." + + d = {'Pixmap': 'Draft_Finish', + 'MenuText': QT_TRANSLATE_NOOP("Draft_FinishLine", "Finish line"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_FinishLine", _tip), + 'CmdType': 'ForEdit'} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `finish(False)` method of the active Draft command. + """ + super().Activated(action="finish") + + +Gui.addCommand('Draft_FinishLine', FinishLine()) + + +class CloseLine(LineAction): + """GuiCommand to close the line being drawn and finish the operation.""" + + def __init__(self): + super().__init__(name=_tr("Close line")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Closes the line being drawn, and finishes the operation." + + d = {'Pixmap': 'Draft_Lock', + 'MenuText': QT_TRANSLATE_NOOP("Draft_CloseLine", "Close Line"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_CloseLine", _tip), + 'CmdType': 'ForEdit'} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `finish(True)` method of the active Draft command. + """ + super().Activated(action="close") + + +Gui.addCommand('Draft_CloseLine', CloseLine()) + + +class UndoLine(LineAction): + """GuiCommand to undo the last drawn segment of a line.""" + + def __init__(self): + super().__init__(name=_tr("Undo line")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Undoes the last drawn segment of the line being drawn." + + d = {'Pixmap': 'Draft_Rotate', + 'MenuText': QT_TRANSLATE_NOOP("Draft_UndoLine", + "Undo last segment"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_UndoLine", _tip), + 'CmdType': 'ForEdit'} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `undolast` method of the active Draft command. + """ + super().Activated(action="undo") + + +Gui.addCommand('Draft_UndoLine', UndoLine()) From 9758d983ef580520b7cc6ddaf75a96bd4407f319 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Thu, 12 Mar 2020 00:00:15 -0600 Subject: [PATCH 097/142] Draft: move mode commands to gui_togglemodes module `Draft_ToggleConstructionMode` and `Draft_ToggleContinueMode`. They call a base class `BaseMode` which also uses the base `gui_base.GuiCommandSimplest` class. Also add a new icon for continue mode. --- src/Mod/Draft/CMakeLists.txt | 1 + src/Mod/Draft/DraftTools.py | 28 +--- src/Mod/Draft/Resources/Draft.qrc | 1 + .../Draft/Resources/icons/Draft_Continue.svg | 146 +++++++++++++++++ .../Draft/draftguitools/gui_togglemodes.py | 153 ++++++++++++++++++ 5 files changed, 303 insertions(+), 26 deletions(-) create mode 100644 src/Mod/Draft/Resources/icons/Draft_Continue.svg create mode 100644 src/Mod/Draft/draftguitools/gui_togglemodes.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 5d3a234361..2b903053fe 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -97,6 +97,7 @@ SET(Draft_GUI_tools draftguitools/gui_trackers.py draftguitools/gui_edit.py draftguitools/gui_lineops.py + draftguitools/gui_togglemodes.py draftguitools/README.md ) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index e76434bb95..b59b4dedea 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -80,6 +80,8 @@ import draftguitools.gui_planeproxy from draftguitools.gui_lineops import FinishLine from draftguitools.gui_lineops import CloseLine from draftguitools.gui_lineops import UndoLine +from draftguitools.gui_togglemodes import ToggleConstructionMode +from draftguitools.gui_togglemodes import ToggleContinueMode # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -4124,30 +4126,6 @@ class Scale(Modifier): for ghost in self.ghosts: ghost.finalize() -class ToggleConstructionMode(): - """The Draft_ToggleConstructionMode FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_Construction', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleConstructionMode", "Toggle Construction Mode"), - 'Accel' : "C, M", - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleConstructionMode", "Toggles the Construction Mode for next objects.")} - - def Activated(self): - FreeCADGui.draftToolBar.constrButton.toggle() - - -class ToggleContinueMode(): - """The Draft_ToggleContinueMode FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_Rotate', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleContinueMode", "Toggle Continue Mode"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleContinueMode", "Toggles the Continue Mode for next commands.")} - - def Activated(self): - FreeCADGui.draftToolBar.toggleContinue() - class Drawing(Modifier): """The Draft Drawing command definition""" @@ -5444,8 +5422,6 @@ FreeCADGui.addCommand('Draft_Slope',Draft_Slope()) FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands -FreeCADGui.addCommand('Draft_ToggleConstructionMode',ToggleConstructionMode()) -FreeCADGui.addCommand('Draft_ToggleContinueMode',ToggleContinueMode()) FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) FreeCADGui.addCommand('Draft_ToggleDisplayMode',ToggleDisplayMode()) FreeCADGui.addCommand('Draft_AddToGroup',AddToGroup()) diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index 5e0c16a03b..45efb36c65 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -20,6 +20,7 @@ icons/Draft_CircularArray.svg icons/Draft_Clone.svg icons/Draft_Construction.svg + icons/Draft_Continue.svg icons/Draft_CubicBezCurve.svg icons/Draft_Cursor.svg icons/Draft_DelPoint.svg diff --git a/src/Mod/Draft/Resources/icons/Draft_Continue.svg b/src/Mod/Draft/Resources/icons/Draft_Continue.svg new file mode 100644 index 0000000000..519d134230 --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_Continue.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Mon Oct 10 13:44:52 2011 +0000 + + + [vocx] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_Continue.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson, [wmayer] + + + + + arrow + double + right + + + A large blue arrow pointing to the right, and another one following it. + + + + + + + + + + diff --git a/src/Mod/Draft/draftguitools/gui_togglemodes.py b/src/Mod/Draft/draftguitools/gui_togglemodes.py new file mode 100644 index 0000000000..e0eb690ae4 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_togglemodes.py @@ -0,0 +1,153 @@ +# *************************************************************************** +# * (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 to control the mode of other tools in the Draft Workbench. + +For example, a construction mode, and a continue mode to repeat commands. +""" +## @package gui_togglemodes +# \ingroup DRAFT +# \brief Provides certain mode operations of the Draft Workbench. +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import Draft_rc +import draftguitools.gui_base as gui_base +from draftutils.messages import _msg +from draftutils.translate import _tr + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class BaseMode(gui_base.GuiCommandSimplest): + """Base class for mode context GuiCommands. + + This is inherited by the other GuiCommand classes to run + a set of similar actions when changing modes. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def Activated(self, mode="None"): + """Execute when the command is called. + + Parameters + ---------- + action: str + Indicates the type of mode to switch to. + It can be `'construction'` or `'continue'`. + """ + super().Activated() + + if hasattr(Gui, "draftToolBar"): + _ui = Gui.draftToolBar + else: + _msg(_tr("No active Draft Toolbar.")) + return + + if _ui is not None: + if mode == "construction" and hasattr(_ui, "constrButton"): + _ui.constrButton.toggle() + elif mode == "continue": + _ui.toggleContinue() + + +class ToggleConstructionMode(BaseMode): + """GuiCommand for the Draft_ToggleConstructionMode tool. + + When construction mode is active, the following objects created + will be included in the construction group, and will be drawn + with the specified color and properties. + """ + + def __init__(self): + super().__init__(name=_tr("Construction mode")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Toggle construction mode" + _tip = ("Toggles the Construction mode.\n" + "When this is active, the following objects created " + "will be included in the construction group, " + "and will be drawn with the specified color " + "and properties.") + + d = {'Pixmap': 'Draft_Construction', + 'MenuText': QT_TRANSLATE_NOOP("Draft_ToggleConstructionMode", + _menu), + 'Accel': "C, M", + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ToggleConstructionMode", + _tip)} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `toggle()` method of the construction button + in the `DraftToolbar` class. + """ + super().Activated(mode="construction") + + +Gui.addCommand('Draft_ToggleConstructionMode', ToggleConstructionMode()) + + +class ToggleContinueMode(BaseMode): + """GuiCommand for the Draft_ToggleContinueMode tool. + + When continue mode is active, any drawing tool that is terminated + will automatically start again. This can be used to draw several + objects one after the other in succession. + """ + + def __init__(self): + super().__init__(name=_tr("Continue mode")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Toggle continue mode" + _tip = ("Toggles the Continue mode.\n" + "When this is active, any drawing tool that is terminated " + "will automatically start again.\n" + "This can be used to draw several objects " + "one after the other in succession.") + + d = {'Pixmap': 'Draft_Continue', + 'MenuText': QT_TRANSLATE_NOOP("Draft_ToggleContinueMode", + _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ToggleContinueMode", + _tip)} + return d + + def Activated(self): + """Execute when the command is called. + + It calls the `toggleContinue()` method of the `DraftToolbar` class. + """ + super().Activated(mode="continue") + + +Gui.addCommand('Draft_ToggleContinueMode', ToggleContinueMode()) From 14352677c5c1dd316c243417f027722f0fc1f657 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Thu, 12 Mar 2020 00:34:16 -0600 Subject: [PATCH 098/142] Draft: move DisplayMode command to gui_togglemodes module --- src/Mod/Draft/DraftTools.py | 26 +-------- .../Draft/draftguitools/gui_togglemodes.py | 56 ++++++++++++++++++- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index b59b4dedea..82e0ed2990 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -82,6 +82,7 @@ from draftguitools.gui_lineops import CloseLine from draftguitools.gui_lineops import UndoLine from draftguitools.gui_togglemodes import ToggleConstructionMode from draftguitools.gui_togglemodes import ToggleContinueMode +from draftguitools.gui_togglemodes import ToggleDisplayMode # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -4198,30 +4199,6 @@ class Drawing(Modifier): return page -class ToggleDisplayMode(): - """The ToggleDisplayMode FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_SwitchMode', - 'Accel' : "Shift+Space", - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleDisplayMode", "Toggle display mode"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_ToggleDisplayMode", "Swaps display mode of selected objects between wireframe and flatlines")} - - def IsActive(self): - if FreeCADGui.Selection.getSelection(): - return True - else: - return False - - def Activated(self): - for obj in FreeCADGui.Selection.getSelection(): - if obj.ViewObject.DisplayMode == "Flat Lines": - if "Wireframe" in obj.ViewObject.listDisplayModes(): - obj.ViewObject.DisplayMode = "Wireframe" - elif obj.ViewObject.DisplayMode == "Wireframe": - if "Flat Lines" in obj.ViewObject.listDisplayModes(): - obj.ViewObject.DisplayMode = "Flat Lines" - class SubelementHighlight(Modifier): """The Draft_SubelementHighlight FreeCAD command definition""" @@ -5423,7 +5400,6 @@ FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) -FreeCADGui.addCommand('Draft_ToggleDisplayMode',ToggleDisplayMode()) FreeCADGui.addCommand('Draft_AddToGroup',AddToGroup()) FreeCADGui.addCommand('Draft_SelectGroup',SelectGroup()) FreeCADGui.addCommand('Draft_Shape2DView',Shape2DView()) diff --git a/src/Mod/Draft/draftguitools/gui_togglemodes.py b/src/Mod/Draft/draftguitools/gui_togglemodes.py index e0eb690ae4..2a6644e5ce 100644 --- a/src/Mod/Draft/draftguitools/gui_togglemodes.py +++ b/src/Mod/Draft/draftguitools/gui_togglemodes.py @@ -24,7 +24,8 @@ # *************************************************************************** """Provides tools to control the mode of other tools in the Draft Workbench. -For example, a construction mode, and a continue mode to repeat commands. +For example, a construction mode, a continue mode to repeat commands, +and to toggle the appearance of certain shapes to wireframe. """ ## @package gui_togglemodes # \ingroup DRAFT @@ -151,3 +152,56 @@ class ToggleContinueMode(BaseMode): Gui.addCommand('Draft_ToggleContinueMode', ToggleContinueMode()) + + +class ToggleDisplayMode(gui_base.GuiCommandNeedsSelection): + """GuiCommand for the Draft_ToggleDisplayMode tool. + + Switches the display mode of selected objects from flatlines + to wireframe and back. + + It inherits `GuiCommandNeedsSelection` to only be availbale + when there is a document and a selection. + See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Toggle display mode")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Toggle normal/wireframe display" + _tip = ("Switches the display mode of selected objects " + "from flatlines to wireframe and back.\n" + "This is helpful to quickly visualize objects " + "that are hidden by other objects.\n" + "This is intended to be used with closed shapes " + "and solids, and doesn't affect open wires.") + + d = {'Pixmap': 'Draft_SwitchMode', + 'Accel': "Shift+Space", + 'MenuText': QT_TRANSLATE_NOOP("Draft_ToggleDisplayMode", + _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ToggleDisplayMode", + _tip)} + return d + + def Activated(self): + """Execute when the command is called. + + It tests the view provider of the selected objects + and changes their `DisplayMode` from `'Wireframe'` + to `'Flat Lines'`, and the other way around, if possible. + """ + super().Activated() + + for obj in Gui.Selection.getSelection(): + if obj.ViewObject.DisplayMode == "Flat Lines": + if "Wireframe" in obj.ViewObject.listDisplayModes(): + obj.ViewObject.DisplayMode = "Wireframe" + elif obj.ViewObject.DisplayMode == "Wireframe": + if "Flat Lines" in obj.ViewObject.listDisplayModes(): + obj.ViewObject.DisplayMode = "Flat Lines" + + +Gui.addCommand('Draft_ToggleDisplayMode', ToggleDisplayMode()) From 8d94a0e33402db1324e1f8b4fed035eb558810ea Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sun, 12 Apr 2020 22:05:30 -0500 Subject: [PATCH 099/142] Draft: new utility toolbar with some useful commands These commands are useful but are "hiden" inside the "Utilities" menu, so not many poeple know about them. By placing them in a toolbar, they will be more discoverable. --- src/Mod/Draft/InitGui.py | 2 ++ src/Mod/Draft/draftutils/init_tools.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Mod/Draft/InitGui.py b/src/Mod/Draft/InitGui.py index 8c3dba77c1..bbbc7c0fca 100644 --- a/src/Mod/Draft/InitGui.py +++ b/src/Mod/Draft/InitGui.py @@ -102,11 +102,13 @@ class DraftWorkbench(FreeCADGui.Workbench): self.context_commands = it.get_draft_context_commands() self.line_commands = it.get_draft_line_commands() self.utility_commands = it.get_draft_utility_commands() + self.utility_small = it.get_draft_small_commands() # Set up toolbars self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft creation tools"), self.drawing_commands) self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft annotation tools"), self.annotation_commands) self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft modification tools"), self.modification_commands) + self.appendToolbar(QT_TRANSLATE_NOOP("Draft", "Draft utility tools"), self.utility_small) # Set up menus self.appendMenu(QT_TRANSLATE_NOOP("Draft", "&Drafting"), self.drawing_commands) diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index d9e71c7578..eec5493a37 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -57,6 +57,13 @@ def get_draft_array_commands(): return ["Draft_ArrayTools"] +def get_draft_small_commands(): + """Return a list with only some utilities.""" + return ["Draft_Layer", + "Draft_WorkingPlaneProxy", + "Draft_ToggleDisplayMode"] + + def get_draft_modification_commands(): """Return the modification commands list.""" lst = ["Draft_Move", "Draft_Rotate", @@ -74,8 +81,7 @@ def get_draft_modification_commands(): "Separator", "Draft_WireToBSpline", "Draft_Draft2Sketch", "Separator", - "Draft_Shape2DView", "Draft_Drawing", - "Draft_WorkingPlaneProxy"] + "Draft_Shape2DView", "Draft_Drawing"] return lst From d05a786e570fcca7237fdcbc559b57baaa37af62 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sun, 15 Mar 2020 00:49:01 -0600 Subject: [PATCH 100/142] Draft: move AddToGroup command to gui_groups module --- src/Mod/Draft/CMakeLists.txt | 1 + src/Mod/Draft/DraftTools.py | 45 +------- src/Mod/Draft/draftguitools/gui_groups.py | 128 ++++++++++++++++++++++ src/Mod/Draft/draftutils/init_tools.py | 3 +- 4 files changed, 132 insertions(+), 45 deletions(-) create mode 100644 src/Mod/Draft/draftguitools/gui_groups.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 2b903053fe..b5048c9286 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -98,6 +98,7 @@ SET(Draft_GUI_tools draftguitools/gui_edit.py draftguitools/gui_lineops.py draftguitools/gui_togglemodes.py + draftguitools/gui_groups.py draftguitools/README.md ) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 82e0ed2990..b0fa144960 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -83,6 +83,7 @@ from draftguitools.gui_lineops import UndoLine from draftguitools.gui_togglemodes import ToggleConstructionMode from draftguitools.gui_togglemodes import ToggleContinueMode from draftguitools.gui_togglemodes import ToggleDisplayMode +from draftguitools.gui_groups import AddToGroup # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -4288,49 +4289,6 @@ class SubelementHighlight(Modifier): # This can occur if objects have had graph changing operations pass -class AddToGroup(): - """The AddToGroup FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_AddToGroup', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_AddToGroup", "Move to group..."), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_AddToGroup", "Moves the selected object(s) to an existing group")} - - def IsActive(self): - if FreeCADGui.Selection.getSelection(): - return True - else: - return False - - def Activated(self): - self.groups = ["Ungroup"] - self.groups.extend(Draft.getGroupNames()) - self.labels = ["Ungroup"] - for g in self.groups: - o = FreeCAD.ActiveDocument.getObject(g) - if o: self.labels.append(o.Label) - self.ui = FreeCADGui.draftToolBar - self.ui.sourceCmd = self - self.ui.popupMenu(self.labels) - - def proceed(self,labelname): - self.ui.sourceCmd = None - if labelname == "Ungroup": - for obj in FreeCADGui.Selection.getSelection(): - try: - Draft.ungroup(obj) - except: - pass - else: - if labelname in self.labels: - i = self.labels.index(labelname) - g = FreeCAD.ActiveDocument.getObject(self.groups[i]) - for obj in FreeCADGui.Selection.getSelection(): - try: - g.addObject(obj) - except: - pass - class WireToBSpline(Modifier): """The Draft_Wire2BSpline FreeCAD command definition""" @@ -5400,7 +5358,6 @@ FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) -FreeCADGui.addCommand('Draft_AddToGroup',AddToGroup()) FreeCADGui.addCommand('Draft_SelectGroup',SelectGroup()) FreeCADGui.addCommand('Draft_Shape2DView',Shape2DView()) FreeCADGui.addCommand('Draft_ShowSnapBar',ShowSnapBar()) diff --git a/src/Mod/Draft/draftguitools/gui_groups.py b/src/Mod/Draft/draftguitools/gui_groups.py new file mode 100644 index 0000000000..58aabac47d --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_groups.py @@ -0,0 +1,128 @@ +# *************************************************************************** +# * (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 to do various operations with groups. + +For example, add objects to groups. +""" +## @package gui_groups +# \ingroup DRAFT +# \brief Provides tools to do various operations with groups. +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import Draft_rc +import draftutils.utils as utils +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class AddToGroup(gui_base.GuiCommandNeedsSelection): + """GuiCommand for the Draft_AddToGroup tool. + + It adds selected objects to a group, or removes them from any group. + + It inherits `GuiCommandNeedsSelection` to only be availbale + when there is a document and a selection. + See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Add to group")) + self.ungroup = QT_TRANSLATE_NOOP("Draft_AddToGroup", + "Ungroup") + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tooltip = ("Moves the selected objects to an existing group, " + "or removes them from any group.\n" + "Create a group first to use this tool.") + + d = {'Pixmap': 'Draft_AddToGroup', + 'MenuText': QT_TRANSLATE_NOOP("Draft_AddToGroup", + "Move to group"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_AddToGroup", + _tooltip)} + return d + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + self.groups = [self.ungroup] + self.groups.extend(utils.get_group_names()) + + self.labels = [self.ungroup] + for group in self.groups: + obj = self.doc.getObject(group) + if obj: + self.labels.append(obj.Label) + + # It uses the `DraftToolBar` class defined in the `DraftGui` module + # and globally initialized in the `Gui` namespace, + # in order to pop up a menu with group labels + # or the default `Ungroup` text. + # Once the desired option is chosen + # it launches the `proceed` method. + self.ui = Gui.draftToolBar + self.ui.sourceCmd = self + self.ui.popupMenu(self.labels) + + def proceed(self, labelname): + """Place the selected objects in the chosen group or ungroup them. + + Parameters + ---------- + labelname: str + The passed string with the name of the group. + It puts the selected objects inside this group. + """ + # Deactivate the source command of the `DraftToolBar` class + # so that it doesn't do more with this command. + self.ui.sourceCmd = None + + # If the selected group matches the ungroup label, + # remove the selection from all groups. + if labelname == self.ungroup: + for obj in Gui.Selection.getSelection(): + try: + utils.ungroup(obj) + except Exception: + pass + else: + # Otherwise try to add all selected objects to the chosen group + if labelname in self.labels: + i = self.labels.index(labelname) + g = self.doc.getObject(self.groups[i]) + for obj in Gui.Selection.getSelection(): + try: + g.addObject(obj) + except Exception: + pass + + +Gui.addCommand('Draft_AddToGroup', AddToGroup()) diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index eec5493a37..a8ebe63941 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -61,7 +61,8 @@ def get_draft_small_commands(): """Return a list with only some utilities.""" return ["Draft_Layer", "Draft_WorkingPlaneProxy", - "Draft_ToggleDisplayMode"] + "Draft_ToggleDisplayMode", + "Draft_AddToGroup"] def get_draft_modification_commands(): From ef3ad5121bd922a4cfa1e62bdbf042593fe87527 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sat, 14 Mar 2020 21:32:01 -0600 Subject: [PATCH 101/142] Draft: move SelectGroup command to gui_groups module --- src/Mod/Draft/DraftTools.py | 34 +------ src/Mod/Draft/draftguitools/gui_groups.py | 105 +++++++++++++++++++++- src/Mod/Draft/draftutils/init_tools.py | 3 +- 3 files changed, 107 insertions(+), 35 deletions(-) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index b0fa144960..521ee687ed 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -84,6 +84,7 @@ from draftguitools.gui_togglemodes import ToggleConstructionMode from draftguitools.gui_togglemodes import ToggleContinueMode from draftguitools.gui_togglemodes import ToggleDisplayMode from draftguitools.gui_groups import AddToGroup +from draftguitools.gui_groups import SelectGroup # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -4337,38 +4338,6 @@ class WireToBSpline(Modifier): self.finish() -class SelectGroup(): - """The SelectGroup FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_SelectGroup', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_SelectGroup", "Select group"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_SelectGroup", "Selects all objects with the same parents as this group")} - - def IsActive(self): - if FreeCADGui.Selection.getSelection(): - return True - else: - return False - - def Activated(self): - sellist = [] - sel = FreeCADGui.Selection.getSelection() - if len(sel) == 1: - if sel[0].isDerivedFrom("App::DocumentObjectGroup"): - cts = Draft.getGroupContents(FreeCADGui.Selection.getSelection()) - for o in cts: - FreeCADGui.Selection.addSelection(o) - return - for ob in sel: - for child in ob.OutList: - FreeCADGui.Selection.addSelection(child) - for parent in ob.InList: - FreeCADGui.Selection.addSelection(parent) - for child in parent.OutList: - FreeCADGui.Selection.addSelection(child) - - class Shape2DView(Modifier): """The Shape2DView FreeCAD command definition""" @@ -5358,7 +5327,6 @@ FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) -FreeCADGui.addCommand('Draft_SelectGroup',SelectGroup()) FreeCADGui.addCommand('Draft_Shape2DView',Shape2DView()) FreeCADGui.addCommand('Draft_ShowSnapBar',ShowSnapBar()) FreeCADGui.addCommand('Draft_ToggleGrid',ToggleGrid()) diff --git a/src/Mod/Draft/draftguitools/gui_groups.py b/src/Mod/Draft/draftguitools/gui_groups.py index 58aabac47d..51501e4c83 100644 --- a/src/Mod/Draft/draftguitools/gui_groups.py +++ b/src/Mod/Draft/draftguitools/gui_groups.py @@ -24,7 +24,7 @@ # *************************************************************************** """Provides tools to do various operations with groups. -For example, add objects to groups. +For example, add objects to groups, and select objects inside groups. """ ## @package gui_groups # \ingroup DRAFT @@ -126,3 +126,106 @@ class AddToGroup(gui_base.GuiCommandNeedsSelection): Gui.addCommand('Draft_AddToGroup', AddToGroup()) + + +class SelectGroup(gui_base.GuiCommandNeedsSelection): + """GuiCommand for the Draft_SelectGroup tool. + + If the selection is a group, it selects all objects + with the same "parents" as this object. This means all objects + that are inside this group, including those in nested sub-groups. + + If the selection is a simple object inside a group, + it will select the "brother" objects, that is, those objects that are + at the same level as this object, including the upper group + that contains them all. + + NOTE: the second functionality is a bit strange, as it produces results + that are not very intuitive. Maybe we should change it and restrict + this command to only groups (`App::DocumentObjectGroup`) because + in this case it works in an intuitive manner, selecting + only the objects under the group. + + It inherits `GuiCommandNeedsSelection` to only be availbale + when there is a document and a selection. + See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Select group")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tooltip = ("If the selection is a group, it selects all objects " + "that are inside this group, including those in " + "nested sub-groups.\n" + "\n" + "If the selection is a simple object inside a group, " + 'it will select the "brother" objects, that is,\n' + "those that are at the same level as this object, " + "including the upper group that contains them all.") + + d = {'Pixmap': 'Draft_SelectGroup', + 'MenuText': QT_TRANSLATE_NOOP("Draft_SelectGroup", + "Select group"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_SelectGroup", + _tooltip)} + return d + + def Activated(self): + """Execute when the command is called. + + If the selection is a single group, it selects all objects + inside this group. + + In other cases it selects all objects (children) + in the OutList of this object, and also all objects (parents) + in the InList of this object. + For all parents, it also selects the children of these. + """ + super().Activated() + + sel = Gui.Selection.getSelection() + if len(sel) == 1: + if sel[0].isDerivedFrom("App::DocumentObjectGroup"): + cts = utils.get_group_contents(Gui.Selection.getSelection()) + for o in cts: + Gui.Selection.addSelection(o) + return + for obj in sel: + # This selects the objects in the `OutList` + # which are actually `parents` but appear below in the tree. + # Regular objects usually have an empty `OutList` + # so this is skipped. + # But for groups, it selects the objects + # that it contains under it. + for child in obj.OutList: + Gui.Selection.addSelection(child) + + # This selects the upper group that contains `obj`. + # Then for this group, it selects the objects in its `OutList`, + # which are at the same level as `obj` (brothers). + for parent in obj.InList: + Gui.Selection.addSelection(parent) + for child in parent.OutList: + Gui.Selection.addSelection(child) + # ------------------------------------------------------------------- + # NOTE: the terminology here may be confusing. + # Those in the `InList` are actually `children` (dependents) + # but appear above in the tree view, + # and this is the reason they are called `parents`. + # + # Those in the `OutList` are actually `parents` (suppliers) + # but appear below in the tree, and this is the reason + # they are called `children`. + # + # InList + # | + # - object + # | + # - OutList + # + # ------------------------------------------------------------------- + + +Gui.addCommand('Draft_SelectGroup', SelectGroup()) diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index a8ebe63941..d926fae601 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -62,7 +62,8 @@ def get_draft_small_commands(): return ["Draft_Layer", "Draft_WorkingPlaneProxy", "Draft_ToggleDisplayMode", - "Draft_AddToGroup"] + "Draft_AddToGroup", + "Draft_SelectGroup"] def get_draft_modification_commands(): From 9eab03b5933e46430e92ff0f52a8c688655a30ac Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 16 Mar 2020 12:17:55 -0600 Subject: [PATCH 102/142] Draft: add correct docstring to old Array tools Previously they mentioned incorrectly the Shape2DView tool. --- src/Mod/Draft/DraftTools.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 521ee687ed..26521cf4ac 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -4441,9 +4441,16 @@ class Draft2Sketch(Modifier): class Array(Modifier): - """The Shape2DView FreeCAD command definition""" + """GuiCommand for the Draft_Array tool. - def __init__(self,use_link=False): + Parameters + ---------- + use_link: bool, optional + It defaults to `False`. If it is `True`, the created object + will be a `Link array`. + """ + + def __init__(self, use_link=False): Modifier.__init__(self) self.use_link = use_link @@ -4474,11 +4481,12 @@ class Array(Modifier): 'FreeCAD.ActiveDocument.recompute()']) self.finish() + class LinkArray(Array): - "The Shape2DView FreeCAD command definition" + """GuiCommand for the Draft_LinkArray tool.""" def __init__(self): - Array.__init__(self,True) + Array.__init__(self, use_link=True) def GetResources(self): return {'Pixmap' : 'Draft_LinkArray', From 11206b1d3a22ab5abeeca1b24db23d5028d39715 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 16 Mar 2020 12:48:47 -0600 Subject: [PATCH 103/142] Draft: move Draft_ShowSnapBar to gui_snaps module --- src/Mod/Draft/DraftTools.py | 14 +------------- src/Mod/Draft/draftguitools/gui_snaps.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 26521cf4ac..a02cb65129 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -4655,17 +4655,6 @@ class Point(Creator): if self.ui.continueMode: self.Activated() -class ShowSnapBar(): - """The ShowSnapBar FreeCAD command definition""" - - def GetResources(self): - return {'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ShowSnapBar", "Show Snap Bar"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_ShowSnapBar", "Shows Draft snap toolbar")} - - def Activated(self): - if hasattr(FreeCADGui,"Snapper"): - FreeCADGui.Snapper.show() - class Draft_Clone(Modifier): """The Draft Clone command definition""" @@ -5262,7 +5251,7 @@ class Draft_Arc_3Points: #--------------------------------------------------------------------------- # Snap tools #--------------------------------------------------------------------------- -import draftguitools.gui_snaps +from draftguitools.gui_snaps import ShowSnapBar #--------------------------------------------------------------------------- # Adds the icons & commands to the FreeCAD command manager, and sets defaults @@ -5336,7 +5325,6 @@ FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) FreeCADGui.addCommand('Draft_Shape2DView',Shape2DView()) -FreeCADGui.addCommand('Draft_ShowSnapBar',ShowSnapBar()) FreeCADGui.addCommand('Draft_ToggleGrid',ToggleGrid()) FreeCADGui.addCommand('Draft_FlipDimension',Draft_FlipDimension()) FreeCADGui.addCommand('Draft_AutoGroup',SetAutoGroup()) diff --git a/src/Mod/Draft/draftguitools/gui_snaps.py b/src/Mod/Draft/draftguitools/gui_snaps.py index 019d93c47a..2d2745c7db 100644 --- a/src/Mod/Draft/draftguitools/gui_snaps.py +++ b/src/Mod/Draft/draftguitools/gui_snaps.py @@ -377,3 +377,26 @@ class Draft_Snap_WorkingPlane: Gui.addCommand('Draft_Snap_WorkingPlane', Draft_Snap_WorkingPlane()) + + +class ShowSnapBar: + """GuiCommand for the Draft_ShowSnapBar tool. + + Show the snap toolbar if it's hidden. + """ + + def GetResources(self): + """Set icon, menu and tooltip.""" + return {'Pixmap': 'Draft_Snap', + 'MenuText': QT_TRANSLATE_NOOP("Draft_ShowSnapBar", + "Show snap toolbar"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ShowSnapBar", + "Shows Draft snap toolbar.")} + + def Activated(self): + """Execute when the command is called.""" + if hasattr(Gui, "Snapper"): + Gui.Snapper.show() + + +Gui.addCommand('Draft_ShowSnapBar', ShowSnapBar()) From 1ed3df9df01e3a3ce15132fd103772b41673b951 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 16 Mar 2020 18:19:21 -0600 Subject: [PATCH 104/142] Draft: add base class for Snap commands --- src/Mod/Draft/DraftTools.py | 15 ++ src/Mod/Draft/draftguitools/gui_snaps.py | 294 +++++++++++++++++------ 2 files changed, 242 insertions(+), 67 deletions(-) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index a02cb65129..2816304b38 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -5251,6 +5251,21 @@ class Draft_Arc_3Points: #--------------------------------------------------------------------------- # Snap tools #--------------------------------------------------------------------------- +from draftguitools.gui_snaps import Draft_Snap_Lock +from draftguitools.gui_snaps import Draft_Snap_Midpoint +from draftguitools.gui_snaps import Draft_Snap_Perpendicular +from draftguitools.gui_snaps import Draft_Snap_Grid +from draftguitools.gui_snaps import Draft_Snap_Intersection +from draftguitools.gui_snaps import Draft_Snap_Parallel +from draftguitools.gui_snaps import Draft_Snap_Endpoint +from draftguitools.gui_snaps import Draft_Snap_Angle +from draftguitools.gui_snaps import Draft_Snap_Center +from draftguitools.gui_snaps import Draft_Snap_Extension +from draftguitools.gui_snaps import Draft_Snap_Near +from draftguitools.gui_snaps import Draft_Snap_Ortho +from draftguitools.gui_snaps import Draft_Snap_Special +from draftguitools.gui_snaps import Draft_Snap_Dimensions +from draftguitools.gui_snaps import Draft_Snap_WorkingPlane from draftguitools.gui_snaps import ShowSnapBar #--------------------------------------------------------------------------- diff --git a/src/Mod/Draft/draftguitools/gui_snaps.py b/src/Mod/Draft/draftguitools/gui_snaps.py index 2d2745c7db..36ca85ffa0 100644 --- a/src/Mod/Draft/draftguitools/gui_snaps.py +++ b/src/Mod/Draft/draftguitools/gui_snaps.py @@ -31,23 +31,34 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCADGui as Gui +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr -class Draft_Snap_Lock: - """Command to activate or deactivate all snap commands.""" +class Draft_Snap_Lock(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Lock tool. + + Activate or deactivate all snap methods at once. + """ + + def __init__(self): + super().__init__(name=_tr("Main toggle snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _menu = "Toggle On/Off" + _menu = "Main snapping toggle On/Off" _tip = ("Activates or deactivates " - "all snap tools at once") + "all snap methods at once.") + return {'Pixmap': 'Snap_Lock', 'Accel': "Shift+S", 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Lock", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Lock", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "masterbutton"): Gui.Snapper.masterbutton.toggle() @@ -56,19 +67,28 @@ class Draft_Snap_Lock: Gui.addCommand('Draft_Snap_Lock', Draft_Snap_Lock()) -class Draft_Snap_Midpoint: - """Command to snap to the midpoint of an edge.""" +class Draft_Snap_Midpoint(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Midpoint tool. + + Set snapping to the midpoint of an edge. + """ + + def __init__(self): + super().__init__(name=_tr("Midpoint snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Midpoint" - _tip = "Snaps to midpoints of edges" + _tip = "Set snapping to the midpoint of an edge." + return {'Pixmap': 'Snap_Midpoint', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Midpoint", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Midpoint", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -79,13 +99,20 @@ class Draft_Snap_Midpoint: Gui.addCommand('Draft_Snap_Midpoint', Draft_Snap_Midpoint()) -class Draft_Snap_Perpendicular: - """Command to snap to perdendicular of an edge.""" +class Draft_Snap_Perpendicular(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Perpendicular tool. + + Set snapping to a direction that is perpendicular to an edge. + """ + + def __init__(self): + super().__init__(name=_tr("Perpendicular snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Perpendicular" - _tip = "Snaps to perpendicular points on edges" + _tip = "Set snapping to a direction that is perpendicular to an edge." + return {'Pixmap': 'Snap_Perpendicular', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Perpendicular", _menu), @@ -93,7 +120,9 @@ class Draft_Snap_Perpendicular: _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -104,18 +133,27 @@ class Draft_Snap_Perpendicular: Gui.addCommand('Draft_Snap_Perpendicular', Draft_Snap_Perpendicular()) -class Draft_Snap_Grid: - """Command to snap to the intersection of grid lines.""" +class Draft_Snap_Grid(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Grid tool. + + Set snapping to the intersection of grid lines. + """ + + def __init__(self): + super().__init__(name=_tr("Grid snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to grid points" + _tip = "Set snapping to the intersection of grid lines." + return {'Pixmap': 'Snap_Grid', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Grid", "Grid"), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Grid", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -126,13 +164,20 @@ class Draft_Snap_Grid: Gui.addCommand('Draft_Snap_Grid', Draft_Snap_Grid()) -class Draft_Snap_Intersection: - """Command to snap to the intersection of two edges.""" +class Draft_Snap_Intersection(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Intersection tool. + + Set snapping to the intersection of edges. + """ + + def __init__(self): + super().__init__(name=_tr("Intersection snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Intersection" - _tip = "Snaps to edges intersections" + _tip = "Set snapping to the intersection of edges." + return {'Pixmap': 'Snap_Intersection', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Intersection", _menu), @@ -140,7 +185,9 @@ class Draft_Snap_Intersection: _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -151,19 +198,28 @@ class Draft_Snap_Intersection: Gui.addCommand('Draft_Snap_Intersection', Draft_Snap_Intersection()) -class Draft_Snap_Parallel: - """Command to snap to the parallel of an edge.""" +class Draft_Snap_Parallel(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Parallel tool. + + Set snapping to a direction that is parallel to an edge. + """ + + def __init__(self): + super().__init__(name=_tr("Parallel snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Parallel" - _tip = "Snaps to parallel directions of edges" + _tip = "Set snapping to a direction that is parallel to an edge." + return {'Pixmap': 'Snap_Parallel', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Parallel", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Parallel", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -174,19 +230,28 @@ class Draft_Snap_Parallel: Gui.addCommand('Draft_Snap_Parallel', Draft_Snap_Parallel()) -class Draft_Snap_Endpoint: - """Command to snap to an endpoint of an edge.""" +class Draft_Snap_Endpoint(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Endpoint tool. + + Set snapping to endpoints of an edge. + """ + + def __init__(self): + super().__init__(name=_tr("Endpoint snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Endpoint" - _tip = "Snaps to endpoints of edges" + _tip = "Set snapping to endpoints of an edge." + return {'Pixmap': 'Snap_Endpoint', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Endpoint", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Endpoint", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -197,18 +262,30 @@ class Draft_Snap_Endpoint: Gui.addCommand('Draft_Snap_Endpoint', Draft_Snap_Endpoint()) -class Draft_Snap_Angle: - """Command to snap to 90 degree angles.""" +class Draft_Snap_Angle(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Angle tool. + + Set snapping to points in a circular arc located at multiples + of 30 and 45 degree angles. + """ + + def __init__(self): + super().__init__(name=_tr("Angle snap (30 and 45 degrees)")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to 45 and 90 degrees points on arcs and circles" + _menu = "Angles (30 and 45 degrees)" + _tip = ("Set snapping to points in a circular arc located " + "at multiples of 30 and 45 degree angles.") + return {'Pixmap': 'Snap_Angle', - 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Angle", "Angles"), + 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Angle", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Angle", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -219,18 +296,27 @@ class Draft_Snap_Angle: Gui.addCommand('Draft_Snap_Angle', Draft_Snap_Angle()) -class Draft_Snap_Center: - """Command to snap to the centers of arcs and circumferences.""" +class Draft_Snap_Center(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Center tool. + + Set snapping to the center of a circular arc. + """ + + def __init__(self): + super().__init__(name=_tr("Arc center snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to center of circles and arcs" + _tip = "Set snapping to the center of a circular arc." + return {'Pixmap': 'Snap_Center', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Center", "Center"), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Center", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -241,19 +327,28 @@ class Draft_Snap_Center: Gui.addCommand('Draft_Snap_Center', Draft_Snap_Center()) -class Draft_Snap_Extension: - """Command to snap to the extension of an edge.""" +class Draft_Snap_Extension(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Extension tool. + + Set snapping to the extension of an edge. + """ + + def __init__(self): + super().__init__(name=_tr("Edge extension snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Extension" - _tip = "Snaps to extension of edges" + _tip = "Set snapping to the extension of an edge." + return {'Pixmap': 'Snap_Extension', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Extension", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Extension", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -264,18 +359,27 @@ class Draft_Snap_Extension: Gui.addCommand('Draft_Snap_Extension', Draft_Snap_Extension()) -class Draft_Snap_Near: - """Command to snap to the nearest point of an edge.""" +class Draft_Snap_Near(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Near tool. + + Set snapping to the nearest point of an edge. + """ + + def __init__(self): + super().__init__(name=_tr("Near snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to nearest point on edges" + _tip = "Set snapping to the nearest point of an edge." + return {'Pixmap': 'Snap_Near', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Near", "Nearest"), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Near", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -286,18 +390,30 @@ class Draft_Snap_Near: Gui.addCommand('Draft_Snap_Near', Draft_Snap_Near()) -class Draft_Snap_Ortho: - """Command to snap to the orthogonal directions.""" +class Draft_Snap_Ortho(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Ortho tool. + + Set snapping to a direction that is a multiple of 45 degrees + from a point. + """ + + def __init__(self): + super().__init__(name=_tr("Orthogonal snap")) def GetResources(self): """Set icon, menu and tooltip.""" - _tip = "Snaps to orthogonal and 45 degrees directions" + _menu = "Orthogonal angles (45 degrees)" + _tip = ("Set snapping to a direction that is a multiple " + "of 45 degrees from a point.") + return {'Pixmap': 'Snap_Ortho', - 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Ortho", "Ortho"), + 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Ortho", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Ortho", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -308,19 +424,28 @@ class Draft_Snap_Ortho: Gui.addCommand('Draft_Snap_Ortho', Draft_Snap_Ortho()) -class Draft_Snap_Special: - """Command to snap to the special point of an object.""" +class Draft_Snap_Special(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Special tool. + + Set snapping to the special points defined inside an object. + """ + + def __init__(self): + super().__init__(name=_tr("Special point snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Special" - _tip = "Snaps to special locations of objects" + _tip = "Set snapping to the special points defined inside an object." + return {'Pixmap': 'Snap_Special', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Special", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Special", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -331,19 +456,30 @@ class Draft_Snap_Special: Gui.addCommand('Draft_Snap_Special', Draft_Snap_Special()) -class Draft_Snap_Dimensions: - """Command to temporary show dimensions when snapping.""" +class Draft_Snap_Dimensions(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Dimensions tool. + + Show temporary linear dimensions when editing an object + and using other snapping methods. + """ + + def __init__(self): + super().__init__(name=_tr("Dimension display")) def GetResources(self): """Set icon, menu and tooltip.""" - _menu = "Dimensions" - _tip = "Shows temporary dimensions when snapping to Arch objects" + _menu = "Show dimensions" + _tip = ("Show temporary linear dimensions when editing an object " + "and using other snapping methods.") + return {'Pixmap': 'Snap_Dimensions', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_Dimensions", _menu), 'ToolTip': QT_TRANSLATE_NOOP("Draft_Snap_Dimensions", _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -354,13 +490,28 @@ class Draft_Snap_Dimensions: Gui.addCommand('Draft_Snap_Dimensions', Draft_Snap_Dimensions()) -class Draft_Snap_WorkingPlane: - """Command to snap to a point in the current working plane.""" +class Draft_Snap_WorkingPlane(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_WorkingPlane tool. + + Restricts snapping to a point in the current working plane. + If you select a point outside the working plane, for example, + by using other snapping methods, it will snap to that point's + projection in the current working plane. + """ + + def __init__(self): + super().__init__(name=_tr("Working plane snap")) def GetResources(self): """Set icon, menu and tooltip.""" _menu = "Working plane" - _tip = "Restricts the snapped point to the current working plane" + _tip = ("Restricts snapping to a point in the current " + "working plane.\n" + "If you select a point outside the working plane, " + "for example, by using other snapping methods,\n" + "it will snap to that point's projection " + "in the current working plane.") + return {'Pixmap': 'Snap_WorkingPlane', 'MenuText': QT_TRANSLATE_NOOP("Draft_Snap_WorkingPlane", _menu), @@ -368,7 +519,9 @@ class Draft_Snap_WorkingPlane: _tip)} def Activated(self): - """Execute this when the command is called.""" + """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): if hasattr(Gui.Snapper, "toolbarButtons"): for b in Gui.Snapper.toolbarButtons: @@ -379,22 +532,29 @@ class Draft_Snap_WorkingPlane: Gui.addCommand('Draft_Snap_WorkingPlane', Draft_Snap_WorkingPlane()) -class ShowSnapBar: +class ShowSnapBar(gui_base.GuiCommandSimplest): """GuiCommand for the Draft_ShowSnapBar tool. - Show the snap toolbar if it's hidden. + Show the snap toolbar if it is hidden. """ + def __init__(self): + super().__init__(name=_tr("Show snap toolbar")) + def GetResources(self): """Set icon, menu and tooltip.""" + _tip = "Show the snap toolbar if it is hidden." + return {'Pixmap': 'Draft_Snap', 'MenuText': QT_TRANSLATE_NOOP("Draft_ShowSnapBar", "Show snap toolbar"), 'ToolTip': QT_TRANSLATE_NOOP("Draft_ShowSnapBar", - "Shows Draft snap toolbar.")} + _tip)} def Activated(self): """Execute when the command is called.""" + super().Activated() + if hasattr(Gui, "Snapper"): Gui.Snapper.show() From 8c177c7fe87cd7cb8220965431f0f6d2ef645185 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 16 Mar 2020 19:17:46 -0600 Subject: [PATCH 105/142] Draft: move Draft_ToggleGrid to gui_grid module --- src/Mod/Draft/CMakeLists.txt | 1 + src/Mod/Draft/DraftTools.py | 23 +------ src/Mod/Draft/draftguitools/gui_grid.py | 79 +++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 src/Mod/Draft/draftguitools/gui_grid.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index b5048c9286..cf2844e8d9 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -99,6 +99,7 @@ SET(Draft_GUI_tools draftguitools/gui_lineops.py draftguitools/gui_togglemodes.py draftguitools/gui_groups.py + draftguitools/gui_grid.py draftguitools/README.md ) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 2816304b38..ee758a6977 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -85,6 +85,7 @@ from draftguitools.gui_togglemodes import ToggleContinueMode from draftguitools.gui_togglemodes import ToggleDisplayMode from draftguitools.gui_groups import AddToGroup from draftguitools.gui_groups import SelectGroup +from draftguitools.gui_grid import ToggleGrid # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -4704,27 +4705,6 @@ class Draft_Clone(Modifier): ToDo.delay(FreeCADGui.runCommand, "Draft_Move") -class ToggleGrid(): - """The Draft ToggleGrid command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_Grid', - 'Accel' : "G,R", - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_ToggleGrid", "Toggle Grid"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_ToggleGrid", "Toggles the Draft grid on/off"), - 'CmdType' : 'ForEdit'} - - def Activated(self): - if hasattr(FreeCADGui,"Snapper"): - FreeCADGui.Snapper.setTrackers() - if FreeCADGui.Snapper.grid: - if FreeCADGui.Snapper.grid.Visible: - FreeCADGui.Snapper.grid.off() - FreeCADGui.Snapper.forceGridOff=True - else: - FreeCADGui.Snapper.grid.on() - FreeCADGui.Snapper.forceGridOff=False - class Heal(): """The Draft Heal command definition""" @@ -5340,7 +5320,6 @@ FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) FreeCADGui.addCommand('Draft_Shape2DView',Shape2DView()) -FreeCADGui.addCommand('Draft_ToggleGrid',ToggleGrid()) FreeCADGui.addCommand('Draft_FlipDimension',Draft_FlipDimension()) FreeCADGui.addCommand('Draft_AutoGroup',SetAutoGroup()) FreeCADGui.addCommand('Draft_AddConstruction',Draft_AddConstruction()) diff --git a/src/Mod/Draft/draftguitools/gui_grid.py b/src/Mod/Draft/draftguitools/gui_grid.py new file mode 100644 index 0000000000..d500a14e09 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_grid.py @@ -0,0 +1,79 @@ +# *************************************************************************** +# * (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 * +# * * +# *************************************************************************** +"""Provide the Draft_ToggleGrid command to show the Draft grid.""" +## @package gui_grid +# \ingroup DRAFT +# \brief Provide the Draft_ToggleGrid command to show the Draft grid. + +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr + + +class ToggleGrid(gui_base.GuiCommandSimplest): + """The Draft ToggleGrid command definition. + + If the grid tracker is invisible (hidden), it makes it visible (shown); + and if it is visible, it hides it. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Toggle grid")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Toggles the Draft grid on and off." + + d = {'Pixmap': 'Draft_Grid', + 'Accel': "G,R", + 'MenuText': QT_TRANSLATE_NOOP("Draft_ToggleGrid", + "Toggle grid"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_ToggleGrid", + _tip), + 'CmdType': 'ForEdit'} + + return d + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + if hasattr(Gui, "Snapper"): + Gui.Snapper.setTrackers() + if Gui.Snapper.grid: + if Gui.Snapper.grid.Visible: + Gui.Snapper.grid.off() + Gui.Snapper.forceGridOff = True + else: + Gui.Snapper.grid.on() + Gui.Snapper.forceGridOff = False + + +Gui.addCommand('Draft_ToggleGrid', ToggleGrid()) From ed55c6e8248644162e405cb19641c813377416d4 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 16 Mar 2020 19:56:14 -0600 Subject: [PATCH 106/142] Draft: move Draft_Heal to gui_heal module --- src/Mod/Draft/CMakeLists.txt | 1 + src/Mod/Draft/DraftTools.py | 20 +------ src/Mod/Draft/draftguitools/gui_heal.py | 76 +++++++++++++++++++++++++ src/Mod/Draft/draftutils/init_tools.py | 3 +- 4 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 src/Mod/Draft/draftguitools/gui_heal.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index cf2844e8d9..a663a4674c 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -100,6 +100,7 @@ SET(Draft_GUI_tools draftguitools/gui_togglemodes.py draftguitools/gui_groups.py draftguitools/gui_grid.py + draftguitools/gui_heal.py draftguitools/README.md ) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index ee758a6977..13ddf33a38 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -86,6 +86,7 @@ from draftguitools.gui_togglemodes import ToggleDisplayMode from draftguitools.gui_groups import AddToGroup from draftguitools.gui_groups import SelectGroup from draftguitools.gui_grid import ToggleGrid +from draftguitools.gui_heal import Heal # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -4705,24 +4706,6 @@ class Draft_Clone(Modifier): ToDo.delay(FreeCADGui.runCommand, "Draft_Move") -class Heal(): - """The Draft Heal command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_Heal', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_Heal", "Heal"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_Heal", "Heal faulty Draft objects saved from an earlier FreeCAD version")} - - def Activated(self): - s = FreeCADGui.Selection.getSelection() - FreeCAD.ActiveDocument.openTransaction("Heal") - if s: - Draft.heal(s) - else: - Draft.heal() - FreeCAD.ActiveDocument.commitTransaction() - - class Draft_Facebinder(Creator): """The Draft Facebinder command definition""" @@ -5312,7 +5295,6 @@ FreeCADGui.addCommand('Draft_Clone',Draft_Clone()) FreeCADGui.addCommand('Draft_PathArray',PathArray()) FreeCADGui.addCommand('Draft_PathLinkArray',PathLinkArray()) FreeCADGui.addCommand('Draft_PointArray',PointArray()) -FreeCADGui.addCommand('Draft_Heal',Heal()) FreeCADGui.addCommand('Draft_Mirror',Mirror()) FreeCADGui.addCommand('Draft_Slope',Draft_Slope()) FreeCADGui.addCommand('Draft_Stretch',Stretch()) diff --git a/src/Mod/Draft/draftguitools/gui_heal.py b/src/Mod/Draft/draftguitools/gui_heal.py new file mode 100644 index 0000000000..0c6a3ac300 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_heal.py @@ -0,0 +1,76 @@ +# *************************************************************************** +# * (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 the Draft_Heal command to heal older Draft files.""" +## @package gui_health +# \ingroup DRAFT +# \brief Provides the Draft_Heal command to heal older Draft files. + +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import Draft +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr + + +class Heal(gui_base.GuiCommandSimplest): + """The Draft Heal command definition. + + Heal faulty Draft objects saved with an earlier version of the program. + + It inherits `GuiCommandSimplest` to set up the document + and other behavior. See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Heal")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = ("Heal faulty Draft objects saved with an earlier version " + "of the program.\n" + "If an object is selected it will try to heal that object " + "in particular,\n" + "otherwise it will try to heal all objects " + "in the active document.") + + return {'Pixmap': 'Draft_Heal', + 'MenuText': QT_TRANSLATE_NOOP("Draft_Heal", "Heal"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_Heal", _tip)} + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + s = Gui.Selection.getSelection() + self.doc.openTransaction("Heal") + if s: + Draft.heal(s) + else: + Draft.heal() + self.doc.commitTransaction() + + +Gui.addCommand('Draft_Heal', Heal()) diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index d926fae601..258b62025f 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -63,7 +63,8 @@ def get_draft_small_commands(): "Draft_WorkingPlaneProxy", "Draft_ToggleDisplayMode", "Draft_AddToGroup", - "Draft_SelectGroup"] + "Draft_SelectGroup", + "Draft_Heal"] def get_draft_modification_commands(): From 8aeb33f20314fcc6db9e5442f92f32c5c37ab82f Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 16 Mar 2020 21:38:14 -0600 Subject: [PATCH 107/142] Draft: move Draft_FlipDimension to gui_dimension_ops module --- src/Mod/Draft/CMakeLists.txt | 1 + src/Mod/Draft/DraftTools.py | 16 +--- .../Draft/draftguitools/gui_dimension_ops.py | 82 +++++++++++++++++++ src/Mod/Draft/draftutils/init_tools.py | 1 + 4 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 src/Mod/Draft/draftguitools/gui_dimension_ops.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index a663a4674c..d7ada4c5ce 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -101,6 +101,7 @@ SET(Draft_GUI_tools draftguitools/gui_groups.py draftguitools/gui_grid.py draftguitools/gui_heal.py + draftguitools/gui_dimension_ops.py draftguitools/README.md ) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 13ddf33a38..9ae7836ff2 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -87,6 +87,7 @@ from draftguitools.gui_groups import AddToGroup from draftguitools.gui_groups import SelectGroup from draftguitools.gui_grid import ToggleGrid from draftguitools.gui_heal import Heal +from draftguitools.gui_dimension_ops import Draft_FlipDimension # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -4739,20 +4740,6 @@ class Draft_Facebinder(Creator): FreeCAD.ActiveDocument.recompute() self.finish() -class Draft_FlipDimension(): - def GetResources(self): - return {'Pixmap' : 'Draft_FlipDimension', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_FlipDimension", "Flip Dimension"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_FlipDimension", "Flip the normal direction of a dimension")} - - def Activated(self): - for o in FreeCADGui.Selection.getSelection(): - if Draft.getType(o) in ["Dimension","AngularDimension"]: - FreeCAD.ActiveDocument.openTransaction("Flip dimension") - FreeCADGui.doCommand("FreeCAD.ActiveDocument."+o.Name+".Normal = FreeCAD.ActiveDocument."+o.Name+".Normal.negative()") - FreeCAD.ActiveDocument.commitTransaction() - FreeCAD.ActiveDocument.recompute() - class Mirror(Modifier): """The Draft_Mirror FreeCAD command definition""" @@ -5302,7 +5289,6 @@ FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) FreeCADGui.addCommand('Draft_Shape2DView',Shape2DView()) -FreeCADGui.addCommand('Draft_FlipDimension',Draft_FlipDimension()) FreeCADGui.addCommand('Draft_AutoGroup',SetAutoGroup()) FreeCADGui.addCommand('Draft_AddConstruction',Draft_AddConstruction()) diff --git a/src/Mod/Draft/draftguitools/gui_dimension_ops.py b/src/Mod/Draft/draftguitools/gui_dimension_ops.py new file mode 100644 index 0000000000..3f541e6daf --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_dimension_ops.py @@ -0,0 +1,82 @@ +# *************************************************************************** +# * (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 to modify Draft dimensions. + +For example, a tool to flip the direction of the text in the dimension +as the normal is sometimes not correctly calculated automatically. +""" +## @package gui_dimension_ops +# \ingroup DRAFT +# \brief Provides tools to modify Draft dimensions. + +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import draftutils.utils as utils +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr + + +class FlipDimension(gui_base.GuiCommandNeedsSelection): + """The Draft FlipDimension command definition. + + Flip the normal direction of the selected dimensions. + + It inherits `GuiCommandNeedsSelection` to set up the document + and other behavior. See this class for more information. + """ + + def __init__(self): + super().__init__(name=_tr("Flip dimension")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = ("Flip the normal direction of the selected dimensions " + "(linear, radial, angular).\n" + "If other objects are selected they are ignored.") + + return {'Pixmap': 'Draft_FlipDimension', + 'MenuText': QT_TRANSLATE_NOOP("Draft_FlipDimension", + "Flip dimension"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_FlipDimension", + _tip)} + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + for o in Gui.Selection.getSelection(): + if utils.get_type(o) in ("Dimension", "AngularDimension"): + self.doc.openTransaction("Flip dimension") + _cmd = "App.activeDocument()." + o.Name + ".Normal" + _cmd += " = " + _cmd += "App.activeDocument()." + o.Name + ".Normal.negative()" + Gui.doCommand(_cmd) + self.doc.commitTransaction() + self.doc.recompute() + + +Draft_FlipDimension = FlipDimension +Gui.addCommand('Draft_FlipDimension', FlipDimension()) diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index 258b62025f..5ec19ec8d0 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -83,6 +83,7 @@ def get_draft_modification_commands(): "Draft_Upgrade", "Draft_Downgrade", "Separator", "Draft_WireToBSpline", "Draft_Draft2Sketch", + "Draft_FlipDimension", "Separator", "Draft_Shape2DView", "Draft_Drawing"] return lst From 60f3155651f5f4c1aa01f140368f08f5de3a9b1a Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Thu, 19 Mar 2020 18:21:29 -0600 Subject: [PATCH 108/142] Draft: move Draft_Slope to gui_lineslope module The class name was renamed to `LineSlope` as it is fundamentally inteded to control slopes of lines. --- src/Mod/Draft/CMakeLists.txt | 1 + src/Mod/Draft/DraftTools.py | 56 +------ src/Mod/Draft/draftguitools/gui_lineslope.py | 156 +++++++++++++++++++ src/Mod/Draft/draftutils/init_tools.py | 2 +- 4 files changed, 159 insertions(+), 56 deletions(-) create mode 100644 src/Mod/Draft/draftguitools/gui_lineslope.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index d7ada4c5ce..26edb6e83c 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -102,6 +102,7 @@ SET(Draft_GUI_tools draftguitools/gui_grid.py draftguitools/gui_heal.py draftguitools/gui_dimension_ops.py + draftguitools/gui_lineslope.py draftguitools/README.md ) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 9ae7836ff2..7756e64550 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -88,6 +88,7 @@ from draftguitools.gui_groups import SelectGroup from draftguitools.gui_grid import ToggleGrid from draftguitools.gui_heal import Heal from draftguitools.gui_dimension_ops import Draft_FlipDimension +from draftguitools.gui_lineslope import Draft_Slope # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -4865,60 +4866,6 @@ class Mirror(Modifier): self.finish() -class Draft_Slope(): - - def GetResources(self): - return {'Pixmap' : 'Draft_Slope', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_Slope", "Set Slope"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_Slope", "Sets the slope of a selected Line or Wire")} - - def Activated(self): - if not FreeCADGui.Selection.getSelection(): - return - for obj in FreeCADGui.Selection.getSelection(): - if Draft.getType(obj) != "Wire": - FreeCAD.Console.PrintMessage(translate("draft", "This tool only works with Wires and Lines")+"\n") - return - w = QtGui.QWidget() - w.setWindowTitle(translate("Draft","Slope")) - layout = QtGui.QHBoxLayout(w) - label = QtGui.QLabel(w) - label.setText(translate("Draft", "Slope")+":") - layout.addWidget(label) - self.spinbox = QtGui.QDoubleSpinBox(w) - self.spinbox.setMinimum(-9999.99) - self.spinbox.setMaximum(9999.99) - self.spinbox.setSingleStep(0.01) - self.spinbox.setToolTip(translate("Draft", "Slope to give selected Wires/Lines: 0 = horizontal, 1 = 45deg up, -1 = 45deg down")) - layout.addWidget(self.spinbox) - taskwidget = QtGui.QWidget() - taskwidget.form = w - taskwidget.accept = self.accept - FreeCADGui.Control.showDialog(taskwidget) - - def accept(self): - if hasattr(self,"spinbox"): - pc = self.spinbox.value() - FreeCAD.ActiveDocument.openTransaction("Change slope") - for obj in FreeCADGui.Selection.getSelection(): - if Draft.getType(obj) == "Wire": - if len(obj.Points) > 1: - lp = None - np = [] - for p in obj.Points: - if not lp: - lp = p - else: - v = p.sub(lp) - z = pc*FreeCAD.Vector(v.x,v.y,0).Length - lp = FreeCAD.Vector(p.x,p.y,lp.z+z) - np.append(lp) - obj.Points = np - FreeCAD.ActiveDocument.commitTransaction() - FreeCADGui.Control.closeDialog() - FreeCAD.ActiveDocument.recompute() - - class SetAutoGroup(): """The SetAutoGroup FreeCAD command definition""" @@ -5283,7 +5230,6 @@ FreeCADGui.addCommand('Draft_PathArray',PathArray()) FreeCADGui.addCommand('Draft_PathLinkArray',PathLinkArray()) FreeCADGui.addCommand('Draft_PointArray',PointArray()) FreeCADGui.addCommand('Draft_Mirror',Mirror()) -FreeCADGui.addCommand('Draft_Slope',Draft_Slope()) FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands diff --git a/src/Mod/Draft/draftguitools/gui_lineslope.py b/src/Mod/Draft/draftguitools/gui_lineslope.py new file mode 100644 index 0000000000..3613c76444 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_lineslope.py @@ -0,0 +1,156 @@ +# *************************************************************************** +# * (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 to change the slope of a line over the working plane. + +It currently only works for a line in the XY plane, it changes the height +of one of its points in the Z direction to create a sloped line. +""" +## @package gui_lineslope +# \ingroup DRAFT +# \brief Provides tools to change the slope of a line over the working plane. + +import PySide.QtGui as QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc +import draftutils.utils as utils +import draftguitools.gui_base as gui_base +from draftutils.translate import _tr, translate + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class LineSlope(gui_base.GuiCommandNeedsSelection): + """Gui Command for the Line slope tool. + + For a line in the XY plane, it changes the height of one of its points + to create a sloped line. + + To Do + ----- + Make it work also with lines lying on the YZ and XZ planes, + or in an arbitrary plane, for which the normal is known. + """ + + def __init__(self): + super().__init__(name=_tr("Change slope")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Set slope" + _tip = ("Sets the slope of the selected line " + "by changing the value of the Z value of one of its points.\n" + "If a polyline is selected, it will apply the slope " + "transformation to each of its segments.\n\n" + "The slope will always change the Z value, therefore " + "this command only works well for\n" + "straight Draft lines that are drawn in the XY plane. " + "Selected objects that aren't single lines will be ignored.") + + return {'Pixmap': 'Draft_Slope', + 'MenuText': QT_TRANSLATE_NOOP("Draft_Slope", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_Slope", _tip)} + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + # for obj in Gui.Selection.getSelection(): + # if utils.get_type(obj) != "Wire": + # _msg(translate("draft", + # "This tool only works with " + # "Draft Lines and Wires")) + # return + + # TODO: create a .ui file with QtCreator and import it here + # instead of creating the interface programmatically, + # see the `gui_othoarray` module for an example. + w = QtGui.QWidget() + w.setWindowTitle(translate("Draft", "Slope")) + layout = QtGui.QHBoxLayout(w) + label = QtGui.QLabel(w) + label.setText(translate("Draft", "Slope")+":") + layout.addWidget(label) + self.spinbox = QtGui.QDoubleSpinBox(w) + self.spinbox.setMinimum(-9999.99) + self.spinbox.setMaximum(9999.99) + self.spinbox.setSingleStep(0.01) + _tip = ("New slope of the selected lines.\n" + "This is the tangent of the horizontal angle:\n" + "0 = horizontal\n" + "1 = 45 deg up\n" + "-1 = 45deg down\n") + label.setToolTip(translate("Draft", _tip)) + self.spinbox.setToolTip(translate("Draft", _tip)) + layout.addWidget(self.spinbox) + + # In order to display our interface inside the task panel + # we must contain our interface inside a parent widget. + # Then our interface must be installed in this parent widget + # inside the attribute called "form". + taskwidget = QtGui.QWidget() + taskwidget.form = w + + # The "accept" attribute of the parent widget + # should also contain a reference to a function that will be called + # when we press the "OK" button. + # Then we must show the container widget. + taskwidget.accept = self.accept + Gui.Control.showDialog(taskwidget) + + def accept(self): + """Execute when clicking the OK button or pressing Enter key. + + It changes the slope of the line that lies on the XY plane. + + TODO: make it work also with lines lying on the YZ and XZ planes. + """ + if hasattr(self, "spinbox"): + pc = self.spinbox.value() + self.doc.openTransaction("Change slope") + for obj in Gui.Selection.getSelection(): + if utils.get_type(obj) == "Wire": + if len(obj.Points) > 1: + lp = None + np = [] + for p in obj.Points: + if not lp: + lp = p + else: + v = p.sub(lp) + z = pc * App.Vector(v.x, v.y, 0).Length + lp = App.Vector(p.x, p.y, lp.z + z) + np.append(lp) + obj.Points = np + self.doc.commitTransaction() + Gui.Control.closeDialog() + self.doc.recompute() + + +Draft_Slope = LineSlope +Gui.addCommand('Draft_Slope', LineSlope()) diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index 5ec19ec8d0..eb6e881daf 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -83,7 +83,7 @@ def get_draft_modification_commands(): "Draft_Upgrade", "Draft_Downgrade", "Separator", "Draft_WireToBSpline", "Draft_Draft2Sketch", - "Draft_FlipDimension", + "Draft_Slope", "Draft_FlipDimension", "Separator", "Draft_Shape2DView", "Draft_Drawing"] return lst From 68119de02fd89a00f07ed6d2f25c2601ea78e51f Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Thu, 19 Mar 2020 20:36:17 -0600 Subject: [PATCH 109/142] Draft: move Draft_AutoGroup to gui_groups module --- src/Mod/Draft/DraftTools.py | 59 +------------ src/Mod/Draft/draftguitools/gui_groups.py | 103 +++++++++++++++++++++- 2 files changed, 102 insertions(+), 60 deletions(-) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 7756e64550..c1183b9ef6 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -85,6 +85,7 @@ from draftguitools.gui_togglemodes import ToggleContinueMode from draftguitools.gui_togglemodes import ToggleDisplayMode from draftguitools.gui_groups import AddToGroup from draftguitools.gui_groups import SelectGroup +from draftguitools.gui_groups import SetAutoGroup from draftguitools.gui_grid import ToggleGrid from draftguitools.gui_heal import Heal from draftguitools.gui_dimension_ops import Draft_FlipDimension @@ -4866,63 +4867,6 @@ class Mirror(Modifier): self.finish() -class SetAutoGroup(): - """The SetAutoGroup FreeCAD command definition""" - - def GetResources(self): - return {'Pixmap' : 'Draft_AutoGroup', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_AutoGroup", "AutoGroup"), - 'ToolTip': QtCore.QT_TRANSLATE_NOOP("Draft_AutoGroup", "Select a group to automatically add all Draft & Arch objects to")} - - def IsActive(self): - if FreeCADGui.ActiveDocument: - return True - else: - return False - - def Activated(self): - if hasattr(FreeCADGui,"draftToolBar"): - self.ui = FreeCADGui.draftToolBar - s = FreeCADGui.Selection.getSelection() - if len(s) == 1: - if (Draft.getType(s[0]) == "Layer") or \ - ( FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").GetBool("AutogroupAddGroups",False) and \ - (s[0].isDerivedFrom("App::DocumentObjectGroup") or (Draft.getType(s[0]) in ["Site","Building","Floor","BuildingPart",]))): - self.ui.setAutoGroup(s[0].Name) - return - self.groups = ["None"] - gn = [o.Name for o in FreeCAD.ActiveDocument.Objects if Draft.getType(o) == "Layer"] - if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").GetBool("AutogroupAddGroups",False): - gn.extend(Draft.getGroupNames()) - if gn: - self.groups.extend(gn) - self.labels = [translate("draft","None")] - self.icons = [self.ui.getIcon(":/icons/button_invalid.svg")] - for g in gn: - o = FreeCAD.ActiveDocument.getObject(g) - if o: - self.labels.append(o.Label) - self.icons.append(o.ViewObject.Icon) - self.labels.append(translate("draft","Add new Layer")) - self.icons.append(self.ui.getIcon(":/icons/document-new.svg")) - self.ui.sourceCmd = self - from PySide import QtCore - pos = self.ui.autoGroupButton.mapToGlobal(QtCore.QPoint(0,self.ui.autoGroupButton.geometry().height())) - self.ui.popupMenu(self.labels,self.icons,pos) - - def proceed(self,labelname): - self.ui.sourceCmd = None - if labelname in self.labels: - if labelname == self.labels[0]: - self.ui.setAutoGroup(None) - elif labelname == self.labels[-1]: - FreeCADGui.runCommand("Draft_Layer") - else: - i = self.labels.index(labelname) - self.ui.setAutoGroup(self.groups[i]) - - - class Draft_Label(Creator): """The Draft_Label command definition""" @@ -5235,7 +5179,6 @@ FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) FreeCADGui.addCommand('Draft_Shape2DView',Shape2DView()) -FreeCADGui.addCommand('Draft_AutoGroup',SetAutoGroup()) FreeCADGui.addCommand('Draft_AddConstruction',Draft_AddConstruction()) # a global place to look for active draft Command diff --git a/src/Mod/Draft/draftguitools/gui_groups.py b/src/Mod/Draft/draftguitools/gui_groups.py index 51501e4c83..afd0403598 100644 --- a/src/Mod/Draft/draftguitools/gui_groups.py +++ b/src/Mod/Draft/draftguitools/gui_groups.py @@ -24,18 +24,22 @@ # *************************************************************************** """Provides tools to do various operations with groups. -For example, add objects to groups, and select objects inside groups. +For example, add objects to groups, select objects inside groups, +and set the automatic group in which to create objects. """ ## @package gui_groups # \ingroup DRAFT # \brief Provides tools to do various operations with groups. + +import PySide.QtCore as QtCore from PySide.QtCore import QT_TRANSLATE_NOOP +import FreeCAD as App import FreeCADGui as Gui import Draft_rc import draftutils.utils as utils import draftguitools.gui_base as gui_base -from draftutils.translate import _tr +from draftutils.translate import _tr, translate # The module is used to prevent complaints from code checkers (flake8) True if Draft_rc.__name__ else False @@ -229,3 +233,98 @@ class SelectGroup(gui_base.GuiCommandNeedsSelection): Gui.addCommand('Draft_SelectGroup', SelectGroup()) + + +class SetAutoGroup(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_AutoGroup tool.""" + + def __init__(self): + super().__init__(name=_tr("Autogroup")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _tip = "Select a group to add all Draft and Arch objects to." + + return {'Pixmap': 'Draft_AutoGroup', + 'MenuText': QT_TRANSLATE_NOOP("Draft_AutoGroup", "Autogroup"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_AutoGroup", _tip)} + + def Activated(self): + """Execute when the command is called. + + It calls the `setAutogroup` method of the `DraftToolBar` class + installed inside the global `Gui` namespace. + """ + super().Activated() + + if not hasattr(Gui, "draftToolBar"): + return + + # It uses the `DraftToolBar` class defined in the `DraftGui` module + # and globally initialized in the `Gui` namespace to run + # some actions. + # If there is only a group selected, it runs the `AutoGroup` method. + self.ui = Gui.draftToolBar + s = Gui.Selection.getSelection() + if len(s) == 1: + if (utils.get_type(s[0]) == "Layer") or \ +- (App.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").GetBool("AutogroupAddGroups", False) + and (s[0].isDerivedFrom("App::DocumentObjectGroup") + or utils.get_type(s[0]) in ["Site", "Building", + "Floor", "BuildingPart"])): + self.ui.setAutoGroup(s[0].Name) + return + + # Otherwise it builds a list of layers, with names and icons, + # including the options "None" and "Add new layer". + self.groups = ["None"] + gn = [o.Name for o in self.doc.Objects if utils.get_type(o) == "Layer"] + if App.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM").GetBool("AutogroupAddGroups", False): + gn.extend(utils.get_group_names()) + if gn: + self.groups.extend(gn) + self.labels = [translate("draft", "None")] + self.icons = [self.ui.getIcon(":/icons/button_invalid.svg")] + for g in gn: + o = self.doc.getObject(g) + if o: + self.labels.append(o.Label) + self.icons.append(o.ViewObject.Icon) + self.labels.append(translate("draft", "Add new Layer")) + self.icons.append(self.ui.getIcon(":/icons/document-new.svg")) + + # With the lists created is uses the interface + # to pop up a menu with layer options. + # Once the desired option is chosen + # it launches the `proceed` method. + self.ui.sourceCmd = self + pos = self.ui.autoGroupButton.mapToGlobal(QtCore.QPoint(0, self.ui.autoGroupButton.geometry().height())) + self.ui.popupMenu(self.labels, self.icons, pos) + + def proceed(self, labelname): + """Set the defined autogroup, or create a new layer. + + Parameters + ---------- + labelname: str + The passed string with the name of the group or layer. + """ + # Deactivate the source command of the `DraftToolBar` class + # so that it doesn't do more with this command + # when it finishes. + self.ui.sourceCmd = None + + if labelname in self.labels: + if labelname == self.labels[0]: + # First option "None" deactivates autogrouping + self.ui.setAutoGroup(None) + elif labelname == self.labels[-1]: + # Last option "Add new layer" creates new layer + Gui.runCommand("Draft_Layer") + else: + # Set autogroup to the chosen layer + i = self.labels.index(labelname) + self.ui.setAutoGroup(self.groups[i]) + + +Gui.addCommand('Draft_AutoGroup', SetAutoGroup()) From 1f3a88f1ce7c1a16c92871791db0aa8a49fbc328 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Thu, 19 Mar 2020 23:08:33 -0600 Subject: [PATCH 110/142] Draft: move Draft_AddConstruction to gui_groups module Also add a new icon for adding to the construction group. --- src/Mod/Draft/DraftTools.py | 33 +-- src/Mod/Draft/Resources/Draft.qrc | 1 + .../Resources/icons/Draft_AddConstruction.svg | 227 ++++++++++++++++++ src/Mod/Draft/draftguitools/gui_groups.py | 68 +++++- src/Mod/Draft/draftutils/init_tools.py | 1 + 5 files changed, 297 insertions(+), 33 deletions(-) create mode 100644 src/Mod/Draft/Resources/icons/Draft_AddConstruction.svg diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index c1183b9ef6..8e9c5400f6 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -86,6 +86,7 @@ from draftguitools.gui_togglemodes import ToggleDisplayMode from draftguitools.gui_groups import AddToGroup from draftguitools.gui_groups import SelectGroup from draftguitools.gui_groups import SetAutoGroup +from draftguitools.gui_groups import Draft_AddConstruction from draftguitools.gui_grid import ToggleGrid from draftguitools.gui_heal import Heal from draftguitools.gui_dimension_ops import Draft_FlipDimension @@ -5002,37 +5003,6 @@ class Draft_Label(Creator): self.create() -class Draft_AddConstruction(): - - def GetResources(self): - return {'Pixmap' : 'Draft_Construction', - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_AddConstruction", "Add to Construction group"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_AddConstruction", "Adds the selected objects to the Construction group")} - - def Activated(self): - import FreeCADGui - if hasattr(FreeCADGui,"draftToolBar"): - col = FreeCADGui.draftToolBar.getDefaultColor("constr") - col = (float(col[0]),float(col[1]),float(col[2]),0.0) - gname = Draft.getParam("constructiongroupname","Construction") - grp = FreeCAD.ActiveDocument.getObject(gname) - if not grp: - grp = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup",gname) - for obj in FreeCADGui.Selection.getSelection(): - grp.addObject(obj) - obrep = obj.ViewObject - if "TextColor" in obrep.PropertiesList: - obrep.TextColor = col - if "PointColor" in obrep.PropertiesList: - obrep.PointColor = col - if "LineColor" in obrep.PropertiesList: - obrep.LineColor = col - if "ShapeColor" in obrep.PropertiesList: - obrep.ShapeColor = col - if hasattr(obrep,"Transparency"): - obrep.Transparency = 80 - - class Draft_Arc_3Points: @@ -5179,7 +5149,6 @@ FreeCADGui.addCommand('Draft_Stretch',Stretch()) # context commands FreeCADGui.addCommand('Draft_ApplyStyle',ApplyStyle()) FreeCADGui.addCommand('Draft_Shape2DView',Shape2DView()) -FreeCADGui.addCommand('Draft_AddConstruction',Draft_AddConstruction()) # a global place to look for active draft Command FreeCAD.activeDraftCommand = None diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index 45efb36c65..9b9d29ed6a 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -1,6 +1,7 @@ icons/Draft_2DShapeView.svg + icons/Draft_AddConstruction.svg icons/Draft_AddPoint.svg icons/Draft_AddToGroup.svg icons/Draft_Annotation_Style.svg diff --git a/src/Mod/Draft/Resources/icons/Draft_AddConstruction.svg b/src/Mod/Draft/Resources/icons/Draft_AddConstruction.svg new file mode 100644 index 0000000000..f1a2be3a75 --- /dev/null +++ b/src/Mod/Draft/Resources/icons/Draft_AddConstruction.svg @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Mon Oct 10 13:44:52 2011 +0000 + + + [vocx] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_AddConstruction.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson, [wmayer] + + + + + trowel + tool + plus sign + + + A trowel, and a plus sign + + + + + + + + + + diff --git a/src/Mod/Draft/draftguitools/gui_groups.py b/src/Mod/Draft/draftguitools/gui_groups.py index afd0403598..bc790bbf0c 100644 --- a/src/Mod/Draft/draftguitools/gui_groups.py +++ b/src/Mod/Draft/draftguitools/gui_groups.py @@ -25,7 +25,8 @@ """Provides tools to do various operations with groups. For example, add objects to groups, select objects inside groups, -and set the automatic group in which to create objects. +set the automatic group in which to create objects, and add objects +to the construction group. """ ## @package gui_groups # \ingroup DRAFT @@ -328,3 +329,68 @@ class SetAutoGroup(gui_base.GuiCommandSimplest): Gui.addCommand('Draft_AutoGroup', SetAutoGroup()) + + +class AddToConstruction(gui_base.GuiCommandSimplest): + """Gui Command for the AddToConstruction tool. + + It adds the selected objects to the construction group + defined in the `DraftToolBar` class which is initialized + in the `Gui` namespace when the workbench loads. + + It adds a construction group if it doesn't exist. + + Added objects are also given the visual properties of the construction + group. + """ + + def __init__(self): + super().__init__(name=_tr("Add to construction group")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Add to Construction group" + _tip = ("Adds the selected objects to the construction group,\n" + "and changes their appearance to the construction style.\n" + "It creates a construction group if it doesn't exist.") + + d = {'Pixmap': 'Draft_AddConstruction', + 'MenuText': QT_TRANSLATE_NOOP("Draft_AddConstruction", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_AddConstruction", _tip)} + return d + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + if not hasattr(Gui, "draftToolBar"): + return + + col = Gui.draftToolBar.getDefaultColor("constr") + col = (float(col[0]), float(col[1]), float(col[2]), 0.0) + + # Get the construction group or create it if it doesn't exist + gname = utils.get_param("constructiongroupname", "Construction") + grp = self.doc.getObject(gname) + if not grp: + grp = self.doc.addObject("App::DocumentObjectGroup", gname) + + for obj in Gui.Selection.getSelection(): + grp.addObject(obj) + + # Change the appearance to the construction colors + vobj = obj.ViewObject + if "TextColor" in vobj.PropertiesList: + vobj.TextColor = col + if "PointColor" in vobj.PropertiesList: + vobj.PointColor = col + if "LineColor" in vobj.PropertiesList: + vobj.LineColor = col + if "ShapeColor" in vobj.PropertiesList: + vobj.ShapeColor = col + if hasattr(vobj, "Transparency"): + vobj.Transparency = 80 + + +Draft_AddConstruction = AddToConstruction +Gui.addCommand('Draft_AddConstruction', AddToConstruction()) diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index eb6e881daf..49b85cbcf6 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -64,6 +64,7 @@ def get_draft_small_commands(): "Draft_ToggleDisplayMode", "Draft_AddToGroup", "Draft_SelectGroup", + "Draft_AddConstruction", "Draft_Heal"] From 1a2e79ecb97f1443c7bb5828e1a3540fbec02c51 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Fri, 20 Mar 2020 17:39:00 -0600 Subject: [PATCH 111/142] Draft: move Draft_Arc_3Points to gui_arcs module --- src/Mod/Draft/CMakeLists.txt | 1 + src/Mod/Draft/DraftTools.py | 58 +-------- src/Mod/Draft/draftguitools/gui_arcs.py | 156 ++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 57 deletions(-) create mode 100644 src/Mod/Draft/draftguitools/gui_arcs.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 26edb6e83c..f076366bb0 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -103,6 +103,7 @@ SET(Draft_GUI_tools draftguitools/gui_heal.py draftguitools/gui_dimension_ops.py draftguitools/gui_lineslope.py + draftguitools/gui_arcs.py draftguitools/README.md ) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 8e9c5400f6..23b6f1bc9d 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -91,6 +91,7 @@ from draftguitools.gui_grid import ToggleGrid from draftguitools.gui_heal import Heal from draftguitools.gui_dimension_ops import Draft_FlipDimension from draftguitools.gui_lineslope import Draft_Slope +from draftguitools.gui_arcs import Draft_Arc_3Points # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale @@ -5003,62 +5004,6 @@ class Draft_Label(Creator): self.create() -class Draft_Arc_3Points: - - - def GetResources(self): - - return {'Pixmap' : "Draft_Arc_3Points.svg", - 'MenuText': QtCore.QT_TRANSLATE_NOOP("Draft_Arc_3Points", "Arc 3 points"), - 'ToolTip' : QtCore.QT_TRANSLATE_NOOP("Draft_Arc_3Points", "Creates an arc by 3 points"), - 'Accel' : 'A,T'} - - def IsActive(self): - - if FreeCAD.ActiveDocument: - return True - else: - return False - - def Activated(self): - - self.points = [] - self.normal = None - self.tracker = trackers.arcTracker() - self.tracker.autoinvert = False - if hasattr(FreeCAD,"DraftWorkingPlane"): - FreeCAD.DraftWorkingPlane.setup() - FreeCADGui.Snapper.getPoint(callback=self.getPoint,movecallback=self.drawArc) - - def getPoint(self,point,info): - if not point: # cancelled - self.tracker.off() - return - if not(point in self.points): # avoid same point twice - self.points.append(point) - if len(self.points) < 3: - if len(self.points) == 2: - self.tracker.on() - FreeCADGui.Snapper.getPoint(last=self.points[-1],callback=self.getPoint,movecallback=self.drawArc) - else: - import draftobjects.arc_3points as arc3 - if Draft.getParam("UsePartPrimitives",False): - arc3.make_arc_3points([self.points[0], - self.points[1], - self.points[2]], primitive=True) - else: - arc3.make_arc_3points([self.points[0], - self.points[1], - self.points[2]], primitive=False) - self.tracker.off() - FreeCAD.ActiveDocument.recompute() - - def drawArc(self,point,info): - if len(self.points) == 2: - if point.sub(self.points[1]).Length > 0.001: - self.tracker.setBy3Points(self.points[0],self.points[1],point) - - #--------------------------------------------------------------------------- # Snap tools #--------------------------------------------------------------------------- @@ -5098,7 +5043,6 @@ class CommandArcGroup: def IsActive(self): return not FreeCAD.ActiveDocument is None FreeCADGui.addCommand('Draft_Arc',Arc()) -FreeCADGui.addCommand('Draft_Arc_3Points',Draft_Arc_3Points()) FreeCADGui.addCommand('Draft_ArcTools', CommandArcGroup()) FreeCADGui.addCommand('Draft_Text',Text()) FreeCADGui.addCommand('Draft_Rectangle',Rectangle()) diff --git a/src/Mod/Draft/draftguitools/gui_arcs.py b/src/Mod/Draft/draftguitools/gui_arcs.py new file mode 100644 index 0000000000..b29766c1f4 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_arcs.py @@ -0,0 +1,156 @@ +# *************************************************************************** +# * (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 creating arcs with the Draft Workbench.""" +## @package gui_arcs +# \ingroup DRAFT +# \brief Provides tools for creating arcs with the Draft Workbench. +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCAD as App +import FreeCADGui as Gui +import Draft_rc +import draftobjects.arc_3points as arc3 +import draftguitools.gui_base as gui_base +import draftguitools.gui_trackers as trackers +import draftutils.utils as utils +from draftutils.translate import _tr + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class Arc_3Points(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Arc_3Points tool.""" + + def __init__(self): + super().__init__(name=_tr("Arc by 3 points")) + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Arc by 3 points" + _tip = ("Creates a circular arc by picking 3 points.\n" + "CTRL to snap, SHIFT to constrain.") + + d = {'Pixmap': "Draft_Arc_3Points", + 'MenuText': QT_TRANSLATE_NOOP("Draft_Arc_3Points", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_Arc_3Points", _tip), + 'Accel': 'A,T'} + return d + + def Activated(self): + """Execute when the command is called.""" + super().Activated() + + # Reset the values + self.points = [] + self.normal = None + self.tracker = trackers.arcTracker() + self.tracker.autoinvert = False + + # Set up the working plane and launch the Snapper + # with the indicated callbacks: one for when the user clicks + # on the 3D view, and another for when the user moves the pointer. + if hasattr(App, "DraftWorkingPlane"): + App.DraftWorkingPlane.setup() + + Gui.Snapper.getPoint(callback=self.getPoint, + movecallback=self.drawArc) + + def getPoint(self, point, info): + """Get the point by clicking on the 3D view. + + Every time the user clicks on the 3D view this method is run. + In this case, a point is appended to the list of points, + and the tracker is updated. + The object is finally created when three points are picked. + + Parameters + ---------- + point: Base::Vector + The point selected in the 3D view. + + info: str + Some information obtained about the point passed by the Snapper. + """ + # If there is not point, the command was cancelled + # so the command exits. + if not point: + self.tracker.off() + return + + # Avoid adding the same point twice + if point not in self.points: + self.points.append(point) + + if len(self.points) < 3: + # If one or two points were picked, set up again the Snapper + # to get further points, but update the `last` property + # with the last selected point. + # + # When two points are selected then we can turn on + # the arc tracker to show the preview of the final curve. + if len(self.points) == 2: + self.tracker.on() + Gui.Snapper.getPoint(last=self.points[-1], + callback=self.getPoint, + movecallback=self.drawArc) + else: + # If three points were already picked in the 3D view + # proceed with creating the final object. + # Draw a simple `Part::Feature` if the parameter is `True`. + if utils.get_param("UsePartPrimitives", False): + arc3.make_arc_3points([self.points[0], + self.points[1], + self.points[2]], primitive=True) + else: + arc3.make_arc_3points([self.points[0], + self.points[1], + self.points[2]], primitive=False) + self.tracker.off() + self.doc.recompute() + + def drawArc(self, point, info): + """Draw preview arc when we move the pointer in the 3D view. + + It uses the `gui_trackers.arcTracker.setBy3Points` method. + + Parameters + ---------- + point: Base::Vector + The dynamic point pased by the callback + as we move the pointer in the 3D view. + + info: str + Some information obtained from the point by the Snapper. + """ + if len(self.points) == 2: + if point.sub(self.points[1]).Length > 0.001: + self.tracker.setBy3Points(self.points[0], + self.points[1], + point) + + +Draft_Arc_3Points = Arc_3Points +Gui.addCommand('Draft_Arc_3Points', Arc_3Points()) From 2021d0bec9e31ee98ae26abc3798180134d73e2c Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Sun, 22 Mar 2020 23:44:06 -0600 Subject: [PATCH 112/142] Draft: move AddPoint and DelPoint to gui_line_add_delete module These two Gui Commands were inside `DraftTools.py` but they were considered obsolete as they just call `Draft_Edit`. They were completely removed in f5f43913e0 and 8fd55eb6ff. They are restored in this commit and placed in their own module just for historical reasons; however this module is not imported in `DraftTools.py`. --- .../draftguitools/gui_line_add_delete.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/Mod/Draft/draftguitools/gui_line_add_delete.py diff --git a/src/Mod/Draft/draftguitools/gui_line_add_delete.py b/src/Mod/Draft/draftguitools/gui_line_add_delete.py new file mode 100644 index 0000000000..8482f6dd60 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_line_add_delete.py @@ -0,0 +1,109 @@ +# *************************************************************************** +# * (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 certain add and remove line operations of the Draft Workbench. + +These GuiCommands aren't really used anymore, as the same actions +are implemented directly in the Draft_Edit command. +""" +## @package gui_line_add_delete +# \ingroup DRAFT +# \brief Provides certain add and remove line operations. +from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui +import Draft_rc +import DraftTools +import draftutils.utils as utils + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False + + +class AddPoint(DraftTools.Modifier): + """GuiCommand to add a point to a line being drawn.""" + + def __init__(self): + self.running = False + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Add point" + _tip = "Adds a point to an existing Wire or B-spline." + + return {'Pixmap': 'Draft_AddPoint', + 'MenuText': QT_TRANSLATE_NOOP("Draft_AddPoint", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_AddPoint", _tip)} + + def IsActive(self): + """Return True when there is selection and the command is active.""" + if Gui.Selection.getSelection(): + return True + else: + return False + + def Activated(self): + """Execute when the command is called.""" + selection = Gui.Selection.getSelection() + if selection: + if (utils.get_type(selection[0]) in ['Wire', 'BSpline']): + Gui.runCommand("Draft_Edit") + Gui.draftToolBar.vertUi(True) + + +Gui.addCommand('Draft_AddPoint', AddPoint()) + + +class DelPoint(DraftTools.Modifier): + """GuiCommand to delete a point to a line being drawn.""" + + def __init__(self): + self.running = False + + def GetResources(self): + """Set icon, menu and tooltip.""" + _menu = "Remove point" + _tip = "Removes a point from an existing Wire or B-spline." + + return {'Pixmap': 'Draft_DelPoint', + 'MenuText': QT_TRANSLATE_NOOP("Draft_DelPoint", _menu), + 'ToolTip': QT_TRANSLATE_NOOP("Draft_DelPoint", _tip)} + + def IsActive(self): + """Return True when there is selection and the command is active.""" + if Gui.Selection.getSelection(): + return True + else: + return False + + def Activated(self): + """Execute when the command is called.""" + selection = Gui.Selection.getSelection() + if selection: + if (utils.get_type(selection[0]) in ['Wire', 'BSpline']): + Gui.runCommand("Draft_Edit") + Gui.draftToolBar.vertUi(False) + + +Gui.addCommand('Draft_DelPoint', DelPoint()) From 6c890c196601b23e1fd5f8dcc45a5e5892f1b393 Mon Sep 17 00:00:00 2001 From: vocx-fc Date: Mon, 23 Mar 2020 02:18:04 -0600 Subject: [PATCH 113/142] Draft: move array commands to DraftTools Previously they were imported directly in `InitGui.py`, now they are collected in `DraftTools.py`, so that they are imported at the same time as other modules. Also provide an icon so this icon appears in the menu. --- src/Mod/Draft/DraftTools.py | 1 + src/Mod/Draft/InitGui.py | 4 ---- src/Mod/Draft/draftguitools/gui_arrays.py | 28 ++++++++++++++++------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Mod/Draft/DraftTools.py b/src/Mod/Draft/DraftTools.py index 23b6f1bc9d..0b54833b5f 100644 --- a/src/Mod/Draft/DraftTools.py +++ b/src/Mod/Draft/DraftTools.py @@ -92,6 +92,7 @@ from draftguitools.gui_heal import Heal from draftguitools.gui_dimension_ops import Draft_FlipDimension from draftguitools.gui_lineslope import Draft_Slope from draftguitools.gui_arcs import Draft_Arc_3Points +import draftguitools.gui_arrays # import DraftFillet import drafttaskpanels.task_shapestring as task_shapestring import drafttaskpanels.task_scale as task_scale diff --git a/src/Mod/Draft/InitGui.py b/src/Mod/Draft/InitGui.py index bbbc7c0fca..286da11671 100644 --- a/src/Mod/Draft/InitGui.py +++ b/src/Mod/Draft/InitGui.py @@ -82,10 +82,6 @@ class DraftWorkbench(FreeCADGui.Workbench): import DraftTools import DraftGui import DraftFillet - from draftguitools import gui_circulararray - from draftguitools import gui_polararray - from draftguitools import gui_orthoarray - from draftguitools import gui_arrays FreeCADGui.addLanguagePath(":/translations") FreeCADGui.addIconPath(":/icons") except Exception as exc: diff --git a/src/Mod/Draft/draftguitools/gui_arrays.py b/src/Mod/Draft/draftguitools/gui_arrays.py index 81bff17fbb..33da02a8aa 100644 --- a/src/Mod/Draft/draftguitools/gui_arrays.py +++ b/src/Mod/Draft/draftguitools/gui_arrays.py @@ -28,6 +28,16 @@ from PySide.QtCore import QT_TRANSLATE_NOOP import FreeCAD as App import FreeCADGui as Gui +import Draft_rc +import draftguitools.gui_circulararray +import draftguitools.gui_polararray +import draftguitools.gui_orthoarray + +# The module is used to prevent complaints from code checkers (flake8) +True if Draft_rc.__name__ else False +True if draftguitools.gui_circulararray.__name__ else False +True if draftguitools.gui_polararray.__name__ else False +True if draftguitools.gui_orthoarray.__name__ else False class ArrayGroupCommand: @@ -35,22 +45,24 @@ class ArrayGroupCommand: def GetCommands(self): """Tuple of array commands.""" - return tuple(["Draft_OrthoArray", - "Draft_PolarArray", "Draft_CircularArray", - "Draft_PathArray", "Draft_PathLinkArray", - "Draft_PointArray"]) + return ("Draft_OrthoArray", + "Draft_PolarArray", "Draft_CircularArray", + "Draft_PathArray", "Draft_PathLinkArray", + "Draft_PointArray") def GetResources(self): - """Add menu and tooltip.""" + """Set icon, menu and tooltip.""" _tooltip = ("Create various types of arrays, " "including rectangular, polar, circular, " "path, and point") - return {'MenuText': QT_TRANSLATE_NOOP("Draft", "Array tools"), - 'ToolTip': QT_TRANSLATE_NOOP("Arch", _tooltip)} + + return {'Pixmap': 'Draft_Array', + 'MenuText': QT_TRANSLATE_NOOP("Draft", "Array tools"), + 'ToolTip': QT_TRANSLATE_NOOP("Draft", _tooltip)} def IsActive(self): """Return True when this command should be available.""" - if App.ActiveDocument: + if App.activeDocument(): return True else: return False From 5e9869aaefe3361aac87a739ca576a160f2811b6 Mon Sep 17 00:00:00 2001 From: wmayer Date: Thu, 16 Apr 2020 12:24:38 +0200 Subject: [PATCH 114/142] Spreadsheet: [skip ci] add generated Imp files to repository to avoid possible build failures --- .../App/PropertyColumnWidthsPyImp.cpp | 60 +++++++++++++++++++ .../App/PropertyRowHeightsPyImp.cpp | 60 +++++++++++++++++++ .../Spreadsheet/App/PropertySheetPyImp.cpp | 60 +++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 src/Mod/Spreadsheet/App/PropertyColumnWidthsPyImp.cpp create mode 100644 src/Mod/Spreadsheet/App/PropertyRowHeightsPyImp.cpp create mode 100644 src/Mod/Spreadsheet/App/PropertySheetPyImp.cpp diff --git a/src/Mod/Spreadsheet/App/PropertyColumnWidthsPyImp.cpp b/src/Mod/Spreadsheet/App/PropertyColumnWidthsPyImp.cpp new file mode 100644 index 0000000000..91e1c83f57 --- /dev/null +++ b/src/Mod/Spreadsheet/App/PropertyColumnWidthsPyImp.cpp @@ -0,0 +1,60 @@ +/*************************************************************************** + * Copyright (c) Eivind Kvedalen (eivind@kvedalen.name) 2015 * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include "PropertyColumnWidths.h" + +// inclusion of the generated files (generated out of PropertyColumnWidthsPy.xml) +#include "PropertyColumnWidthsPy.h" +#include "PropertyColumnWidthsPy.cpp" + +using namespace Spreadsheet; + +// returns a string which represents the object e.g. when printed in python +std::string PropertyColumnWidthsPy::representation(void) const +{ + return std::string(""); +} + +PyObject *PropertyColumnWidthsPy::PyMake(struct _typeobject *, PyObject *, PyObject *) // Python wrapper +{ + // create a new instance of PropertyColumnWidthsPy and the Twin object + return new PropertyColumnWidthsPy(new PropertyColumnWidths); +} + +// constructor method +int PropertyColumnWidthsPy::PyInit(PyObject* /*args*/, PyObject* /*kwd*/) +{ + return 0; +} + +PyObject *PropertyColumnWidthsPy::getCustomAttributes(const char* /*attr*/) const +{ + return 0; +} + +int PropertyColumnWidthsPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/Spreadsheet/App/PropertyRowHeightsPyImp.cpp b/src/Mod/Spreadsheet/App/PropertyRowHeightsPyImp.cpp new file mode 100644 index 0000000000..7c7e053698 --- /dev/null +++ b/src/Mod/Spreadsheet/App/PropertyRowHeightsPyImp.cpp @@ -0,0 +1,60 @@ +/*************************************************************************** + * Copyright (c) Eivind Kvedalen (eivind@kvedalen.name) 2015 * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include "PropertyRowHeights.h" + +// inclusion of the generated files (generated out of PropertyRowHeightsPy.xml) +#include "PropertyRowHeightsPy.h" +#include "PropertyRowHeightsPy.cpp" + +using namespace Spreadsheet; + +// returns a string which represents the object e.g. when printed in python +std::string PropertyRowHeightsPy::representation(void) const +{ + return std::string(""); +} + +PyObject *PropertyRowHeightsPy::PyMake(struct _typeobject *, PyObject *, PyObject *) // Python wrapper +{ + // create a new instance of PropertyRowHeightsPy and the Twin object + return new PropertyRowHeightsPy(new PropertyRowHeights); +} + +// constructor method +int PropertyRowHeightsPy::PyInit(PyObject* /*args*/, PyObject* /*kwd*/) +{ + return 0; +} + +PyObject *PropertyRowHeightsPy::getCustomAttributes(const char* /*attr*/) const +{ + return 0; +} + +int PropertyRowHeightsPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} diff --git a/src/Mod/Spreadsheet/App/PropertySheetPyImp.cpp b/src/Mod/Spreadsheet/App/PropertySheetPyImp.cpp new file mode 100644 index 0000000000..cd26d811b2 --- /dev/null +++ b/src/Mod/Spreadsheet/App/PropertySheetPyImp.cpp @@ -0,0 +1,60 @@ +/*************************************************************************** + * Copyright (c) Eivind Kvedalen (eivind@kvedalen.name) 2015 * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#include "PreCompiled.h" + +#include "PropertySheet.h" + +// inclusion of the generated files (generated out of PropertySheetPy.xml) +#include "PropertySheetPy.h" +#include "PropertySheetPy.cpp" + +using namespace Spreadsheet; + +// returns a string which represents the object e.g. when printed in python +std::string PropertySheetPy::representation(void) const +{ + return std::string(""); +} + +PyObject *PropertySheetPy::PyMake(struct _typeobject *, PyObject *, PyObject *) // Python wrapper +{ + // create a new instance of PropertySheetPy and the Twin object + return new PropertySheetPy(new PropertySheet); +} + +// constructor method +int PropertySheetPy::PyInit(PyObject* /*args*/, PyObject* /*kwd*/) +{ + return 0; +} + +PyObject *PropertySheetPy::getCustomAttributes(const char* /*attr*/) const +{ + return 0; +} + +int PropertySheetPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} From 1318a73646420a1dbff001d9b39699dec44364e0 Mon Sep 17 00:00:00 2001 From: wmayer Date: Thu, 16 Apr 2020 14:58:30 +0200 Subject: [PATCH 115/142] Mesh: [skip ci] add sphere and cylinder fitting algorithms --- src/Mod/Mesh/App/CMakeLists.txt | 4 + src/Mod/Mesh/App/Core/Approximation.cpp | 97 +++- src/Mod/Mesh/App/Core/CylinderFit.cpp | 654 ++++++++++++++++++++++++ src/Mod/Mesh/App/Core/CylinderFit.h | 151 ++++++ src/Mod/Mesh/App/Core/SphereFit.cpp | 427 ++++++++++++++++ src/Mod/Mesh/App/Core/SphereFit.h | 130 +++++ 6 files changed, 1459 insertions(+), 4 deletions(-) create mode 100644 src/Mod/Mesh/App/Core/CylinderFit.cpp create mode 100644 src/Mod/Mesh/App/Core/CylinderFit.h create mode 100644 src/Mod/Mesh/App/Core/SphereFit.cpp create mode 100644 src/Mod/Mesh/App/Core/SphereFit.h diff --git a/src/Mod/Mesh/App/CMakeLists.txt b/src/Mod/Mesh/App/CMakeLists.txt index ca6c698a70..7ef7aa1354 100644 --- a/src/Mod/Mesh/App/CMakeLists.txt +++ b/src/Mod/Mesh/App/CMakeLists.txt @@ -94,6 +94,10 @@ SET(Core_SRCS Core/Utilities.h Core/Visitor.cpp Core/Visitor.h + Core/CylinderFit.cpp + Core/CylinderFit.h + Core/SphereFit.cpp + Core/SphereFit.h ) SOURCE_GROUP("Core" FILES ${Core_SRCS}) diff --git a/src/Mod/Mesh/App/Core/Approximation.cpp b/src/Mod/Mesh/App/Core/Approximation.cpp index 97658d35fa..c04343db0d 100644 --- a/src/Mod/Mesh/App/Core/Approximation.cpp +++ b/src/Mod/Mesh/App/Core/Approximation.cpp @@ -32,6 +32,8 @@ #include "Approximation.h" #include "Elements.h" #include "Utilities.h" +#include "CylinderFit.h" +#include "SphereFit.h" #include #include @@ -1051,6 +1053,29 @@ float CylinderFit::Fit() _fRadius = float(radius); _fLastResult = double(fit); + +#if defined(_DEBUG) + Base::Console().Message(" WildMagic Cylinder Fit: Base: (%0.4f, %0.4f, %0.4f), Axis: (%0.6f, %0.6f, %0.6f), Radius: %0.4f, Std Dev: %0.4f\n", + _vBase.x, _vBase.y, _vBase.z, _vAxis.x, _vAxis.y, _vAxis.z, _fRadius, GetStdDeviation()); +#endif + + MeshCoreFit::CylinderFit cylFit; + cylFit.AddPoints(_vPoints); + //cylFit.SetApproximations(_fRadius, Base::Vector3d(_vBase.x, _vBase.y, _vBase.z), Base::Vector3d(_vAxis.x, _vAxis.y, _vAxis.z)); + + float result = cylFit.Fit(); + if (result < FLOAT_MAX) { + Base::Vector3d base = cylFit.GetBase(); + Base::Vector3d dir = cylFit.GetAxis(); +#if defined(_DEBUG) + Base::Console().Message("MeshCoreFit::Cylinder Fit: Base: (%0.4f, %0.4f, %0.4f), Axis: (%0.6f, %0.6f, %0.6f), Radius: %0.4f, Std Dev: %0.4f, Iterations: %d\n", + base.x, base.y, base.z, dir.x, dir.y, dir.z, cylFit.GetRadius(), cylFit.GetStdDeviation(), cylFit.GetNumIterations()); +#endif + _vBase = Base::convertTo(base); + _vAxis = Base::convertTo(dir); + _fRadius = (float)cylFit.GetRadius(); + _fLastResult = result; + } #else int m = static_cast(_vPoints.size()); int n = 7; @@ -1238,24 +1263,88 @@ float SphereFit::Fit() _fRadius = float(sphere.Radius); // TODO - _fLastResult = 0; + +#if defined(_DEBUG) + Base::Console().Message(" WildMagic Sphere Fit: Center: (%0.4f, %0.4f, %0.4f), Radius: %0.4f, Std Dev: %0.4f\n", + _vCenter.x, _vCenter.y, _vCenter.z, _fRadius, GetStdDeviation()); +#endif + + MeshCoreFit::SphereFit sphereFit; + sphereFit.AddPoints(_vPoints); + sphereFit.ComputeApproximations(); + float result = sphereFit.Fit(); + if (result < FLOAT_MAX) { + Base::Vector3d center = sphereFit.GetCenter(); +#if defined(_DEBUG) + Base::Console().Message("MeshCoreFit::Sphere Fit: Center: (%0.4f, %0.4f, %0.4f), Radius: %0.4f, Std Dev: %0.4f, Iterations: %d\n", + center.x, center.y, center.z, sphereFit.GetRadius(), sphereFit.GetStdDeviation(), sphereFit.GetNumIterations()); +#endif + _vCenter = Base::convertTo(center); + _fRadius = (float)sphereFit.GetRadius(); + _fLastResult = result; + } + return _fLastResult; } -float SphereFit::GetDistanceToSphere(const Base::Vector3f &) const +float SphereFit::GetDistanceToSphere(const Base::Vector3f& rcPoint) const { - return FLOAT_MAX; + float fResult = FLOAT_MAX; + if (_bIsFitted) { + fResult = Base::Vector3f(rcPoint - _vCenter).Length() - _fRadius; + } + return fResult; } float SphereFit::GetStdDeviation() const { - return FLOAT_MAX; + // Mean: M=(1/N)*SUM Xi + // Variance: VAR=(N/N-3)*[(1/N)*SUM(Xi^2)-M^2] + // Standard deviation: SD=SQRT(VAR) + // Standard error of the mean: SE=SD/SQRT(N) + if (!_bIsFitted) + return FLOAT_MAX; + + float fSumXi = 0.0f, fSumXi2 = 0.0f, + fMean = 0.0f, fDist = 0.0f; + + float ulPtCt = float(CountPoints()); + std::list< Base::Vector3f >::const_iterator cIt; + + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { + fDist = GetDistanceToSphere( *cIt ); + fSumXi += fDist; + fSumXi2 += ( fDist * fDist ); + } + + fMean = (1.0f / ulPtCt) * fSumXi; + return sqrt((ulPtCt / (ulPtCt - 3.0f)) * ((1.0f / ulPtCt) * fSumXi2 - fMean * fMean)); } void SphereFit::ProjectToSphere() { + for (std::list< Base::Vector3f >::iterator it = _vPoints.begin(); it != _vPoints.end(); ++it) { + Base::Vector3f& cPnt = *it; + // Compute unit vector from sphere centre to point. + // Because this vector is orthogonal to the sphere's surface at the + // intersection point we can easily compute the projection point on the + // closest surface point using the radius of the sphere + Base::Vector3f diff = cPnt - _vCenter; + double length = diff.Length(); + if (length == 0.0) + { + // Point is exactly at the sphere center, so it can be projected in any direction onto the sphere! + // So here just project in +Z direction + cPnt.z += _fRadius; + } + else + { + diff /= length; // normalizing the vector + cPnt = _vCenter + diff * _fRadius; + } + } } // ------------------------------------------------------------------------------- diff --git a/src/Mod/Mesh/App/Core/CylinderFit.cpp b/src/Mod/Mesh/App/Core/CylinderFit.cpp new file mode 100644 index 0000000000..3cd2fa770a --- /dev/null +++ b/src/Mod/Mesh/App/Core/CylinderFit.cpp @@ -0,0 +1,654 @@ +/*************************************************************************** + * Copyright (c) 2020 Graeme van der Vlugt * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +// Definitions: +// Cylinder axis goes through a point (Xc,Yc,Zc) and has direction (L,M,N) +// Cylinder radius is R +// A point on the axis (X0i,Y0i,Z0i) can be described by: +// (X0i,Y0i,Z0i) = (Xc,Yc,Zc) + s(L,M,N) +// where s is the distance from (Xc,Yc,Zc) to (X0i,Y0i,Z0i) when (L,M,N) is +// of unit length (normalized). +// The distance between a cylinder surface point (Xi,Yi,Zi) and its +// projection onto the axis (X0i,Y0i,Z0i) is the radius: +// (Xi - X0i)^2 + (Yi - Y0i)^2 + (Zi - Z0i)^2 = R^2 +// Also the vector to a cylinder surface point (Xi,Yi,Zi) from its +// projection onto the axis (X0i,Y0i,Z0i) is orthogonal to the axis so we can +// write: +// (Xi - X0i, Yi - Y0i, Zi - Z0i).(L,M,N) = 0 or +// L(Xi - X0i) + M(Yi - Y0i) + N(Zi - Z0i) = 0 +// If we substitute these various equations into each other and further add +// the constraint that L^2 + M^2 + N^2 = 1 then we can arrive at a single +// equation for the cylinder surface points: +// (Xi - Xc + L*L*(Xc - Xi) + L*M*(Yc - Yi) + L*N*(Zc - Zi))^2 + +// (Yi - Yc + M*L*(Xc - Xi) + M*M*(Yc - Yi) + M*N*(Zc - Zi))^2 + +// (Zi - Zc + N*L*(Xc - Xi) + N*M*(Yc - Yi) + N*N*(Zc - Zi))^2 - R^2 = 0 +// This equation is what is used in the least squares solution below. Because +// we are constraining the direction vector to a unit length and also because +// we need to stop the axis point from moving along the axis we need to fix one +// of the ordinates in the solution. So from our initial approximations for the +// axis direction (L0,M0,N0): +// if (L0 > M0) && (L0 > N0) then fix Xc = 0 and use L = sqrt(1 - M^2 - N^2) +// else if (M0 > L0) && (M0 > N0) then fix Yc = 0 and use M = sqrt(1 - L^2 - N^2) +// else if (N0 > L0) && (N0 > M0) then fix Zc = 0 and use N = sqrt(1 - L^2 - M^2) +// We thus solve for 5 unknown parameters. +// Thus for the solution to succeed the initial axis direction should be reasonable. + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +# include +# include +#endif + +#include "CylinderFit.h" +#include +#include + +using namespace MeshCoreFit; + +CylinderFit::CylinderFit() + : _vBase(0,0,0) + , _vAxis(0,0,1) + , _dRadius(0) + , _numIter(0) + , _posConvLimit(0.0001) + , _dirConvLimit(0.000001) + , _vConvLimit(0.001) + , _maxIter(50) +{ +} + +CylinderFit::~CylinderFit() +{ +} + +// Set approximations before calling the fitting +void CylinderFit::SetApproximations(double radius, const Base::Vector3d &base, const Base::Vector3d &axis) +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + _dRadius = radius; + _vBase = base; + _vAxis = axis; + _vAxis.Normalize(); +} + +// Set iteration convergence criteria for the fit if special values are needed. +// The default values set in the constructor are suitable for most uses +void CylinderFit::SetConvergenceCriteria(double posConvLimit, double dirConvLimit, double vConvLimit, int maxIter) +{ + if (posConvLimit > 0.0) + _posConvLimit = posConvLimit; + if (dirConvLimit > 0.0) + _dirConvLimit = dirConvLimit; + if (vConvLimit > 0.0) + _vConvLimit = vConvLimit; + if (maxIter > 0) + _maxIter = maxIter; +} + + +double CylinderFit::GetRadius() const +{ + if (_bIsFitted) + return _dRadius; + else + return 0.0; +} + +Base::Vector3d CylinderFit::GetBase() const +{ + if (_bIsFitted) + return _vBase; + else + return Base::Vector3d(); +} + +Base::Vector3d CylinderFit::GetAxis() const +{ + if (_bIsFitted) + return _vAxis; + else + return Base::Vector3d(); +} + +int CylinderFit::GetNumIterations() const +{ + if (_bIsFitted) + return _numIter; + else + return 0; +} + +float CylinderFit::GetDistanceToCylinder(const Base::Vector3f &rcPoint) const +{ + float fResult = FLOAT_MAX; + if (_bIsFitted) + { + fResult = Base::Vector3d(rcPoint.x, rcPoint.y, rcPoint.z).DistanceToLine(_vBase, _vAxis) - _dRadius; + } + return fResult; +} + +float CylinderFit::GetStdDeviation() const +{ + // Mean: M=(1/N)*SUM Xi + // Variance: VAR=(N/N-3)*[(1/N)*SUM(Xi^2)-M^2] + // Standard deviation: SD=SQRT(VAR) + // Standard error of the mean: SE=SD/SQRT(N) + if (!_bIsFitted) + return FLOAT_MAX; + + float fSumXi = 0.0f, fSumXi2 = 0.0f, + fMean = 0.0f, fDist = 0.0f; + + float ulPtCt = float(CountPoints()); + std::list< Base::Vector3f >::const_iterator cIt; + + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { + fDist = GetDistanceToCylinder( *cIt ); + fSumXi += fDist; + fSumXi2 += ( fDist * fDist ); + } + + fMean = (1.0f / ulPtCt) * fSumXi; + return sqrt((ulPtCt / (ulPtCt - 3.0f)) * ((1.0f / ulPtCt) * fSumXi2 - fMean * fMean)); +} + +void CylinderFit::ProjectToCylinder() +{ + Base::Vector3f cBase(_vBase.x, _vBase.y, _vBase.z); + Base::Vector3f cAxis(_vAxis.x, _vAxis.y, _vAxis.z); + + for (std::list< Base::Vector3f >::iterator it = _vPoints.begin(); it != _vPoints.end(); ++it) { + Base::Vector3f& cPnt = *it; + if (cPnt.DistanceToLine(cBase, cAxis) > 0) { + Base::Vector3f proj; + cBase.ProjectToPlane(cPnt, cAxis, proj); + Base::Vector3f diff = cPnt - proj; + diff.Normalize(); + cPnt = proj + diff * _dRadius; + } + else { + // Point is on the cylinder axis, so it can be moved in + // any direction perpendicular to the cylinder axis + Base::Vector3f cMov(cPnt); + do { + float x = (float(rand()) / float(RAND_MAX)); + float y = (float(rand()) / float(RAND_MAX)); + float z = (float(rand()) / float(RAND_MAX)); + cMov.Move(x,y,z); + } + while (cMov.DistanceToLine(cBase, cAxis) == 0); + + Base::Vector3f proj; + cMov.ProjectToPlane(cPnt, cAxis, proj); + Base::Vector3f diff = cPnt - proj; + diff.Normalize(); + cPnt = proj + diff * _dRadius; + } + } +} + +// Compute approximations for the parameters using all points by computing a +// line through the points. This doesn't work well if the points are only from +// one small surface area. +// In that case rather use SetApproximations() with a better estimate. +void CylinderFit::ComputeApproximationsLine() +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + _vBase.Set(0.0, 0.0, 0.0); + _vAxis.Set(0.0, 0.0, 0.0); + _dRadius = 0.0; + if (_vPoints.size() > 0) + { + std::vector input; + std::transform(_vPoints.begin(), _vPoints.end(), std::back_inserter(input), + [](const Base::Vector3f& v) { return Wm4::Vector3d(v.x, v.y, v.z); }); + Wm4::Line3 kLine = Wm4::OrthogonalLineFit3(input.size(), input.data()); + _vBase.Set(kLine.Origin.X(), kLine.Origin.Y(), kLine.Origin.Z()); + _vAxis.Set(kLine.Direction.X(), kLine.Direction.Y(), kLine.Direction.Z()); + + for (std::list< Base::Vector3f >::const_iterator cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) + _dRadius += Base::Vector3d(cIt->x, cIt->y, cIt->z).DistanceToLine(_vBase, _vAxis); + _dRadius /= (double)_vPoints.size(); + } +} + +float CylinderFit::Fit() +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + + // A minimum of 5 surface points is needed to define a cylinder + if (CountPoints() < 5) + return FLOAT_MAX; + + // If approximations have not been set/computed then compute some now using the line fit method + if (_dRadius == 0.0) + ComputeApproximationsLine(); + + // Check parameters to define the best solution direction + // There are 7 parameters but 2 are actually dependent on the others + // so we are actually solving for 5 parameters. + // order of parameters depending on the solution direction: + // solL: Yc, Zc, M, N, R + // solM: Xc, Zc, L, N, R + // solN: Xc, Yc, L, M, R + SolutionD solDir; + findBestSolDirection(solDir); + + // Initialise some matrices and vectors + std::vector< Base::Vector3d > residuals(CountPoints(), Base::Vector3d(0.0, 0.0, 0.0)); + Matrix5x5 atpa; + Eigen::VectorXd atpl(5); + + // Iteration loop... + double sigma0; + bool cont = true; + while (cont && (_numIter < _maxIter)) + { + ++_numIter; + + // Set up the quasi parameteric normal equations + setupNormalEquationMatrices(solDir, residuals, atpa, atpl); + + // Solve the equations for the unknown corrections + Eigen::LLT< Matrix5x5 > llt(atpa); + if (llt.info() != Eigen::Success) + return FLOAT_MAX; + Eigen::VectorXd x = llt.solve(atpl); + + // Check parameter convergence + cont = false; + if ((fabs(x(0)) > _posConvLimit) || (fabs(x(1)) > _posConvLimit) || // the two position parameter corrections + (fabs(x(2)) > _dirConvLimit) || (fabs(x(3)) > _dirConvLimit) || // the two direction parameter corrections + (fabs(x(4)) > _posConvLimit)) // the radius correction + cont = true; + + // Before updating the unknowns, compute the residuals and sigma0 and check the residual convergence + bool vConverged; + if (!computeResiduals(solDir, x, residuals, sigma0, _vConvLimit, vConverged)) + return FLOAT_MAX; + if (!vConverged) + cont = true; + + // Update the parameters + if (!updateParameters(solDir, x)) + return FLOAT_MAX; + } + + // Check for convergence + if (cont) + return FLOAT_MAX; + + _bIsFitted = true; + _fLastResult = sigma0; + + return _fLastResult; +} + +// Checks initial parameter values and defines the best solution direction to use +// Axis direction = (L,M,N) +// solution L: L is biggest axis component and L = f(M,N) and X = 0 (we move the base point along axis so that x = 0) +// solution M: M is biggest axis component and M = f(L,N) and Y = 0 (we move the base point along axis so that y = 0) +// solution N: N is biggest axis component and N = f(L,M) and Z = 0 (we move the base point along axis so that z = 0) +// IMPLEMENT: use fix X,Y,or Z to value of associated centre of gravity coordinate +// (because 0 could be along way away from cylinder points) +void CylinderFit::findBestSolDirection(SolutionD &solDir) +{ + // Choose the best of the three solution 'directions' to use + // This is to avoid a square root of a negative number when computing the dependent parameters + Base::Vector3d dir = _vAxis; + Base::Vector3d pos = _vBase; + dir.Normalize(); + double biggest = dir.x; + solDir = solL; + if (fabs (dir.y) > fabs (biggest)) + { + biggest = dir.y; + solDir = solM; + } + if (fabs (dir.z) > fabs (biggest)) + { + biggest = dir.z; + solDir = solN; + } + if (biggest < 0.0) + dir.Set(-dir.x, -dir.y, -dir.z); // multiplies by -1 + + double lambda; + switch (solDir) + { + case solL: + lambda = -pos.x / dir.x; + pos.x = 0.0; + pos.y = pos.y + lambda * dir.y; + pos.z = pos.z + lambda * dir.z; + break; + case solM: + lambda = -pos.y / dir.y; + pos.x = pos.x + lambda * dir.x; + pos.y = 0.0; + pos.z = pos.z + lambda * dir.z; + break; + case solN: + lambda = -pos.z / dir.z; + pos.x = pos.x + lambda * dir.x; + pos.y = pos.y + lambda * dir.y; + pos.z = 0.0; + break; + } + _vAxis = dir; + _vBase = pos; +} + +// Set up the normal equation matrices +// atpa ... 5x5 normal matrix +// atpl ... 5x1 matrix (right-hand side of equation) +void CylinderFit::setupNormalEquationMatrices(SolutionD solDir, const std::vector< Base::Vector3d > &residuals, Matrix5x5 &atpa, Eigen::VectorXd &atpl) const +{ + // Zero matrices + atpa.setZero(); + atpl.setZero(); + + // For each point, setup the observation equation coefficients and add their + // contribution into the the normal equation matrices + double a[5], b[3]; + double f0, qw; + std::vector< Base::Vector3d >::const_iterator vIt = residuals.begin(); + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt, ++vIt) + { + // if (using this point) { // currently all given points are used (could modify this if eliminating outliers, etc.... + setupObservation(solDir, *cIt, *vIt, a, f0, qw, b); + addObservationU(a, f0, qw, atpa, atpl); + // } + } + setLowerPart(atpa); +} + +// Sets up contributions of given observation to the quasi parameteric +// normal equation matrices. Assumes uncorrelated coordinates. +// point ... point +// residual ... residual for this point computed from previous iteration (zero for first iteration) +// a[5] ... parameter partials +// f0 ... reference to f0 term +// qw ... reference to quasi weight (here we are assuming equal unit weights for each observed point coordinate) +// b[3] ... observation partials +void CylinderFit::setupObservation(SolutionD solDir, const Base::Vector3f &point, const Base::Vector3d &residual, double a[5], double &f0, double &qw, double b[3]) const +{ + // This adjustment requires an update of the observation approximations + // because the residuals do not have a linear relationship. + // New estimates for the observations: + double xEstimate = (double)point.x + residual.x; + double yEstimate = (double)point.y + residual.y; + double zEstimate = (double)point.z + residual.z; + + // intermediate parameters + double lambda = _vAxis.x * (xEstimate - _vBase.x) + _vAxis.y * (yEstimate - _vBase.y) + _vAxis.z * (zEstimate - _vBase.z); + double x0 = _vBase.x + lambda * _vAxis.x; + double y0 = _vBase.y + lambda * _vAxis.y; + double z0 = _vBase.z + lambda * _vAxis.z; + double dx = xEstimate - x0; + double dy = yEstimate - y0; + double dz = zEstimate - z0; + double dx00 = _vBase.x - xEstimate; + double dy00 = _vBase.y - yEstimate; + double dz00 = _vBase.z - zEstimate; + + // partials of the observations + b[0] = 2.0 * (dx - _vAxis.x * _vAxis.x * dx - _vAxis.x * _vAxis.y * dy - _vAxis.x * _vAxis.z * dz); + b[1] = 2.0 * (dy - _vAxis.x * _vAxis.y * dx - _vAxis.y * _vAxis.y * dy - _vAxis.y * _vAxis.z * dz); + b[2] = 2.0 * (dz - _vAxis.x * _vAxis.z * dx - _vAxis.y * _vAxis.z * dy - _vAxis.z * _vAxis.z * dz); + + // partials of the parameters + switch (solDir) + { + double ddxdl, ddydl, ddzdl; + double ddxdm, ddydm, ddzdm; + double ddxdn, ddydn, ddzdn; + case solL: + // order of parameters: Yc, Zc, M, N, R + ddxdm = -2.0 * _vAxis.y * dx00 + (_vAxis.x - _vAxis.y * _vAxis.y / _vAxis.x) * dy00 - (_vAxis.y * _vAxis.z / _vAxis.x) * dz00; + ddydm = (_vAxis.x - _vAxis.y * _vAxis.y / _vAxis.x) * dx00 + 2.0 * _vAxis.y * dy00 + _vAxis.z * dz00; + ddzdm = -(_vAxis.y * _vAxis.z / _vAxis.x) * dx00 + _vAxis.z * dy00; + ddxdn = -2.0 * _vAxis.z * dx00 - (_vAxis.y * _vAxis.z / _vAxis.x) * dy00 + (_vAxis.x - _vAxis.z * _vAxis.z / _vAxis.x) * dz00; + ddydn = -(_vAxis.y * _vAxis.z / _vAxis.x) * dx00 + _vAxis.y * dz00; + ddzdn = (_vAxis.x - _vAxis.z * _vAxis.z / _vAxis.x) * dx00 + _vAxis.y * dy00 + 2.0 * _vAxis.z * dz00; + a[0] = -b[1]; + a[1] = -b[2]; + a[2] = 2.0 * (dx * ddxdm + dy * ddydm + dz * ddzdm); + a[3] = 2.0 * (dx * ddxdn + dy * ddydn + dz * ddzdn); + a[4] = -2.0 * _dRadius; + break; + case solM: + // order of parameters: Xc, Zc, L, N, R + ddxdl = 2.0 * _vAxis.x * dx00 + (_vAxis.y - _vAxis.x * _vAxis.x / _vAxis.y) * dy00 + _vAxis.z * dz00; + ddydl = (_vAxis.y - _vAxis.x * _vAxis.x / _vAxis.y) * dx00 - 2.0 * _vAxis.x * dy00 - (_vAxis.x * _vAxis.z / _vAxis.y) * dz00; + ddzdl = _vAxis.z * dx00 - (_vAxis.x * _vAxis.z / _vAxis.y) * dy00; + ddxdn = -(_vAxis.x * _vAxis.z / _vAxis.y) * dy00 + _vAxis.x * dz00; + ddydn = -(_vAxis.x * _vAxis.z / _vAxis.y) * dx00 - 2.0 * _vAxis.z * dy00 + (_vAxis.y - _vAxis.z * _vAxis.z / _vAxis.y) * dz00; + ddzdn = _vAxis.x * dx00 + (_vAxis.y - _vAxis.z * _vAxis.z / _vAxis.y) * dy00 + 2.0 * _vAxis.z * dz00; + a[0] = -b[0]; + a[1] = -b[2]; + a[2] = 2.0 * (dx * ddxdl + dy * ddydl + dz * ddzdl); + a[3] = 2.0 * (dx * ddxdn + dy * ddydn + dz * ddzdn); + a[4] = -2.0 * _dRadius; + break; + case solN: + // order of parameters: Xc, Yc, L, M, R + ddxdl = 2.0 * _vAxis.x * dx00 + _vAxis.y * dy00 + (_vAxis.z - _vAxis.x * _vAxis.x / _vAxis.z) * dz00; + ddydl = _vAxis.y * dx00 - (_vAxis.x * _vAxis.y / _vAxis.z) * dz00; + ddzdl = (_vAxis.z - _vAxis.x * _vAxis.x / _vAxis.z) * dx00 - (_vAxis.x * _vAxis.y / _vAxis.z) * dy00 - 2.0 * _vAxis.x * dz00; + ddxdm = _vAxis.x * dy00 - (_vAxis.x * _vAxis.y / _vAxis.z) * dz00; + ddydm = _vAxis.x * dx00 + 2.0 * _vAxis.y * dy00 + (_vAxis.z - _vAxis.y * _vAxis.y / _vAxis.z) * dz00; + ddzdm = - (_vAxis.x * _vAxis.y / _vAxis.z) * dx00 + (_vAxis.z - _vAxis.y * _vAxis.y / _vAxis.z) * dy00 - 2.0 * _vAxis.y * dz00; + a[0] = -b[0]; + a[1] = -b[1]; + a[2] = 2.0 * (dx * ddxdl + dy * ddydl + dz * ddzdl); + a[3] = 2.0 * (dx * ddxdm + dy * ddydm + dz * ddzdm); + a[4] = -2.0 * _dRadius; + break; + } + + // free term + f0 = _dRadius * _dRadius - dx * dx - dy * dy - dz * dz + b[0] * residual.x + b[1] * residual.y + b[2] * residual.z; + + // quasi weight (using equal weights for cylinder point coordinate observations) + //w[0] = 1.0; + //w[1] = 1.0; + //w[2] = 1.0; + //qw = 1.0 / (b[0] * b[0] / w[0] + b[1] * b[1] / w[1] + b[2] * b[2] / w[2]); + qw = 1.0 / (b[0] * b[0] + b[1] * b[1] + b[2] * b[2]); +} + +// Computes contribution of the given observation equation on the normal equation matrices +// Call this for each observation (point) +// Here we only add the contribution to the upper part of the normal matrix +// and then after all observations have been added we need to set the lower part +// (which is symmetrical to the upper part) +// a[5] ... parameter partials +// li ... free term (f0) +// pi ... weight of observation (= quasi weight qw for this solution) +// atpa ... 5x5 normal equation matrix +// atpl ... 5x1 matrix/vector (right-hand side of equations) +void CylinderFit::addObservationU(double a[5], double li, double pi, Matrix5x5 &atpa, Eigen::VectorXd &atpl) const +{ + for (int i = 0; i < 5; ++i) + { + double aipi = a[i] * pi; + for (int j = i; j < 5; ++j) + { + atpa(i, j) += aipi * a[j]; + //atpa(j, i) = atpa(i, j); // it's a symmetrical matrix, we'll set this later after all observations processed + } + atpl(i) += aipi * li; + } +} + +// Set the lower part of the normal matrix equal to the upper part +// This is done after all the observations have been added +void CylinderFit::setLowerPart(Matrix5x5 &atpa) const +{ + for (int i = 0; i < 5; ++i) + for (int j = i+1; j < 5; ++j) // skip the diagonal elements + atpa(j, i) = atpa(i, j); +} + +// Compute the residuals and sigma0 and check the residual convergence +bool CylinderFit::computeResiduals(SolutionD solDir, const Eigen::VectorXd &x, std::vector< Base::Vector3d > &residuals, double &sigma0, double vConvLimit, bool &vConverged) const +{ + vConverged = true; + int nPtsUsed = 0; + sigma0 = 0.0; + double a[5], b[3]; + double f0, qw; + //double maxdVx = 0.0; + //double maxdVy = 0.0; + //double maxdVz = 0.0; + //double rmsVv = 0.0; + std::vector< Base::Vector3d >::iterator vIt = residuals.begin(); + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt, ++vIt) + { + // if (using this point) { // currently all given points are used (could modify this if eliminating outliers, etc.... + ++nPtsUsed; + Base::Vector3d &v = *vIt; + setupObservation(solDir, *cIt, v, a, f0, qw, b); + double qv = -f0; + for (int i = 0; i < 5; ++i) + qv += a[i] * x(i); + + // We are using equal weights for cylinder point coordinate observations (see setupObservation) + // i.e. w[0] = w[1] = w[2] = 1.0; + //double vx = -qw * qv * b[0] / w[0]; + //double vy = -qw * qv * b[1] / w[1]; + //double vz = -qw * qv * b[2] / w[2]; + double vx = -qw * qv * b[0]; + double vy = -qw * qv * b[1]; + double vz = -qw * qv * b[2]; + double dVx = fabs(vx - v.x); + double dVy = fabs(vy - v.y); + double dVz = fabs(vz - v.z); + v.x = vx; + v.y = vy; + v.z = vz; + + //double vv = v.x * v.x + v.y * v.y + v.z * v.z; + //rmsVv += vv * vv; + + //sigma0 += v.x * w[0] * v.x + v.y * w[1] * v.y + v.z * w[2] * v.z; + sigma0 += v.x * v.x + v.y * v.y + v.z * v.z; + + if ((dVx > vConvLimit) || (dVy > vConvLimit) || (dVz > vConvLimit)) + vConverged = false; + + //if (dVx > maxdVx) + // maxdVx = dVx; + //if (dVy > maxdVy) + // maxdVy = dVy; + //if (dVz > maxdVz) + // maxdVz = dVz; + } + + // Compute degrees of freedom and sigma0 + if (nPtsUsed < 5) // A minimum of 5 surface points is needed to define a cylinder + { + sigma0 = 0.0; + return false; + } + int df = nPtsUsed - 5; + if (df == 0) + sigma0 = 0.0; + else + sigma0 = sqrt (sigma0 / (double)df); + + //rmsVv = sqrt(rmsVv / (double)nPtsUsed); + //Base::Console().Message("X: %0.3e %0.3e %0.3e %0.3e %0.3e , Max dV: %0.4f %0.4f %0.4f , RMS Vv: %0.4f\n", x(0), x(1), x(2), x(3), x(4), maxdVx, maxdVy, maxdVz, rmsVv); + + return true; +} + +// Update the parameters after solving the normal equations +bool CylinderFit::updateParameters(SolutionD solDir, const Eigen::VectorXd &x) +{ + // Update the parameters used as unknowns in the solution + switch (solDir) + { + case solL: // order of parameters: Yc, Zc, M, N, R + _vBase.y += x(0); + _vBase.z += x(1); + _vAxis.y += x(2); + _vAxis.z += x(3); + _dRadius += x(4); + break; + case solM: // order of parameters: Xc, Zc, L, N, R + _vBase.x += x(0); + _vBase.z += x(1); + _vAxis.x += x(2); + _vAxis.z += x(3); + _dRadius += x(4); + break; + case solN: // order of parameters: Xc, Yc, L, M, R + _vBase.x += x(0); + _vBase.y += x(1); + _vAxis.x += x(2); + _vAxis.y += x(3); + _dRadius += x(4); + break; + } + + // Update the dependent axis direction parameter + double l2, m2, n2; + switch (solDir) + { + case solL: + l2 = 1.0 - _vAxis.y * _vAxis.y - _vAxis.z * _vAxis.z; + if (l2 <= 0.0) + return false; // L*L <= 0 ! + _vAxis.x = sqrt(l2); + //_vBase.x = 0.0; // should already be 0 + break; + case solM: + m2 = 1.0 - _vAxis.x * _vAxis.x - _vAxis.z * _vAxis.z; + if (m2 <= 0.0) + return false; // M*M <= 0 ! + _vAxis.y = sqrt(m2); + //_vBase.y = 0.0; // should already be 0 + break; + case solN: + n2 = 1.0 - _vAxis.x * _vAxis.x - _vAxis.y * _vAxis.y; + if (n2 <= 0.0) + return false; // N*N <= 0 ! + _vAxis.z = sqrt(n2); + //_vBase.z = 0.0; // should already be 0 + break; + } + + return true; +} diff --git a/src/Mod/Mesh/App/Core/CylinderFit.h b/src/Mod/Mesh/App/Core/CylinderFit.h new file mode 100644 index 0000000000..520fad2c9f --- /dev/null +++ b/src/Mod/Mesh/App/Core/CylinderFit.h @@ -0,0 +1,151 @@ +/*************************************************************************** + * Copyright (c) 2020 Graeme van der Vlugt * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef MESH_CYLINDER_FIT_H +#define MESH_CYLINDER_FIT_H + +#include "Approximation.h" +#include + +// ------------------------------------------------------------------------------- +namespace MeshCoreFit { + +typedef Eigen::Matrix Matrix5x5; + +/** + * Best-fit cylinder for a given set of points. + * Doesn't expect points on any top or bottom end-planes, only points on the side surface + */ +class MeshExport CylinderFit : public MeshCore::Approximation +{ +protected: + // Solution 'direction' enumeration + enum SolutionD {solL = 0, // solution L: L is biggest axis component and L = f(M,N) + solM = 1, // solution M: M is biggest axis component and M = f(L,N) + solN = 2 // solution N: N is biggest axis component and N = f(L,M) + }; +public: + /** + * Construction + */ + CylinderFit(); + /** + * Destruction + */ + virtual ~CylinderFit(); + + /** + * Set approximations before calling Fit() + */ + void SetApproximations(double radius, const Base::Vector3d &base, const Base::Vector3d &axis); + /** + * Set iteration convergence criteria for the fit if special values are needed. + * The default values set in the constructor are suitable for most uses + */ + void SetConvergenceCriteria(double posConvLimit, double dirConvLimit, double vConvLimit, int maxIter); + /** + * Returns the radius of the fitted cylinder. If Fit() has not been called then zero is returned. + */ + double GetRadius() const; + /** + * Returns the base of the fitted cylinder. If Fit() has not been called the null vector is returned. + */ + Base::Vector3d GetBase() const; + /** + * Returns the axis of the fitted cylinder. If Fit() has not been called the null vector is returned. + */ + Base::Vector3d GetAxis() const; + /** + * Returns the number of iterations that Fit() needed to converge. If Fit() has not been called then zero is returned. + */ + int GetNumIterations() const; + /** + * Fit a cylinder into the given points. If the fit fails FLOAT_MAX is returned. + */ + float Fit(); + /** + * Returns the distance from the point \a rcPoint to the fitted cylinder. If Fit() has not been + * called FLOAT_MAX is returned. + */ + float GetDistanceToCylinder(const Base::Vector3f &rcPoint) const; + /** + * Returns the standard deviation from the points to the fitted cylinder. If Fit() has not been + * called FLOAT_MAX is returned. + */ + float GetStdDeviation() const; + /** + * Projects the points onto the fitted cylinder. + */ + void ProjectToCylinder(); + +protected: + /** + * Compute approximations for the parameters using all points using the line fit method + */ + void ComputeApproximationsLine(); + /** + * Checks initial parameter values and defines the best solution direction to use + */ + void findBestSolDirection(SolutionD &solDir); + /** + * Set up the normal equations + */ + void setupNormalEquationMatrices(SolutionD solDir, const std::vector< Base::Vector3d > &residuals, Matrix5x5 &atpa, Eigen::VectorXd &atpl) const; + /** + * Sets up contributions of given observation to the normal equation matrices. + */ + void setupObservation(SolutionD solDir, const Base::Vector3f &point, const Base::Vector3d &residual, double a[5], double &f0, double &qw, double b[3]) const; + /** + * Computes contribution of the given observation equation on the normal equation matrices + */ + void addObservationU(double a[5], double li, double pi, Matrix5x5 &atpa, Eigen::VectorXd &atpl) const; + /** + * Set the lower part of the normal matrix equal to the upper part + */ + void setLowerPart(Matrix5x5 &atpa) const; + + /** + * Compute the residuals and sigma0 and check the residual convergence + */ + bool computeResiduals(SolutionD solDir, const Eigen::VectorXd &x, std::vector< Base::Vector3d > &residuals, double &sigma0, double vConvLimit, bool &vConverged) const; + /** + * Update the parameters after solving the normal equations + */ + bool updateParameters(SolutionD solDir, const Eigen::VectorXd &x); + +protected: + Base::Vector3d _vBase; /**< Base vector of the cylinder (point on axis). */ + Base::Vector3d _vAxis; /**< Axis of the cylinder. */ + double _dRadius; /**< Radius of the cylinder. */ + int _numIter; /**< Number of iterations for solution to converge. */ + double _posConvLimit; /**< Position and radius parameter convergence threshold. */ + double _dirConvLimit; /**< Direction parameter convergence threshold. */ + double _vConvLimit; /**< Residual convergence threshold. */ + int _maxIter; /**< Maximum number of iterations. */ + +}; + + +} // namespace MeshCore + +#endif // MESH_CYLINDER_FIT_H diff --git a/src/Mod/Mesh/App/Core/SphereFit.cpp b/src/Mod/Mesh/App/Core/SphereFit.cpp new file mode 100644 index 0000000000..5285c69d1f --- /dev/null +++ b/src/Mod/Mesh/App/Core/SphereFit.cpp @@ -0,0 +1,427 @@ +/*************************************************************************** + * Copyright (c) 2020 Graeme van der Vlugt * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +# include +# include +#endif + +#include "SphereFit.h" +#include + +using namespace MeshCoreFit; + +SphereFit::SphereFit() + : _vCenter(0,0,0) + , _dRadius(0) + , _numIter(0) + , _posConvLimit(0.0001) + , _vConvLimit(0.001) + , _maxIter(50) +{ +} + +SphereFit::~SphereFit() +{ +} + +// Set approximations before calling the fitting +void SphereFit::SetApproximations(double radius, const Base::Vector3d ¢er) +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + _dRadius = radius; + _vCenter = center; +} + +// Set iteration convergence criteria for the fit if special values are needed. +// The default values set in the constructor are suitable for most uses +void SphereFit::SetConvergenceCriteria(double posConvLimit, double vConvLimit, int maxIter) +{ + if (posConvLimit > 0.0) + _posConvLimit = posConvLimit; + if (vConvLimit > 0.0) + _vConvLimit = vConvLimit; + if (maxIter > 0) + _maxIter = maxIter; +} + + +double SphereFit::GetRadius() const +{ + if (_bIsFitted) + return _dRadius; + else + return 0.0; +} + +Base::Vector3d SphereFit::GetCenter() const +{ + if (_bIsFitted) + return _vCenter; + else + return Base::Vector3d(); +} + +int SphereFit::GetNumIterations() const +{ + if (_bIsFitted) + return _numIter; + else + return 0; +} + +float SphereFit::GetDistanceToSphere(const Base::Vector3f &rcPoint) const +{ + float fResult = FLOAT_MAX; + if (_bIsFitted) + { + fResult = Base::Vector3d((double)rcPoint.x - _vCenter.x, (double)rcPoint.y - _vCenter.y, (double)rcPoint.z - _vCenter.z).Length() - _dRadius; + } + return fResult; +} + +float SphereFit::GetStdDeviation() const +{ + // Mean: M=(1/N)*SUM Xi + // Variance: VAR=(N/N-3)*[(1/N)*SUM(Xi^2)-M^2] + // Standard deviation: SD=SQRT(VAR) + // Standard error of the mean: SE=SD/SQRT(N) + if (!_bIsFitted) + return FLOAT_MAX; + + float fSumXi = 0.0f, fSumXi2 = 0.0f, + fMean = 0.0f, fDist = 0.0f; + + float ulPtCt = float(CountPoints()); + std::list< Base::Vector3f >::const_iterator cIt; + + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) { + fDist = GetDistanceToSphere( *cIt ); + fSumXi += fDist; + fSumXi2 += ( fDist * fDist ); + } + + fMean = (1.0f / ulPtCt) * fSumXi; + return sqrt((ulPtCt / (ulPtCt - 3.0f)) * ((1.0f / ulPtCt) * fSumXi2 - fMean * fMean)); +} + +void SphereFit::ProjectToSphere() +{ + for (std::list< Base::Vector3f >::iterator it = _vPoints.begin(); it != _vPoints.end(); ++it) { + Base::Vector3f& cPnt = *it; + + // Compute unit vector from sphere centre to point. + // Because this vector is orthogonal to the sphere's surface at the + // intersection point we can easily compute the projection point on the + // closest surface point using the radius of the sphere + Base::Vector3d diff((double)cPnt.x - _vCenter.x, (double)cPnt.y - _vCenter.y, (double)cPnt.z - _vCenter.z); + double length = diff.Length(); + if (length == 0.0) + { + // Point is exactly at the sphere center, so it can be projected in any direction onto the sphere! + // So here just project in +Z direction + cPnt.z += (float)_dRadius; + } + else + { + diff /= length; // normalizing the vector + Base::Vector3d proj = _vCenter + diff * _dRadius; + cPnt.x = (float)proj.x; + cPnt.y = (float)proj.y; + cPnt.z = (float)proj.z; + } + } +} + +// Compute approximations for the parameters using all points: +// Set centre to centre of gravity of points and radius to the average +// distance from the centre of gravity to the points. +void SphereFit::ComputeApproximations() +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + _vCenter.Set(0.0, 0.0, 0.0); + _dRadius = 0.0; + if (_vPoints.size() > 0) + { + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) + { + _vCenter.x += cIt->x; + _vCenter.y += cIt->y; + _vCenter.z += cIt->z; + } + _vCenter /= (double)_vPoints.size(); + + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt) + { + Base::Vector3d diff((double)cIt->x - _vCenter.x, (double)cIt->y - _vCenter.y, (double)cIt->z - _vCenter.z); + _dRadius += diff.Length(); + } + _dRadius /= (double)_vPoints.size(); + } +} + +float SphereFit::Fit() +{ + _bIsFitted = false; + _fLastResult = FLOAT_MAX; + _numIter = 0; + + // A minimum of 4 surface points is needed to define a sphere + if (CountPoints() < 4) + return FLOAT_MAX; + + // If approximations have not been set/computed then compute some now + if (_dRadius == 0.0) + ComputeApproximations(); + + // Initialise some matrices and vectors + std::vector< Base::Vector3d > residuals(CountPoints(), Base::Vector3d(0.0, 0.0, 0.0)); + Matrix4x4 atpa; + Eigen::VectorXd atpl(4); + + // Iteration loop... + double sigma0; + bool cont = true; + while (cont && (_numIter < _maxIter)) + { + ++_numIter; + + // Set up the quasi parameteric normal equations + setupNormalEquationMatrices(residuals, atpa, atpl); + + // Solve the equations for the unknown corrections + Eigen::LLT< Matrix4x4 > llt(atpa); + if (llt.info() != Eigen::Success) + return FLOAT_MAX; + Eigen::VectorXd x = llt.solve(atpl); + + // Check parameter convergence (order of parameters: X,Y,Z,R) + cont = false; + if ((fabs(x(0)) > _posConvLimit) || (fabs(x(1)) > _posConvLimit) || + (fabs(x(2)) > _posConvLimit) || (fabs(x(3)) > _posConvLimit)) + cont = true; + + // Before updating the unknowns, compute the residuals and sigma0 and check the residual convergence + bool vConverged; + if (!computeResiduals(x, residuals, sigma0, _vConvLimit, vConverged)) + return FLOAT_MAX; + if (!vConverged) + cont = true; + + // Update the parameters (order of parameters: X,Y,Z,R) + _vCenter.x += x(0); + _vCenter.y += x(1); + _vCenter.z += x(2); + _dRadius += x(3); + } + + // Check for convergence + if (cont) + return FLOAT_MAX; + + _bIsFitted = true; + _fLastResult = sigma0; + + return _fLastResult; +} + +// Set up the normal equation matrices +// atpa ... 4x4 normal matrix +// atpl ... 4x1 matrix (right-hand side of equation) +void SphereFit::setupNormalEquationMatrices(const std::vector< Base::Vector3d > &residuals, Matrix4x4 &atpa, Eigen::VectorXd &atpl) const +{ + // Zero matrices + atpa.setZero(); + atpl.setZero(); + + // For each point, setup the observation equation coefficients and add their + // contribution into the the normal equation matrices + double a[4], b[3]; + double f0, qw; + std::vector< Base::Vector3d >::const_iterator vIt = residuals.begin(); + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt, ++vIt) + { + // if (using this point) { // currently all given points are used (could modify this if eliminating outliers, etc.... + setupObservation(*cIt, *vIt, a, f0, qw, b); + addObservationU(a, f0, qw, atpa, atpl); + // } + } + setLowerPart(atpa); +} + +// Sets up contributions of given observation to the quasi parameteric +// normal equation matrices. Assumes uncorrelated coordinates. +// point ... point +// residual ... residual for this point computed from previous iteration (zero for first iteration) +// a[4] ... parameter partials (order of parameters: X,Y,Z,R) +// f0 ... reference to f0 term +// qw ... reference to quasi weight (here we are assuming equal unit weights for each observed point coordinate) +// b[3] ... observation partials +void SphereFit::setupObservation(const Base::Vector3f &point, const Base::Vector3d &residual, double a[4], double &f0, double &qw, double b[3]) const +{ + // This adjustment requires an update of the observation approximations + // because the residuals do not have a linear relationship. + // New estimates for the observations: + double xEstimate = (double)point.x + residual.x; + double yEstimate = (double)point.y + residual.y; + double zEstimate = (double)point.z + residual.z; + + // partials of the observations + double dx = xEstimate - _vCenter.x; + double dy = yEstimate - _vCenter.y; + double dz = zEstimate - _vCenter.z; + b[0] = 2.0 * dx; + b[1] = 2.0 * dy; + b[2] = 2.0 * dz; + + // partials of the parameters + a[0] = -b[0]; + a[1] = -b[1]; + a[2] = -b[2]; + a[3] = -2.0 * _dRadius; + + // free term + f0 = _dRadius * _dRadius - dx * dx - dy * dy - dz * dz + b[0] * residual.x + b[1] * residual.y + b[2] * residual.z; + + // quasi weight (using equal weights for sphere point coordinate observations) + //w[0] = 1.0; + //w[1] = 1.0; + //w[2] = 1.0; + //qw = 1.0 / (b[0] * b[0] / w[0] + b[1] * b[1] / w[1] + b[2] * b[2] / w[2]); + qw = 1.0 / (b[0] * b[0] + b[1] * b[1] + b[2] * b[2]); +} + +// Computes contribution of the given observation equation on the normal equation matrices +// Call this for each observation (point) +// Here we only add the contribution to the upper part of the normal matrix +// and then after all observations have been added we need to set the lower part +// (which is symmetrical to the upper part) +// a[4] ... parameter partials +// li ... free term (f0) +// pi ... weight of observation (= quasi weight qw for this solution) +// atpa ... 4x4 normal equation matrix +// atpl ... 4x1 matrix/vector (right-hand side of equations) +void SphereFit::addObservationU(double a[4], double li, double pi, Matrix4x4 &atpa, Eigen::VectorXd &atpl) const +{ + for (int i = 0; i < 4; ++i) + { + double aipi = a[i] * pi; + for (int j = i; j < 4; ++j) + { + atpa(i, j) += aipi * a[j]; + //atpa(j, i) = atpa(i, j); // it's a symmetrical matrix, we'll set this later after all observations processed + } + atpl(i) += aipi * li; + } +} + +// Set the lower part of the normal matrix equal to the upper part +// This is done after all the observations have been added +void SphereFit::setLowerPart(Matrix4x4 &atpa) const +{ + for (int i = 0; i < 4; ++i) + for (int j = i+1; j < 4; ++j) // skip the diagonal elements + atpa(j, i) = atpa(i, j); +} + +// Compute the residuals and sigma0 and check the residual convergence +bool SphereFit::computeResiduals(const Eigen::VectorXd &x, std::vector< Base::Vector3d > &residuals, double &sigma0, double vConvLimit, bool &vConverged) const +{ + vConverged = true; + int nPtsUsed = 0; + sigma0 = 0.0; + double a[4], b[3]; + double f0, qw; + //double maxdVx = 0.0; + //double maxdVy = 0.0; + //double maxdVz = 0.0; + //double rmsVv = 0.0; + std::vector< Base::Vector3d >::iterator vIt = residuals.begin(); + std::list< Base::Vector3f >::const_iterator cIt; + for (cIt = _vPoints.begin(); cIt != _vPoints.end(); ++cIt, ++vIt) + { + // if (using this point) { // currently all given points are used (could modify this if eliminating outliers, etc.... + ++nPtsUsed; + Base::Vector3d &v = *vIt; + setupObservation(*cIt, v, a, f0, qw, b); + double qv = -f0; + for (int i = 0; i < 4; ++i) + qv += a[i] * x(i); + + // We are using equal weights for sphere point coordinate observations (see setupObservation) + // i.e. w[0] = w[1] = w[2] = 1.0; + //double vx = -qw * qv * b[0] / w[0]; + //double vy = -qw * qv * b[1] / w[1]; + //double vz = -qw * qv * b[2] / w[2]; + double vx = -qw * qv * b[0]; + double vy = -qw * qv * b[1]; + double vz = -qw * qv * b[2]; + double dVx = fabs(vx - v.x); + double dVy = fabs(vy - v.y); + double dVz = fabs(vz - v.z); + v.x = vx; + v.y = vy; + v.z = vz; + + //double vv = v.x * v.x + v.y * v.y + v.z * v.z; + //rmsVv += vv * vv; + + //sigma0 += v.x * w[0] * v.x + v.y * w[1] * v.y + v.z * w[2] * v.z; + sigma0 += v.x * v.x + v.y * v.y + v.z * v.z; + + if ((dVx > vConvLimit) || (dVy > vConvLimit) || (dVz > vConvLimit)) + vConverged = false; + + //if (dVx > maxdVx) + // maxdVx = dVx; + //if (dVy > maxdVy) + // maxdVy = dVy; + //if (dVz > maxdVz) + // maxdVz = dVz; + } + + // Compute degrees of freedom and sigma0 + if (nPtsUsed < 4) // A minimum of 4 surface points is needed to define a sphere + { + sigma0 = 0.0; + return false; + } + int df = nPtsUsed - 4; + if (df == 0) + sigma0 = 0.0; + else + sigma0 = sqrt (sigma0 / (double)df); + + //rmsVv = sqrt(rmsVv / (double)nPtsUsed); + //Base::Console().Message("X: %0.3e %0.3e %0.3e %0.3e , Max dV: %0.4f %0.4f %0.4f , RMS Vv: %0.4f\n", x(0), x(1), x(2), x(3), maxdVx, maxdVy, maxdVz, rmsVv); + + return true; +} diff --git a/src/Mod/Mesh/App/Core/SphereFit.h b/src/Mod/Mesh/App/Core/SphereFit.h new file mode 100644 index 0000000000..1b475f36a0 --- /dev/null +++ b/src/Mod/Mesh/App/Core/SphereFit.h @@ -0,0 +1,130 @@ +/*************************************************************************** + * Copyright (c) 2020 Graeme van der Vlugt * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + + +#ifndef MESH_SPHERE_FIT_H +#define MESH_SPHERE_FIT_H + +#include "Approximation.h" +#include + +// ------------------------------------------------------------------------------- +namespace MeshCoreFit { + +typedef Eigen::Matrix Matrix4x4; + +/** + * Best-fit sphere for a given set of points. + */ +class MeshExport SphereFit : public MeshCore::Approximation +{ +public: + /** + * Construction + */ + SphereFit(); + /** + * Destruction + */ + virtual ~SphereFit(); + + /** + * Set approximations before calling Fit() + */ + void SetApproximations(double radius, const Base::Vector3d ¢er); + /** + * Set iteration convergence criteria for the fit if special values are needed. + * The default values set in the constructor are suitable for most uses + */ + void SetConvergenceCriteria(double posConvLimit, double vConvLimit, int maxIter); + /** + * Returns the radius of the fitted sphere. If Fit() has not been called then zero is returned. + */ + double GetRadius() const; + /** + * Returns the center of the fitted sphere. If Fit() has not been called the null vector is returned. + */ + Base::Vector3d GetCenter() const; + /** + * Returns the number of iterations that Fit() needed to converge. If Fit() has not been called then zero is returned. + */ + int GetNumIterations() const; + /** + * Compute approximations for the parameters using all points + */ + void ComputeApproximations(); + /** + * Fit a sphere onto the given points. If the fit fails FLOAT_MAX is returned. + */ + float Fit(); + /** + * Returns the distance from the point \a rcPoint to the fitted sphere. If Fit() has not been + * called FLOAT_MAX is returned. + */ + float GetDistanceToSphere(const Base::Vector3f &rcPoint) const; + /** + * Returns the standard deviation from the points to the fitted sphere. If Fit() has not been + * called FLOAT_MAX is returned. + */ + float GetStdDeviation() const; + /** + * Projects the points onto the fitted sphere. + */ + void ProjectToSphere(); + +protected: + /** + * Set up the normal equations + */ + void setupNormalEquationMatrices(const std::vector< Base::Vector3d > &residuals, Matrix4x4 &atpa, Eigen::VectorXd &atpl) const; + /** + * Sets up contributions of given observation to the normal equation matrices. + */ + void setupObservation(const Base::Vector3f &point, const Base::Vector3d &residual, double a[4], double &f0, double &qw, double b[3]) const; + /** + * Computes contribution of the given observation equation on the normal equation matrices + */ + void addObservationU(double a[4], double li, double pi, Matrix4x4 &atpa, Eigen::VectorXd &atpl) const; + /** + * Set the lower part of the normal matrix equal to the upper part + */ + void setLowerPart(Matrix4x4 &atpa) const; + + /** + * Compute the residuals and sigma0 and check the residual convergence + */ + bool computeResiduals(const Eigen::VectorXd &x, std::vector< Base::Vector3d > &residuals, double &sigma0, double vConvLimit, bool &vConverged) const; + +protected: + Base::Vector3d _vCenter;/**< Center of sphere. */ + double _dRadius; /**< Radius of the sphere. */ + int _numIter; /**< Number of iterations for solution to converge. */ + double _posConvLimit; /**< Position and radius parameter convergence threshold. */ + double _vConvLimit; /**< Residual convergence threshold. */ + int _maxIter; /**< Maximum number of iterations. */ + +}; + + +} // namespace MeshCore + +#endif // MESH_SPHERE_FIT_H From b899d6c8b11a63dc0c3433956ad128fffffb0c56 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 16 Apr 2020 12:53:17 -0500 Subject: [PATCH 116/142] Path: Optimization for open edges update Remove required usage of DocObject creation, in lieu of Part geometry usage - the preferred method. Limit DocObject creation to debugging mode only. Remove dependency on Draft module. Drawback is top edge must be selected, and Final Depth set appropriately when using profiling open edges. --- src/Mod/Path/PathScripts/PathProfileEdges.py | 336 ++++++++----------- 1 file changed, 132 insertions(+), 204 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathProfileEdges.py b/src/Mod/Path/PathScripts/PathProfileEdges.py index 515e0de5dd..26ee36d541 100644 --- a/src/Mod/Path/PathScripts/PathProfileEdges.py +++ b/src/Mod/Path/PathScripts/PathProfileEdges.py @@ -34,7 +34,6 @@ import PySide # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader -Draft = LazyLoader('Draft', globals(), 'Draft') Part = LazyLoader('Part', globals(), 'Part') DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') @@ -51,6 +50,7 @@ __title__ = "Path Profile Edges Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" __doc__ = "Path Profile operation based on edges." +__contributors__ = "russ4262 (Russell Johnson)" class ObjectProfile(PathProfileBase.ObjectProfile): @@ -69,8 +69,10 @@ class ObjectProfile(PathProfileBase.ObjectProfile): '''areaOpShapes(obj) ... returns envelope for all wires formed by the base edges.''' PathLog.track() - self.tmpGrp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', 'tmpDebugGrp') - tmpGrpNm = self.tmpGrp.Name + inaccessible = translate('PathProfileEdges', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') + if PathLog.getLevel(PathLog.thisModule()) == 4: + self.tmpGrp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', 'tmpDebugGrp') + tmpGrpNm = self.tmpGrp.Name self.JOB = PathUtils.findParentJob(obj) self.offsetExtra = abs(obj.OffsetExtra.Value) @@ -104,7 +106,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile): # f = Part.makeFace(wire, 'Part::FaceMakerSimple') # if planar error, Comment out previous line, uncomment the next two (origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value) - f = origWire.Shape.Wires[0] + f = origWire.Wires[0] if f is not False: # shift the compound to the bottom of the base object for proper sectioning zShift = zMin - f.BoundBox.ZMin @@ -113,7 +115,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile): env = PathUtils.getEnvelope(base.Shape, subshape=f, depthparams=self.depthparams) shapes.append((env, False)) else: - PathLog.error(translate('PathProfileEdges', 'The selected edge(s) are inaccessible.')) + PathLog.error(inaccessible) else: if self.JOB.GeometryTolerance.Value == 0.0: msg = self.JOB.Label + '.GeometryTolerance = 0.0.' @@ -121,76 +123,64 @@ class ObjectProfile(PathProfileBase.ObjectProfile): PathLog.error(msg) else: cutWireObjs = False - (origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value) - cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) - if cutShp is not False: - cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) + flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value) + if flattened: + (origWire, flatWire) = flattened + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlatWire') + os.Shape = flatWire + os.purgeTouched() + self.tmpGrp.addObject(os) + cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) + if cutShp is not False: + cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) - if cutWireObjs is not False: - for cW in cutWireObjs: - shapes.append((cW, False)) - self.profileEdgesIsOpen = True + if cutWireObjs is not False: + for cW in cutWireObjs: + shapes.append((cW, False)) + self.profileEdgesIsOpen = True + else: + PathLog.error(inaccessible) else: - PathLog.error(translate('PathProfileEdges', 'The selected edge(s) are inaccessible.')) + PathLog.error(inaccessible) # Delete the temporary objects - if PathLog.getLevel(PathLog.thisModule()) != 4: - for to in self.tmpGrp.Group: - FreeCAD.ActiveDocument.removeObject(to.Name) - FreeCAD.ActiveDocument.removeObject(tmpGrpNm) - else: + if PathLog.getLevel(PathLog.thisModule()) == 4: if FreeCAD.GuiUp: import FreeCADGui FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False - + self.tmpGrp.purgeTouched() + return shapes def _flattenWire(self, obj, wire, trgtDep): '''_flattenWire(obj, wire)... Return a flattened version of the wire''' PathLog.debug('_flattenWire()') wBB = wire.BoundBox - tmpGrp = self.tmpGrp - - OW = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOriginalWire') - OW.Shape = wire - OW.purgeTouched() - tmpGrp.addObject(OW) if wBB.ZLength > 0.0: PathLog.debug('Wire is not horizontally co-planar. Flattening it.') # Extrude non-horizontal wire extFwdLen = wBB.ZLength * 2.2 - mbbEXT = self._extrudeObject(OW, extFwdLen, False) + mbbEXT = wire.extrude(FreeCAD.Vector(0, 0, extFwdLen)) # Create cross-section of shape and translate sliceZ = wire.BoundBox.ZMin + (extFwdLen / 2) - crsectFaceShp = self._makeCrossSection(mbbEXT.Shape, sliceZ, trgtDep) + crsectFaceShp = self._makeCrossSection(mbbEXT, sliceZ, trgtDep) if crsectFaceShp is not False: - # srtWire = Part.Wire(Part.__sortEdges__(crsectFaceShp.Edges)) - FW = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlattenedWire') - FW.Shape = crsectFaceShp # srtWire - FW.recompute() - FW.purgeTouched() - tmpGrp.addObject(FW) - - return (OW, FW) + return (wire, crsectFaceShp) else: return False else: srtWire = Part.Wire(Part.__sortEdges__(wire.Edges)) srtWire.translate(FreeCAD.Vector(0, 0, trgtDep - srtWire.BoundBox.ZMin)) - FW = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOriginalWireSorted') - FW.Shape = srtWire - FW.purgeTouched() - tmpGrp.addObject(FW) - return (OW, FW) + return (wire, srtWire) # Open-edges methods - def _getCutAreaCrossSection(self, obj, base, origWire, flatWireObj): + def _getCutAreaCrossSection(self, obj, base, origWire, flatWire): PathLog.debug('_getCutAreaCrossSection()') - tmpGrp = self.tmpGrp FCAD = FreeCAD.ActiveDocument tolerance = self.JOB.GeometryTolerance.Value toolDiam = 2 * self.radius # self.radius defined in PathAreaOp or PathProfileBase modules @@ -198,16 +188,16 @@ class ObjectProfile(PathProfileBase.ObjectProfile): bbBfr = (self.ofstRadius * 2) * 1.25 if bbBfr < minBfr: bbBfr = minBfr - fwBB = flatWireObj.Shape.BoundBox - wBB = origWire.Shape.BoundBox + fwBB = flatWire.BoundBox + wBB = origWire.BoundBox minArea = (self.ofstRadius - tolerance)**2 * math.pi - useWire = origWire.Shape.Wires[0] + useWire = origWire.Wires[0] numOrigEdges = len(useWire.Edges) sdv = wBB.ZMax fdv = obj.FinalDepth.Value extLenFwd = sdv - fdv - WIRE = flatWireObj.Shape.Wires[0] + WIRE = flatWire.Wires[0] numEdges = len(WIRE.Edges) # Identify first/last edges and first/last vertex on wire @@ -242,15 +232,24 @@ class ObjectProfile(PathProfileBase.ObjectProfile): # Create intersection tags for determining which side of wire to cut (begInt, begExt, iTAG, eTAG) = self._makeIntersectionTags(useWire, numOrigEdges, fdv) + if not begInt or not begExt: + return False self.iTAG = iTAG self.eTAG = eTAG # Create extended wire boundbox, and extrude extBndbox = self._makeExtendedBoundBox(wBB, bbBfr, fdv) - extBndboxEXT = self._extrudeObject(extBndbox, extLenFwd) # (objToExt, extFwdLen) + extBndboxEXT = extBndbox.extrude(FreeCAD.Vector(0, 0, extLenFwd)) # Cut model(selected edges) from extended edges boundbox - cutArea = extBndboxEXT.Shape.cut(base.Shape) + cutArea = extBndboxEXT.cut(base.Shape) + if PathLog.getLevel(PathLog.thisModule()) == 4: + CA = FCAD.addObject('Part::Feature', 'tmpCutArea') + CA.Shape = cutArea + CA.recompute() + CA.purgeTouched() + self.tmpGrp.addObject(CA) + # Get top and bottom faces of cut area (CA), and combine faces when necessary topFc = list() @@ -270,8 +269,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile): topComp.translate(FreeCAD.Vector(0, 0, fdv - topComp.BoundBox.ZMin)) # Translate face to final depth if len(botFc) > 1: PathLog.debug('len(botFc) > 1') - bndboxFace = Part.Face(extBndbox.Shape.Wires[0]) - tmpFace = Part.Face(extBndbox.Shape.Wires[0]) + bndboxFace = Part.Face(extBndbox.Wires[0]) + tmpFace = Part.Face(extBndbox.Wires[0]) for f in botFc: Q = tmpFace.cut(cutArea.Faces[f]) tmpFace = Q @@ -280,38 +279,20 @@ class ObjectProfile(PathProfileBase.ObjectProfile): botComp = Part.makeCompound([cutArea.Faces[f] for f in botFc]) # Part.makeCompound([CA.Shape.Faces[f] for f in botFc]) botComp.translate(FreeCAD.Vector(0, 0, fdv - botComp.BoundBox.ZMin)) # Translate face to final depth - # Convert compound shapes to FC objects for use in multicommon operation - TP = FCAD.addObject('Part::Feature', 'tmpTopCompound') - TP.Shape = topComp - TP.recompute() - TP.purgeTouched() - tmpGrp.addObject(TP) - BT = FCAD.addObject('Part::Feature', 'tmpBotCompound') - BT.Shape = botComp - BT.recompute() - BT.purgeTouched() - tmpGrp.addObject(BT) - # Make common of the two - comFC = FCAD.addObject('Part::MultiCommon', 'tmpCommonTopBotFaces') - comFC.Shapes = [TP, BT] - comFC.recompute() - TP.purgeTouched() - BT.purgeTouched() - comFC.purgeTouched() - tmpGrp.addObject(comFC) + comFC = topComp.common(botComp) # Determine with which set of intersection tags the model intersects (cmnIntArea, cmnExtArea) = self._checkTagIntersection(iTAG, eTAG, 'QRY', comFC) if cmnExtArea > cmnIntArea: PathLog.debug('Cutting on Ext side.') self.cutSide = 'E' - self.cutSideTags = eTAG.Shape + self.cutSideTags = eTAG tagCOM = begExt.CenterOfMass else: PathLog.debug('Cutting on Int side.') self.cutSide = 'I' - self.cutSideTags = iTAG.Shape + self.cutSideTags = iTAG tagCOM = begInt.CenterOfMass # Make two beginning style(oriented) 'L' shape stops @@ -331,9 +312,9 @@ class ObjectProfile(PathProfileBase.ObjectProfile): pathStops.translate(FreeCAD.Vector(0, 0, fdv - pathStops.BoundBox.ZMin)) # Identify closed wire in cross-section that corresponds to user-selected edge(s) - workShp = comFC.Shape + workShp = comFC fcShp = workShp - wire = origWire.Shape # flatWireObj.Shape + wire = origWire WS = workShp.Wires lenWS = len(WS) if lenWS < 3: @@ -354,7 +335,6 @@ class ObjectProfile(PathProfileBase.ObjectProfile): if wi is None: PathLog.error('The cut area cross-section wire does not coincide with selected edge. Wires[] index is None.') - tmpGrp.purgeTouched() return False else: PathLog.debug('Cross-section Wires[] index is {}.'.format(wi)) @@ -368,13 +348,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile): if wi > 0: # and isInterior is False: PathLog.debug('Multiple wires in cut area. First choice is not 0. Testing.') testArea = fcShp.cut(base.Shape) - # testArea = fcShp - TA = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpCutFaceTest') - TA.Shape = testArea - TA.purgeTouched() - tmpGrp.addObject(TA) - isReady = self._checkTagIntersection(iTAG, eTAG, self.cutSide, TA) + isReady = self._checkTagIntersection(iTAG, eTAG, self.cutSide, testArea) PathLog.debug('isReady {}.'.format(isReady)) if isReady is False: @@ -395,38 +370,18 @@ class ObjectProfile(PathProfileBase.ObjectProfile): # Add path stops at ends of wire cutShp = workShp.cut(pathStops) - - CF = FreeCAD.ActiveDocument.addObject('Part::Feature','tmpCutFace') - CF.Shape = cutShp - CF.recompute() - CF.purgeTouched() - tmpGrp.addObject(CF) - - tmpGrp.purgeTouched() - return cutShp # CF.Shape + return cutShp def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj): # Identify intersection of Common area and Interior Tags - intCmn = FreeCAD.ActiveDocument.addObject('Part::MultiCommon', 'tmpCmnIntTags') - intCmn.Shapes = [tstObj, iTAG] - intCmn.recompute() - tstObj.purgeTouched() - iTAG.purgeTouched() - intCmn.purgeTouched() - self.tmpGrp.addObject(intCmn) + intCmn = tstObj.common(iTAG) # Identify intersection of Common area and Exterior Tags - extCmn = FreeCAD.ActiveDocument.addObject('Part::MultiCommon', 'tmpCmnExtTags') - extCmn.Shapes = [tstObj, eTAG] - extCmn.recompute() - tstObj.purgeTouched() - eTAG.purgeTouched() - extCmn.purgeTouched() - self.tmpGrp.addObject(extCmn) + extCmn = tstObj.common(eTAG) # Calculate common intersection (solid model side, or the non-cut side) area with tags, to determine physical cut side - cmnIntArea = intCmn.Shape.Area - cmnExtArea = extCmn.Shape.Area + cmnIntArea = intCmn.Area + cmnExtArea = extCmn.Area if cutSide == 'QRY': return (cmnIntArea, cmnExtArea) @@ -440,16 +395,15 @@ class ObjectProfile(PathProfileBase.ObjectProfile): return True return False - def _extractPathWire(self, obj, base, fWire, cutShp): + def _extractPathWire(self, obj, base, flatWire, cutShp): PathLog.debug('_extractPathWire()') subLoops = list() rtnWIRES = list() osWrIdxs = list() subDistFactor = 1.0 # Raise to include sub wires at greater distance from original - tmpGrp = self.tmpGrp fdv = obj.FinalDepth.Value - wire = fWire.Shape + wire = flatWire lstVrtIdx = len(wire.Vertexes) - 1 lstVrt = wire.Vertexes[lstVrtIdx] frstVrt = wire.Vertexes[0] @@ -468,14 +422,14 @@ class ObjectProfile(PathProfileBase.ObjectProfile): osArea = ofstShp.Area except Exception as ee: PathLog.error('No area to offset shape returned.') - tmpGrp.purgeTouched() return False - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape') - os.Shape = ofstShp - os.recompute() - os.purgeTouched() - tmpGrp.addObject(os) + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape') + os.Shape = ofstShp + os.recompute() + os.purgeTouched() + self.tmpGrp.addObject(os) numOSWires = len(ofstShp.Wires) for w in range(0, numOSWires): @@ -491,10 +445,12 @@ class ObjectProfile(PathProfileBase.ObjectProfile): min0 = N[4] min0i = n (w0, vi0, pnt0, vrt0, d0) = NEAR0[0] # min0i - near0 = Draft.makeWire([cent0, pnt0], placement=pl, closed=False, face=False, support=None) - near0.recompute() - near0.purgeTouched() - tmpGrp.addObject(near0) + if PathLog.getLevel(PathLog.thisModule()) == 4: + near0 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear0') + near0.Shape = Part.makeLine(cent0, pnt0) + near0.recompute() + near0.purgeTouched() + self.tmpGrp.addObject(near0) NEAR1 = self._findNearestVertex(ofstShp, cent1) min1i = 0 @@ -505,24 +461,23 @@ class ObjectProfile(PathProfileBase.ObjectProfile): min1 = N[4] min1i = n (w1, vi1, pnt1, vrt1, d1) = NEAR1[0] # min1i - near1 = Draft.makeWire([cent1, pnt1], placement=pl, closed=False, face=False, support=None) - near1.recompute() - near1.purgeTouched() - tmpGrp.addObject(near1) + if PathLog.getLevel(PathLog.thisModule()) == 4: + near1 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear1') + near1.Shape = Part.makeLine(cent1, pnt1) + near1.recompute() + near1.purgeTouched() + self.tmpGrp.addObject(near1) if w0 != w1: - PathLog.debug('w0 is {}.'.format(w0)) - PathLog.debug('w1 is {}.'.format(w1)) - PathLog.warning('Offset wire endpoint indexes are not equal: {}, {}'.format(w0, w1)) + PathLog.warning('Offset wire endpoint indexes are not equal - w0, w1: {}, {}'.format(w0, w1)) - ''' - PathLog.debug('min0i is {}.'.format(min0i)) - PathLog.debug('min1i is {}.'.format(min1i)) - PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) - PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) - PathLog.debug('NEAR0 is {}.'.format(NEAR0)) - PathLog.debug('NEAR1 is {}.'.format(NEAR1)) - ''' + if PathLog.getLevel(PathLog.thisModule()) == 4: + PathLog.debug('min0i is {}.'.format(min0i)) + PathLog.debug('min1i is {}.'.format(min1i)) + PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) + PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) + PathLog.debug('NEAR0 is {}.'.format(NEAR0)) + PathLog.debug('NEAR1 is {}.'.format(NEAR1)) mainWire = ofstShp.Wires[w0] @@ -553,7 +508,6 @@ class ObjectProfile(PathProfileBase.ObjectProfile): # Eif # Break offset loop into two wires - one of which is the desired profile path wire. - # (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, ofstShp.Vertexes[vi0], ofstShp.Vertexes[vi1]) (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1]) edgs0 = list() edgs1 = list() @@ -573,7 +527,6 @@ class ObjectProfile(PathProfileBase.ObjectProfile): rtnWIRES.append(part1) rtnWIRES.extend(subLoops) - tmpGrp.purgeTouched() return rtnWIRES def _extractFaceOffset(self, obj, fcShape, isHole): @@ -608,13 +561,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile): area.add(fcShape) # obj.Shape to use for extracting offset area.setParams(**areaParams) # set parameters - # Save parameters for debugging - # obj.AreaParams = str(area.getParams()) - # PathLog.debug("Area with params: {}".format(area.getParams())) - - offsetShape = area.getShape() - - return offsetShape + return area.getShape() def _findNearestVertex(self, shape, point): PathLog.debug('_findNearestVertex()') @@ -795,29 +742,17 @@ class ObjectProfile(PathProfileBase.ObjectProfile): return False def _makeExtendedBoundBox(self, wBB, bbBfr, zDep): - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) - bb = Draft.makeWire([p1, p2, p3, p4], placement=pl, closed=True, face=False, support=None) - bb.Label = 'ProfileEdges_BoundBox' - bb.recompute() - bb.purgeTouched() - self.tmpGrp.addObject(bb) - return bb + L1 = Part.makeLine(p1, p2) + L2 = Part.makeLine(p2, p3) + L3 = Part.makeLine(p3, p4) + L4 = Part.makeLine(p4, p1) - def _makeSimpleCircle(self, rad, plcmnt, isFace=False, label='SimpleCircle'): - C = Draft.makeCircle(rad, placement=plcmnt, face=isFace) - C.Label = 'tmp' + label - C.recompute() - C.purgeTouched() - self.tmpGrp.addObject(C) - return C + return Part.Face(Part.Wire([L1, L2, L3, L4])) def _makeIntersectionTags(self, useWire, numOrigEdges, fdv): # Create circular probe tags around perimiter of wire @@ -825,8 +760,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile): intTags = list() tagRad = (self.radius / 2) tagCnt = 0 - begInt = None - begExt = None + begInt = False + begExt = False for e in range(0, numOrigEdges): E = useWire.Edges[e] LE = E.Length @@ -846,31 +781,22 @@ class ObjectProfile(PathProfileBase.ObjectProfile): cp1 = E.valueAt(E.getParameterByLength(0)) cp2 = E.valueAt(E.getParameterByLength(aspc)) (intTObj, extTObj) = self._makeOffsetCircleTag(cp1, cp2, tagRad, fdv, 'BeginEdge[{}]_'.format(e)) - if intTObj is not False: - begInt = intTObj.Shape - begExt = extTObj.Shape + if intTObj and extTObj: + begInt = intTObj + begExt = extTObj else: d = i * mid cp1 = E.valueAt(E.getParameterByLength(d - spc)) cp2 = E.valueAt(E.getParameterByLength(d + spc)) (intTObj, extTObj) = self._makeOffsetCircleTag(cp1, cp2, tagRad, fdv, 'Edge[{}]_'.format(e)) - if intTObj is not False: + if intTObj and extTObj: tagCnt += nt intTags.append(intTObj) extTags.append(extTObj) tagArea = math.pi * tagRad**2 * tagCnt - # FreeCAD object required for Part::MultiCommon usage - intTagsComp = Part.makeCompound([T.Shape for T in intTags]) - iTAG = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpInteriorTags') - iTAG.Shape = intTagsComp - iTAG.purgeTouched() - self.tmpGrp.addObject(iTAG) - # FreeCAD object required for Part::MultiCommon usage - extTagsComp = Part.makeCompound([T.Shape for T in extTags]) - eTAG = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpExteriorTags') - eTAG.Shape = extTagsComp - eTAG.purgeTouched() - self.tmpGrp.addObject(eTAG) + iTAG = Part.makeCompound(intTags) + eTAG = Part.makeCompound(extTags) + return (begInt, begExt, iTAG, eTAG) def _makeOffsetCircleTag(self, p1, p2, cutterRad, depth, lbl, reverse=False): @@ -890,32 +816,19 @@ class ObjectProfile(PathProfileBase.ObjectProfile): pl = FreeCAD.Placement() pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) # make exterior tag - adjlbl = lbl + 'Ext' - pl.Base = extPnt.add(FreeCAD.Vector(0, 0, depth)) - extTag = self._makeSimpleCircle((cutterRad / 2), pl, True, adjlbl) + eCntr = extPnt.add(FreeCAD.Vector(0, 0, depth)) + ecw = Part.Wire(Part.makeCircle((cutterRad / 2), eCntr).Edges[0]) + extTag = Part.Face(ecw) # make interior tag - adjlbl = lbl + 'Int' perpI = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(cutFactor) # interior tag intPnt = pb.add(toMid.add(perpI)) - pl.Base = intPnt.add(FreeCAD.Vector(0, 0, depth)) - intTag = self._makeSimpleCircle((cutterRad / 2), pl, True, adjlbl) + iCntr = intPnt.add(FreeCAD.Vector(0, 0, depth)) + icw = Part.Wire(Part.makeCircle((cutterRad / 2), iCntr).Edges[0]) + intTag = Part.Face(icw) return (intTag, extTag) - def _extrudeObject(self, objToExt, extFwdLen, solid=True): - # Extrude non-horizontal wire - E = FreeCAD.ActiveDocument.addObject('Part::Extrusion', 'tmpExtrusion') - E.Base = objToExt - E.DirMode = 'Custom' - E.Dir = FreeCAD.Vector(0, 0, 1) - E.LengthFwd = extFwdLen - E.Solid = solid - E.recompute() - E.purgeTouched() - self.tmpGrp.addObject(E) - return E - def _makeStop(self, sType, pA, pB, lbl): rad = self.radius ofstRad = self.ofstRadius @@ -953,7 +866,13 @@ class ObjectProfile(PathProfileBase.ObjectProfile): p5 = self._makePerp2DVector(p3, p4, -1 * (1 + extra)) # E4 p6 = self._makePerp2DVector(p4, p5, -1 * (ofstRad + extra)) # E5 p7 = E # E6 - S = Draft.makeWire([p1, p2, p3, p4, p5, p6, p7], placement=pl, closed=True, face=True) + L1 = Part.makeLine(p1, p2) + L2 = Part.makeLine(p2, p3) + L3 = Part.makeLine(p3, p4) + L4 = Part.makeLine(p4, p5) + L5 = Part.makeLine(p5, p6) + L6 = Part.makeLine(p6, p7) + wire = Part.Wire([L1, L2, L3, L4, L5, L6]) else: # 'L' stop shape and edge legend # : @@ -973,13 +892,22 @@ class ObjectProfile(PathProfileBase.ObjectProfile): p4 = self._makePerp2DVector(p2, p3, -1 * (0.5 + abs(self.offsetExtra))) # FIRST POINT p5 = self._makePerp2DVector(p3, p4, -1 * (0.25 + abs(self.offsetExtra))) # E1 SECOND p6 = p1 # E4 - S = Draft.makeWire([p1, p2, p3, p4, p5, p6], placement=pl, closed=True, face=True) + L1 = Part.makeLine(p1, p2) + L2 = Part.makeLine(p2, p3) + L3 = Part.makeLine(p3, p4) + L4 = Part.makeLine(p4, p5) + L5 = Part.makeLine(p5, p6) + wire = Part.Wire([L1, L2, L3, L4, L5]) # Eif - S.Label = 'tmp' + lbl - S.recompute() - S.purgeTouched() - self.tmpGrp.addObject(S) - return S.Shape + face = Part.Face(wire) + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + lbl) + os.Shape = face + os.recompute() + os.purgeTouched() + self.tmpGrp.addObject(os) + + return face def _makePerp2DVector(self, v1, v2, dist): p1 = FreeCAD.Vector(v1.x, v1.y, 0.0) From 1c5b2c8e27122a2d451c3743ac7abfb4704b4a83 Mon Sep 17 00:00:00 2001 From: wandererfan Date: Fri, 17 Apr 2020 07:38:40 -0400 Subject: [PATCH 117/142] [TD]fix crash on bad centerline selection --- src/Mod/TechDraw/Gui/CommandAnnotate.cpp | 57 +++++++++++------------- src/Mod/TechDraw/Gui/TaskCenterLine.cpp | 23 ++++++---- src/Mod/TechDraw/Gui/TaskCenterLine.h | 13 ++++-- 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/Mod/TechDraw/Gui/CommandAnnotate.cpp b/src/Mod/TechDraw/Gui/CommandAnnotate.cpp index 702f4fd9dc..d36a178fc4 100644 --- a/src/Mod/TechDraw/Gui/CommandAnnotate.cpp +++ b/src/Mod/TechDraw/Gui/CommandAnnotate.cpp @@ -581,13 +581,13 @@ void CmdTechDrawCenterLineGroup::activated(int iMsg) Gui::ActionGroup* pcAction = qobject_cast(_pcAction); pcAction->setIcon(pcAction->actions().at(iMsg)->icon()); switch(iMsg) { - case 0: + case 0: //faces execCenterLine(this); break; - case 1: + case 1: //2 lines exec2LineCenterLine(this); break; - case 2: + case 2: //2 points exec2PointCenterLine(this); break; default: @@ -743,29 +743,23 @@ void execCenterLine(Gui::Command* cmd) if (!faceNames.empty()) { Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat, page, - faceNames)); + faceNames, + false)); } else if (edgeNames.empty()) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), QObject::tr("No CenterLine in selection.")); return; } else { - std::string edgeName = edgeNames.front(); - int geomIdx = DrawUtil::getIndexFromName(edgeName); - const std::vector &geoms = baseFeat->getEdgeGeometry(); - BaseGeom* bg = geoms.at(geomIdx); -// int clIdx = bg->sourceIndex(); -// TechDraw::CenterLine* cl = baseFeat->getCenterLineByIndex(clIdx); - std::string tag = bg->getCosmeticTag(); - TechDraw::CenterLine* cl = baseFeat->getCenterLine(tag); + TechDraw::CenterLine* cl = baseFeat->getCenterLineBySelection(edgeNames.front()); if (cl == nullptr) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), - QObject::tr("No CenterLine in selection.")); + QObject::tr("Selection is not a CenterLine.")); return; } - Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat, page, - edgeNames.front())); + edgeNames.front(), + true)); } } @@ -825,25 +819,19 @@ void exec2LineCenterLine(Gui::Command* cmd) if (selectedEdges.size() == 2) { Gui::Control().showDialog(new TaskDlgCenterLine(dvp, page, - selectedEdges)); + selectedEdges, + false)); } else if (selectedEdges.size() == 1) { - std::string edgeName = selectedEdges.front(); - int geomIdx = DrawUtil::getIndexFromName(edgeName); - const std::vector &geoms = dvp->getEdgeGeometry(); - BaseGeom* bg = geoms.at(geomIdx); -// int clIdx = bg->sourceIndex(); -// TechDraw::CenterLine* cl = dvp->getCenterLineByIndex(clIdx); - std::string tag = bg->getCosmeticTag(); - TechDraw::CenterLine* cl = dvp->getCenterLine(tag); + TechDraw::CenterLine* cl = dvp->getCenterLineBySelection(selectedEdges.front()); if (cl == nullptr) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), - QObject::tr("No CenterLine in selection.")); + QObject::tr("Selection is not a CenterLine.")); return; } else { -// Base::Console().Message("CMD::2LineCenter - show edit dialog here\n"); Gui::Control().showDialog(new TaskDlgCenterLine(dvp, page, - selectedEdges.front())); + selectedEdges.front(), + true)); } } else { //not create, not edit, what is this??? QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), @@ -942,14 +930,23 @@ void exec2PointCenterLine(Gui::Command* cmd) if (!vertexNames.empty() && (vertexNames.size() == 2)) { Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat, page, - vertexNames)); + vertexNames, + false)); } else if (!edgeNames.empty() && (edgeNames.size() == 1)) { + TechDraw::CenterLine* cl = baseFeat->getCenterLineBySelection(edgeNames.front()); + if (cl == nullptr) { + QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), + QObject::tr("Selection is not a CenterLine.")); + return; + } + Gui::Control().showDialog(new TaskDlgCenterLine(baseFeat, page, - edgeNames.front())); + edgeNames.front(), + false)); } else if (vertexNames.empty()) { QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong Selection"), - QObject::tr("No CenterLine in selection.")); + QObject::tr("Need 2 Vertices or 1 CenterLine.")); return; } } diff --git a/src/Mod/TechDraw/Gui/TaskCenterLine.cpp b/src/Mod/TechDraw/Gui/TaskCenterLine.cpp index 79d110eca2..f6701cfa86 100644 --- a/src/Mod/TechDraw/Gui/TaskCenterLine.cpp +++ b/src/Mod/TechDraw/Gui/TaskCenterLine.cpp @@ -74,15 +74,16 @@ using namespace TechDrawGui; //ctor for edit TaskCenterLine::TaskCenterLine(TechDraw::DrawViewPart* partFeat, TechDraw::DrawPage* page, - std::string edgeName) : + std::string edgeName, + bool editMode) : ui(new Ui_TaskCenterLine), m_partFeat(partFeat), m_basePage(page), m_createMode(false), m_edgeName(edgeName), m_type(0), //0 - Face, 1 - 2 Lines, 2 - 2 points - m_mode(0) //0 - vertical, 1 - horizontal, 2 - aligned - + m_mode(0), //0 - vertical, 1 - horizontal, 2 - aligned + m_editMode(editMode) { // Base::Console().Message("TCL::TCL() - edit mode\n"); ui->setupUi(this); @@ -104,14 +105,16 @@ TaskCenterLine::TaskCenterLine(TechDraw::DrawViewPart* partFeat, //ctor for creation TaskCenterLine::TaskCenterLine(TechDraw::DrawViewPart* partFeat, TechDraw::DrawPage* page, - std::vector subNames) : + std::vector subNames, + bool editMode) : ui(new Ui_TaskCenterLine), m_partFeat(partFeat), m_basePage(page), m_createMode(true), m_subNames(subNames), m_type(0), //0 - Face, 1 - 2 Lines, 2 - 2 points - m_mode(0) //0 - vertical, 1 - horizontal, 2 - aligned + m_mode(0), //0 - vertical, 1 - horizontal, 2 - aligned + m_editMode(editMode) { // Base::Console().Message("TCL::TCL() - create mode\n"); if ( (m_basePage == nullptr) || @@ -501,10 +504,11 @@ bool TaskCenterLine::reject() ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TaskDlgCenterLine::TaskDlgCenterLine(TechDraw::DrawViewPart* partFeat, TechDraw::DrawPage* page, - std::vector subNames) + std::vector subNames, + bool editMode) : TaskDialog() { - widget = new TaskCenterLine(partFeat,page,subNames); + widget = new TaskCenterLine(partFeat,page,subNames, editMode); taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/techdraw-facecenterline"), widget->windowTitle(), true, 0); taskbox->groupLayout()->addWidget(widget); @@ -513,10 +517,11 @@ TaskDlgCenterLine::TaskDlgCenterLine(TechDraw::DrawViewPart* partFeat, TaskDlgCenterLine::TaskDlgCenterLine(TechDraw::DrawViewPart* partFeat, TechDraw::DrawPage* page, - std::string edgeName) + std::string edgeName, + bool editMode) : TaskDialog() { - widget = new TaskCenterLine(partFeat,page, edgeName); + widget = new TaskCenterLine(partFeat,page, edgeName, editMode); taskbox = new Gui::TaskView::TaskBox(Gui::BitmapFactory().pixmap("actions/techdraw-facecenterline"), widget->windowTitle(), true, 0); taskbox->groupLayout()->addWidget(widget); diff --git a/src/Mod/TechDraw/Gui/TaskCenterLine.h b/src/Mod/TechDraw/Gui/TaskCenterLine.h index e63973e944..43f6fdb0dc 100644 --- a/src/Mod/TechDraw/Gui/TaskCenterLine.h +++ b/src/Mod/TechDraw/Gui/TaskCenterLine.h @@ -75,10 +75,12 @@ class TaskCenterLine : public QWidget public: TaskCenterLine(TechDraw::DrawViewPart* baseFeat, TechDraw::DrawPage* page, - std::vector subNames); + std::vector subNames, + bool editMode); TaskCenterLine(TechDraw::DrawViewPart* baseFeat, TechDraw::DrawPage* page, - std::string edgeName); + std::string edgeName, + bool editMode); ~TaskCenterLine(); public Q_SLOTS: @@ -145,6 +147,7 @@ private: int m_clIdx; int m_type; int m_mode; + bool m_editMode; }; class TaskDlgCenterLine : public Gui::TaskView::TaskDialog @@ -154,10 +157,12 @@ class TaskDlgCenterLine : public Gui::TaskView::TaskDialog public: TaskDlgCenterLine(TechDraw::DrawViewPart* baseFeat, TechDraw::DrawPage* page, - std::vector subNames); + std::vector subNames, + bool editMode); TaskDlgCenterLine(TechDraw::DrawViewPart* baseFeat, TechDraw::DrawPage* page, - std::string edgeName); + std::string edgeName, + bool editMode); ~TaskDlgCenterLine(); public: From 1b887fa0f5f640147980c7b4bda6f3feb0ab4a0b Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Fri, 17 Apr 2020 17:18:52 +0200 Subject: [PATCH 118/142] Draft: [WIP] Annotation styles editor --- src/Mod/Draft/Resources/Draft.qrc | 1 + .../ui/dialog_AnnotationStyleEditor.ui | 447 ++++++++++++++++++ .../gui_annotationstyleeditor.py | 175 +++++++ 3 files changed, 623 insertions(+) create mode 100644 src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui create mode 100644 src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index 9b9d29ed6a..5467200ade 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -165,5 +165,6 @@ ui/TaskPanel_PolarArray.ui ui/TaskSelectPlane.ui ui/TaskShapeString.ui + ui/dialog_AnnotationStyleEditor.ui diff --git a/src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui b/src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui new file mode 100644 index 0000000000..eddf18cecc --- /dev/null +++ b/src/Mod/Draft/Resources/ui/dialog_AnnotationStyleEditor.ui @@ -0,0 +1,447 @@ + + + Dialog + + + + 0 + 0 + 418 + 694 + + + + Dialog + + + + + + Style name + + + + + + The name of your style. Existing style names can be edited + + + false + + + + + + + + + Add new... + + + + + + + + false + + + + 80 + 16777215 + + + + Renames the selected style + + + Rename + + + + + + + false + + + + 80 + 16777215 + + + + Deletes the selected style + + + Delete + + + + + + + + + + Text + + + + + + Font size + + + + + + + Font name + + + + + + + Line spacing + + + + + + + The size of the text in real-world units + + + + + + + + + + The spacing between lines of text in real-world units + + + + + + + + + + The font to use for texts and dimensions + + + + + + + + + + Units + + + + + + Scale multiplier + + + + + + + Decimals + + + + + + + Únit override + + + + + + + Show unit + + + + + + + A multiplier value that affects distances shown by dimensions + + + 4 + + + 1.000000000000000 + + + + + + + Forces dimensions to be shown in a specific unit + + + + + + + The number of decimals to show on dimensions + + + + + + + Shows the units suffix on dimensions or not + + + Qt::RightToLeft + + + + + + + + + + + + + Line and arrows + + + + + + Line width + + + + + + + Extension overshoot + + + + + + + Arrow size + + + + + + + Show lines + + + + + + + Dimension overshoot + + + + + + + Extension lines + + + + + + + Arrow type + + + + + + + Line / text color + + + + + + + Shows the dimension line or not + + + Qt::RightToLeft + + + + + + true + + + + + + + The width of the dimension lines + + + px + + + 1 + + + + + + + The color of dimension lines, arrows and texts + + + + 0 + 0 + 0 + + + + + + + + The typeof arrows to use for dimensions + + + + Dot + + + + + Arrow + + + + + Tick + + + + + + + + The size of dimension arrows + + + + + + + + + + How far must the main dimension line extend pass the measured points + + + + + + + + + + The length of extension lines + + + + + + + + + + How far must the extension lines extend above the main dimension line + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Gui::QuantitySpinBox + QWidget +
    Gui/QuantitySpinBox.h
    +
    + + Gui::ColorButton + QPushButton +
    Gui/Widgets.h
    +
    +
    + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
    diff --git a/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py new file mode 100644 index 0000000000..eed1d8daf8 --- /dev/null +++ b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * Copyright (c) 2020 Yorik van Havre * +# * * +# * 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. * +# * * +# * This program 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 this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +""" +Provides all gui and tools to create and edit annotation styles +Provides Draft_AnnotationStyleEditor command +""" + +import FreeCAD,FreeCADGui +import json + +EMPTYSTYLE = { + "FontName":"Sans", + "FontSize":0, + "LineSpacing":0, + "ScaleMultiplier":1, + "ShowUnit":False, + "UnitOverride":"", + "Decimals":0, + "ShowLines":True, + "LineWidth":1, + "LineColor":255, + "ArrowType":0, + "ArrowSize":0, + "DimensionOvershoot":0, + "ExtensionLines":0, + "ExtensionOvershoot":0, + } + + +class Draft_AnnotationStyleEditor: + + def __init__(self): + + self.styles = {} + + def GetResources(self): + + return {'Pixmap' : ":icons/Draft_AnnotationStyleEditor.svg", + 'MenuText': QT_TRANSLATE_NOOP("Draft_AnnotationStyleEditor", "Annotation styles..."), + 'ToolTip' : QT_TRANSLATE_NOOP("Draft_AnnotationStyleEditor", "Manage or create annotation styles")} + + def IsActive(self): + + return bool(FreeCAD.ActiveDocument) + + def Activated(self): + + from PySide import QtGui + + # load dialog + self.form = FreeCADGui.PySideUic.loadUi(":/ui/dialog_AnnotationStyleEditor.ui") + + # center the dialog over FreeCAD window + mw = FreeCADGui.getMainWindow() + self.form.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.form.rect().center()) + + # set icons + self.form.pushButtonDelete.setIcon(QtGui.QIcon(":/icons/edit_Cancel.svg")) + self.form.pushButtonRename.setIcon(QtGui.QIcon(":/icons/edit_Cancel.svg")) + + # fill the styles combo + self.styles = self.read_meta() + for style in self.styles.keys(): + self.form.comboBoxStyles.addItem(style) + + # connect signals/slots + self.form.comboBoxStyles.currentIndexChanged.connect(self.on_style_changed) + self.form.pushButtonDelete.clicked.connect(self.on_delete) + self.form.pushButtonRename.clicked.connect(self.on_rename) + # TODO connect all other controls to a function that saves to self.styles + + # show editor dialog + result = self.form.exec_() + + # process if OK was clicked + if result: + self.save_meta(self.styles) + + return + + def read_meta(self): + + styles = {} + meta = FreeCAD.ActiveDocument.Meta + for key,value in meta.keys: + if key.startswith("Draft_Style_"): + styles[key[12:]] = json.loads(value) + return styles + + def save_meta(self,styles): + + meta = FreeCAD.ActiveDocument.Meta + for key,value in styles: + meta["Draft_Style_"+key] = json.dumps(value) + FreeCAD.ActiveDocument.Meta = meta + + # TODO must also save meta and also update the styles + # of comboboxes of all dimensions and texts + # found in the doc. If a dimension/text uses a style + # that has now been deleted, that case must also be handled + # maybe warn the user if the style is in use in on_delete? + + + def on_style_changed(self,index): + + from PySide import QtGui + + if index <= 1: + self.form.pushButtonDelete.setEnabled(False) + self.form.pushButtonRename.setEnabled(False) + self.fill_editor(None) + if index == 1: + reply = QtGui.QInputDialog.getText(None, "Create new style","Style name:") + if reply[1]: # OK or Enter pressed + name = reply[0] + self.form.comboBoxStyles.addItem(name) + self.form.comboBoxStyles.setCurrentIndex(self.form.comboBoxStyles.count()-1) + elif index > 1: + self.form.pushButtonDelete.setEnabled(True) + self.form.pushButtonRename.setEnabled(True) + self.fill_editor(self.form.comboBoxStyles.itemText(index)) + + def on_delete(self): + + index = self.form.comboBox.currentIndex() + if index > 1: + style = self.form.comboBoxStyles.itemText(index) + self.form.comboBoxStyles.removeItem(index) + del self.styles[style] + + def on_rename(self): + + from PySide import QtGui + + index = self.form.comboBox.currentIndex() + if index > 1: + style = self.form.comboBoxStyles.itemText(index) + reply = QtGui.QInputDialog.getText(None, "Rename style","New name:",QtGui.QLineEdit.Normal,style) + if reply[1]: # OK or Enter pressed + newname = reply[0] + self.form.comboBoxStyles.setItemText(index,newname) + value = self.styles[style] + del self.styles[style] + self.styles[newname] = value + + def fill_editor(self,style): + + if style is None: + style = EMPTYSTYLE + for key,value in style: + setattr(self.form,key,value) + + +FreeCADGui.addCommand('Draft_AnnotationStyleEditor', Draft_AnnotationStyleEditor()) From 4555a776636e5f53c9465b18dad94edf63ff91fe Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Fri, 17 Apr 2020 18:51:30 +0200 Subject: [PATCH 119/142] Draft: Annotation styles editor --- .../gui_annotationstyleeditor.py | 145 ++++++++++++++---- 1 file changed, 111 insertions(+), 34 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py index eed1d8daf8..08c1dd698b 100644 --- a/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py +++ b/src/Mod/Draft/draftguitools/gui_annotationstyleeditor.py @@ -88,7 +88,12 @@ class Draft_AnnotationStyleEditor: self.form.comboBoxStyles.currentIndexChanged.connect(self.on_style_changed) self.form.pushButtonDelete.clicked.connect(self.on_delete) self.form.pushButtonRename.clicked.connect(self.on_rename) - # TODO connect all other controls to a function that saves to self.styles + for attr in EMPTYSTYLE.keys(): + control = getattr(self.form,attr) + for signal in ["textChanged","valueChanged","stateChanged"]: + if hasattr(control,signal): + getattr(control,signal).connect(self.update_style) + break # show editor dialog result = self.form.exec_() @@ -101,75 +106,147 @@ class Draft_AnnotationStyleEditor: def read_meta(self): + """reads the document Meta property and returns a dict""" + styles = {} meta = FreeCAD.ActiveDocument.Meta - for key,value in meta.keys: + for key,value in meta.items(): if key.startswith("Draft_Style_"): styles[key[12:]] = json.loads(value) return styles def save_meta(self,styles): + """saves a dict to the document Meta property and updates objects""" + + # save meta + changedstyles = [] meta = FreeCAD.ActiveDocument.Meta - for key,value in styles: - meta["Draft_Style_"+key] = json.dumps(value) + for key,value in styles.items(): + strvalue = json.dumps(value) + if meta["Draft_Style_"+key] and (meta["Draft_Style_"+key] != strvalue): + changedstyles.append(style) + meta["Draft_Style_"+key] = strvalue FreeCAD.ActiveDocument.Meta = meta - # TODO must also save meta and also update the styles - # of comboboxes of all dimensions and texts - # found in the doc. If a dimension/text uses a style - # that has now been deleted, that case must also be handled - # maybe warn the user if the style is in use in on_delete? - + # propagate changes to all annotations + for obj in self.get_annotations(): + if obj.ViewObject.AnnotationStyle in styles.keys(): + if obj.ViewObject.AnnotationStyle in changedstyles: + for attr,attrvalue in styles[obj.ViewObject.AnnotationStyle].items(): + if hasattr(obj.ViewObject,attr): + setattr(obj.ViewObject,attr,attrvalue) + else: + obj.ViewObject.AnnotationStyle = " " + obj.ViewObject.AnnotationStyle == [" "] + styles.keys() def on_style_changed(self,index): + + """called when the styles combobox is changed""" from PySide import QtGui - if index <= 1: + if index <= 1: + # nothing happens self.form.pushButtonDelete.setEnabled(False) self.form.pushButtonRename.setEnabled(False) self.fill_editor(None) - if index == 1: + if index == 1: + # Add new... entry reply = QtGui.QInputDialog.getText(None, "Create new style","Style name:") - if reply[1]: # OK or Enter pressed + if reply[1]: + # OK or Enter pressed name = reply[0] - self.form.comboBoxStyles.addItem(name) - self.form.comboBoxStyles.setCurrentIndex(self.form.comboBoxStyles.count()-1) - elif index > 1: + if name in self.styles: + reply = QtGui.QMessageBox.information(None,"Style exists","This style name already exists") + else: + # create new default style + self.styles[name] = EMPTYSTYLE + self.form.comboBoxStyles.addItem(name) + self.form.comboBoxStyles.setCurrentIndex(self.form.comboBoxStyles.count()-1) + elif index > 1: + # Existing style self.form.pushButtonDelete.setEnabled(True) self.form.pushButtonRename.setEnabled(True) self.fill_editor(self.form.comboBoxStyles.itemText(index)) def on_delete(self): - - index = self.form.comboBox.currentIndex() - if index > 1: - style = self.form.comboBoxStyles.itemText(index) - self.form.comboBoxStyles.removeItem(index) - del self.styles[style] - - def on_rename(self): + + """called when the Delete button is pressed""" from PySide import QtGui index = self.form.comboBox.currentIndex() - if index > 1: - style = self.form.comboBoxStyles.itemText(index) - reply = QtGui.QInputDialog.getText(None, "Rename style","New name:",QtGui.QLineEdit.Normal,style) - if reply[1]: # OK or Enter pressed - newname = reply[0] - self.form.comboBoxStyles.setItemText(index,newname) - value = self.styles[style] - del self.styles[style] - self.styles[newname] = value + style = self.form.comboBoxStyles.itemText(index) + if self.get_style_users(style): + reply = QtGui.QMessageBox.question(None, "Style in use", "This style is used by some objects in this document. Are you sure?", + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) + if reply == QtGui.QMessageBox.No: + return + self.form.comboBoxStyles.removeItem(index) + del self.styles[style] + + def on_rename(self): + + """called when the Rename button is pressed""" + + from PySide import QtGui + + index = self.form.comboBox.currentIndex() + style = self.form.comboBoxStyles.itemText(index) + reply = QtGui.QInputDialog.getText(None, "Rename style","New name:",QtGui.QLineEdit.Normal,style) + if reply[1]: + # OK or Enter pressed + newname = reply[0] + self.form.comboBoxStyles.setItemText(index,newname) + value = self.styles[style] + del self.styles[style] + self.styles[newname] = value def fill_editor(self,style): + + """fills the editor fields with the contents of a style""" if style is None: style = EMPTYSTYLE - for key,value in style: + for key,value in style.items(): setattr(self.form,key,value) + def update_style(self,arg=None): + + """updates the current style with the values from the editor""" + + index = self.form.comboBox.currentIndex() + if index > 1: + values = {} + style = self.form.comboBoxStyles.itemText(index) + for key in EMPTYSTYLE.keys(): + control = getattr(self.form,key) + for attr in ["text","value","state"]: + if hasattr(control,attr): + values[key] = getattr(control,attr) + self.styles[style] = values + + def get_annotations(self): + + """gets all the objects that support annotation styles""" + + users = [] + for obj in FreeCAD.ActiveDocument.Objects: + vobj = obj.ViewObject + if hasattr(vobj,"AnnotationStyle"): + users.append(obj) + return users + + def get_style_users(self,style): + + """get all objects using a certain style""" + + users = [] + for obj in self.get_annotations(): + if obj.ViewObject.AnnotationStyle == style: + users.append(obj) + return users + FreeCADGui.addCommand('Draft_AnnotationStyleEditor', Draft_AnnotationStyleEditor()) From 656087fb98393e09bd253e0f46a18608b49a863c Mon Sep 17 00:00:00 2001 From: Bernd Hahnebach Date: Fri, 17 Apr 2020 21:10:28 +0200 Subject: [PATCH 120/142] FEM: mesh tools, better logs --- src/Mod/Fem/femmesh/meshtools.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Mod/Fem/femmesh/meshtools.py b/src/Mod/Fem/femmesh/meshtools.py index 97b353baec..8c2241d7f8 100644 --- a/src/Mod/Fem/femmesh/meshtools.py +++ b/src/Mod/Fem/femmesh/meshtools.py @@ -1699,17 +1699,17 @@ def get_contact_obj_faces( "(example: multiple element faces per master or slave\n" ) - FreeCAD.Console.PrintLog("Slave: {}, {}\n".format(slave_ref[0].Name, slave_ref)) - FreeCAD.Console.PrintLog("Master: {}, {}\n".format(master_ref[0].Name, master_ref)) + FreeCAD.Console.PrintLog(" Slave: {}, {}\n".format(slave_ref[0].Name, slave_ref)) + FreeCAD.Console.PrintLog(" Master: {}, {}\n".format(master_ref[0].Name, master_ref)) if is_solid_femmesh(femmesh): - # get the nodes, sorted and duplicates removed + FreeCAD.Console.PrintLog(" Get the nodes, sorted and duplicates removed.\n") slaveface_nds = sorted(list(set(get_femnodes_by_refshape(femmesh, slave_ref)))) masterface_nds = sorted(list(set(get_femnodes_by_refshape(femmesh, master_ref)))) - # FreeCAD.Console.PrintLog("slaveface_nds: {}\n".format(slaveface_nds)) - # FreeCAD.Console.PrintLog("masterface_nds: {}\n".format(slaveface_nds)) + FreeCAD.Console.PrintLog(" slaveface_nds: {}\n".format(slaveface_nds)) + FreeCAD.Console.PrintLog(" masterface_nds: {}\n".format(slaveface_nds)) - # fill the bit_pattern_dict and search for the faces + FreeCAD.Console.PrintLog(" Fill the bit_pattern_dict and search for the faces.\n") slave_bit_pattern_dict = get_bit_pattern_dict( femelement_table, femnodes_ele_table, @@ -1721,16 +1721,18 @@ def get_contact_obj_faces( masterface_nds ) - # get the faces ids + FreeCAD.Console.PrintLog(" Get the FaceIDs.\n") slave_faces = get_ccxelement_faces_from_binary_search(slave_bit_pattern_dict) master_faces = get_ccxelement_faces_from_binary_search(master_bit_pattern_dict) elif is_face_femmesh(femmesh): slave_ref_shape = slave_ref[0].Shape.getElement(slave_ref[1][0]) master_ref_shape = master_ref[0].Shape.getElement(master_ref[1][0]) - # get the faces ids + + FreeCAD.Console.PrintLog(" Get the FaceIDs.\n") slave_face_ids = femmesh.getFacesByFace(slave_ref_shape) master_face_ids = femmesh.getFacesByFace(master_ref_shape) + # build slave_faces and master_faces # face 2 for tria6 element # is it face 2 for all shell elements @@ -1739,8 +1741,13 @@ def get_contact_obj_faces( for fid in master_face_ids: master_faces.append([fid, 2]) - FreeCAD.Console.PrintLog("slave_faces: {}\n".format(slave_faces)) - FreeCAD.Console.PrintLog("master_faces: {}\n".format(master_faces)) + FreeCAD.Console.PrintLog(" Master and slave face ready to use for writer:\n") + FreeCAD.Console.PrintLog(" slave_faces: {}\n".format(slave_faces)) + FreeCAD.Console.PrintLog(" master_faces: {}\n".format(master_faces)) + if len(slave_faces) == 0: + FreeCAD.Console.PrintError("No faces found for contact slave face.\n") + if len(master_faces) == 0: + FreeCAD.Console.PrintError("No faces found for contact master face.\n") return [slave_faces, master_faces] From ba34cc6a96c15d64224e8f0c8b5da874f1e03a07 Mon Sep 17 00:00:00 2001 From: Bernd Hahnebach Date: Fri, 17 Apr 2020 22:46:14 +0200 Subject: [PATCH 121/142] FEM: mesh export, add export to Python module --- src/Mod/Fem/CMakeLists.txt | 1 + src/Mod/Fem/Init.py | 2 + src/Mod/Fem/feminout/importPyMesh.py | 148 +++++++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 src/Mod/Fem/feminout/importPyMesh.py diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index 18f21804ca..304ee9d7e3 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -72,6 +72,7 @@ SET(FemInOut_SRCS feminout/importCcxFrdResults.py feminout/importFenicsMesh.py feminout/importInpMesh.py + feminout/importPyMesh.py feminout/importToolsFem.py feminout/importVTKResults.py feminout/importYamlJsonMesh.py diff --git a/src/Mod/Fem/Init.py b/src/Mod/Fem/Init.py index f12096a6fa..d06b24904f 100644 --- a/src/Mod/Fem/Init.py +++ b/src/Mod/Fem/Init.py @@ -27,6 +27,8 @@ import FreeCAD +FreeCAD.addExportType("FEM mesh Python (*.meshpy)", "feminout.importPyMesh") + FreeCAD.addExportType("FEM mesh TetGen (*.poly)", "feminout.convert2TetGen") # see FemMesh::read() and FemMesh::write() methods in src/Mod/Fem/App/FemMesh.cpp diff --git a/src/Mod/Fem/feminout/importPyMesh.py b/src/Mod/Fem/feminout/importPyMesh.py new file mode 100644 index 0000000000..7c64bec66a --- /dev/null +++ b/src/Mod/Fem/feminout/importPyMesh.py @@ -0,0 +1,148 @@ +# *************************************************************************** +# * Copyright (c) 2016 Bernd Hahnebach * +# * * +# * 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. * +# * * +# * This program 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 this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +__title__ = "FreeCAD Python Mesh reader and writer" +__author__ = "Bernd Hahnebach" +__url__ = "http://www.freecadweb.org" + +## @package importPyMesh +# \ingroup FEM +# \brief FreeCAD Python Mesh reader and writer for FEM workbench + +import FreeCAD +from femmesh import meshtools + +# ************************************************************************************************ +# ********* generic FreeCAD import and export methods ******************************************** +# names are fix given from FreeCAD, these methods are called from FreeCAD +# they are set in FEM modules Init.py + +if open.__module__ == "__builtin__": + # because we'll redefine open below (Python2) + pyopen = open +elif open.__module__ == "io": + # because we'll redefine open below (Python3) + pyopen = open + + +# export mesh to python +def export( + objectslist, + filename +): + "called when freecad exports a file" + if len(objectslist) != 1: + FreeCAD.Console.PrintError("This exporter can only export one object.\n") + return + obj = objectslist[0] + if not obj.isDerivedFrom("Fem::FemMeshObject"): + FreeCAD.Console.PrintError("No FEM mesh object selected.\n") + return + femnodes_mesh = obj.FemMesh.Nodes + femelement_table = meshtools.get_femelement_table(obj.FemMesh) + if meshtools.is_solid_femmesh(obj.FemMesh): + fem_mesh_type = "Solid" + elif meshtools.is_face_femmesh(obj.FemMesh): + fem_mesh_type = "Face" + elif meshtools.is_edge_femmesh(obj.FemMesh): + fem_mesh_type = "Edge" + else: + FreeCAD.Console.PrintError("Export of this FEM mesh to Python not supported.\n") + return + f = pyopen(filename, "w") + write_python_mesh_to_file(femnodes_mesh, femelement_table, fem_mesh_type, f) + f.close() + + +# ************************************************************************************************ +# ********* module specific methods ************************************************************** +# writer: +# - a method directly writes a FemMesh to the mesh file +# - a method takes a file handle, mesh data and writes to the file handle + +# ********* writer ******************************************************************************* + +def write( + fem_mesh, + filename +): + """directly write a FemMesh to a Python mesh file + fem_mesh: a FemMesh""" + + if not fem_mesh.isDerivedFrom("Fem::FemMesh"): + FreeCAD.Console.PrintError("Not a FemMesh was given as parameter.\n") + return + femnodes_mesh = fem_mesh.Nodes + femelement_table = meshtools.get_femelement_table(fem_mesh) + if meshtools.is_solid_femmesh(fem_mesh): + fem_mesh_type = "Solid" + elif meshtools.is_face_femmesh(fem_mesh): + fem_mesh_type = "Face" + elif meshtools.is_edge_femmesh(fem_mesh): + fem_mesh_type = "Edge" + else: + FreeCAD.Console.PrintError("Export of this FEM mesh to Python not supported.\n") + return + f = pyopen(filename, "w") + write_python_mesh_to_file(femnodes_mesh, femelement_table, fem_mesh_type, f) + f.close() + + +def write_python_mesh_to_file(femnodes_mesh, femelement_table, fem_mesh_type, f): + + mesh_name = "femmesh" + + # nodes + f.write("def create_nodes(femmesh):\n") + f.write(" # nodes\n") + for node in femnodes_mesh: + # print(node, ' --> ', femnodes_mesh[node]) + vec = femnodes_mesh[node] + f.write( + " {0}.addNode({1}, {2}, {3}, {4})\n" + .format(mesh_name, vec.x, vec.y, vec.z, node) + ) + f.write(" return True\n") + f.write("\n\n") + + # elements + f.write("def create_elements(femmesh):\n") + f.write(" # elements\n") + for element in femelement_table: + # print(element, ' --> ', femelement_table[element]) + if fem_mesh_type == "Solid": + f.write( + " {0}.addVolume({1}, {2})\n" + .format(mesh_name, list(femelement_table[element]), element) + ) + elif fem_mesh_type == "Face": + f.write( + " {0}.addFace({1}, {2})\n" + .format(mesh_name, list(femelement_table[element]), element) + ) + elif fem_mesh_type == "Edge": + f.write( + " {0}.addEdge({1}, {2})\n" + .format(mesh_name, list(femelement_table[element]), element) + ) + f.write(" return True\n") From 1025ef4da34676bb6f679c6c5fb0f6a7ed8c6a56 Mon Sep 17 00:00:00 2001 From: WandererFan Date: Mon, 13 Apr 2020 22:31:18 -0400 Subject: [PATCH 122/142] [TD]Piecewise Sectioning Algo --- src/Mod/TechDraw/App/DrawViewSection.cpp | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Mod/TechDraw/App/DrawViewSection.cpp b/src/Mod/TechDraw/App/DrawViewSection.cpp index d3cb579bb6..0d76ef8ec9 100644 --- a/src/Mod/TechDraw/App/DrawViewSection.cpp +++ b/src/Mod/TechDraw/App/DrawViewSection.cpp @@ -347,7 +347,6 @@ App::DocumentObjectExecReturn *DrawViewSection::execute(void) } } - dvp->requestPaint(); //to refresh section line return DrawView::execute(); } @@ -382,13 +381,26 @@ void DrawViewSection::sectionExec(TopoDS_Shape baseShape) BRepBuilderAPI_Copy BuilderCopy(baseShape); TopoDS_Shape myShape = BuilderCopy.Shape(); - BRepAlgoAPI_Cut mkCut(myShape, prism); - if (!mkCut.IsDone()) { - Base::Console().Warning("DVS: Section cut has failed in %s\n",getNameInDocument()); - return; + BRep_Builder builder; + TopoDS_Compound pieces; + builder.MakeCompound(pieces); + TopExp_Explorer expl(myShape, TopAbs_SOLID); + int indb = 0; + int outdb = 0; + for (; expl.More(); expl.Next()) { + indb++; + const TopoDS_Solid& s = TopoDS::Solid(expl.Current()); + BRepAlgoAPI_Cut mkCut(s, prism); + if (!mkCut.IsDone()) { + Base::Console().Warning("DVS: Section cut has failed in %s\n",getNameInDocument()); + continue; + } + TopoDS_Shape cut = mkCut.Shape(); + builder.Add(pieces, cut); + outdb++; } - TopoDS_Shape rawShape = mkCut.Shape(); + TopoDS_Shape rawShape = pieces; if (debugSection()) { BRepTools::Write(myShape, "DVSCopy.brep"); //debug BRepTools::Write(aProjFace, "DVSFace.brep"); //debug From 8a7ebe6fadb8409bd95778f5c3110853ed46d18b Mon Sep 17 00:00:00 2001 From: WandererFan Date: Fri, 17 Apr 2020 21:53:03 -0400 Subject: [PATCH 123/142] [TD]Piecewise Detail Algo --- src/Mod/TechDraw/App/DrawViewDetail.cpp | 63 +++++++++++++++---------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/src/Mod/TechDraw/App/DrawViewDetail.cpp b/src/Mod/TechDraw/App/DrawViewDetail.cpp index 53c02f5894..8a491fea4a 100644 --- a/src/Mod/TechDraw/App/DrawViewDetail.cpp +++ b/src/Mod/TechDraw/App/DrawViewDetail.cpp @@ -255,9 +255,9 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, double scale = getScale(); BRepBuilderAPI_Copy BuilderCopy(shape); - TopoDS_Shape copyShape = BuilderCopy.Shape(); + TopoDS_Shape myShape = BuilderCopy.Shape(); - gp_Pnt gpCenter = TechDraw::findCentroid(copyShape, + gp_Pnt gpCenter = TechDraw::findCentroid(myShape, dirDetail); Base::Vector3d shapeCenter = Base::Vector3d(gpCenter.X(),gpCenter.Y(),gpCenter.Z()); m_saveCentroid = shapeCenter; //centroid of original shape @@ -265,7 +265,7 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, if (dvs != nullptr) { //section cutShape should already be on origin } else { - copyShape = TechDraw::moveShape(copyShape, //centre shape on origin + myShape = TechDraw::moveShape(myShape, //centre shape on origin -shapeCenter); } @@ -280,7 +280,7 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, Bnd_Box bbxSource; bbxSource.SetGap(0.0); - BRepBndLib::Add(copyShape, bbxSource); + BRepBndLib::Add(myShape, bbxSource); double diag = sqrt(bbxSource.SquareExtent()); Base::Vector3d toolPlaneOrigin = anchorOffset3d + dirDetail * diag * -1.0; //center tool about anchor @@ -301,33 +301,47 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, gp_Vec extrudeDir(extrudeVec.x,extrudeVec.y,extrudeVec.z); TopoDS_Shape tool = BRepPrimAPI_MakePrism(aProjFace, extrudeDir, false, true).Shape(); - BRepAlgoAPI_Common mkCommon(copyShape,tool); - if (!mkCommon.IsDone()) { - Base::Console().Warning("DVD::execute - %s - detail cut operation failed (1)\n", getNameInDocument()); - return; - } - if (mkCommon.Shape().IsNull()) { - Base::Console().Warning("DVD::execute - %s - detail cut operation failed (2)\n", getNameInDocument()); - return; - } - //Did we get a solid? - TopExp_Explorer xp; - xp.Init(mkCommon.Shape(),TopAbs_SOLID); - if (!(xp.More() == Standard_True)) { - Base::Console().Warning("DVD::execute - mkCommon.Shape is not a solid!\n"); + BRep_Builder builder; + TopoDS_Compound pieces; + builder.MakeCompound(pieces); + TopExp_Explorer expl(myShape, TopAbs_SOLID); + int indb = 0; + int outdb = 0; + for (; expl.More(); expl.Next()) { + indb++; + const TopoDS_Solid& s = TopoDS::Solid(expl.Current()); + + BRepAlgoAPI_Common mkCommon(s,tool); + if (!mkCommon.IsDone()) { +// Base::Console().Warning("DVD::execute - %s - detail cut operation failed (1)\n", getNameInDocument()); + continue; + } + if (mkCommon.Shape().IsNull()) { +// Base::Console().Warning("DVD::execute - %s - detail cut operation failed (2)\n", getNameInDocument()); + continue; + } + //this might be overkill for piecewise algo + //Did we get at least 1 solid? + TopExp_Explorer xp; + xp.Init(mkCommon.Shape(),TopAbs_SOLID); + if (!(xp.More() == Standard_True)) { +// Base::Console().Warning("DVD::execute - mkCommon.Shape is not a solid!\n"); + continue; + } + builder.Add(pieces, mkCommon.Shape()); + outdb++; } - TopoDS_Shape detail = mkCommon.Shape(); if (debugDetail()) { BRepTools::Write(tool, "DVDTool.brep"); //debug - BRepTools::Write(copyShape, "DVDCopy.brep"); //debug - BRepTools::Write(detail, "DVDCommon.brep"); //debug + BRepTools::Write(myShape, "DVDCopy.brep"); //debug + BRepTools::Write(pieces, "DVDCommon.brep"); //debug } Bnd_Box testBox; testBox.SetGap(0.0); - BRepBndLib::Add(detail, testBox); + BRepBndLib::Add(pieces, testBox); if (testBox.IsVoid()) { TechDraw::GeometryObject* go = getGeometryObject(); if (go != nullptr) { @@ -343,7 +357,7 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, // TopoDS_Compound Comp; // builder.MakeCompound(Comp); // builder.Add(Comp, tool); -// builder.Add(Comp, copyShape); +// builder.Add(Comp, myShape); gp_Pnt inputCenter; try { @@ -358,7 +372,8 @@ void DrawViewDetail::detailExec(TopoDS_Shape shape, gp_Ax2 viewAxis = dvp->getProjectionCS(stdOrg); //sb same CS as base view. //center shape on origin - TopoDS_Shape centeredShape = TechDraw::moveShape(detail, +// TopoDS_Shape centeredShape = TechDraw::moveShape(detail, + TopoDS_Shape centeredShape = TechDraw::moveShape(pieces, centroid * -1.0); TopoDS_Shape scaledShape = TechDraw::scaleShape(centeredShape, From a020dc2afcb878fa8293e459fec1b62cbe441058 Mon Sep 17 00:00:00 2001 From: Bernd Hahnebach Date: Sun, 19 Apr 2020 21:26:55 +0200 Subject: [PATCH 124/142] FEM: meshtools, fix element names in face search --- src/Mod/Fem/femmesh/meshtools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mod/Fem/femmesh/meshtools.py b/src/Mod/Fem/femmesh/meshtools.py index 8c2241d7f8..e8bc641764 100644 --- a/src/Mod/Fem/femmesh/meshtools.py +++ b/src/Mod/Fem/femmesh/meshtools.py @@ -1441,7 +1441,7 @@ def build_mesh_faces_of_volume_elements( else: FreeCAD.Console.PrintError( "Error in build_mesh_faces_of_volume_elements(): " - "hexa20: face not found! {}\n" + "tetra10: face not found! {}\n" .format(face_node_indexs) ) elif vol_node_ct == 4: @@ -1459,7 +1459,7 @@ def build_mesh_faces_of_volume_elements( else: FreeCAD.Console.PrintError( "Error in build_mesh_faces_of_volume_elements(): " - "hexa20: face not found! {}\n" + "tetra4: face not found! {}\n" .format(face_node_indexs) ) elif vol_node_ct == 20: @@ -1503,7 +1503,7 @@ def build_mesh_faces_of_volume_elements( else: FreeCAD.Console.PrintError( "Error in build_mesh_faces_of_volume_elements(): " - "hexa20: face not found! {}\n" + "hexa8: face not found! {}\n" .format(face_node_indexs) ) elif vol_node_ct == 15: @@ -1543,7 +1543,7 @@ def build_mesh_faces_of_volume_elements( else: FreeCAD.Console.PrintError( "Error in build_mesh_faces_of_volume_elements(): " - "pent6: face not found! {}\n" + "penta6: face not found! {}\n" .format(face_node_indexs) ) else: From 9dc688ff3b86b2c5f5fe86d6e42f333e1c57988b Mon Sep 17 00:00:00 2001 From: Bernd Hahnebach Date: Sun, 19 Apr 2020 21:36:26 +0200 Subject: [PATCH 125/142] FEM: result task panel, avoid zero float division --- src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py b/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py index 5c1551fc91..b4e25b14be 100644 --- a/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py +++ b/src/Mod/Fem/femguiobjects/_ViewProviderFemResultMechanical.py @@ -703,6 +703,8 @@ def get_displacement_scale_factor(res_obj): z_span = abs(p_z_max - p_z_min) span = max(x_span, y_span, z_span) max_disp = max(x_max, y_max, z_max) + if max_disp == 0.0: + return 0.0 # avoid float division by zero # FIXME - add max_allowed_disp to Preferences max_allowed_disp = 0.01 * span scale = max_allowed_disp / max_disp From 45aef7b028a54c5420db49ff3284fc7377475e90 Mon Sep 17 00:00:00 2001 From: Bernd Hahnebach Date: Sun, 19 Apr 2020 21:56:35 +0200 Subject: [PATCH 126/142] FEM: meshtools, init empty node numbers in face search --- src/Mod/Fem/femmesh/meshtools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Fem/femmesh/meshtools.py b/src/Mod/Fem/femmesh/meshtools.py index e8bc641764..c3f1cd0a96 100644 --- a/src/Mod/Fem/femmesh/meshtools.py +++ b/src/Mod/Fem/femmesh/meshtools.py @@ -1426,6 +1426,7 @@ def build_mesh_faces_of_volume_elements( FreeCAD.Console.PrintLog("VolElement: {}\n".format(veID)) vol_node_ct = len(femelement_table[veID]) face_node_indexs = sorted(face_nodenumber_table[veID]) + node_numbers = () if vol_node_ct == 10: FreeCAD.Console.PrintLog(" --> tetra10 --> tria6 face\n") # node order of face in tetra10 volume element From 1eef7064f8ad1d250e40fbd3d1d522dcca602d48 Mon Sep 17 00:00:00 2001 From: Adam Spontarelli Date: Sat, 28 Mar 2020 12:57:36 -0400 Subject: [PATCH 127/142] Initial addition of fcsprocket feature. This is a PartDesign tool that allows for the simple creation of ANSI standard roller chain sprockets. --- src/Mod/PartDesign/CMakeLists.txt | 19 +- .../PartDesign/Gui/Resources/PartDesign.qrc | 1 + .../Resources/icons/PartDesign_Sprocket.svg | 492 ++++++++++++++++++ src/Mod/PartDesign/Gui/Workbench.cpp | 7 +- src/Mod/PartDesign/InitGui.py | 1 + src/Mod/PartDesign/SprocketFeature.py | 232 +++++++++ src/Mod/PartDesign/SprocketFeature.ui | 232 +++++++++ src/Mod/PartDesign/fcsprocket/README.md | 24 + src/Mod/PartDesign/fcsprocket/__init__.py | 1 + src/Mod/PartDesign/fcsprocket/fcsprocket.py | 105 ++++ .../PartDesign/fcsprocket/fcsprocketdialog.py | 66 +++ src/Mod/PartDesign/fcsprocket/sprocket.py | 144 +++++ 12 files changed, 1320 insertions(+), 4 deletions(-) create mode 100644 src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Sprocket.svg create mode 100644 src/Mod/PartDesign/SprocketFeature.py create mode 100644 src/Mod/PartDesign/SprocketFeature.ui create mode 100644 src/Mod/PartDesign/fcsprocket/README.md create mode 100644 src/Mod/PartDesign/fcsprocket/__init__.py create mode 100644 src/Mod/PartDesign/fcsprocket/fcsprocket.py create mode 100644 src/Mod/PartDesign/fcsprocket/fcsprocketdialog.py create mode 100644 src/Mod/PartDesign/fcsprocket/sprocket.py diff --git a/src/Mod/PartDesign/CMakeLists.txt b/src/Mod/PartDesign/CMakeLists.txt index 177550408a..435ce86d35 100644 --- a/src/Mod/PartDesign/CMakeLists.txt +++ b/src/Mod/PartDesign/CMakeLists.txt @@ -16,6 +16,8 @@ if(BUILD_GUI) TestPartDesignGui.py InvoluteGearFeature.py InvoluteGearFeature.ui + SprocketFeature.py + SprocketFeature.ui ) endif(BUILD_GUI) @@ -60,6 +62,13 @@ set(PartDesign_GearScripts fcgear/svggear.py ) +set(PartDesign_SprocketScripts + fcsprocket/__init__.py + fcsprocket/fcsprocket.py + fcsprocket/fcsprocketdialog.py + fcsprocket/sprocket.py +) + set(PartDesign_WizardShaft WizardShaft/__init__.py WizardShaft/WizardShaft.svg @@ -76,6 +85,7 @@ add_custom_target(PartDesignScripts ALL SOURCES ${PartDesign_OtherScripts} ${PartDesign_TestScripts} ${PartDesign_GearScripts} + ${PartDesign_SprocketScripts} ) fc_target_copy_resource(PartDesignScripts @@ -85,6 +95,7 @@ fc_target_copy_resource(PartDesignScripts ${PartDesign_OtherScripts} ${PartDesign_TestScripts} ${PartDesign_GearScripts} + ${PartDesign_SprocketScripts} ) INSTALL( @@ -113,7 +124,13 @@ INSTALL( ${PartDesign_GearScripts} DESTINATION Mod/PartDesign/fcgear - +) + +INSTALL( + FILES + ${PartDesign_SprocketScripts} + DESTINATION + Mod/PartDesign/fcsprocket ) if(BUILD_FEM) diff --git a/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc b/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc index e8db9a825e..34550529ba 100644 --- a/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc +++ b/src/Mod/PartDesign/Gui/Resources/PartDesign.qrc @@ -37,6 +37,7 @@ icons/PartDesign_Revolution.svg icons/PartDesign_Scaled.svg icons/PartDesign_ShapeBinder.svg + icons/PartDesign_Sprocket.svg icons/PartDesign_SubShapeBinder.svg icons/PartDesign_Subtractive_Box.svg icons/PartDesign_Subtractive_Cone.svg diff --git a/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Sprocket.svg b/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Sprocket.svg new file mode 100644 index 0000000000..15bc199d19 --- /dev/null +++ b/src/Mod/PartDesign/Gui/Resources/icons/PartDesign_Sprocket.svg @@ -0,0 +1,492 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/src/Mod/PartDesign/Gui/Workbench.cpp b/src/Mod/PartDesign/Gui/Workbench.cpp index 350c14dbe3..b8489d36fe 100644 --- a/src/Mod/PartDesign/Gui/Workbench.cpp +++ b/src/Mod/PartDesign/Gui/Workbench.cpp @@ -509,9 +509,10 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "PartDesign_Boolean" << "Separator" //<< "PartDesign_Hole" - << "PartDesign_InvoluteGear" - << "Separator" - << "PartDesign_Migrate"; + << "PartDesign_Migrate" + << "PartDesign_Sprocket" + << "PartDesign_InvoluteGear"; + // For 0.13 a couple of python packages like numpy, matplotlib and others // are not deployed with the installer on Windows. Thus, the WizardShaft is diff --git a/src/Mod/PartDesign/InitGui.py b/src/Mod/PartDesign/InitGui.py index f25bd79358..c935ffb033 100644 --- a/src/Mod/PartDesign/InitGui.py +++ b/src/Mod/PartDesign/InitGui.py @@ -51,6 +51,7 @@ class PartDesignWorkbench ( Workbench ): import PartDesign try: from PartDesign import InvoluteGearFeature + from PartDesign import SprocketFeature except ImportError: print("Involute gear module cannot be loaded") #try: diff --git a/src/Mod/PartDesign/SprocketFeature.py b/src/Mod/PartDesign/SprocketFeature.py new file mode 100644 index 0000000000..d9824c49cd --- /dev/null +++ b/src/Mod/PartDesign/SprocketFeature.py @@ -0,0 +1,232 @@ +#*************************************************************************** +#* Copyright (c) 2020 Adam Spontarelli * +#* * +#* 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. * +#* * +#* This program 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 this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +import FreeCAD, Part +from fcsprocket import fcsprocket +from fcsprocket import sprocket + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore, QtGui + from FreeCADGui import PySideUic as uic + +__title__="PartDesign SprocketObject management" +__author__ = "Adam Spontarelli" +__url__ = "http://www.freecadweb.org" + + +def makeSprocket(name): + '''makeSprocket(name): makes a Sprocket''' + obj = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython",name) + _Sprocket(obj) + if FreeCAD.GuiUp: + _ViewProviderSprocket(obj.ViewObject) + #FreeCAD.ActiveDocument.recompute() + if FreeCAD.GuiUp: + body=FreeCADGui.ActiveDocument.ActiveView.getActiveObject("pdbody") + part=FreeCADGui.ActiveDocument.ActiveView.getActiveObject("part") + if body: + body.Group=body.Group+[obj] + elif part: + part.Group=part.Group+[obj] + return obj + +class _CommandSprocket: + "the Fem Sprocket command definition" + def GetResources(self): + return {'Pixmap' : 'PartDesign_Sprocket', + 'MenuText': QtCore.QT_TRANSLATE_NOOP("PartDesign_Sprocket","Sprocket..."), + 'Accel': "", + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PartDesign_Sprocket","Creates or edit the sprocket definition.")} + + def Activated(self): + + FreeCAD.ActiveDocument.openTransaction("Create Sprocket") + FreeCADGui.addModule("SprocketFeature") + FreeCADGui.doCommand("SprocketFeature.makeSprocket('Sprocket')") + FreeCADGui.doCommand("Gui.activeDocument().setEdit(App.ActiveDocument.ActiveObject.Name,0)") + + def IsActive(self): + if FreeCAD.ActiveDocument: + return True + else: + return False + + +class _Sprocket: + "The Sprocket object" + def __init__(self,obj): + self.Type = "Sprocket" + obj.addProperty("App::PropertyInteger","NumberOfTeeth","Sprocket","Number of gear teeth") + obj.addProperty("App::PropertyLength","Pitch","Sprocket","Chain Pitch") + obj.addProperty("App::PropertyLength","RollerDiameter","Sprocket","Roller Diameter") + obj.addProperty("App::PropertyString","ANSISize","Sprocket","ANSI Size") + + obj.NumberOfTeeth = 50 + obj.Pitch = "0.375 in" + obj.RollerDiameter = "0.20 in" + obj.ANSISize = "35" + + obj.Proxy = self + + + def execute(self,obj): + w = fcsprocket.FCWireBuilder() + sprocket.CreateSprocket(w, obj.Pitch.Value, obj.NumberOfTeeth, obj.RollerDiameter.Value) + + sprocketw = Part.Wire([o.toShape() for o in w.wire]) + obj.Shape = sprocketw + obj.positionBySupport(); + return + + +class _ViewProviderSprocket: + "A View Provider for the Sprocket object" + + def __init__(self,vobj): + vobj.Proxy = self + + def getIcon(self): + return ":/icons/PartDesign_Sprocket.svg" + + def attach(self, vobj): + self.ViewObject = vobj + self.Object = vobj.Object + + + def setEdit(self,vobj,mode): + taskd = _SprocketTaskPanel(self.Object,mode) + taskd.obj = vobj.Object + taskd.update() + FreeCADGui.Control.showDialog(taskd) + return True + + def unsetEdit(self,vobj,mode): + FreeCADGui.Control.closeDialog() + return + + def __getstate__(self): + return None + + def __setstate__(self,state): + return None + + +class _SprocketTaskPanel: + '''The editmode TaskPanel for Sprocket objects''' + def __init__(self,obj,mode): + self.obj = obj + + self.form=FreeCADGui.PySideUic.loadUi(FreeCAD.getHomePath() + "Mod/PartDesign/SprocketFeature.ui") + self.form.setWindowIcon(QtGui.QIcon(":/icons/PartDesign_Sprocket.svg")) + + QtCore.QObject.connect(self.form.Quantity_Pitch, QtCore.SIGNAL("valueChanged(double)"), self.pitchChanged) + QtCore.QObject.connect(self.form.Quantity_RollerDiameter, QtCore.SIGNAL("valueChanged(double)"), self.rollerDiameterChanged) + QtCore.QObject.connect(self.form.spinBox_NumberOfTeeth, QtCore.SIGNAL("valueChanged(int)"), self.numTeethChanged) + QtCore.QObject.connect(self.form.comboBox_ANSISize, QtCore.SIGNAL("currentTextChanged(const QString)"), self.ANSISizeChanged) + + self.update() + + if mode == 0: # fresh created + self.obj.Proxy.execute(self.obj) # calculate once + FreeCAD.Gui.SendMsgToActiveView("ViewFit") + + def transferTo(self): + "Transfer from the dialog to the object" + self.obj.NumberOfTeeth = self.form.spinBox_NumberOfTeeth.value() + self.obj.Pitch = self.form.Quantity_Pitch.text() + self.obj.RollerDiameter = self.form.Quantity_RollerDiameter.text() + self.obj.ANSISize = self.form.comboBox_ANSISize.currentText() + + def transferFrom(self): + "Transfer from the object to the dialog" + self.form.spinBox_NumberOfTeeth.setValue(self.obj.NumberOfTeeth) + self.form.Quantity_Pitch.setText(self.obj.Pitch.UserString) + self.form.Quantity_RollerDiameter.setText(self.obj.RollerDiameter.UserString) + self.form.comboBox_ANSISize.setCurrentText(self.obj.ANSISize) + + def pitchChanged(self, value): + self.obj.Pitch = value + self.obj.Proxy.execute(self.obj) + FreeCAD.Gui.SendMsgToActiveView("ViewFit") + + def ANSISizeChanged(self, size): + """ + ANSI B29.1-2011 standard roller chain sizes in USCS units (inches) + {size: [Pitch, Roller Diameter]} + """ + ANSIRollerTable = {"25": [0.250, 0.130], + "35": [0.375, 0.200], + "41": [0.500, 0.306], + "40": [0.500, 0.312], + "50": [0.625, 0.400], + "60": [0.750, 0.469], + "80": [1.000, 0.625], + "100":[1.250, 0.750], + "120":[1.500, 0.875], + "140":[1.750, 1.000], + "160":[2.000, 1.125], + "180":[2.250, 1.460], + "200":[2.500, 1.562], + "240":[3.000, 1.875]} + + self.obj.Pitch = str(ANSIRollerTable[size][0]) + " in" + self.obj.RollerDiameter = str(ANSIRollerTable[size][1]) + " in" + self.form.Quantity_Pitch.setText(self.obj.Pitch.UserString) + self.form.Quantity_RollerDiameter.setText(self.obj.RollerDiameter.UserString) + + self.obj.Proxy.execute(self.obj) + FreeCAD.Gui.SendMsgToActiveView("ViewFit") + + def rollerDiameterChanged(self, value): + self.obj.RollerDiameter = value + self.obj.Proxy.execute(self.obj) + + def numTeethChanged(self, value): + self.obj.NumberOfTeeth = value + self.obj.Proxy.execute(self.obj) + FreeCAD.Gui.SendMsgToActiveView("ViewFit") + + def getStandardButtons(self): + return int(QtGui.QDialogButtonBox.Ok) | int(QtGui.QDialogButtonBox.Cancel)| int(QtGui.QDialogButtonBox.Apply) + + def clicked(self,button): + if button == QtGui.QDialogButtonBox.Apply: + self.transferTo() + self.obj.Proxy.execute(self.obj) + + def update(self): + 'fills the widgets' + self.transferFrom() + + def accept(self): + self.transferTo() + FreeCAD.ActiveDocument.recompute() + FreeCADGui.ActiveDocument.resetEdit() + + def reject(self): + FreeCADGui.ActiveDocument.resetEdit() + FreeCAD.ActiveDocument.abortTransaction() + + + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('PartDesign_Sprocket',_CommandSprocket()) diff --git a/src/Mod/PartDesign/SprocketFeature.ui b/src/Mod/PartDesign/SprocketFeature.ui new file mode 100644 index 0000000000..c1960a8837 --- /dev/null +++ b/src/Mod/PartDesign/SprocketFeature.ui @@ -0,0 +1,232 @@ + + + SprocketParameter + + + + 0 + 0 + 195 + 142 + + + + Sprocket parameter + + + + + + + Number of teeth: + + + + + + + 3 + + + 9999 + + + 50 + + + + + + + + ANSI Size: + + + + + + + + + 25 + + + + + 35 + + + + + 41 + + + + + 40 + + + + + 50 + + + + + 60 + + + + + 80 + + + + + 100 + + + + + 120 + + + + + 140 + + + + + 160 + + + + + 180 + + + + + 200 + + + + + 240 + + + + + + + + + + Chain Pitch: + + + + + + + + + + 0 + 0 + + + + + 80 + 20 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + in + + + 3 + + + 2000.000000000000000 + + + 0.01 + + + + 0.001 + + + 0.375 + + + + + + + + + Roller Diameter: + + + + + + + + 0 + 0 + + + + + 80 + 20 + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + in + + + 3 + + + 50.000000000000000 + + + 0.01 + + + + 0.01 + + + 0.20 + + + + + + + + + Gui::InputField + QLineEdit +
    Gui/InputField.h
    +
    +
    + + +
    diff --git a/src/Mod/PartDesign/fcsprocket/README.md b/src/Mod/PartDesign/fcsprocket/README.md new file mode 100644 index 0000000000..3759e676ce --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/README.md @@ -0,0 +1,24 @@ +================================================ + FCSprocket: a Sprocket Generator for FreeCAD +================================================ + +This is a simple sprocket generation tool. Sprockets are used in combination +with roller chain to transmit power. The tooth profiles are drawn according +to ANSI standards from the methods outlined in: + + Standard handbook of chains : chains for power transmission and material + handling. Boca Raton: Taylor & Francis, 2006. Print. + + AND + + Oberg, Erik, et al. Machinery's handbook : a reference book for the + mechanical engineer, designer, manufacturing engineer, draftsman, + toolmaker, and machinist. New York: Industrial Press, 2016. Print. + + +This code is based on the work of David Douard and his implementation of the +gear generator (fcgear) found in FreeCAD. + + +Copyright 2020 Adam Spontarelli . +Distributed under the LGPL licence. diff --git a/src/Mod/PartDesign/fcsprocket/__init__.py b/src/Mod/PartDesign/fcsprocket/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/__init__.py @@ -0,0 +1 @@ + diff --git a/src/Mod/PartDesign/fcsprocket/fcsprocket.py b/src/Mod/PartDesign/fcsprocket/fcsprocket.py new file mode 100644 index 0000000000..b27309c470 --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/fcsprocket.py @@ -0,0 +1,105 @@ +# (c) 2020 Adam Spontarelli +# +# 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. +# +# FCGear 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 FCGear; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + +from math import cos, sin, pi, acos, asin, atan, sqrt + +import FreeCAD, Part +from FreeCAD import Base, Console +from . import sprocket +rotate = sprocket.rotate + +def makeSprocket(P, N, Dr): + if FreeCAD.ActiveDocument is None: + FreeCAD.newDocument("Sprocket") + doc = FreeCAD.ActiveDocument + w = FCWireBuilder() + sprocket.CreateSprocket(w, P, N, Dr) + sprocketw = Part.Wire([o.toShape() for o in w.wire]) + sprocket = doc.addObject("Part::Feature", "Sprocket") + sprocket.Shape = sprocketw + return sprocket + +class FCWireBuilder(object): + """A helper class to prepare a Part.Wire object""" + def __init__(self): + self.pos = None + self.theta = 0.0 + self.wire = [] + + def move(self, p): + """set current position""" + self.pos = Base.Vector(*p) + + def line(self, p): + """Add a segment between self.pos and p""" + p = rotate(p, self.theta) + end = Base.Vector(*p) + self.wire.append(Part.LineSegment(self.pos, end)) + self.pos = end + + def arc(self, p, r, sweep): + """"Add an arc from self.pos to p which radius is r + sweep (0 or 1) determine the orientation of the arc + """ + p = rotate(p, self.theta) + end = Base.Vector(*p) + mid = Base.Vector(*(midpoints(p, self.pos, r)[sweep])) + self.wire.append(Part.Arc(self.pos, mid, end)) + self.pos = end + + def curve(self, *points): + """Add a Bezier curve from self.pos to points[-1] + every other points are the control points of the Bezier curve (which + will thus be of degree len(points) ) + """ + points = [Base.Vector(*rotate(p, self.theta)) for p in points] + bz = Part.BezierCurve() + bz.setPoles([self.pos] + points) + self.wire.append(bz) + self.pos = points[-1] + + def close(self): + pass + +def midpoints(p1, p2, r): + """A very ugly function that returns the midpoint of a p1 and p2 + on the circle which radius is r and which pass through p1 and + p2 + + Return the 2 possible solutions + """ + vx, vy = p2[0]-p1[0], p2[1]-p1[1] + b = (vx**2 + vy**2)**.5 + v = (vx/b, vy/b) + cosA = b**2 / (2*b*r) + A = acos(cosA) + + vx, vy = rotate(v, A) + c1 = (p1[0]+r*vx, p1[1]+r*vy) + m1x, m1y = ((p1[0]+p2[0])/2 - c1[0], (p1[1]+p2[1])/2 - c1[1]) + dm1 = (m1x**2+m1y**2)**.5 + m1x, m1y = (c1[0] + r*m1x/dm1, c1[1] + r*m1y/dm1) + m1 = (m1x, m1y) + + vx, vy = rotate(v, -A) + c2 = (p1[0]+r*vx, p1[1]+r*vy) + m2x, m2y = ((p1[0]+p2[0])/2 - c2[0], (p1[1]+p2[1])/2 - c2[1]) + dm2 = (m2x**2+m2y**2)**.5 + m2x, m2y = (c2[0] + r*m2x/dm2, c2[1] + r*m2y/dm2) + m2 = (m2x, m2y) + + return m1, m2 diff --git a/src/Mod/PartDesign/fcsprocket/fcsprocketdialog.py b/src/Mod/PartDesign/fcsprocket/fcsprocketdialog.py new file mode 100644 index 0000000000..7360a7235d --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/fcsprocketdialog.py @@ -0,0 +1,66 @@ +# (c) 2020 Adam Spontarelli +# +# 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. +# +# FCGear 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 FCGear; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + +from PySide import QtGui as qt +import fcsprocket +import FreeCAD, FreeCADGui + +class SprocketCreationFrame(qt.QFrame): + def __init__(self, parent=None): + super(SprocketCreationFrame, self).__init__(parent) + self.P = qt.QSpinBox(value=0.375) + self.N = qt.QDoubleSpinBox(value=45) + self.Dr = qt.QDoubleSpinBox(value=0.20) + + l = qt.QFormLayout(self) + l.setFieldGrowthPolicy(l.ExpandingFieldsGrow) + l.addRow('Number of teeth:', self.N) + l.addRow('Chain Pitch (in):', self.P) + l.addRow('Roller Diameter (in):', self.Dr) + + +class SprocketDialog(qt.QDialog): + def __init__(self, parent=None): + super(SprocketDialog, self).__init__(parent) + self.gc = SprocketCreationFrame() + + btns = qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel + buttonBox = qt.QDialogButtonBox(btns, + accepted=self.accept, + rejected=self.reject) + l = qt.QVBoxLayout(self) + l.addWidget(self.gc) + l.addWidget(buttonBox) + self.setWindowTitle('Sprocket creation dialog') + + def accept(self): + if FreeCAD.ActiveDocument is None: + FreeCAD.newDocument("Sprocket") + + gear = fcgear.makeSprocket(self.gc.m.value(), + self.gc.Z.value(), + self.gc.angle.value(), + not self.gc.split.currentIndex()) + FreeCADGui.SendMsgToActiveView("ViewFit") + return super(SprocketDialog, self).accept() + + +if __name__ == '__main__': + a = qt.QApplication([]) + w = SprocketDialog() + w.show() + a.exec_() diff --git a/src/Mod/PartDesign/fcsprocket/sprocket.py b/src/Mod/PartDesign/fcsprocket/sprocket.py new file mode 100644 index 0000000000..a35bb48e70 --- /dev/null +++ b/src/Mod/PartDesign/fcsprocket/sprocket.py @@ -0,0 +1,144 @@ +# (c) 2020 Adam Spontarelli +# +# 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. +# +# FCGear 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 FCGear; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + +from math import cos, sin, tan, sqrt, radians, acos, atan, asin, degrees +import math + +import sys +if sys.version_info.major >= 3: + xrange = range + + +def CreateSprocket(w, P, N, Dr): + """ + Create a sprocket + + w is the wirebuilder object (in which the sprocket will be constructed) + P is the chain pitch + N is the number of teeth + Dr is the roller diameter + + Remaining variables can be found in Standard Handbook of Chains + """ + Ds = 1.005 * Dr + (0.003 * 25.4) + R = Ds / 2 + alpha = 35 + 60/N + beta = 18 - 56 / N + M = 0.8 * Dr * cos(radians(35) + radians(60/N)) + T = 0.8 * Dr * sin(radians(35) + radians(60/N)) + E = 1.3025 * Dr + (0.0015 * 25.4) + W = 1.4 * Dr * cos(radians(180/N)) + V = 1.4 * Dr * sin(radians(180/N)) + F = Dr * (0.8 * cos(radians(18) - radians(56)/N) + 1.4 * + cos(radians(17) - radians(64) / N) - 1.3025) - (0.0015 * 25.4) + PD = P / (sin(radians(180)/N)) + H = sqrt(F**2 - (1.4 * Dr - P/2)**2) + OD = P * (0.6 + 1/tan(radians(180/N))) + + # The sprocket tooth gullet consists of four segments + x0 = 0 + y0 = PD/2 - R + + # ---- Segment 1 ----- + alpha = 35 + 60/N + x1 = -R * cos(radians(alpha)) + y1 = PD/2 - R * sin(radians(alpha)) + arc_end = [x1, y1] + + # ---- Segment 2 ----- + alpha = 35 + 60/N + beta = 18 - 56 / N + x2 = M - E * cos(radians(alpha-beta)) + y2 = T - E * sin(radians(alpha-beta)) + PD/2 + + # # ---- Segment 3 ----- + y2o = y2 - PD/2 + hyp = sqrt((-W-x2)**2 + (-V-y2o)**2) + AP = sqrt(hyp**2 - F**2) + gamma = atan((y2o + V)/(x2 + W)) + alpha = asin(AP / hyp) + beta = 180 - (90 - degrees(alpha)) - (90 - degrees(gamma)) + x3o = AP * sin(radians(beta)) + y3o = AP * cos(radians(beta)) + x3 = x2 - x3o + y3 = y2 + y3o + + # ---- Segment 4 ----- + alpha = 180/N + m = -1/tan(radians(alpha)) + yf = PD/2 - V + A = 1 + m**2 + B = 2*m*yf - 2*W + C = W**2 + yf**2 - F**2 + # x4a = (-B - sqrt(B**2 - 4 * A * C)) / (2*A) + x4b = (-B + sqrt(B**2 - 4 * A * C)) / (2*A) + x4 = -x4b + y4 = m * x4 + + p0 = [x0,y0] + p1 = [x1,y1] + p2 = [x2,y2] + p3 = [x3,y3] + p4 = [x4,y4] + p5 = [-x1,y1] + p6 = [-x2,y2] + p7 = [-x3,y3] + p8 = [-x4,y4] + + w.move(p4) # vectors are lists [x,y] + w.arc(p3, F, 0) + w.line(p2) + w.arc(p1, E, 1) + w.arc(p0, R, 1) + + # ---- Mirror ----- + w.arc(p5, R, 1) + w.arc(p6, E, 1) + w.line(p7) + w.arc(p8, F, 0) + + # ---- Polar Array ---- + alpha = -radians(360/N) + for n in range(1,N): + # falling gullet slope + w.arc(rotate(p3, alpha*n), F, 0) + w.line(rotate(p2, alpha*n)) + w.arc(rotate(p1, alpha*n), E, 1) + w.arc(rotate(p0, alpha*n), R, 1) + + # rising gullet slope + w.arc(rotate(p5, alpha*n), R, 1) + w.line(rotate(p6, alpha*n)) + w.arc(rotate(p7, alpha*n), E, 0) + w.arc(rotate(p8, alpha*n), F, 0) + + w.close() + + return w + + +def rotate(pt, rads): + """ + rotate pt by rads radians about origin + """ + sinA = sin(rads) + cosA = cos(rads) + return (pt[0] * cosA - pt[1] * sinA, + pt[0] * sinA + pt[1] * cosA) + + + From 1123e271a9c7abbb8c5e6232af122a355d5f103f Mon Sep 17 00:00:00 2001 From: Adam Spontarelli Date: Mon, 30 Mar 2020 16:23:55 -0400 Subject: [PATCH 128/142] Converted class names from private to public and corrected docstring formatting, per feedback from pull request --- src/Mod/PartDesign/SprocketFeature.py | 46 +++++++++++++-------- src/Mod/PartDesign/fcsprocket/fcsprocket.py | 2 +- src/Mod/PartDesign/fcsprocket/sprocket.py | 5 --- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/Mod/PartDesign/SprocketFeature.py b/src/Mod/PartDesign/SprocketFeature.py index d9824c49cd..5fa8f7fd31 100644 --- a/src/Mod/PartDesign/SprocketFeature.py +++ b/src/Mod/PartDesign/SprocketFeature.py @@ -34,11 +34,13 @@ __url__ = "http://www.freecadweb.org" def makeSprocket(name): - '''makeSprocket(name): makes a Sprocket''' + """ + makeSprocket(name): makes a Sprocket + """ obj = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython",name) - _Sprocket(obj) + Sprocket(obj) if FreeCAD.GuiUp: - _ViewProviderSprocket(obj.ViewObject) + ViewProviderSprocket(obj.ViewObject) #FreeCAD.ActiveDocument.recompute() if FreeCAD.GuiUp: body=FreeCADGui.ActiveDocument.ActiveView.getActiveObject("pdbody") @@ -49,8 +51,10 @@ def makeSprocket(name): part.Group=part.Group+[obj] return obj -class _CommandSprocket: - "the Fem Sprocket command definition" +class CommandSprocket: + """ + the Fem Sprocket command definition + """ def GetResources(self): return {'Pixmap' : 'PartDesign_Sprocket', 'MenuText': QtCore.QT_TRANSLATE_NOOP("PartDesign_Sprocket","Sprocket..."), @@ -71,8 +75,10 @@ class _CommandSprocket: return False -class _Sprocket: - "The Sprocket object" +class Sprocket: + """ + The Sprocket object + """ def __init__(self,obj): self.Type = "Sprocket" obj.addProperty("App::PropertyInteger","NumberOfTeeth","Sprocket","Number of gear teeth") @@ -98,8 +104,10 @@ class _Sprocket: return -class _ViewProviderSprocket: - "A View Provider for the Sprocket object" +class ViewProviderSprocket: + """ + A View Provider for the Sprocket object + """ def __init__(self,vobj): vobj.Proxy = self @@ -111,9 +119,8 @@ class _ViewProviderSprocket: self.ViewObject = vobj self.Object = vobj.Object - def setEdit(self,vobj,mode): - taskd = _SprocketTaskPanel(self.Object,mode) + taskd = SprocketTaskPanel(self.Object,mode) taskd.obj = vobj.Object taskd.update() FreeCADGui.Control.showDialog(taskd) @@ -130,8 +137,10 @@ class _ViewProviderSprocket: return None -class _SprocketTaskPanel: - '''The editmode TaskPanel for Sprocket objects''' +class SprocketTaskPanel: + """ + The editmode TaskPanel for Sprocket objects + """ def __init__(self,obj,mode): self.obj = obj @@ -150,14 +159,18 @@ class _SprocketTaskPanel: FreeCAD.Gui.SendMsgToActiveView("ViewFit") def transferTo(self): - "Transfer from the dialog to the object" + """ + Transfer from the dialog to the object + """ self.obj.NumberOfTeeth = self.form.spinBox_NumberOfTeeth.value() self.obj.Pitch = self.form.Quantity_Pitch.text() self.obj.RollerDiameter = self.form.Quantity_RollerDiameter.text() self.obj.ANSISize = self.form.comboBox_ANSISize.currentText() def transferFrom(self): - "Transfer from the object to the dialog" + """ + Transfer from the object to the dialog + """ self.form.spinBox_NumberOfTeeth.setValue(self.obj.NumberOfTeeth) self.form.Quantity_Pitch.setText(self.obj.Pitch.UserString) self.form.Quantity_RollerDiameter.setText(self.obj.RollerDiameter.UserString) @@ -214,7 +227,6 @@ class _SprocketTaskPanel: self.obj.Proxy.execute(self.obj) def update(self): - 'fills the widgets' self.transferFrom() def accept(self): @@ -229,4 +241,4 @@ class _SprocketTaskPanel: if FreeCAD.GuiUp: - FreeCADGui.addCommand('PartDesign_Sprocket',_CommandSprocket()) + FreeCADGui.addCommand('PartDesign_Sprocket', CommandSprocket()) diff --git a/src/Mod/PartDesign/fcsprocket/fcsprocket.py b/src/Mod/PartDesign/fcsprocket/fcsprocket.py index b27309c470..533c5d47c5 100644 --- a/src/Mod/PartDesign/fcsprocket/fcsprocket.py +++ b/src/Mod/PartDesign/fcsprocket/fcsprocket.py @@ -1,4 +1,4 @@ -# (c) 2020 Adam Spontarelli +# (c) 2014 David Douard # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License (LGPL) diff --git a/src/Mod/PartDesign/fcsprocket/sprocket.py b/src/Mod/PartDesign/fcsprocket/sprocket.py index a35bb48e70..9c1bc1c6b2 100644 --- a/src/Mod/PartDesign/fcsprocket/sprocket.py +++ b/src/Mod/PartDesign/fcsprocket/sprocket.py @@ -16,13 +16,8 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 from math import cos, sin, tan, sqrt, radians, acos, atan, asin, degrees -import math -import sys -if sys.version_info.major >= 3: - xrange = range - def CreateSprocket(w, P, N, Dr): """ Create a sprocket From add624353da123fbb3aba8f25c888fd54a0187df Mon Sep 17 00:00:00 2001 From: Adam Spontarelli Date: Mon, 30 Mar 2020 10:30:47 -0400 Subject: [PATCH 129/142] Converted class names from private to public, per feedback from pull request --- src/Mod/PartDesign/SprocketFeature.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Mod/PartDesign/SprocketFeature.py b/src/Mod/PartDesign/SprocketFeature.py index 5fa8f7fd31..0cfa0c9adc 100644 --- a/src/Mod/PartDesign/SprocketFeature.py +++ b/src/Mod/PartDesign/SprocketFeature.py @@ -52,9 +52,11 @@ def makeSprocket(name): return obj class CommandSprocket: + """ the Fem Sprocket command definition """ + def GetResources(self): return {'Pixmap' : 'PartDesign_Sprocket', 'MenuText': QtCore.QT_TRANSLATE_NOOP("PartDesign_Sprocket","Sprocket..."), @@ -79,6 +81,7 @@ class Sprocket: """ The Sprocket object """ + def __init__(self,obj): self.Type = "Sprocket" obj.addProperty("App::PropertyInteger","NumberOfTeeth","Sprocket","Number of gear teeth") @@ -138,9 +141,13 @@ class ViewProviderSprocket: class SprocketTaskPanel: +<<<<<<< HEAD """ The editmode TaskPanel for Sprocket objects """ +======= + '''The editmode TaskPanel for Sprocket objects''' +>>>>>>> 07b2401c1... Converted class names from private to public, per feedback from pull request def __init__(self,obj,mode): self.obj = obj From 55e537d79dd25ec2300cc073225c16774f576ef2 Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 21 Mar 2020 10:09:34 +0100 Subject: [PATCH 130/142] [Draft] Improved Snapper Toolbar Behaviour Changed snap toolbar behaviour: - create a list of available snaps (Gui.Snapper.snaps) - make it consistent with Snap Gui Commands (in gui_snaps module) - create a list of active snaps (Gui.Snapper.active_snaps) - refactor the isEnabled() method to allow it to check if the given snap is in Gui.Snapper.active_snaps and not if the snap toolbar button isChecked() - updated and reordered the new list of gui snap commands in draftutils.init_tools and used it as a base to refactor the creation of draft toolbar - updated all the draft snap gui tools to make them control the toolbar buttons directly . . . --- src/Mod/Draft/DraftGui.py | 3 +- src/Mod/Draft/draftguitools/gui_snapper.py | 438 +++++++++++---------- src/Mod/Draft/draftguitools/gui_snaps.py | 20 +- src/Mod/Draft/draftutils/init_tools.py | 18 +- 4 files changed, 259 insertions(+), 220 deletions(-) diff --git a/src/Mod/Draft/DraftGui.py b/src/Mod/Draft/DraftGui.py index 4c12910ac3..8076310f39 100644 --- a/src/Mod/Draft/DraftGui.py +++ b/src/Mod/Draft/DraftGui.py @@ -1893,8 +1893,7 @@ class DraftToolBar: return str(a) def togglesnap(self): - if hasattr(FreeCADGui,"Snapper"): - FreeCADGui.Snapper.toggle() + FreeCADGui.doCommand('FreeCADGui.runCommand("Draft_Snap_Lock")') def togglenearsnap(self): if hasattr(FreeCADGui,"Snapper"): diff --git a/src/Mod/Draft/draftguitools/gui_snapper.py b/src/Mod/Draft/draftguitools/gui_snapper.py index 37a4a9e211..542acf76d8 100644 --- a/src/Mod/Draft/draftguitools/gui_snapper.py +++ b/src/Mod/Draft/draftguitools/gui_snapper.py @@ -39,19 +39,19 @@ import math from pivy import coin from PySide import QtCore, QtGui -import FreeCAD -import FreeCADGui +import FreeCAD as App +import FreeCADGui as Gui import Draft import DraftVecUtils from FreeCAD import Vector import draftguitools.gui_trackers as trackers +from draftutils.init_tools import get_draft_snap_commands from draftutils.messages import _msg, _wrn __title__ = "FreeCAD Draft Snap tools" __author__ = "Yorik van Havre" __url__ = "https://www.freecadweb.org" - class Snapper: """Classes to manage snapping in Draft and Arch. @@ -117,6 +117,27 @@ class Snapper: self.callbackMove = None self.snapObjectIndex = 0 + # snap keys, it's important tha they are in this order for + # saving in preferences and for properly restore the toolbar + self.snaps = ['Lock', # 0 + 'Near', # 1 former "passive" snap + 'Extension', # 2 + 'Parallel', # 3 + 'Grid', # 4 + "Endpoint", # 5 + 'Midpoint', # 6 + 'Perpendicular', # 7 + 'Angle', # 8 + 'Center', # 9 + 'Ortho', # 10 + 'Intersection', # 11 + 'Special', # 12 + 'Dimensions', # 13 + 'WorkingPlane' # 14 + ] + + self.init_active_snaps() + # the snapmarker has "dot","circle" and "square" available styles if self.snapStyle: self.mk = coll.OrderedDict([('passive', 'empty'), @@ -159,6 +180,19 @@ class Snapper: ('intersection', ':/icons/Snap_Intersection.svg'), ('special', ':/icons/Snap_Special.svg')]) + def init_active_snaps(self): + """ + set self.active_snaps according to user prefs + """ + self.active_snaps = [] + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + snap_modes = param.GetString("snapModes") + i = 0 + for snap in snap_modes: + if bool(int(snap)): + self.active_snaps.append(self.snaps[i]) + i += 1 + def cstr(self, lastpoint, constrain, point): """Return constraints if needed.""" if constrain or self.mask: @@ -197,8 +231,8 @@ class Snapper: if not hasattr(self, "toolbar"): self.makeSnapToolBar() - mw = FreeCADGui.getMainWindow() - bt = mw.findChild(QtGui.QToolBar, "Draft Snap") + mw = Gui.getMainWindow() + bt = mw.findChild(QtGui.QToolBar,"Draft Snap") if not bt: mw.addToolBar(self.toolbar) else: @@ -310,7 +344,7 @@ class Snapper: subname = self.snapInfo['SubName'] obj = parent.getSubObject(subname, retType=1) else: - obj = FreeCAD.ActiveDocument.getObject(self.snapInfo['Object']) + obj = App.ActiveDocument.getObject(self.snapInfo['Object']) parent = obj subname = self.snapInfo['Component'] if not obj: @@ -434,9 +468,9 @@ class Snapper: # calculating the nearest snap point shortest = 1000000000000000000 - origin = Vector(self.snapInfo['x'], - self.snapInfo['y'], - self.snapInfo['z']) + origin = App.Vector(self.snapInfo['x'], + self.snapInfo['y'], + self.snapInfo['z']) winner = None fp = point for snap in snaps: @@ -454,7 +488,7 @@ class Snapper: if self.radius: dv = point.sub(winner[2]) if (dv.Length > self.radius): - if (not oldActive) and self.isEnabled("passive"): + if (not oldActive) and self.isEnabled("Near"): winner = self.snapToVertex(self.snapInfo) # setting the cursors @@ -482,8 +516,8 @@ class Snapper: def toWP(self, point): """Project the given point on the working plane, if needed.""" if self.isEnabled("WorkingPlane"): - if hasattr(FreeCAD, "DraftWorkingPlane"): - return FreeCAD.DraftWorkingPlane.projectPoint(point) + if hasattr(App, "DraftWorkingPlane"): + return App.DraftWorkingPlane.projectPoint(point) return point def getApparentPoint(self, x, y): @@ -491,14 +525,14 @@ class Snapper: view = Draft.get3DView() pt = view.getPoint(x, y) if self.mask != "z": - if hasattr(FreeCAD, "DraftWorkingPlane"): + if hasattr(App,"DraftWorkingPlane"): if view.getCameraType() == "Perspective": camera = view.getCameraNode() p = camera.getField("position").getValue() - dv = pt.sub(Vector(p[0], p[1], p[2])) + dv = pt.sub(App.Vector(p[0], p[1], p[2])) else: dv = view.getViewDirection() - return FreeCAD.DraftWorkingPlane.projectPoint(pt, dv) + return App.DraftWorkingPlane.projectPoint(pt, dv) return pt def snapToDim(self, obj): @@ -526,7 +560,7 @@ class Snapper: self.extLine.on() self.setCursor(tsnap[1]) return tsnap[2], eline - if self.isEnabled("extension"): + if self.isEnabled("Extension"): tsnap = self.snapToExtOrtho(last, constrain, eline) if tsnap: if (tsnap[0].sub(point)).Length < self.radius: @@ -553,10 +587,10 @@ class Snapper: self.setCursor(tsnap[1]) return tsnap[2], eline - for o in (self.lastObj[1], self.lastObj[0]): - if o and (self.isEnabled('extension') - or self.isEnabled('parallel')): - ob = FreeCAD.ActiveDocument.getObject(o) + for o in [self.lastObj[1], self.lastObj[0]]: + if o and (self.isEnabled('Extension') + or self.isEnabled('Parallel')): + ob = App.ActiveDocument.getObject(o) if ob: if ob.isDerivedFrom("Part::Feature"): edges = ob.Shape.Edges @@ -572,7 +606,7 @@ class Snapper: np = self.getPerpendicular(e,point) if not DraftGeomUtils.isPtOnEdge(np,e): if (np.sub(point)).Length < self.radius: - if self.isEnabled('extension'): + if self.isEnabled('Extension'): if np != e.Vertexes[0].Point: p0 = e.Vertexes[0].Point if self.tracker and not self.selectMode: @@ -603,7 +637,7 @@ class Snapper: self.lastExtensions[0] = ne return np,ne else: - if self.isEnabled('parallel'): + if self.isEnabled('Parallel'): if last: ve = DraftGeomUtils.vec(e) if not DraftVecUtils.isNull(ve): @@ -620,7 +654,7 @@ class Snapper: def snapToCrossExtensions(self, point): """Snap to the intersection of the last 2 extension lines.""" - if self.isEnabled('extension'): + if self.isEnabled('Extension'): if len(self.lastExtensions) == 2: np = DraftGeomUtils.findIntersection(self.lastExtensions[0], self.lastExtensions[1], True, True) if np: @@ -648,19 +682,19 @@ class Snapper: return p return None - def snapToPolar(self, point, last): + def snapToPolar(self,point,last): """Snap to polar lines from the given point.""" - if self.isEnabled('ortho') and (not self.mask): + if self.isEnabled('Ortho') and (not self.mask): if last: vecs = [] - if hasattr(FreeCAD, "DraftWorkingPlane"): - ax = [FreeCAD.DraftWorkingPlane.u, - FreeCAD.DraftWorkingPlane.v, - FreeCAD.DraftWorkingPlane.axis] + if hasattr(App,"DraftWorkingPlane"): + ax = [App.DraftWorkingPlane.u, + App.DraftWorkingPlane.v, + App.DraftWorkingPlane.axis] else: - ax = [FreeCAD.Vector(1, 0, 0), - FreeCAD.Vector(0, 1, 0), - FreeCAD.Vector(0, 0, 1)] + ax = [App.Vector(1, 0, 0), + App.Vector(0, 1, 0), + App.Vector(0, 0, 1)] for a in self.polarAngles: if a == 90: vecs.extend([ax[0], ax[0].negative()]) @@ -691,7 +725,7 @@ class Snapper: """Return a grid snap point if available.""" if self.grid: if self.grid.Visible: - if self.isEnabled("grid"): + if self.isEnabled("Grid"): np = self.grid.getClosestNode(point) if np: dv = point.sub(np) @@ -707,7 +741,7 @@ class Snapper: def snapToEndpoints(self, shape): """Return a list of endpoints snap locations.""" snaps = [] - if self.isEnabled("endpoint"): + if self.isEnabled("Endpoint"): if hasattr(shape, "Vertexes"): for v in shape.Vertexes: snaps.append([v.Point, 'endpoint', self.toWP(v.Point)]) @@ -725,7 +759,7 @@ class Snapper: def snapToMidpoint(self, shape): """Return a list of midpoints snap locations.""" snaps = [] - if self.isEnabled("midpoint"): + if self.isEnabled("Midpoint"): if isinstance(shape, Part.Edge): mp = DraftGeomUtils.findMidpoint(shape) if mp: @@ -735,7 +769,7 @@ class Snapper: def snapToPerpendicular(self, shape, last): """Return a list of perpendicular snap locations.""" snaps = [] - if self.isEnabled("perpendicular"): + if self.isEnabled("Perpendicular"): if last: if isinstance(shape, Part.Edge): if DraftGeomUtils.geomType(shape) == "Line": @@ -758,7 +792,7 @@ class Snapper: def snapToOrtho(self, shape, last, constrain): """Return a list of ortho snap locations.""" snaps = [] - if self.isEnabled("ortho"): + if self.isEnabled("Ortho"): if constrain: if isinstance(shape, Part.Edge): if last: @@ -774,7 +808,7 @@ class Snapper: def snapToExtOrtho(self, last, constrain, eline): """Return an ortho X extension snap location.""" - if self.isEnabled("extension") and self.isEnabled("ortho"): + if self.isEnabled("Extension") and self.isEnabled("Ortho"): if constrain and last and self.constraintAxis and self.extLine: tmpEdge1 = Part.LineSegment(last, last.add(self.constraintAxis)).toShape() tmpEdge2 = Part.LineSegment(self.extLine.p1(), self.extLine.p2()).toShape() @@ -800,15 +834,15 @@ class Snapper: """ if not self.holdPoints: return None - if hasattr(FreeCAD, "DraftWorkingPlane"): - u = FreeCAD.DraftWorkingPlane.u - v = FreeCAD.DraftWorkingPlane.v + if hasattr(App, "DraftWorkingPlane"): + u = App.DraftWorkingPlane.u + v = App.DraftWorkingPlane.v else: - u = FreeCAD.Vector(1, 0, 0) - v = FreeCAD.Vector(0, 1, 0) + u = App.Vector(1, 0, 0) + v = App.Vector(0, 1, 0) if len(self.holdPoints) > 1: # first try mid points - if self.isEnabled("midpoint"): + if self.isEnabled("Midpoint"): l = list(self.holdPoints) for p1, p2 in itertools.combinations(l, 2): p3 = p1.add((p2.sub(p1)).multiply(0.5)) @@ -845,7 +879,7 @@ class Snapper: def snapToExtPerpendicular(self, last): """Return a perpendicular X extension snap location.""" - if self.isEnabled("extension") and self.isEnabled("perpendicular"): + if self.isEnabled("Extension") and self.isEnabled("Perpendicular"): if last and self.extLine: if self.extLine.p1() != self.extLine.p2(): tmpEdge = Part.LineSegment(self.extLine.p1(), self.extLine.p2()).toShape() @@ -856,7 +890,7 @@ class Snapper: def snapToElines(self, e1, e2): """Return a snap at the infinite intersection of the given edges.""" snaps = [] - if self.isEnabled("intersection") and self.isEnabled("extension"): + if self.isEnabled("Intersection") and self.isEnabled("Extension"): if e1 and e2: # get the intersection points pts = DraftGeomUtils.findIntersection(e1, e2, True, True) @@ -868,7 +902,7 @@ class Snapper: def snapToAngles(self, shape): """Return a list of angle snap locations.""" snaps = [] - if self.isEnabled("angle"): + if self.isEnabled("Angle"): rad = shape.Curve.Radius pos = shape.Curve.Center for i in (0, 30, 45, 60, 90, @@ -885,7 +919,7 @@ class Snapper: def snapToCenter(self, shape): """Return a list of center snap locations.""" snaps = [] - if self.isEnabled("center"): + if self.isEnabled("Center"): pos = shape.Curve.Center c = self.toWP(pos) if hasattr(shape.Curve, "Radius"): @@ -906,7 +940,7 @@ class Snapper: def snapToFace(self, shape): """Return a face center snap location.""" snaps = [] - if self.isEnabled("center"): + if self.isEnabled("Center"): pos = shape.CenterOfMass c = self.toWP(pos) snaps.append([pos, 'center', c]) @@ -915,10 +949,10 @@ class Snapper: def snapToIntersection(self, shape): """Return a list of intersection snap locations.""" snaps = [] - if self.isEnabled("intersection"): + if self.isEnabled("Intersection"): # get the stored objects to calculate intersections if self.lastObj[0]: - obj = FreeCAD.ActiveDocument.getObject(self.lastObj[0]) + obj = App.ActiveDocument.getObject(self.lastObj[0]) if obj: if obj.isDerivedFrom("Part::Feature") or (Draft.getType(obj) == "Axis"): if (not self.maxEdges) or (len(obj.Shape.Edges) <= self.maxEdges): @@ -947,7 +981,7 @@ class Snapper: def snapToPolygon(self, obj): """Return a list of polygon center snap locations.""" snaps = [] - if self.isEnabled("center"): + if self.isEnabled("Center"): c = obj.Placement.Base for edge in obj.Shape.Edges: p1 = edge.Vertexes[0].Point @@ -958,29 +992,16 @@ class Snapper: snaps.append([v2, 'center', self.toWP(c)]) return snaps - def snapToVertex(self, info, active=False): - """Return a vertex snap location.""" - p = Vector(info['x'], info['y'], info['z']) - if active: - if self.isEnabled("passive"): - return [p, 'endpoint', self.toWP(p)] - else: - return [] - elif self.isEnabled("passive"): - return [p, 'passive', p] - else: - return [] def snapToSpecials(self, obj, lastpoint=None, eline=None): """Return special snap locations, if any.""" snaps = [] - if self.isEnabled("special"): + if self.isEnabled("Special"): if (Draft.getType(obj) == "Wall"): # special snapping for wall: snap to its base shape if it is linear if obj.Base: if not obj.Base.Shape.Solids: - for v in obj.Base.Shape.Vertexes: snaps.append([v.Point, 'special', self.toWP(v.Point)]) elif (Draft.getType(obj) == "Structure"): @@ -1040,13 +1061,13 @@ class Snapper: def setCursor(self, mode=None): """Set or reset the cursor to the given mode or resets.""" if self.selectMode: - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() for w in mw.findChild(QtGui.QMdiArea).findChildren(QtGui.QWidget): if w.metaObject().className() == "SIM::Coin3D::Quarter::QuarterWidget": w.unsetCursor() self.cursorMode = None elif not mode: - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() for w in mw.findChild(QtGui.QMdiArea).findChildren(QtGui.QWidget): if w.metaObject().className() == "SIM::Coin3D::Quarter::QuarterWidget": w.unsetCursor() @@ -1064,7 +1085,7 @@ class Snapper: qp.drawPixmap(QtCore.QPoint(16, 8), tp) qp.end() cur = QtGui.QCursor(newicon, 8, 8) - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() for w in mw.findChild(QtGui.QMdiArea).findChildren(QtGui.QWidget): if w.metaObject().className() == "SIM::Coin3D::Quarter::QuarterWidget": w.setCursor(cur) @@ -1121,7 +1142,7 @@ class Snapper: """Keep the current angle.""" if delta: self.mask = delta - elif isinstance(self.mask, FreeCAD.Vector): + elif isinstance(self.mask, App.Vector): self.mask = None elif self.trackLine: if self.trackLine.Visible: @@ -1138,15 +1159,15 @@ class Snapper: used as basepoint. """ # without the Draft module fully loaded, no axes system!" - if not hasattr(FreeCAD, "DraftWorkingPlane"): + if not hasattr(App, "DraftWorkingPlane"): return point - point = Vector(point) + point = App.Vector(point) # setup trackers if needed if not self.constrainLine: if self.snapStyle: - self.constrainLine = trackers.lineTracker(scolor=FreeCADGui.draftToolBar.getDefaultColor("snap")) + self.constrainLine = trackers.lineTracker(scolor=Gui.draftToolBar.getDefaultColor("snap")) else: self.constrainLine = trackers.lineTracker(dotted=True) @@ -1162,23 +1183,23 @@ class Snapper: if self.mask: self.affinity = self.mask if not self.affinity: - self.affinity = FreeCAD.DraftWorkingPlane.getClosestAxis(delta) - if isinstance(axis, FreeCAD.Vector): + self.affinity = App.DraftWorkingPlane.getClosestAxis(delta) + if isinstance(axis, App.Vector): self.constraintAxis = axis elif axis == "x": - self.constraintAxis = FreeCAD.DraftWorkingPlane.u + self.constraintAxis = App.DraftWorkingPlane.u elif axis == "y": - self.constraintAxis = FreeCAD.DraftWorkingPlane.v + self.constraintAxis = App.DraftWorkingPlane.v elif axis == "z": - self.constraintAxis = FreeCAD.DraftWorkingPlane.axis + self.constraintAxis = App.DraftWorkingPlane.axis else: if self.affinity == "x": - self.constraintAxis = FreeCAD.DraftWorkingPlane.u + self.constraintAxis = App.DraftWorkingPlane.u elif self.affinity == "y": - self.constraintAxis = FreeCAD.DraftWorkingPlane.v + self.constraintAxis = App.DraftWorkingPlane.v elif self.affinity == "z": - self.constraintAxis = FreeCAD.DraftWorkingPlane.axis - elif isinstance(self.affinity, FreeCAD.Vector): + self.constraintAxis = App.DraftWorkingPlane.axis + elif isinstance(self.affinity, App.Vector): self.constraintAxis = self.affinity else: self.constraintAxis = None @@ -1229,7 +1250,7 @@ class Snapper: if point: print "got a 3D point: ",point - FreeCADGui.Snapper.getPoint(callback=cb) + Gui.Snapper.getPoint(callback=cb) If the callback function accepts more than one argument, it will also receive the last snapped object. Finally, a qt widget @@ -1243,7 +1264,7 @@ class Snapper: self.pt = None self.lastSnappedObject = None self.holdPoints = [] - self.ui = FreeCADGui.draftToolBar + self.ui = Gui.draftToolBar self.view = Draft.get3DView() # remove any previous leftover callbacks @@ -1259,20 +1280,20 @@ class Snapper: mousepos = event.getPosition() ctrl = event.wasCtrlDown() shift = event.wasShiftDown() - self.pt = FreeCADGui.Snapper.snap(mousepos, lastpoint=last, - active=ctrl, constrain=shift) - if hasattr(FreeCAD, "DraftWorkingPlane"): + self.pt = Gui.Snapper.snap(mousepos, lastpoint=last, + active=ctrl, constrain=shift) + if hasattr(App, "DraftWorkingPlane"): self.ui.displayPoint(self.pt, last, - plane=FreeCAD.DraftWorkingPlane, - mask=FreeCADGui.Snapper.affinity) + plane = App.DraftWorkingPlane, + mask = App.Snapper.affinity) if movecallback: movecallback(self.pt, self.snapInfo) def getcoords(point, relative=False): """Get the global coordinates from a point.""" self.pt = point - if relative and last and hasattr(FreeCAD, "DraftWorkingPlane"): - v = FreeCAD.DraftWorkingPlane.getGlobalCoords(point) + if relative and last and hasattr(App, "DraftWorkingPlane"): + v = App.DraftWorkingPlane.getGlobalCoords(point) self.pt = last.add(v) accept() @@ -1289,8 +1310,8 @@ class Snapper: self.view.removeEventCallbackPivy(coin.SoLocation2Event.getClassTypeId(), self.callbackMove) self.callbackClick = None self.callbackMove = None - obj = FreeCADGui.Snapper.lastSnappedObject - FreeCADGui.Snapper.off() + obj = Gui.Snapper.lastSnappedObject + Gui.Snapper.off() self.ui.offUi() if callback: if len(inspect.getargspec(callback).args) > 1: @@ -1306,7 +1327,7 @@ class Snapper: self.view.removeEventCallbackPivy(coin.SoLocation2Event.getClassTypeId(), self.callbackMove) self.callbackClick = None self.callbackMove = None - FreeCADGui.Snapper.off() + Gui.Snapper.off() self.ui.offUi() if callback: if len(inspect.getargspec(callback).args) > 1: @@ -1332,121 +1353,90 @@ class Snapper: def makeSnapToolBar(self): """Build the Snap toolbar.""" - mw = FreeCADGui.getMainWindow() + mw = Gui.getMainWindow() self.toolbar = QtGui.QToolBar(mw) mw.addToolBar(QtCore.Qt.TopToolBarArea, self.toolbar) self.toolbar.setObjectName("Draft Snap") self.toolbar.setWindowTitle(QtCore.QCoreApplication.translate("Workbench", "Draft Snap")) - self.toolbarButtons = [] - # grid button - self.gridbutton = QtGui.QAction(mw) - self.gridbutton.setIcon(QtGui.QIcon.fromTheme("Draft_Grid", QtGui.QIcon(":/icons/Draft_Grid.svg"))) - self.gridbutton.setText(QtCore.QCoreApplication.translate("Draft_ToggleGrid", "Grid")) - self.gridbutton.setToolTip(QtCore.QCoreApplication.translate("Draft_ToggleGrid", "Toggles the Draft grid On/Off")) - self.gridbutton.setObjectName("GridButton") - self.gridbutton.setWhatsThis("Draft_ToggleGrid") - QtCore.QObject.connect(self.gridbutton, QtCore.SIGNAL("triggered()"), self.toggleGrid) - self.toolbar.addAction(self.gridbutton) + snap_gui_commands = get_draft_snap_commands() + self.init_draft_snap_buttons(snap_gui_commands, self.toolbar, "_Button") + self.restore_snap_buttons_state(self.toolbar,"_Button") - # master button - self.masterbutton = QtGui.QAction(mw) - self.masterbutton.setIcon(QtGui.QIcon.fromTheme("Snap_Lock", QtGui.QIcon(":/icons/Snap_Lock.svg"))) - self.masterbutton.setText(QtCore.QCoreApplication.translate("Draft_Snap_Lock", "Lock")) - self.masterbutton.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap_Lock", "Toggle On/Off")) - self.masterbutton.setObjectName("SnapButtonMain") - self.masterbutton.setWhatsThis("Draft_ToggleSnap") - self.masterbutton.setCheckable(True) - self.masterbutton.setChecked(True) - QtCore.QObject.connect(self.masterbutton, - QtCore.SIGNAL("toggled(bool)"), self.toggle) - self.toolbar.addAction(self.masterbutton) - for c,i in self.cursors.items(): - if i: - b = QtGui.QAction(mw) - b.setIcon(QtGui.QIcon.fromTheme(i.replace(':/icons/', '').replace('.svg', ''), QtGui.QIcon(i))) - if c == "passive": - b.setText(QtCore.QCoreApplication.translate("Draft_Snap_Near", "Nearest")) - b.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap_Near", "Nearest")) - else: - b.setText(QtCore.QCoreApplication.translate("Draft_Snap_"+c.capitalize(), c.capitalize())) - b.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap_"+c.capitalize(), c.capitalize())) - b.setObjectName("SnapButton" + c) - b.setWhatsThis("Draft_" + c.capitalize()) - b.setCheckable(True) - b.setChecked(True) - self.toolbar.addAction(b) - self.toolbarButtons.append(b) - QtCore.QObject.connect(b, QtCore.SIGNAL("toggled(bool)"), - self.saveSnapModes) + if not Draft.getParam("showSnapBar",True): + self.toolbar.hide() - # adding non-snap button - for n in ("Dimensions", "WorkingPlane"): - b = QtGui.QAction(mw) - b.setIcon(QtGui.QIcon.fromTheme("Snap_" + n, QtGui.QIcon(":/icons/Snap_"+n+".svg"))) - b.setText(QtCore.QCoreApplication.translate("Draft_Snap_" + n,n)) - b.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap_" + n,n)) - b.setObjectName("SnapButton" + n) - b.setWhatsThis("Draft_" + n) + def init_draft_snap_buttons(self, commands, context, button_suffix): + """ + Init Draft Snap toolbar buttons. + + Parameters: + commands Snap command list, + use: get_draft_snap_commands(): + context The toolbar or action group the buttons have + to be added to + button_suffix The suffix that have to be applied to the command name + to define the button name + """ + for gc in commands: + # setup toolbar buttons + command = 'Gui.runCommand("' + gc + '")' + b = QtGui.QAction(context) + b.setIcon(QtGui.QIcon(':/icons/' + gc[6:] + '.svg')) + b.setText(QtCore.QCoreApplication.translate("Draft_Snap", "Snap " + gc[11:])) + b.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap", "Snap " + gc[11:])) + b.setObjectName(gc + button_suffix) + b.setWhatsThis("Draft_"+gc[11:].capitalize()) b.setCheckable(True) b.setChecked(True) - self.toolbar.addAction(b) - QtCore.QObject.connect(b, QtCore.SIGNAL("toggled(bool)"), - self.saveSnapModes) - self.toolbarButtons.append(b) + context.addAction(b) + QtCore.QObject.connect(b, + QtCore.SIGNAL("triggered()"), + lambda f=Gui.doCommand, + arg=command:f(arg)) - # set status tip where needed - for b in self.toolbar.actions(): + for b in context.actions(): if len(b.statusTip()) == 0: b.setStatusTip(b.toolTip()) - # restoring states - t = Draft.getParam("snapModes", "111111111101111") - if t: - c = 0 - for b in [self.masterbutton] + self.toolbarButtons: - if len(t) > c: - state = bool(int(t[c])) - b.setChecked(state) + def restore_snap_buttons_state(self, toolbar, button_suffix): + """ + Restore toolbar button's checked state according to + "snapModes" saved in preferences + """ + # set status tip where needed + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + snap_modes = param.GetString("snapModes") + + for b in toolbar.actions(): + if len(b.statusTip()) == 0: + b.setStatusTip(b.toolTip()) + + # restore toolbar buttons state + if snap_modes: + for a in toolbar.findChildren(QtGui.QAction): + snap = a.objectName()[11:].replace(button_suffix,"") + if snap in Gui.Snapper.snaps: + i = Gui.Snapper.snaps.index(snap) + state = bool(int(snap_modes[i])) + a.setChecked(state) if state: - b.setToolTip(b.toolTip() + " (ON)") + a.setToolTip(a.toolTip()+" (ON)") else: - b.setToolTip(b.toolTip() + " (OFF)") - c += 1 - if not Draft.getParam("showSnapBar", True): - self.toolbar.hide() + a.setToolTip(a.toolTip()+" (OFF)") + + def get_snap_toolbar(self): + """retuns snap toolbar object""" + mw = Gui.getMainWindow() + if mw: + toolbar = mw.findChild(QtGui.QToolBar,"Draft Snap") + if toolbar: + return toolbar + return None def toggleGrid(self): - """Run Draft_ToggleGrid.""" - FreeCADGui.runCommand("Draft_ToggleGrid") - - def saveSnapModes(self): - """Save the snap modes for next sessions.""" - t = '' - for b in [self.masterbutton] + self.toolbarButtons: - t += str(int(b.isChecked())) - if b.isChecked(): - b.setToolTip(b.toolTip().replace("OFF", "ON")) - else: - b.setToolTip(b.toolTip().replace("ON", "OFF")) - Draft.setParam("snapModes", t) - - def toggle(self, checked=None): - """Toggle the snap mode.""" - if hasattr(self, "toolbarButtons"): - if checked is None: - self.masterbutton.toggle() - elif checked: - if hasattr(self, "savedButtonStates"): - for i in range(len(self.toolbarButtons)): - self.toolbarButtons[i].setEnabled(True) - self.toolbarButtons[i].setChecked(self.savedButtonStates[i]) - else: - self.savedButtonStates = [] - for i in range(len(self.toolbarButtons)): - self.savedButtonStates.append(self.toolbarButtons[i].isChecked()) - self.toolbarButtons[i].setEnabled(False) - self.saveSnapModes() + "toggle FreeCAD Draft Grid" + Gui.runCommand("Draft_ToggleGrid") def showradius(self): """Show the snap radius indicator.""" @@ -1456,32 +1446,66 @@ class Snapper: self.radiusTracker.update(self.radius) self.radiusTracker.on() - def isEnabled(self, but): - """Return true if the given button is turned on.""" - for b in self.toolbarButtons: - if str(b.objectName()) == "SnapButton" + but: - return (b.isEnabled() and b.isChecked()) - return False + def isEnabled(self, snap): + "Returns true if the given snap is on" + if "Lock" in self.active_snaps and snap in self.active_snaps: + return True + else: + return False + + def toggle_snap(self, snap, set_to = None): + "Sets the given snap on/off according to the given parameter" + if set_to: # set mode + if set_to is True: + if not snap in self.active_snaps: + self.active_snaps.append(snap) + status = True + elif set_to is False: + if snap in self.active_snaps: + self.active_snaps.remove(snap) + status = False + else: # toggle mode, default + if not snap in self.active_snaps: + self.active_snaps.append(snap) + status = True + elif snap in self.active_snaps: + self.active_snaps.remove(snap) + status = False + self.save_snap_state() + return status + + def save_snap_state(self): + """ + save snap state to user preferences to be restored in next session + """ + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + snap_modes = "" + for snap in self.snaps: + if snap in self.active_snaps: + snap_modes += "1" + else: + snap_modes += "0" + param.SetString("snapModes",snap_modes) def show(self): """Show the toolbar and the grid.""" if not hasattr(self, "toolbar"): self.makeSnapToolBar() - mw = FreeCADGui.getMainWindow() - bt = mw.findChild(QtGui.QToolBar, "Draft Snap") + bt = self.get_snap_toolbar() if not bt: + mw = FreeCADGui.getMainWindow() mw.addToolBar(self.toolbar) self.toolbar.setParent(mw) self.toolbar.show() self.toolbar.toggleViewAction().setVisible(True) - if FreeCADGui.ActiveDocument: + if Gui.ActiveDocument: self.setTrackers() - if not FreeCAD.ActiveDocument.Objects: - if FreeCADGui.ActiveDocument.ActiveView: - if FreeCADGui.ActiveDocument.ActiveView.getCameraType() == 'Orthographic': - c = FreeCADGui.ActiveDocument.ActiveView.getCameraNode() + if not App.ActiveDocument.Objects: + if Gui.ActiveDocument.ActiveView: + if Gui.ActiveDocument.ActiveView.getCameraType() == 'Orthographic': + c = Gui.ActiveDocument.ActiveView.getCameraNode() if c.orientation.getValue().getValue() == (0.0, 0.0, 0.0, 1.0): - p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + p = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") h = p.GetInt("defaultCameraHeight",0) if h: c.height.setValue(h) @@ -1523,7 +1547,7 @@ class Snapper: self.tracker = trackers.snapTracker() self.trackLine = trackers.lineTracker() if self.snapStyle: - c = FreeCADGui.draftToolBar.getDefaultColor("snap") + c = Gui.draftToolBar.getDefaultColor("snap") self.extLine = trackers.lineTracker(scolor=c) self.extLine2 = trackers.lineTracker(scolor=c) else: diff --git a/src/Mod/Draft/draftguitools/gui_snaps.py b/src/Mod/Draft/draftguitools/gui_snaps.py index 36ca85ffa0..75eb999335 100644 --- a/src/Mod/Draft/draftguitools/gui_snaps.py +++ b/src/Mod/Draft/draftguitools/gui_snaps.py @@ -28,12 +28,23 @@ # \brief Provide the Draft_Snap commands used by the snapping mechanism # in Draft. -from PySide.QtCore import QT_TRANSLATE_NOOP - +import draftguitools.gui_base as gui_base import FreeCADGui as Gui import draftguitools.gui_base as gui_base from draftutils.translate import _tr +from PySide.QtCore import QT_TRANSLATE_NOOP +from draftutils.translate import _tr + + +class Draft_Snap_Lock(gui_base.GuiCommandSimplest): + """GuiCommand for the Draft_Snap_Lock tool. + + Activate or deactivate all snap methods at once. + """ + + def __init__(self): + super().__init__(name=_tr("Main toggle snap")) class Draft_Snap_Lock(gui_base.GuiCommandSimplest): """GuiCommand for the Draft_Snap_Lock tool. @@ -63,6 +74,11 @@ class Draft_Snap_Lock(gui_base.GuiCommandSimplest): if hasattr(Gui.Snapper, "masterbutton"): Gui.Snapper.masterbutton.toggle() + if hasattr(Gui, "Snapper"): + if hasattr(Gui.Snapper, "masterbutton"): + Gui.Snapper.masterbutton.toggle() + +Gui.addCommand('Draft_Snap_Lock', Draft_Snap_Lock()) Gui.addCommand('Draft_Snap_Lock', Draft_Snap_Lock()) diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index 49b85cbcf6..7335ae2ccd 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -115,15 +115,15 @@ def get_draft_utility_commands(): def get_draft_snap_commands(): """Return the snapping commands list.""" - return ['Draft_Snap_Lock', 'Draft_Snap_Midpoint', - 'Draft_Snap_Perpendicular', - 'Draft_Snap_Grid', 'Draft_Snap_Intersection', - 'Draft_Snap_Parallel', - 'Draft_Snap_Endpoint', 'Draft_Snap_Angle', - 'Draft_Snap_Center', - 'Draft_Snap_Extension', 'Draft_Snap_Near', - 'Draft_Snap_Ortho', 'Draft_Snap_Special', - 'Draft_Snap_Dimensions', 'Draft_Snap_WorkingPlane'] + return ['Draft_Snap_Lock', + 'Draft_Snap_Endpoint', 'Draft_Snap_Midpoint', + 'Draft_Snap_Center', 'Draft_Snap_Angle', + 'Draft_Snap_Intersection', 'Draft_Snap_Perpendicular', + 'Draft_Snap_Extension', 'Draft_Snap_Parallel', + 'Draft_Snap_Special', 'Draft_Snap_Near', + 'Draft_Snap_Ortho', 'Draft_Snap_Grid', + 'Draft_Snap_WorkingPlane', 'Draft_Snap_Dimensions', + ] def init_draft_toolbars(workbench): From a23224da88d24c24a50896351cceeac3ac31c1f7 Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 21 Mar 2020 01:25:32 +0100 Subject: [PATCH 131/142] [Draft] Added Draft Snap Statusbar . --- src/Mod/Draft/InitGui.py | 2 +- .../Draft/draftutils/init_draft_statusbar.py | 209 +++++++++++++++--- 2 files changed, 178 insertions(+), 33 deletions(-) diff --git a/src/Mod/Draft/InitGui.py b/src/Mod/Draft/InitGui.py index 286da11671..4572cc5b69 100644 --- a/src/Mod/Draft/InitGui.py +++ b/src/Mod/Draft/InitGui.py @@ -140,7 +140,7 @@ class DraftWorkbench(FreeCADGui.Workbench): if hasattr(FreeCADGui, "Snapper"): FreeCADGui.Snapper.hide() import draftutils.init_draft_statusbar as dsb - dsb.hide_draft_statusbar() + dsb.hide_draft_statusbar() FreeCAD.Console.PrintLog("Draft workbench deactivated.\n") def ContextMenu(self, recipient): diff --git a/src/Mod/Draft/draftutils/init_draft_statusbar.py b/src/Mod/Draft/draftutils/init_draft_statusbar.py index 1d55d611fd..515ecda1b2 100644 --- a/src/Mod/Draft/draftutils/init_draft_statusbar.py +++ b/src/Mod/Draft/draftutils/init_draft_statusbar.py @@ -32,9 +32,9 @@ This module provide the code for the Draft Statusbar, activated by initGui import FreeCAD as App import FreeCADGui as Gui -from PySide import QtGui +from PySide import QtCore, QtGui from PySide.QtCore import QT_TRANSLATE_NOOP - +from draftutils.init_tools import get_draft_snap_commands #---------------------------------------------------------------------------- # SCALE WIDGET FUNCTIONS @@ -124,7 +124,8 @@ def label_to_scale(label): return scale except: err = QT_TRANSLATE_NOOP("draft", - "Unable to convert input into a scale factor") + "Unable to convert input into a " + "scale factor") App.Console.PrintWarning(err) return None @@ -137,25 +138,34 @@ def _set_scale(action): mw = Gui.getMainWindow() sb = mw.statusBar() - statuswidget = sb.findChild(QtGui.QToolBar,"draft_status_widget") + scale_widget = sb.findChild(QtGui.QToolBar,"draft_status_scale_widget") if action.text() == QT_TRANSLATE_NOOP("draft","custom"): dialog_text = QT_TRANSLATE_NOOP("draft", - "Set custom annotation scale in format x:x, x=x" + "Set custom annotation scale in " + "format x:x, x=x" ) - custom_scale = QtGui.QInputDialog.getText(None, "Set custom scale", dialog_text) + custom_scale = QtGui.QInputDialog.getText(None, "Set custom scale", + dialog_text) if custom_scale[1]: print(custom_scale[0]) scale = label_to_scale(custom_scale[0]) if scale is None: return param.SetFloat("DraftAnnotationScale", scale) cs = scale_to_label(scale) - statuswidget.scaleLabel.setText(cs) + scale_widget.scaleLabel.setText(cs) else: text_scale = action.text() - statuswidget.scaleLabel.setText(text_scale) + scale_widget.scaleLabel.setText(text_scale) scale = label_to_scale(text_scale) param.SetFloat("DraftAnnotationScale", scale) +#---------------------------------------------------------------------------- +# SNAP WIDGET FUNCTIONS +#---------------------------------------------------------------------------- + +def toggle_ortho(): + Gui.runCommand('Draft_Snap_Ortho',0) + #---------------------------------------------------------------------------- # MAIN DRAFT STATUSBAR FUNCTIONS #---------------------------------------------------------------------------- @@ -164,21 +174,131 @@ def init_draft_statusbar(sb): """ this function initializes draft statusbar """ + param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + + # SNAP WIDGET - init ---------------------------------------------------- - statuswidget = QtGui.QToolBar() - statuswidget.setObjectName("draft_status_widget") + snap_widget = QtGui.QToolBar() + snap_widget.setObjectName("draft_snap_widget") + snap_widget.setIconSize(QtCore.QSize(16,16)) + + # GRID BUTTON - init + gridbutton = QtGui.QPushButton(snap_widget) + gridbutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Draft_Grid.svg"))) + gridbutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Toggles Grid On/Off")) + gridbutton.setObjectName("Grid_Statusbutton") + gridbutton.setWhatsThis("Draft_ToggleGrid") + gridbutton.setFlat(True) + QtCore.QObject.connect(gridbutton,QtCore.SIGNAL("clicked()"), + lambda f=Gui.doCommand, + arg='Gui.runCommand("Draft_ToggleGrid")':f(arg)) + snap_widget.addWidget(gridbutton) + + # SNAP BUTTON - init + snappref = param.GetString("snapModes","111111111101111")[0] + snapbutton = QtGui.QPushButton(snap_widget) + snapbutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Snap_Lock.svg"))) + snapbutton.setObjectName("Snap_Statusbutton") + snapbutton.setWhatsThis("Draft_ToggleLockSnap") + snapbutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Object snapping")) + snapbutton.setCheckable(True) + snapbutton.setChecked(bool(int(snappref))) + snapbutton.setFlat(True) + + snaps_menu = QtGui.QMenu(snapbutton) + snaps_menu.setObjectName("draft_statusbar_snap_toolbar") + + snap_gui_commands = get_draft_snap_commands() + if 'Draft_Snap_Ortho' in snap_gui_commands: + snap_gui_commands.remove('Draft_Snap_Ortho') + if 'Draft_Snap_WorkingPlane' in snap_gui_commands: + snap_gui_commands.remove('Draft_Snap_WorkingPlane') + if 'Draft_Snap_Dimensions' in snap_gui_commands: + snap_gui_commands.remove('Draft_Snap_Dimensions') + Gui.Snapper.init_draft_snap_buttons(snap_gui_commands,snaps_menu, "_Statusbutton") + Gui.Snapper.restore_snap_buttons_state(snaps_menu, "_Statusbutton") + + snapbutton.setMenu(snaps_menu) + snap_widget.addWidget(snapbutton) + + + # DIMENSION BUTTON - init + dimpref = param.GetString("snapModes","111111111101111")[13] + dimbutton = QtGui.QPushButton(snap_widget) + dimbutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Snap_Dimensions.svg"))) + dimbutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Toggles Visual Aid Dimensions On/Off")) + dimbutton.setObjectName("Draft_Snap_Dimensions_Statusbutton") + dimbutton.setWhatsThis("Draft_ToggleDimensions") + dimbutton.setFlat(True) + dimbutton.setCheckable(True) + dimbutton.setChecked(bool(int(dimpref))) + QtCore.QObject.connect(dimbutton,QtCore.SIGNAL("clicked()"), + lambda f=Gui.doCommand, + arg='Gui.runCommand("Draft_Snap_Dimensions")':f(arg)) + snap_widget.addWidget(dimbutton) + + # ORTHO BUTTON - init + ortopref = param.GetString("snapModes","111111111101111")[10] + orthobutton = QtGui.QPushButton(snap_widget) + orthobutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Snap_Ortho.svg"))) + orthobutton.setObjectName("Draft_Snap_Ortho"+"_Statusbutton") + orthobutton.setWhatsThis("Draft_ToggleOrtho") + orthobutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Toggles Ortho On/Off")) + orthobutton.setFlat(True) + orthobutton.setCheckable(True) + orthobutton.setChecked(bool(int(ortopref))) + QtCore.QObject.connect(orthobutton,QtCore.SIGNAL("clicked()"), + lambda f=Gui.doCommand, + arg='Gui.runCommand("Draft_Snap_Ortho")':f(arg)) + snap_widget.addWidget(orthobutton) + + # WORKINGPLANE BUTTON - init + wppref = param.GetString("snapModes","111111111101111")[14] + wpbutton = QtGui.QPushButton(snap_widget) + wpbutton.setIcon(QtGui.QIcon.fromTheme("Draft", + QtGui.QIcon(":/icons/" + "Snap_WorkingPlane.svg"))) + wpbutton.setObjectName("Draft_Snap_WorkingPlane_Statusbutton") + wpbutton.setWhatsThis("Draft_ToggleWorkingPlaneSnap") + wpbutton.setToolTip(QT_TRANSLATE_NOOP("Draft", + "Toggles Constrain to Working Plane On/Off")) + wpbutton.setFlat(True) + wpbutton.setCheckable(True) + wpbutton.setChecked(bool(int(wppref))) + QtCore.QObject.connect(wpbutton,QtCore.SIGNAL("clicked()"), + lambda f=Gui.doCommand, + arg='Gui.runCommand("Draft_Snap_WorkingPlane")':f(arg)) + snap_widget.addWidget(wpbutton) + + # add snap widget to the statusbar + sb.insertPermanentWidget(2, snap_widget) + snap_widget.show() + - # SCALE TOOL ------------------------------------------------------------- + # SCALE WIDGET ---------------------------------------------------------- + scale_widget = QtGui.QToolBar() + scale_widget.setObjectName("draft_status_scale_widget") # get scales list according to system units draft_scales = get_scales() # get draft annotation scale - param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") draft_annotation_scale = param.GetFloat("DraftAnnotationScale", 1.0) # initializes scale widget - statuswidget.draft_scales = draft_scales + scale_widget.draft_scales = draft_scales scaleLabel = QtGui.QPushButton("Scale") scaleLabel.setObjectName("ScaleLabel") scaleLabel.setFlat(True) @@ -194,12 +314,12 @@ def init_draft_statusbar(sb): scaleLabel.setText(scale_label) tooltip = "Set the scale used by draft annotation tools" scaleLabel.setToolTip(QT_TRANSLATE_NOOP("draft",tooltip)) - statuswidget.addWidget(scaleLabel) - statuswidget.scaleLabel = scaleLabel + scale_widget.addWidget(scaleLabel) + scale_widget.scaleLabel = scaleLabel - # ADD TOOLS TO STATUS BAR ------------------------------------------------ - sb.addPermanentWidget(statuswidget) - statuswidget.show() + # add scale widget to the statusbar + sb.insertPermanentWidget(3, scale_widget) + scale_widget.show() def show_draft_statusbar(): """ @@ -208,25 +328,50 @@ def show_draft_statusbar(): mw = Gui.getMainWindow() if mw: sb = mw.statusBar() - statuswidget = sb.findChild(QtGui.QToolBar,"draft_status_widget") - if statuswidget: - statuswidget.show() + + scale_widget = sb.findChild(QtGui.QToolBar, + "draft_status_scale_widget") + if scale_widget: + scale_widget.show() else: init_draft_statusbar(sb) + + snap_widget = sb.findChild(QtGui.QToolBar,"draft_snap_widget") + if snap_widget: + snap_widget.show() + else: + init_draft_statusbar(sb) + def hide_draft_statusbar(): """ hides draft statusbar if present """ mw = Gui.getMainWindow() - if mw: - sb = mw.statusBar() - statuswidget = sb.findChild(QtGui.QToolBar,"draft_status_widget") - if statuswidget: - statuswidget.hide() - else: - # when switching workbenches, the toolbar sometimes "jumps" - # out of the status bar to any other dock area... - statuswidget = mw.findChild(QtGui.QToolBar,"draft_status_widget") - if statuswidget: - statuswidget.hide() \ No newline at end of file + if not mw: + return + sb = mw.statusBar() + + # hide scale widget + scale_widget = sb.findChild(QtGui.QToolBar, + "draft_status_scale_widget") + if scale_widget: + scale_widget.hide() + else: + # when switching workbenches, the toolbar sometimes "jumps" + # out of the status bar to any other dock area... + scale_widget = mw.findChild(QtGui.QToolBar, + "draft_status_scale_widget") + if scale_widget: + scale_widget.hide() + + # hide snap widget + snap_widget = sb.findChild(QtGui.QToolBar,"draft_snap_widget") + if snap_widget: + snap_widget.hide() + else: + # when switching workbenches, the toolbar sometimes "jumps" + # out of the status bar to any other dock area... + snap_widget = mw.findChild(QtGui.QToolBar,"draft_snap_widget") + if snap_widget: + snap_widget.hide() \ No newline at end of file From 480216b25af85ff039d0a8e370dec6c45c655a13 Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 21 Mar 2020 09:31:24 +0100 Subject: [PATCH 132/142] [Draft] rough implementation of new preference dialog for interface --- .../ui/preferences-draftinterface.ui | 1122 +++++++++++++++++ 1 file changed, 1122 insertions(+) create mode 100644 src/Mod/Draft/Resources/ui/preferences-draftinterface.ui diff --git a/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui b/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui new file mode 100644 index 0000000000..1d2e2522dc --- /dev/null +++ b/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui @@ -0,0 +1,1122 @@ + + + Gui::Dialog::DlgSettingsDraft + + + + 0 + 0 + 510 + 397 + + + + General settings + + + + 6 + + + 9 + + + + + In-Command Shortcuts + + + + + + + + + + Relative + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + R + + + 1 + + + + + + false + + + inCommandShortcutRelative + + + Mod/Draft + + + + + + + + + + + + + Continue + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + T + + + 1 + + + + + + false + + + inCommandShortcutContinue + + + Mod/Draft + + + + + + + + + + + + + Close + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + O + + + 1 + + + + + + false + + + inCommandShortcutClose + + + Mod/Draft + + + + + + + + + + + + + + + Copy + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + P + + + 1 + + + + + + false + + + inCommandShortcutCopy + + + Mod/Draft + + + + + + + + + + + Subelement Mode + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + D + + + 1 + + + + + + false + + + inCommandShortcutSubelementMode + + + Mod/Draft + + + + + + + + + + + Fill + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + L + + + 1 + + + + + + false + + + inCommandShortcutFill + + + Mod/Draft + + + + + + + + + + + + + + + Exit + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + A + + + 1 + + + + + + false + + + inCommandShortcutExit + + + Mod/Draft + + + + + + + + + + + Select Edge + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + E + + + 1 + + + + + + false + + + inCommandShortcutSelectEdge + + + Mod/Draft + + + + + + + + + + + Add Hold + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + Q + + + 1 + + + + + + false + + + inCommandShortcutAddHold + + + Mod/Draft + + + + + + + + + + + + + + + Length + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + H + + + 1 + + + + + + false + + + inCommandShortcutLength + + + Mod/Draft + + + + + + + + + + + Wipe + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + W + + + 1 + + + + + + false + + + inCommandShortcutWipe + + + Mod/Draft + + + + + + + + + + + Set WP + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + U + + + 1 + + + + + + false + + + inCommandShortcutSetWP + + + Mod/Draft + + + + + + + + + + + + + + + Cycle Snap + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + ` + + + 1 + + + + + + false + + + inCommandShortcutCycleSnap + + + Mod/Draft + + + + + + + + + + + + + + + + + + + + + Snap + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + S + + + 1 + + + + + + false + + + inCommandShortcutSnap + + + Mod/Draft + + + + + + + + + + + Increase Radius + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + [ + + + 1 + + + + + + false + + + inCommandShortcutIncreaseRadius + + + Mod/Draft + + + + + + + + + + + Decrease Radius + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + ] + + + 1 + + + + + + false + + + inCommandShortcutDecreaseRadius + + + Mod/Draft + + + + + + + + + + + + + + + Restrict X + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + X + + + 1 + + + + + + false + + + inCommandShortcutRestrictX + + + Mod/Draft + + + + + + + + + + + Restrict Y + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + Y + + + 1 + + + + + + false + + + inCommandShortcutRestrictY + + + Mod/Draft + + + + + + + + + + + Restrict Z + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + Z + + + 1 + + + + + + false + + + RestrictZ + + + Mod/Draft + + + + + + + + + + + + + + + 0 + 0 + + + + Draft Statusbar + + + true + + + + + + + + + + Normally, after copying objects, the copies get selected. If this option is checked, the base objects will be selected instead. + + + Draft snap widget + + + selectBaseObjects + + + Mod/Draft + + + + + + + + + + + When this is checked, the Draft tools will create Part primitives instead of Draft objects, when available. + + + Annotation scale widget + + + UsePartPrimitives + + + Mod/Draft + + + + + + + + + + + + + + + + + Mouse navigation mode + + + true + + + focusOnLength + + + Mod/Draft + + + + + + + + + If this is checked, objects will appear as filled by default. Otherwise, they will appear as wireframe + + + 3D View size + + + true + + + fillmode + + + Mod/Draft + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + qPixmapFromMimeSource + + + Gui::PrefCheckBox + QCheckBox +
    Gui/PrefWidgets.h
    +
    + + Gui::PrefLineEdit + QLineEdit +
    Gui/PrefWidgets.h
    +
    +
    + + +
    From be75f4febc5fd2d145485350077538f12e15d69a Mon Sep 17 00:00:00 2001 From: carlopav Date: Wed, 25 Mar 2020 09:00:19 +0100 Subject: [PATCH 133/142] [Draft] Statusbar widgets, preferences to disable draft statusba . . . . --- src/Mod/Draft/InitGui.py | 1 + src/Mod/Draft/Resources/Draft.qrc | 1 + .../Draft/Resources/ui/preferences-draft.ui | 1205 ++--------- .../ui/preferences-draftinterface.ui | 1885 ++++++++--------- .../Draft/draftutils/init_draft_statusbar.py | 32 +- 5 files changed, 1018 insertions(+), 2106 deletions(-) diff --git a/src/Mod/Draft/InitGui.py b/src/Mod/Draft/InitGui.py index 4572cc5b69..66330e4c3a 100644 --- a/src/Mod/Draft/InitGui.py +++ b/src/Mod/Draft/InitGui.py @@ -116,6 +116,7 @@ class DraftWorkbench(FreeCADGui.Workbench): if hasattr(FreeCADGui, "draftToolBar"): if not hasattr(FreeCADGui.draftToolBar, "loadedPreferences"): FreeCADGui.addPreferencePage(":/ui/preferences-draft.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) + FreeCADGui.addPreferencePage(":/ui/preferences-draftinterface.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) FreeCADGui.addPreferencePage(":/ui/preferences-draftsnap.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) FreeCADGui.addPreferencePage(":/ui/preferences-draftvisual.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) FreeCADGui.addPreferencePage(":/ui/preferences-drafttexts.ui", QT_TRANSLATE_NOOP("Draft", "Draft")) diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index 5467200ade..c4096b88f3 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -153,6 +153,7 @@ translations/Draft_zh-CN.qm translations/Draft_zh-TW.qm ui/preferences-draft.ui + ui/preferences-draftinterface.ui ui/preferences-draftsnap.ui ui/preferences-drafttexts.ui ui/preferences-draftvisual.ui diff --git a/src/Mod/Draft/Resources/ui/preferences-draft.ui b/src/Mod/Draft/Resources/ui/preferences-draft.ui index 3a41695f63..005858b37e 100644 --- a/src/Mod/Draft/Resources/ui/preferences-draft.ui +++ b/src/Mod/Draft/Resources/ui/preferences-draft.ui @@ -6,8 +6,8 @@ 0 0 - 584 - 881 + 500 + 560 @@ -17,16 +17,7 @@ 6 - - 9 - - - 9 - - - 9 - - + 9 @@ -41,41 +32,6 @@ General Draft Settings - - - - If this is checked, copy mode will be kept across command, otherwise commands will always start in no-copy mode - - - Global copy mode - - - false - - - copymode - - - Mod/Draft - - - - - - - Normally, after copying objects, the copies get selected. If this option is checked, the base objects will be selected instead. - - - Select base objects after copying - - - selectBaseObjects - - - Mod/Draft - - - @@ -233,68 +189,149 @@ Values with differences below this value will be treated as same. This value wil - + - When this is checked, the Draft tools will create Part primitives instead of Draft objects, when available. + If this option is checked, the layers drop-down list will also show groups, allowing you to automatically add objects to groups too. - Use Part Primitives when available - - - UsePartPrimitives - - - Mod/Draft - - - - - - - If this is checked, objects will appear as filled by default. Otherwise, they will appear as wireframe - - - Fill objects with faces whenever possible + Show groups in layers list drop-down button - true + false - fillmode + AutogroupAddGroups Mod/Draft + +
    +
    + + + + Draft tools options + + + + 9 + - - - When drawing lines, set focus on Length instead of X coordinate + + + 0 - - focusOnLength - - - Mod/Draft - - - - - - - If this option is set, when creating Draft objects on top of an existing face of another object, the "Support" property of the Draft object will be set to the base object. This was the standard behaviour before FreeCAD 0.19 - - - Set the Support property when possible - - - useSupport - - - Mod/Draft - - + + + + When drawing lines, set focus on Length instead of X coordinate. +This allows to point the direction and type the distance. + + + Set focus on Length instead of X coordinate + + + focusOnLength + + + Mod/Draft + + + + + + + If this option is set, when creating Draft objects on top of an existing face of another object, the "Support" property of the Draft object will be set to the base object. This was the standard behaviour before FreeCAD 0.19 + + + Set the Support property when possible + + + useSupport + + + Mod/Draft + + + + + + + If this is checked, objects will appear as filled by default. +Otherwise, they will appear as wireframe + + + Fill objects with faces whenever possible + + + true + + + fillmode + + + Mod/Draft + + + + + + + Normally, after copying objects, the copies get selected. +If this option is checked, the base objects will be selected instead. + + + Select base objects after copying + + + selectBaseObjects + + + Mod/Draft + + + + + + + If this is checked, copy mode will be kept across command, +otherwise commands will always start in no-copy mode + + + Global copy mode + + + false + + + copymode + + + Mod/Draft + + + + + + + Force Draft Tools to create Part primitives instead of Draft objects. +Note that this is not fully supported, and many object will be not editable with Draft Modifiers. + + + Use Part Primitives when available + + + UsePartPrimitives + + + Mod/Draft + + + + @@ -320,25 +357,6 @@ Values with differences below this value will be treated as same. This value wil
    - - - - If this option is checked, the layers drop-down list will also show groups, allowing you to automatically add objects to groups too. - - - Show groups in layers list drop-down button - - - false - - - AutogroupAddGroups - - - Mod/Draft - - -
    @@ -408,7 +426,7 @@ Values with differences below this value will be treated as same. This value wil This is the default color for objects being drawn while in construction mode. - + 44 125 @@ -428,965 +446,6 @@ Values with differences below this value will be treated as same. This value wil
    - - - - In-Command Shortcuts - - - - - - - - - - Relative - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - R - - - 1 - - - - - - false - - - inCommandShortcutRelative - - - Mod/Draft - - - - - - - - - - - - - Continue - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - T - - - 1 - - - - - - false - - - inCommandShortcutContinue - - - Mod/Draft - - - - - - - - - - - - - Close - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - O - - - 1 - - - - - - false - - - inCommandShortcutClose - - - Mod/Draft - - - - - - - - - - - - - - - Copy - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - P - - - 1 - - - - - - false - - - inCommandShortcutCopy - - - Mod/Draft - - - - - - - - - - - Subelement Mode - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - D - - - 1 - - - - - - false - - - inCommandShortcutSubelementMode - - - Mod/Draft - - - - - - - - - - - Fill - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - L - - - 1 - - - - - - false - - - inCommandShortcutFill - - - Mod/Draft - - - - - - - - - - - - - - - Exit - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - A - - - 1 - - - - - - false - - - inCommandShortcutExit - - - Mod/Draft - - - - - - - - - - - Select Edge - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - E - - - 1 - - - - - - false - - - inCommandShortcutSelectEdge - - - Mod/Draft - - - - - - - - - - - Add Hold - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - Q - - - 1 - - - - - - false - - - inCommandShortcutAddHold - - - Mod/Draft - - - - - - - - - - - - - - - Length - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - H - - - 1 - - - - - - false - - - inCommandShortcutLength - - - Mod/Draft - - - - - - - - - - - Wipe - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - W - - - 1 - - - - - - false - - - inCommandShortcutWipe - - - Mod/Draft - - - - - - - - - - - Set WP - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - U - - - 1 - - - - - - false - - - inCommandShortcutSetWP - - - Mod/Draft - - - - - - - - - - - - - - - Cycle Snap - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - ` - - - 1 - - - - - - false - - - inCommandShortcutCycleSnap - - - Mod/Draft - - - - - - - - - - - - - - - - - - - - - Snap - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - S - - - 1 - - - - - - false - - - inCommandShortcutSnap - - - Mod/Draft - - - - - - - - - - - Increase Radius - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - [ - - - 1 - - - - - - false - - - inCommandShortcutIncreaseRadius - - - Mod/Draft - - - - - - - - - - - Decrease Radius - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - ] - - - 1 - - - - - - false - - - inCommandShortcutDecreaseRadius - - - Mod/Draft - - - - - - - - - - - - - - - Restrict X - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - X - - - 1 - - - - - - false - - - inCommandShortcutRestrictX - - - Mod/Draft - - - - - - - - - - - Restrict Y - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - Y - - - 1 - - - - - - false - - - inCommandShortcutRestrictY - - - Mod/Draft - - - - - - - - - - - Restrict Z - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - Z - - - 1 - - - - - - false - - - RestrictZ - - - Mod/Draft - - - - - - - - - - diff --git a/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui b/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui index 1d2e2522dc..7f231e4ce7 100644 --- a/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui +++ b/src/Mod/Draft/Resources/ui/preferences-draftinterface.ui @@ -6,12 +6,12 @@ 0 0 - 510 - 397 + 456 + 338 - General settings + User interface settings @@ -25,954 +25,847 @@ In-Command Shortcuts - + - - - - - - - Relative - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - R - - - 1 - - - - - - false - - - inCommandShortcutRelative - - - Mod/Draft - - - - + + + 0 + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + ` + + + 1 + + + + + + false + + + inCommandShortcutCycleSnap + + + Mod/Draft + + - - - - - - - - Continue - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - T - - - 1 - - - - - - false - - - inCommandShortcutContinue - - - Mod/Draft - - - - - - + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + S + + + 1 + + + + + + false + + + inCommandShortcutSnap + + + Mod/Draft + + - - - - - - Close - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - O - - - 1 - - - - - - false - - - inCommandShortcutClose - - - Mod/Draft - - - - + + + + Close + + - - - - - - - - - - Copy - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - P - - - 1 - - - - - - false - - - inCommandShortcutCopy - - - Mod/Draft - - - - + + + + Relative + + - - - - - - Subelement Mode - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - D - - - 1 - - - - - - false - - - inCommandShortcutSubelementMode - - - Mod/Draft - - - - + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + R + + + 1 + + + + + + false + + + inCommandShortcutRelative + + + Mod/Draft + + - - - - - - Fill - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - L - - - 1 - - - - - - false - - - inCommandShortcutFill - - - Mod/Draft - - - - + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + O + + + 1 + + + + + + false + + + inCommandShortcutClose + + + Mod/Draft + + - - - - - - - - - - Exit - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - A - - - 1 - - - - - - false - - - inCommandShortcutExit - - - Mod/Draft - - - - + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + T + + + 1 + + + + + + false + + + inCommandShortcutContinue + + + Mod/Draft + + - - - - - - Select Edge - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - E - - - 1 - - - - - - false - - - inCommandShortcutSelectEdge - - - Mod/Draft - - - - + + + + Continue + + - - - - - - Add Hold - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - Q - - - 1 - - - - - - false - - - inCommandShortcutAddHold - - - Mod/Draft - - - - + + + + Copy + + - - - - - - - - - - Length - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - H - - - 1 - - - - - - false - - - inCommandShortcutLength - - - Mod/Draft - - - - + + + + Increase Radius + + - - - - - - Wipe - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - W - - - 1 - - - - - - false - - - inCommandShortcutWipe - - - Mod/Draft - - - - + + + + Cycle Snap + + - - - - - - Set WP - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - U - - - 1 - - - - - - false - - - inCommandShortcutSetWP - - - Mod/Draft - - - - + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + [ + + + 1 + + + + + + false + + + inCommandShortcutIncreaseRadius + + + Mod/Draft + + - - - - - - - - - - Cycle Snap - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - ` - - - 1 - - - - - - false - - - inCommandShortcutCycleSnap - - - Mod/Draft - - - - + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + ] + + + 1 + + + + + + false + + + inCommandShortcutDecreaseRadius + + + Mod/Draft + + - - + + + + Snap + + - - + + + + Decrease Radius + + - - - - - - - - - - Snap - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - S - - - 1 - - - - - - false - - - inCommandShortcutSnap - - - Mod/Draft - - - - + + + + Length + + - - - - - - Increase Radius - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - [ - - - 1 - - - - - - false - - - inCommandShortcutIncreaseRadius - - - Mod/Draft - - - - + + + + Wipe + + - - - - - - Decrease Radius - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - ] - - - 1 - - - - - - false - - - inCommandShortcutDecreaseRadius - - - Mod/Draft - - - - + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + D + + + 1 + + + + + + false + + + inCommandShortcutSubelementMode + + + Mod/Draft + + - - - - - - - - - - Restrict X - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - X - - - 1 - - - - - - false - - - inCommandShortcutRestrictX - - - Mod/Draft - - - - + + + + Add Hold + + - - - - - - Restrict Y - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - Y - - - 1 - - - - - - false - - - inCommandShortcutRestrictY - - - Mod/Draft - - - - + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + L + + + 1 + + + + + + false + + + inCommandShortcutFill + + + Mod/Draft + + - - - - - - Restrict Z - - - - - - - true - - - - 0 - 0 - - - - - 25 - 16777215 - - - - Z - - - 1 - - - - - - false - - - RestrictZ - - - Mod/Draft - - - - + + + + Exit + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + P + + + 1 + + + + + + false + + + inCommandShortcutCopy + + + Mod/Draft + + + + + + + Fill + + + + + + + Subelement Mode + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + E + + + 1 + + + + + + false + + + inCommandShortcutSelectEdge + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + H + + + 1 + + + + + + false + + + inCommandShortcutLength + + + Mod/Draft + + + + + + + Select Edge + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + W + + + 1 + + + + + + false + + + inCommandShortcutWipe + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + A + + + 1 + + + + + + false + + + inCommandShortcutExit + + + Mod/Draft + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + Q + + + 1 + + + + + + false + + + inCommandShortcutAddHold + + + Mod/Draft + + + + + + + Set WP + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + U + + + 1 + + + + + + false + + + inCommandShortcutSetWP + + + Mod/Draft + + + + + + + Restrict X + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + X + + + 1 + + + + + + false + + + inCommandShortcutRestrictX + + + Mod/Draft + + + + + + + Restrict Y + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + Y + + + 1 + + + + + + false + + + inCommandShortcutRestrictY + + + Mod/Draft + + + + + + + Restrict Z + + + + + + + true + + + + 0 + 0 + + + + + 25 + 16777215 + + + + Z + + + 1 + + + + + + false + + + RestrictZ + + + Mod/Draft + + @@ -987,101 +880,61 @@ 0 + + Enable draft statusbar customization + Draft Statusbar true + + DisplayStatusbar + + + Mod/Draft + - - - - - - - Normally, after copying objects, the copies get selected. If this option is checked, the base objects will be selected instead. - - - Draft snap widget - - - selectBaseObjects - - - Mod/Draft - - - - + + + 0 + + + + + Enable snap statusbar widget + + + Draft snap widget + + + true + + + DisplayStatusbarSnapWidget + + + Mod/Draft + + - - - - - - When this is checked, the Draft tools will create Part primitives instead of Draft objects, when available. - - - Annotation scale widget - - - UsePartPrimitives - - - Mod/Draft - - - - - - - - - - - - - - - - - Mouse navigation mode - - - true - - - focusOnLength - - - Mod/Draft - - - - - - - - - If this is checked, objects will appear as filled by default. Otherwise, they will appear as wireframe - - - 3D View size - - - true - - - fillmode - - - Mod/Draft - - - - + + + + Enable draft statusbar annotation scale widget + + + Annotation scale widget + + + DisplayStatusbarScaleWidget + + + Mod/Draft + + diff --git a/src/Mod/Draft/draftutils/init_draft_statusbar.py b/src/Mod/Draft/draftutils/init_draft_statusbar.py index 515ecda1b2..3373ac713f 100644 --- a/src/Mod/Draft/draftutils/init_draft_statusbar.py +++ b/src/Mod/Draft/draftutils/init_draft_statusbar.py @@ -1,11 +1,3 @@ -"""Draft Statusbar commands. - -This module provide the code for the Draft Statusbar, activated by initGui -""" -## @package init_draft_statusbar -# \ingroup DRAFT -# \brief This module provides the code for the Draft Statusbar. - # *************************************************************************** # * * # * Copyright (c) 2020 Carlo Pavan * @@ -29,6 +21,13 @@ This module provide the code for the Draft Statusbar, activated by initGui # * USA * # * * # *************************************************************************** +"""Draft Statusbar commands. + +This module provide the code for the Draft Statusbar, activated by initGui +""" +## @package init_draft_statusbar +# \ingroup DRAFT +# \brief This module provides the code for the Draft Statusbar. import FreeCAD as App import FreeCADGui as Gui @@ -159,13 +158,6 @@ def _set_scale(action): scale = label_to_scale(text_scale) param.SetFloat("DraftAnnotationScale", scale) -#---------------------------------------------------------------------------- -# SNAP WIDGET FUNCTIONS -#---------------------------------------------------------------------------- - -def toggle_ortho(): - Gui.runCommand('Draft_Snap_Ortho',0) - #---------------------------------------------------------------------------- # MAIN DRAFT STATUSBAR FUNCTIONS #---------------------------------------------------------------------------- @@ -325,6 +317,12 @@ def show_draft_statusbar(): """ shows draft statusbar if present or initializes it """ + params = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + display_statusbar = params.GetBool("DisplayStatusbar", True) + + if not display_statusbar: + return + mw = Gui.getMainWindow() if mw: sb = mw.statusBar() @@ -333,13 +331,13 @@ def show_draft_statusbar(): "draft_status_scale_widget") if scale_widget: scale_widget.show() - else: + elif params.GetBool("DisplayStatusbarScaleWidget", True): init_draft_statusbar(sb) snap_widget = sb.findChild(QtGui.QToolBar,"draft_snap_widget") if snap_widget: snap_widget.show() - else: + elif params.GetBool("DisplayStatusbarSnapWidget", True): init_draft_statusbar(sb) From 64db7219984ba9d0d27e7f5734006dc9e7984926 Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 18 Apr 2020 11:39:48 +0200 Subject: [PATCH 134/142] [Draft] Snap improvement and statusbar cleanup after rebase [Draft] Attempt to make base classes compatible with super on py2 [Draft] Refactored imports of snapper and snaps gui tools . . --- src/Mod/Draft/draftguitools/gui_base.py | 97 +------- src/Mod/Draft/draftguitools/gui_snapper.py | 94 +++++++- src/Mod/Draft/draftguitools/gui_snaps.py | 254 ++++++++++++--------- 3 files changed, 236 insertions(+), 209 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_base.py b/src/Mod/Draft/draftguitools/gui_base.py index d3f072b7d3..86cc109d88 100644 --- a/src/Mod/Draft/draftguitools/gui_base.py +++ b/src/Mod/Draft/draftguitools/gui_base.py @@ -33,7 +33,7 @@ import draftutils.todo as todo from draftutils.messages import _msg, _log -class GuiCommandSimplest: +class GuiCommandSimplest(object): """Simplest base class for GuiCommands. This class only sets up the command name and the document object @@ -126,100 +126,7 @@ class GuiCommandNeedsSelection(GuiCommandSimplest): return False -class GuiCommandSimplest: - """Simplest base class for GuiCommands. - - This class only sets up the command name and the document object - to use for the command. - When it is executed, it logs the command name to the log file, - and prints the command name to the console. - - It implements the `IsActive` method, which must return `True` - when the command should be available. - It should return `True` when there is an active document, - otherwise the command (button or menu) should be disabled. - - This class is meant to be inherited by other GuiCommand classes - to quickly log the command name, and set the correct document object. - - Parameter - --------- - name: str, optional - It defaults to `'None'`. - The name of the action that is being run, - for example, `'Heal'`, `'Flip dimensions'`, - `'Line'`, `'Circle'`, etc. - - doc: App::Document, optional - It defaults to the value of `App.activeDocument()`. - The document object itself, which indicates where the actions - of the command will be executed. - - Attributes - ---------- - command_name: str - This is the command name, which is assigned by `name`. - - doc: App::Document - This is the document object itself, which is assigned by `doc`. - - This attribute should be used by functions to make sure - that the operations are performed in the correct document - and not in other documents. - To set the active document we can use - - >>> App.setActiveDocument(self.doc.Name) - """ - - def __init__(self, name="None", doc=App.activeDocument()): - self.command_name = name - self.doc = doc - - def IsActive(self): - """Return True when this command should be available. - - It is `True` when there is a document. - """ - if App.activeDocument(): - return True - else: - return False - - def Activated(self): - """Execute when the command is called. - - Log the command name to the log file and console. - Also update the `doc` attribute. - """ - self.doc = App.activeDocument() - _log("Document: {}".format(self.doc.Label)) - _log("GuiCommand: {}".format(self.command_name)) - _msg("{}".format(16*"-")) - _msg("GuiCommand: {}".format(self.command_name)) - - -class GuiCommandNeedsSelection(GuiCommandSimplest): - """Base class for GuiCommands that need a selection to be available. - - It re-implements the `IsActive` method to return `True` - when there is both an active document and an active selection. - - It inherits `GuiCommandSimplest` to set up the document - and other behavior. See this class for more information. - """ - - def IsActive(self): - """Return True when this command should be available. - - It is `True` when there is a selection. - """ - if App.activeDocument() and Gui.Selection.getSelection(): - return True - else: - return False - - -class GuiCommandBase: +class GuiCommandBase(object): """Generic class that is the basis of all Gui commands. This class should eventually replace `DraftTools.DraftTool`, diff --git a/src/Mod/Draft/draftguitools/gui_snapper.py b/src/Mod/Draft/draftguitools/gui_snapper.py index 542acf76d8..7e680899d4 100644 --- a/src/Mod/Draft/draftguitools/gui_snapper.py +++ b/src/Mod/Draft/draftguitools/gui_snapper.py @@ -32,26 +32,33 @@ defined by `gui_trackers.gridTracker`. # This module provides tools to handle point snapping and # everything that goes with it (toolbar buttons, cursor icons, etc.). +import FreeCAD as App +import FreeCADGui as Gui + +from pivy import coin +from PySide import QtCore, QtGui + import collections as coll import inspect import itertools import math -from pivy import coin -from PySide import QtCore, QtGui -import FreeCAD as App -import FreeCADGui as Gui import Draft import DraftVecUtils -from FreeCAD import Vector +import DraftGeomUtils + +import Part + import draftguitools.gui_trackers as trackers from draftutils.init_tools import get_draft_snap_commands from draftutils.messages import _msg, _wrn + __title__ = "FreeCAD Draft Snap tools" __author__ = "Yorik van Havre" __url__ = "https://www.freecadweb.org" + class Snapper: """Classes to manage snapping in Draft and Arch. @@ -75,6 +82,7 @@ class Snapper: """ def __init__(self): + self.activeview = None self.lastObj = [None, None] self.maxEdges = 0 @@ -180,6 +188,7 @@ class Snapper: ('intersection', ':/icons/Snap_Intersection.svg'), ('special', ':/icons/Snap_Special.svg')]) + def init_active_snaps(self): """ set self.active_snaps according to user prefs @@ -193,6 +202,7 @@ class Snapper: self.active_snaps.append(self.snaps[i]) i += 1 + def cstr(self, lastpoint, constrain, point): """Return constraints if needed.""" if constrain or self.mask: @@ -204,6 +214,7 @@ class Snapper: self.radiusTracker.update(fpt) return fpt + def snap(self, screenpos, lastpoint=None, active=True, constrain=False, noTracker=False): @@ -225,8 +236,6 @@ class Snapper: self.running = True - global Part, DraftGeomUtils - import Part, DraftGeomUtils self.spoint = None if not hasattr(self, "toolbar"): @@ -332,10 +341,12 @@ class Snapper: self.running = False return fp + def cycleSnapObject(self): """Increse the index of the snap object by one.""" self.snapObjectIndex = self.snapObjectIndex + 1 + def snapToObject(self, lastpoint, active, constrain, eline, point, oldActive): """Snap to an object.""" @@ -513,6 +524,7 @@ class Snapper: self.running = False return self.spoint + def toWP(self, point): """Project the given point on the working plane, if needed.""" if self.isEnabled("WorkingPlane"): @@ -520,6 +532,7 @@ class Snapper: return App.DraftWorkingPlane.projectPoint(point) return point + def getApparentPoint(self, x, y): """Return a 3D point, projected on the current working plane.""" view = Draft.get3DView() @@ -535,6 +548,7 @@ class Snapper: return App.DraftWorkingPlane.projectPoint(pt, dv) return pt + def snapToDim(self, obj): snaps = [] if obj.ViewObject: @@ -543,6 +557,7 @@ class Snapper: snaps.append([obj.ViewObject.Proxy.p3, 'endpoint', self.toWP(obj.ViewObject.Proxy.p3)]) return snaps + def snapToExtensions(self, point, last, constrain, eline): """Return a point snapped to extension or parallel line. @@ -652,6 +667,7 @@ class Snapper: return np,de return point,eline + def snapToCrossExtensions(self, point): """Snap to the intersection of the last 2 extension lines.""" if self.isEnabled('Extension'): @@ -682,6 +698,7 @@ class Snapper: return p return None + def snapToPolar(self,point,last): """Snap to polar lines from the given point.""" if self.isEnabled('Ortho') and (not self.mask): @@ -721,6 +738,7 @@ class Snapper: return np,de return point, None + def snapToGrid(self, point): """Return a grid snap point if available.""" if self.grid: @@ -738,6 +756,7 @@ class Snapper: return np return point + def snapToEndpoints(self, shape): """Return a list of endpoints snap locations.""" snaps = [] @@ -756,6 +775,7 @@ class Snapper: snaps.append([v, 'endpoint', self.toWP(v)]) return snaps + def snapToMidpoint(self, shape): """Return a list of midpoints snap locations.""" snaps = [] @@ -766,6 +786,7 @@ class Snapper: snaps.append([mp, 'midpoint', self.toWP(mp)]) return snaps + def snapToPerpendicular(self, shape, last): """Return a list of perpendicular snap locations.""" snaps = [] @@ -789,6 +810,7 @@ class Snapper: snaps.append([np, 'perpendicular', self.toWP(np)]) return snaps + def snapToOrtho(self, shape, last, constrain): """Return a list of ortho snap locations.""" snaps = [] @@ -806,6 +828,7 @@ class Snapper: snaps.append([p, 'ortho', self.toWP(p)]) return snaps + def snapToExtOrtho(self, last, constrain, eline): """Return an ortho X extension snap location.""" if self.isEnabled("Extension") and self.isEnabled("Ortho"): @@ -827,6 +850,7 @@ class Snapper: return None return None + def snapToHold(self, point): """Return a snap location that is orthogonal to hold points. @@ -877,6 +901,7 @@ class Snapper: return [p, 'extension', fp] return None + def snapToExtPerpendicular(self, last): """Return a perpendicular X extension snap location.""" if self.isEnabled("Extension") and self.isEnabled("Perpendicular"): @@ -887,6 +912,7 @@ class Snapper: return [np, 'perpendicular', np] return None + def snapToElines(self, e1, e2): """Return a snap at the infinite intersection of the given edges.""" snaps = [] @@ -899,6 +925,7 @@ class Snapper: snaps.append([p, 'intersection', self.toWP(p)]) return snaps + def snapToAngles(self, shape): """Return a list of angle snap locations.""" snaps = [] @@ -916,6 +943,7 @@ class Snapper: snaps.append([cur, 'angle', self.toWP(cur)]) return snaps + def snapToCenter(self, shape): """Return a list of center snap locations.""" snaps = [] @@ -929,14 +957,15 @@ class Snapper: 195, 217.5, 232.5, 255, 285, 307.5, 322.5, 345): ang = math.radians(i) - cur = Vector(math.sin(ang) * rad + pos.x, - math.cos(ang) * rad + pos.y, - pos.z) + cur = App.Vector(math.sin(ang) * rad + pos.x, + math.cos(ang) * rad + pos.y, + pos.z) snaps.append([cur, 'center', c]) else: snaps.append([c, 'center', c]) return snaps + def snapToFace(self, shape): """Return a face center snap location.""" snaps = [] @@ -946,6 +975,7 @@ class Snapper: snaps.append([pos, 'center', c]) return snaps + def snapToIntersection(self, shape): """Return a list of intersection snap locations.""" snaps = [] @@ -978,6 +1008,7 @@ class Snapper: # when trying to read their types return snaps + def snapToPolygon(self, obj): """Return a list of polygon center snap locations.""" snaps = [] @@ -993,6 +1024,19 @@ class Snapper: return snaps + def snapToVertex(self,info,active=False): + p = App.Vector(info['x'],info['y'],info['z']) + if active: + if self.isEnabled("Near"): + return [p,'endpoint',self.toWP(p)] + else: + return [] + elif self.isEnabled("Near"): + return [p,'passive',p] + else: + return [] + + def snapToSpecials(self, obj, lastpoint=None, eline=None): """Return special snap locations, if any.""" snaps = [] @@ -1028,6 +1072,7 @@ class Snapper: return snaps + def getScreenDist(self, dist, cursor): """Return a distance in 3D space from a screen pixels distance.""" view = Draft.get3DView() @@ -1035,6 +1080,7 @@ class Snapper: p2 = view.getPoint((cursor[0] + dist, cursor[1])) return (p2.sub(p1)).Length + def getPerpendicular(self, edge, pt): """Return a point on an edge, perpendicular to the given point.""" dv = pt.sub(edge.Vertexes[0].Point) @@ -1042,6 +1088,7 @@ class Snapper: np = (edge.Vertexes[0].Point).add(nv) return np + def setArchDims(self, p1, p2): """Show arc dimensions between 2 points.""" if self.isEnabled("Dimensions"): @@ -1058,6 +1105,7 @@ class Snapper: if self.dim2.Distance: self.dim2.on() + def setCursor(self, mode=None): """Set or reset the cursor to the given mode or resets.""" if self.selectMode: @@ -1091,11 +1139,13 @@ class Snapper: w.setCursor(cur) self.cursorMode = mode + def restack(self): """Lower the grid tracker so it doesn't obscure other objects.""" if self.grid: self.grid.lowerTracker() + def off(self, hideSnapBar=False): """Finish snapping.""" if self.tracker: @@ -1129,6 +1179,7 @@ class Snapper: self.running = False self.holdPoints = [] + def setSelectMode(self, mode): """Set the snapper into select mode (hides snapping temporarily).""" self.selectMode = mode @@ -1138,6 +1189,7 @@ class Snapper: if self.trackLine: self.trackLine.off() + def setAngle(self, delta=None): """Keep the current angle.""" if delta: @@ -1148,6 +1200,7 @@ class Snapper: if self.trackLine.Visible: self.mask = self.trackLine.p2().sub(self.trackLine.p1()) + def constrain(self, point, basepoint=None, axis=None): """Return a constrained point. @@ -1222,6 +1275,7 @@ class Snapper: return npoint + def unconstrain(self): """Unset the basepoint and the constrain line.""" self.basepoint = None @@ -1229,6 +1283,7 @@ class Snapper: if self.constrainLine: self.constrainLine.off() + def getPoint(self, last=None, callback=None, movecallback=None, extradlg=None, title=None, mode="point"): """Get a 3D point from the screen. @@ -1289,6 +1344,7 @@ class Snapper: if movecallback: movecallback(self.pt, self.snapInfo) + def getcoords(point, relative=False): """Get the global coordinates from a point.""" self.pt = point @@ -1297,12 +1353,14 @@ class Snapper: self.pt = last.add(v) accept() + def click(event_cb): event = event_cb.getEvent() if event.getButton() == 1: if event.getState() == coin.SoMouseButtonEvent.DOWN: accept() + def accept(): if self.callbackClick: self.view.removeEventCallbackPivy(coin.SoMouseButtonEvent.getClassTypeId(), self.callbackClick) @@ -1320,6 +1378,7 @@ class Snapper: callback(self.pt) self.pt = None + def cancel(): if self.callbackClick: self.view.removeEventCallbackPivy(coin.SoMouseButtonEvent.getClassTypeId(), self.callbackClick) @@ -1351,6 +1410,7 @@ class Snapper: self.callbackClick = self.view.addEventCallbackPivy(coin.SoMouseButtonEvent.getClassTypeId(),click) self.callbackMove = self.view.addEventCallbackPivy(coin.SoLocation2Event.getClassTypeId(),move) + def makeSnapToolBar(self): """Build the Snap toolbar.""" mw = Gui.getMainWindow() @@ -1366,6 +1426,7 @@ class Snapper: if not Draft.getParam("showSnapBar",True): self.toolbar.hide() + def init_draft_snap_buttons(self, commands, context, button_suffix): """ Init Draft Snap toolbar buttons. @@ -1399,6 +1460,7 @@ class Snapper: if len(b.statusTip()) == 0: b.setStatusTip(b.toolTip()) + def restore_snap_buttons_state(self, toolbar, button_suffix): """ Restore toolbar button's checked state according to @@ -1425,6 +1487,7 @@ class Snapper: else: a.setToolTip(a.toolTip()+" (OFF)") + def get_snap_toolbar(self): """retuns snap toolbar object""" mw = Gui.getMainWindow() @@ -1434,10 +1497,12 @@ class Snapper: return toolbar return None + def toggleGrid(self): "toggle FreeCAD Draft Grid" Gui.runCommand("Draft_ToggleGrid") + def showradius(self): """Show the snap radius indicator.""" self.radius = self.getScreenDist(Draft.getParam("snapRange", 8), @@ -1446,6 +1511,7 @@ class Snapper: self.radiusTracker.update(self.radius) self.radiusTracker.on() + def isEnabled(self, snap): "Returns true if the given snap is on" if "Lock" in self.active_snaps and snap in self.active_snaps: @@ -1453,6 +1519,7 @@ class Snapper: else: return False + def toggle_snap(self, snap, set_to = None): "Sets the given snap on/off according to the given parameter" if set_to: # set mode @@ -1474,6 +1541,7 @@ class Snapper: self.save_snap_state() return status + def save_snap_state(self): """ save snap state to user preferences to be restored in next session @@ -1487,6 +1555,7 @@ class Snapper: snap_modes += "0" param.SetString("snapModes",snap_modes) + def show(self): """Show the toolbar and the grid.""" if not hasattr(self, "toolbar"): @@ -1510,12 +1579,14 @@ class Snapper: if h: c.height.setValue(h) + def hide(self): """Hide the toolbar.""" if hasattr(self, "toolbar"): self.toolbar.hide() self.toolbar.toggleViewAction().setVisible(True) + def setGrid(self): """Set the grid, if visible.""" self.setTrackers() @@ -1523,6 +1594,7 @@ class Snapper: if self.grid.Visible: self.grid.set() + def setTrackers(self): """Set the trackers.""" v = Draft.get3DView() @@ -1570,9 +1642,11 @@ class Snapper: self.trackers[8].append(self.extLine2) self.trackers[9].append(self.holdTracker) self.activeview = v + if self.grid and (not self.forceGridOff): self.grid.set() + def addHoldPoint(self): """Add hold snap point to list of hold points.""" if self.spoint and self.spoint not in self.holdPoints: diff --git a/src/Mod/Draft/draftguitools/gui_snaps.py b/src/Mod/Draft/draftguitools/gui_snaps.py index 75eb999335..57b005c18f 100644 --- a/src/Mod/Draft/draftguitools/gui_snaps.py +++ b/src/Mod/Draft/draftguitools/gui_snaps.py @@ -28,23 +28,72 @@ # \brief Provide the Draft_Snap commands used by the snapping mechanism # in Draft. -import draftguitools.gui_base as gui_base import FreeCADGui as Gui import draftguitools.gui_base as gui_base -from draftutils.translate import _tr +from PySide import QtGui from PySide.QtCore import QT_TRANSLATE_NOOP from draftutils.translate import _tr -class Draft_Snap_Lock(gui_base.GuiCommandSimplest): - """GuiCommand for the Draft_Snap_Lock tool. +# UTILITIES ----------------------------------------------------------------- - Activate or deactivate all snap methods at once. - """ - def __init__(self): - super().__init__(name=_tr("Main toggle snap")) +def get_snap_statusbar_widget(): + """retuns snap statusbar button""" + mw = Gui.getMainWindow() + if mw: + sb = mw.statusBar() + if sb: + return sb.findChild(QtGui.QToolBar,"draft_snap_widget") + return None + + +def sync_snap_toolbar_button(button, status): + """set snap toolbar button to given state""" + snap_toolbar = Gui.Snapper.get_snap_toolbar() + if not snap_toolbar: + return + for a in snap_toolbar.actions(): + if a.objectName() == button: + if button == "Draft_Snap_Lock_Button": + # for lock button + snap_toolbar.actions()[0].setChecked(status) + for a in snap_toolbar.actions()[1:]: + a.setEnabled(status) + else: + # for every other button + a.setChecked(status) + if a.isChecked(): + a.setToolTip(a.toolTip().replace("OFF","ON")) + else: + a.setToolTip(a.toolTip().replace("ON","OFF")) + + +def sync_snap_statusbar_button(button, status): + """set snap statusbar button to given state""" + ssw = get_snap_statusbar_widget() + if not ssw: + return + for child in ssw.children(): + if child.objectName() == "Snap_Statusbutton": + ssb = child + actions = [] + for a in ssb.menu().actions() + ssw.children()[-6:]: + actions.append(a) + + if button == "Draft_Snap_Lock_Statusbutton": + ssb.setChecked(status) + for a in actions[1:]: + a.setEnabled(status) + else: + for a in actions: + if a.objectName() == button: + a.setChecked(status) + + +# SNAP GUI TOOLS ------------------------------------------------------------ + class Draft_Snap_Lock(gui_base.GuiCommandSimplest): """GuiCommand for the Draft_Snap_Lock tool. @@ -53,7 +102,7 @@ class Draft_Snap_Lock(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Main toggle snap")) + super(Draft_Snap_Lock, self).__init__(name=_tr("Main toggle snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -68,17 +117,14 @@ class Draft_Snap_Lock(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() - + super(Draft_Snap_Lock, self).Activated() + if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "masterbutton"): - Gui.Snapper.masterbutton.toggle() + status = Gui.Snapper.toggle_snap('Lock') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Lock"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Lock"+"_Statusbutton", status) - if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "masterbutton"): - Gui.Snapper.masterbutton.toggle() - -Gui.addCommand('Draft_Snap_Lock', Draft_Snap_Lock()) Gui.addCommand('Draft_Snap_Lock', Draft_Snap_Lock()) @@ -90,7 +136,7 @@ class Draft_Snap_Midpoint(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Midpoint snap")) + super(Draft_Snap_Midpoint, self).__init__(name=_tr("Midpoint snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -103,13 +149,13 @@ class Draft_Snap_Midpoint(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Midpoint, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonmidpoint": - b.toggle() + status = Gui.Snapper.toggle_snap('Midpoint') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Midpoint"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Midpoint_Statusbutton", status) Gui.addCommand('Draft_Snap_Midpoint', Draft_Snap_Midpoint()) @@ -122,7 +168,7 @@ class Draft_Snap_Perpendicular(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Perpendicular snap")) + super(Draft_Snap_Perpendicular, self).__init__(name=_tr("Perpendicular snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -137,13 +183,13 @@ class Draft_Snap_Perpendicular(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Perpendicular, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonperpendicular": - b.toggle() + status = Gui.Snapper.toggle_snap('Perpendicular') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Perpendicular"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Perpendicular_Statusbutton", status) Gui.addCommand('Draft_Snap_Perpendicular', Draft_Snap_Perpendicular()) @@ -156,7 +202,7 @@ class Draft_Snap_Grid(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Grid snap")) + super(Draft_Snap_Grid, self).__init__(name=_tr("Grid snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -168,13 +214,13 @@ class Draft_Snap_Grid(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Grid, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtongrid": - b.toggle() + status = Gui.Snapper.toggle_snap('Grid') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Grid"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Grid_Statusbutton", status) Gui.addCommand('Draft_Snap_Grid', Draft_Snap_Grid()) @@ -187,7 +233,7 @@ class Draft_Snap_Intersection(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Intersection snap")) + super(Draft_Snap_Intersection, self).__init__(name=_tr("Intersection snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -202,13 +248,13 @@ class Draft_Snap_Intersection(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Intersection, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonintersection": - b.toggle() + status = Gui.Snapper.toggle_snap('Intersection') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Intersection"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Intersection_Statusbutton", status) Gui.addCommand('Draft_Snap_Intersection', Draft_Snap_Intersection()) @@ -221,7 +267,7 @@ class Draft_Snap_Parallel(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Parallel snap")) + super(Draft_Snap_Parallel, self).__init__(name=_tr("Parallel snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -234,13 +280,13 @@ class Draft_Snap_Parallel(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Parallel, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonparallel": - b.toggle() + status = Gui.Snapper.toggle_snap('Parallel') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Parallel"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Parallel_Statusbutton", status) Gui.addCommand('Draft_Snap_Parallel', Draft_Snap_Parallel()) @@ -253,7 +299,7 @@ class Draft_Snap_Endpoint(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Endpoint snap")) + super(Draft_Snap_Endpoint, self).__init__(name=_tr("Endpoint snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -266,13 +312,13 @@ class Draft_Snap_Endpoint(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Endpoint, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonendpoint": - b.toggle() + status = Gui.Snapper.toggle_snap('Endpoint') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Endpoint"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Endpoint_Statusbutton", status) Gui.addCommand('Draft_Snap_Endpoint', Draft_Snap_Endpoint()) @@ -286,7 +332,7 @@ class Draft_Snap_Angle(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Angle snap (30 and 45 degrees)")) + super(Draft_Snap_Angle, self).__init__(name=_tr("Angle snap (30 and 45 degrees)")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -300,13 +346,13 @@ class Draft_Snap_Angle(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Angle, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonangle": - b.toggle() + status = Gui.Snapper.toggle_snap('Angle') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Angle"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Angle_Statusbutton", status) Gui.addCommand('Draft_Snap_Angle', Draft_Snap_Angle()) @@ -319,7 +365,7 @@ class Draft_Snap_Center(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Arc center snap")) + super(Draft_Snap_Center, self).__init__(name=_tr("Arc center snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -331,13 +377,13 @@ class Draft_Snap_Center(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Center, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtoncenter": - b.toggle() + status = Gui.Snapper.toggle_snap('Center') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Center"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Center_Statusbutton", status) Gui.addCommand('Draft_Snap_Center', Draft_Snap_Center()) @@ -350,7 +396,7 @@ class Draft_Snap_Extension(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Edge extension snap")) + super(Draft_Snap_Extension, self).__init__(name=_tr("Edge extension snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -363,13 +409,13 @@ class Draft_Snap_Extension(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Extension, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonextension": - b.toggle() + status = Gui.Snapper.toggle_snap('Extension') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Extension"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Extension_Statusbutton", status) Gui.addCommand('Draft_Snap_Extension', Draft_Snap_Extension()) @@ -382,7 +428,7 @@ class Draft_Snap_Near(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Near snap")) + super(Draft_Snap_Near, self).__init__(name=_tr("Near snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -394,13 +440,13 @@ class Draft_Snap_Near(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Near, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonpassive": - b.toggle() + status = Gui.Snapper.toggle_snap('Near') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Near"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Near_Statusbutton", status) Gui.addCommand('Draft_Snap_Near', Draft_Snap_Near()) @@ -414,7 +460,7 @@ class Draft_Snap_Ortho(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Orthogonal snap")) + super(Draft_Snap_Ortho, self).__init__(name=_tr("Orthogonal snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -428,13 +474,13 @@ class Draft_Snap_Ortho(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Ortho, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonortho": - b.toggle() + status = Gui.Snapper.toggle_snap('Ortho') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Ortho"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Ortho"+"_Statusbutton", status) Gui.addCommand('Draft_Snap_Ortho', Draft_Snap_Ortho()) @@ -447,7 +493,7 @@ class Draft_Snap_Special(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Special point snap")) + super(Draft_Snap_Special, self).__init__(name=_tr("Special point snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -460,13 +506,13 @@ class Draft_Snap_Special(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Special, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonspecial": - b.toggle() + status = Gui.Snapper.toggle_snap('Special') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Special"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Special_Statusbutton", status) Gui.addCommand('Draft_Snap_Special', Draft_Snap_Special()) @@ -480,7 +526,7 @@ class Draft_Snap_Dimensions(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Dimension display")) + super(Draft_Snap_Dimensions, self).__init__(name=_tr("Dimension display")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -494,13 +540,13 @@ class Draft_Snap_Dimensions(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_Dimensions, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonDimensions": - b.toggle() + status = Gui.Snapper.toggle_snap('Dimensions') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_Dimensions"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_Dimensions"+"_Statusbutton", status) Gui.addCommand('Draft_Snap_Dimensions', Draft_Snap_Dimensions()) @@ -516,7 +562,7 @@ class Draft_Snap_WorkingPlane(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Working plane snap")) + super(Draft_Snap_WorkingPlane, self).__init__(name=_tr("Working plane snap")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -536,13 +582,13 @@ class Draft_Snap_WorkingPlane(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(Draft_Snap_WorkingPlane, self).Activated() if hasattr(Gui, "Snapper"): - if hasattr(Gui.Snapper, "toolbarButtons"): - for b in Gui.Snapper.toolbarButtons: - if b.objectName() == "SnapButtonWorkingPlane": - b.toggle() + status = Gui.Snapper.toggle_snap('WorkingPlane') + # change interface consistently + sync_snap_toolbar_button("Draft_Snap_WorkingPlane"+"_Button", status) + sync_snap_statusbar_button("Draft_Snap_WorkingPlane_Statusbutton", status) Gui.addCommand('Draft_Snap_WorkingPlane', Draft_Snap_WorkingPlane()) @@ -555,7 +601,7 @@ class ShowSnapBar(gui_base.GuiCommandSimplest): """ def __init__(self): - super().__init__(name=_tr("Show snap toolbar")) + super(ShowSnapBar, self).__init__(name=_tr("Show snap toolbar")) def GetResources(self): """Set icon, menu and tooltip.""" @@ -569,7 +615,7 @@ class ShowSnapBar(gui_base.GuiCommandSimplest): def Activated(self): """Execute when the command is called.""" - super().Activated() + super(ShowSnapBar, self).Activated() if hasattr(Gui, "Snapper"): Gui.Snapper.show() From 81df9b76aeffb6f01c1cbbee4a74bb856bdc8c4b Mon Sep 17 00:00:00 2001 From: carlopav Date: Sat, 18 Apr 2020 22:54:40 +0200 Subject: [PATCH 135/142] [Draft] Snapper cleanup Cleanup after @vocx-fc review --- src/Mod/Draft/draftguitools/gui_snapper.py | 181 +++++++++++---------- 1 file changed, 92 insertions(+), 89 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_snapper.py b/src/Mod/Draft/draftguitools/gui_snapper.py index 7e680899d4..84ee1dc9e4 100644 --- a/src/Mod/Draft/draftguitools/gui_snapper.py +++ b/src/Mod/Draft/draftguitools/gui_snapper.py @@ -32,9 +32,6 @@ defined by `gui_trackers.gridTracker`. # This module provides tools to handle point snapping and # everything that goes with it (toolbar buttons, cursor icons, etc.). -import FreeCAD as App -import FreeCADGui as Gui - from pivy import coin from PySide import QtCore, QtGui @@ -47,6 +44,9 @@ import Draft import DraftVecUtils import DraftGeomUtils +import FreeCAD as App +import FreeCADGui as Gui + import Part import draftguitools.gui_trackers as trackers @@ -82,7 +82,6 @@ class Snapper: """ def __init__(self): - self.activeview = None self.lastObj = [None, None] self.maxEdges = 0 @@ -236,6 +235,9 @@ class Snapper: self.running = True + global Part, DraftGeomUtils + import Part, DraftGeomUtils + self.spoint = None if not hasattr(self, "toolbar"): @@ -602,69 +604,73 @@ class Snapper: self.setCursor(tsnap[1]) return tsnap[2], eline - for o in [self.lastObj[1], self.lastObj[0]]: + for o in (self.lastObj[1], self.lastObj[0]): if o and (self.isEnabled('Extension') - or self.isEnabled('Parallel')): + or self.isEnabled('Parallel')): ob = App.ActiveDocument.getObject(o) - if ob: - if ob.isDerivedFrom("Part::Feature"): - edges = ob.Shape.Edges - if Draft.getType(ob) == "Wall": - for so in [ob]+ob.Additions: - if Draft.getType(so) == "Wall": - if so.Base: - edges.extend(so.Base.Shape.Edges) - edges.reverse() - if (not self.maxEdges) or (len(edges) <= self.maxEdges): - for e in edges: - if DraftGeomUtils.geomType(e) == "Line": - np = self.getPerpendicular(e,point) - if not DraftGeomUtils.isPtOnEdge(np,e): - if (np.sub(point)).Length < self.radius: - if self.isEnabled('Extension'): - if np != e.Vertexes[0].Point: - p0 = e.Vertexes[0].Point - if self.tracker and not self.selectMode: - self.tracker.setCoords(np) - self.tracker.setMarker(self.mk['extension']) - self.tracker.on() - if self.extLine: - if self.snapStyle: - dv = np.sub(p0) - self.extLine.p1(p0.add(dv.multiply(0.5))) - else: - self.extLine.p1(p0) - self.extLine.p2(np) - self.extLine.on() - self.setCursor('extension') - ne = Part.LineSegment(p0,np).toShape() - # storing extension line for intersection calculations later - if len(self.lastExtensions) == 0: - self.lastExtensions.append(ne) - elif len(self.lastExtensions) == 1: - if not DraftGeomUtils.areColinear(ne,self.lastExtensions[0]): - self.lastExtensions.append(self.lastExtensions[0]) - self.lastExtensions[0] = ne - else: - if (not DraftGeomUtils.areColinear(ne,self.lastExtensions[0])) and \ - (not DraftGeomUtils.areColinear(ne,self.lastExtensions[1])): - self.lastExtensions[1] = self.lastExtensions[0] - self.lastExtensions[0] = ne - return np,ne + if not ob: + continue + if not ob.isDerivedFrom("Part::Feature"): + continue + edges = ob.Shape.Edges + if Draft.getType(ob) == "Wall": + for so in [ob]+ob.Additions: + if Draft.getType(so) == "Wall": + if so.Base: + edges.extend(so.Base.Shape.Edges) + edges.reverse() + if (not self.maxEdges) or (len(edges) <= self.maxEdges): + for e in edges: + if DraftGeomUtils.geomType(e) != "Line": + continue + np = self.getPerpendicular(e,point) + if DraftGeomUtils.isPtOnEdge(np,e): + continue + if (np.sub(point)).Length < self.radius: + if self.isEnabled('Extension'): + if np != e.Vertexes[0].Point: + p0 = e.Vertexes[0].Point + if self.tracker and not self.selectMode: + self.tracker.setCoords(np) + self.tracker.setMarker(self.mk['extension']) + self.tracker.on() + if self.extLine: + if self.snapStyle: + dv = np.sub(p0) + self.extLine.p1(p0.add(dv.multiply(0.5))) else: - if self.isEnabled('Parallel'): - if last: - ve = DraftGeomUtils.vec(e) - if not DraftVecUtils.isNull(ve): - de = Part.LineSegment(last,last.add(ve)).toShape() - np = self.getPerpendicular(de,point) - if (np.sub(point)).Length < self.radius: - if self.tracker and not self.selectMode: - self.tracker.setCoords(np) - self.tracker.setMarker(self.mk['parallel']) - self.tracker.on() - self.setCursor('parallel') - return np,de + self.extLine.p1(p0) + self.extLine.p2(np) + self.extLine.on() + self.setCursor('extension') + ne = Part.LineSegment(p0,np).toShape() + # storing extension line for intersection calculations later + if len(self.lastExtensions) == 0: + self.lastExtensions.append(ne) + elif len(self.lastExtensions) == 1: + if not DraftGeomUtils.areColinear(ne,self.lastExtensions[0]): + self.lastExtensions.append(self.lastExtensions[0]) + self.lastExtensions[0] = ne + else: + if (not DraftGeomUtils.areColinear(ne,self.lastExtensions[0])) and \ + (not DraftGeomUtils.areColinear(ne,self.lastExtensions[1])): + self.lastExtensions[1] = self.lastExtensions[0] + self.lastExtensions[0] = ne + return np,ne + else: + if self.isEnabled('Parallel'): + if last: + ve = DraftGeomUtils.vec(e) + if not DraftVecUtils.isNull(ve): + de = Part.LineSegment(last,last.add(ve)).toShape() + np = self.getPerpendicular(de,point) + if (np.sub(point)).Length < self.radius: + if self.tracker and not self.selectMode: + self.tracker.setCoords(np) + self.tracker.setMarker(self.mk['parallel']) + self.tracker.on() + self.setCursor('parallel') + return np,de return point,eline @@ -1024,15 +1030,15 @@ class Snapper: return snaps - def snapToVertex(self,info,active=False): - p = App.Vector(info['x'],info['y'],info['z']) + def snapToVertex(self, info, active=False): + p = App.Vector(info['x'], info['y'], info['z']) if active: if self.isEnabled("Near"): - return [p,'endpoint',self.toWP(p)] + return [p, 'endpoint', self.toWP(p)] else: return [] elif self.isEnabled("Near"): - return [p,'passive',p] + return [p, 'passive', p] else: return [] @@ -1046,6 +1052,7 @@ class Snapper: # special snapping for wall: snap to its base shape if it is linear if obj.Base: if not obj.Base.Shape.Solids: + for v in obj.Base.Shape.Vertexes: snaps.append([v.Point, 'special', self.toWP(v.Point)]) elif (Draft.getType(obj) == "Structure"): @@ -1339,12 +1346,11 @@ class Snapper: active=ctrl, constrain=shift) if hasattr(App, "DraftWorkingPlane"): self.ui.displayPoint(self.pt, last, - plane = App.DraftWorkingPlane, - mask = App.Snapper.affinity) + plane=App.DraftWorkingPlane, + mask=App.Snapper.affinity) if movecallback: movecallback(self.pt, self.snapInfo) - def getcoords(point, relative=False): """Get the global coordinates from a point.""" self.pt = point @@ -1353,14 +1359,12 @@ class Snapper: self.pt = last.add(v) accept() - def click(event_cb): event = event_cb.getEvent() if event.getButton() == 1: if event.getState() == coin.SoMouseButtonEvent.DOWN: accept() - def accept(): if self.callbackClick: self.view.removeEventCallbackPivy(coin.SoMouseButtonEvent.getClassTypeId(), self.callbackClick) @@ -1378,7 +1382,6 @@ class Snapper: callback(self.pt) self.pt = None - def cancel(): if self.callbackClick: self.view.removeEventCallbackPivy(coin.SoMouseButtonEvent.getClassTypeId(), self.callbackClick) @@ -1447,7 +1450,7 @@ class Snapper: b.setText(QtCore.QCoreApplication.translate("Draft_Snap", "Snap " + gc[11:])) b.setToolTip(QtCore.QCoreApplication.translate("Draft_Snap", "Snap " + gc[11:])) b.setObjectName(gc + button_suffix) - b.setWhatsThis("Draft_"+gc[11:].capitalize()) + b.setWhatsThis("Draft_" + gc[11:].capitalize()) b.setCheckable(True) b.setChecked(True) context.addAction(b) @@ -1470,36 +1473,36 @@ class Snapper: param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") snap_modes = param.GetString("snapModes") - for b in toolbar.actions(): - if len(b.statusTip()) == 0: - b.setStatusTip(b.toolTip()) + for button in toolbar.actions(): + if len(button.statusTip()) == 0: + button.setStatusTip(button.toolTip()) # restore toolbar buttons state if snap_modes: - for a in toolbar.findChildren(QtGui.QAction): - snap = a.objectName()[11:].replace(button_suffix,"") + for action in toolbar.findChildren(QtGui.QAction): + snap = action.objectName()[11:].replace(button_suffix, "") if snap in Gui.Snapper.snaps: i = Gui.Snapper.snaps.index(snap) state = bool(int(snap_modes[i])) - a.setChecked(state) + action.setChecked(state) if state: - a.setToolTip(a.toolTip()+" (ON)") + action.setToolTip(action.toolTip() + " (ON)") else: - a.setToolTip(a.toolTip()+" (OFF)") + action.setToolTip(action.toolTip() + " (OFF)") def get_snap_toolbar(self): - """retuns snap toolbar object""" + """Retuns snap toolbar object.""" mw = Gui.getMainWindow() if mw: - toolbar = mw.findChild(QtGui.QToolBar,"Draft Snap") + toolbar = mw.findChild(QtGui.QToolBar, "Draft Snap") if toolbar: return toolbar return None def toggleGrid(self): - "toggle FreeCAD Draft Grid" + """Toggle FreeCAD Draft Grid.""" Gui.runCommand("Draft_ToggleGrid") @@ -1513,7 +1516,7 @@ class Snapper: def isEnabled(self, snap): - "Returns true if the given snap is on" + """Returns true if the given snap is on""" if "Lock" in self.active_snaps and snap in self.active_snaps: return True else: @@ -1521,7 +1524,7 @@ class Snapper: def toggle_snap(self, snap, set_to = None): - "Sets the given snap on/off according to the given parameter" + """Sets the given snap on/off according to the given parameter""" if set_to: # set mode if set_to is True: if not snap in self.active_snaps: @@ -1544,7 +1547,7 @@ class Snapper: def save_snap_state(self): """ - save snap state to user preferences to be restored in next session + Save snap state to user preferences to be restored in next session. """ param = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") snap_modes = "" From c8577544609b9588f4c0c37253368371e2c75c12 Mon Sep 17 00:00:00 2001 From: carlopav Date: Sun, 19 Apr 2020 17:47:59 +0200 Subject: [PATCH 136/142] [Draft] Further cleanup of the branch thanks to vocx revisions --- src/Mod/Draft/draftguitools/gui_snaps.py | 13 +++++++------ src/Mod/Draft/draftutils/init_draft_statusbar.py | 10 +++++++--- src/Mod/Draft/draftutils/init_tools.py | 12 ++++++------ 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_snaps.py b/src/Mod/Draft/draftguitools/gui_snaps.py index 57b005c18f..b26a8becea 100644 --- a/src/Mod/Draft/draftguitools/gui_snaps.py +++ b/src/Mod/Draft/draftguitools/gui_snaps.py @@ -28,11 +28,12 @@ # \brief Provide the Draft_Snap commands used by the snapping mechanism # in Draft. -import FreeCADGui as Gui -import draftguitools.gui_base as gui_base - from PySide import QtGui from PySide.QtCore import QT_TRANSLATE_NOOP + +import FreeCADGui as Gui + +import draftguitools.gui_base as gui_base from draftutils.translate import _tr @@ -40,7 +41,7 @@ from draftutils.translate import _tr def get_snap_statusbar_widget(): - """retuns snap statusbar button""" + """Return snap statusbar button.""" mw = Gui.getMainWindow() if mw: sb = mw.statusBar() @@ -50,7 +51,7 @@ def get_snap_statusbar_widget(): def sync_snap_toolbar_button(button, status): - """set snap toolbar button to given state""" + """Set snap toolbar button to given state.""" snap_toolbar = Gui.Snapper.get_snap_toolbar() if not snap_toolbar: return @@ -71,7 +72,7 @@ def sync_snap_toolbar_button(button, status): def sync_snap_statusbar_button(button, status): - """set snap statusbar button to given state""" + """Set snap statusbar button to given state.""" ssw = get_snap_statusbar_widget() if not ssw: return diff --git a/src/Mod/Draft/draftutils/init_draft_statusbar.py b/src/Mod/Draft/draftutils/init_draft_statusbar.py index 3373ac713f..56e1612d75 100644 --- a/src/Mod/Draft/draftutils/init_draft_statusbar.py +++ b/src/Mod/Draft/draftutils/init_draft_statusbar.py @@ -29,10 +29,13 @@ This module provide the code for the Draft Statusbar, activated by initGui # \ingroup DRAFT # \brief This module provides the code for the Draft Statusbar. +from PySide import QtCore +from PySide import QtGui +from PySide.QtCore import QT_TRANSLATE_NOOP + import FreeCAD as App import FreeCADGui as Gui -from PySide import QtCore, QtGui -from PySide.QtCore import QT_TRANSLATE_NOOP + from draftutils.init_tools import get_draft_snap_commands #---------------------------------------------------------------------------- @@ -148,7 +151,8 @@ def _set_scale(action): if custom_scale[1]: print(custom_scale[0]) scale = label_to_scale(custom_scale[0]) - if scale is None: return + if scale is None: + return param.SetFloat("DraftAnnotationScale", scale) cs = scale_to_label(scale) scale_widget.scaleLabel.setText(cs) diff --git a/src/Mod/Draft/draftutils/init_tools.py b/src/Mod/Draft/draftutils/init_tools.py index 7335ae2ccd..a4787f1a9d 100644 --- a/src/Mod/Draft/draftutils/init_tools.py +++ b/src/Mod/Draft/draftutils/init_tools.py @@ -115,13 +115,13 @@ def get_draft_utility_commands(): def get_draft_snap_commands(): """Return the snapping commands list.""" - return ['Draft_Snap_Lock', - 'Draft_Snap_Endpoint', 'Draft_Snap_Midpoint', - 'Draft_Snap_Center', 'Draft_Snap_Angle', + return ['Draft_Snap_Lock', + 'Draft_Snap_Endpoint', 'Draft_Snap_Midpoint', + 'Draft_Snap_Center', 'Draft_Snap_Angle', 'Draft_Snap_Intersection', 'Draft_Snap_Perpendicular', - 'Draft_Snap_Extension', 'Draft_Snap_Parallel', - 'Draft_Snap_Special', 'Draft_Snap_Near', - 'Draft_Snap_Ortho', 'Draft_Snap_Grid', + 'Draft_Snap_Extension', 'Draft_Snap_Parallel', + 'Draft_Snap_Special', 'Draft_Snap_Near', + 'Draft_Snap_Ortho', 'Draft_Snap_Grid', 'Draft_Snap_WorkingPlane', 'Draft_Snap_Dimensions', ] From 33999badba035f3b69883149eaad9d54e74e69bc Mon Sep 17 00:00:00 2001 From: donovaly Date: Sat, 28 Mar 2020 04:09:20 +0100 Subject: [PATCH 137/142] [Part] color preferences: add missing tooltips see: https://forum.freecadweb.org/viewtopic.php?f=8&t=44595 --- src/Mod/Part/Gui/DlgSettingsObjectColor.ui | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Mod/Part/Gui/DlgSettingsObjectColor.ui b/src/Mod/Part/Gui/DlgSettingsObjectColor.ui index 9de8a266bd..0a4d195120 100644 --- a/src/Mod/Part/Gui/DlgSettingsObjectColor.ui +++ b/src/Mod/Part/Gui/DlgSettingsObjectColor.ui @@ -40,7 +40,7 @@ The default color for new shapes - + 204 204 @@ -89,7 +89,7 @@ The default line color for new shapes - + 25 25 @@ -157,7 +157,7 @@ The default color for new vertices - + 25 25 @@ -225,7 +225,7 @@ The color of bounding boxes in the 3D view - + 255 255 @@ -248,6 +248,12 @@ 0 + + Bottom side of surface will be rendered the same way than top. +If not checked, it depends on the option "Backlight color" +(preferences section Display -> 3D View); either the backlight color +will be used or black. + Two-side rendering @@ -303,6 +309,9 @@ + + Text color for document annotations + AnnotationTextColor From 16d44f115b50894cb7d6a177b0161304874c9597 Mon Sep 17 00:00:00 2001 From: Jean-Marie Verdun Date: Sun, 19 Apr 2020 13:10:32 -0400 Subject: [PATCH 138/142] Add initial link support --- src/Mod/Arch/importOBJ.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Mod/Arch/importOBJ.py b/src/Mod/Arch/importOBJ.py index b449846717..29162c691f 100644 --- a/src/Mod/Arch/importOBJ.py +++ b/src/Mod/Arch/importOBJ.py @@ -72,14 +72,23 @@ def getIndices(obj,shape,offsetv,offsetvn): try: if not isinstance(e.Curve,Part.LineSegment): if not curves: - myshape = obj.Shape.copy(False) - myshape.Placement=obj.getGlobalPlacement() + if obj.isDerivedFrom("App::Link"): + myshape = obj.LinkedObject.Shape.copy(False) + myshape.Placement=obj.LinkPlacement + else: + myshape = obj.Shape.copy(False) + myshape.Placement=obj.getGlobalPlacement() mesh=MeshPart.meshFromShape(Shape=myshape, LinearDeflection=0.1, AngularDeflection=0.7, Relative=True) FreeCAD.Console.PrintWarning(translate("Arch","Found a shape containing curves, triangulating")+"\n") break except: # unimplemented curve type - myshape = obj.Shape.copy(False) - myshape.Placement=obj.getGlobalPlacement() + if obj.isDerivedFrom("App::Link"): + if obj.Shape: + myshape = obj.Shape.copy(False) + myshape.Placement=obj.LinkPlacement + else: + myshape = obj.Shape.copy(False) + myshape.Placement=obj.getGlobalPlacement() mesh=MeshPart.meshFromShape(Shape=myshape, LinearDeflection=0.1, AngularDeflection=0.7, Relative=True) FreeCAD.Console.PrintWarning(translate("Arch","Found a shape containing curves, triangulating")+"\n") break @@ -157,7 +166,7 @@ def export(exportList,filename,colors=None): materials = [] outfile.write("mtllib " + os.path.basename(filenamemtl) + "\n") for obj in objectslist: - if obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("Mesh::Feature"): + if obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("Mesh::Feature") or obj.isDerivedFrom("App::Link") or obj.isDerivedFrom("App::Link"): hires = None if FreeCAD.GuiUp: visible = obj.ViewObject.isVisible() From bf643bb6e1672a149b69891a87193f50a0ab924e Mon Sep 17 00:00:00 2001 From: Jean-Marie Verdun Date: Sun, 19 Apr 2020 14:17:04 -0400 Subject: [PATCH 139/142] Fix indent and double "or" --- src/Mod/Arch/importOBJ.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Mod/Arch/importOBJ.py b/src/Mod/Arch/importOBJ.py index 29162c691f..12919344ce 100644 --- a/src/Mod/Arch/importOBJ.py +++ b/src/Mod/Arch/importOBJ.py @@ -73,22 +73,22 @@ def getIndices(obj,shape,offsetv,offsetvn): if not isinstance(e.Curve,Part.LineSegment): if not curves: if obj.isDerivedFrom("App::Link"): - myshape = obj.LinkedObject.Shape.copy(False) - myshape.Placement=obj.LinkPlacement + myshape = obj.LinkedObject.Shape.copy(False) + myshape.Placement=obj.LinkPlacement else: - myshape = obj.Shape.copy(False) - myshape.Placement=obj.getGlobalPlacement() + myshape = obj.Shape.copy(False) + myshape.Placement=obj.getGlobalPlacement() mesh=MeshPart.meshFromShape(Shape=myshape, LinearDeflection=0.1, AngularDeflection=0.7, Relative=True) FreeCAD.Console.PrintWarning(translate("Arch","Found a shape containing curves, triangulating")+"\n") break except: # unimplemented curve type if obj.isDerivedFrom("App::Link"): if obj.Shape: - myshape = obj.Shape.copy(False) - myshape.Placement=obj.LinkPlacement + myshape = obj.Shape.copy(False) + myshape.Placement=obj.LinkPlacement else: - myshape = obj.Shape.copy(False) - myshape.Placement=obj.getGlobalPlacement() + myshape = obj.Shape.copy(False) + myshape.Placement=obj.getGlobalPlacement() mesh=MeshPart.meshFromShape(Shape=myshape, LinearDeflection=0.1, AngularDeflection=0.7, Relative=True) FreeCAD.Console.PrintWarning(translate("Arch","Found a shape containing curves, triangulating")+"\n") break @@ -166,7 +166,7 @@ def export(exportList,filename,colors=None): materials = [] outfile.write("mtllib " + os.path.basename(filenamemtl) + "\n") for obj in objectslist: - if obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("Mesh::Feature") or obj.isDerivedFrom("App::Link") or obj.isDerivedFrom("App::Link"): + if obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("Mesh::Feature") or obj.isDerivedFrom("App::Link"): hires = None if FreeCAD.GuiUp: visible = obj.ViewObject.isVisible() From 0c326f094f707f7ee77a4db3479d3af33141705a Mon Sep 17 00:00:00 2001 From: "luz.paz" Date: Sat, 18 Apr 2020 08:00:16 -0400 Subject: [PATCH 140/142] [skip ci] fix documentation typos Found via codespell v1.17.0.dev0 ``` codespell -q 3 -L aci,ake,aline,alle,alledges,alocation,als,ang,anid,ba,beginn,behaviour,bloaded,byteorder,calculater,cancelled,cancelling,cas,cascade,centimetre,childs,colour,colours,commen,connexion,currenty,dof,doubleclick,dum,eiter,elemente,ende,feld,finde,findf,freez,hist,iff,indicies,initialisation,initialise,initialised,initialises,initialisiert,ist,kilometre,lod,mantatory,methode,metres,millimetre,modell,nd,noe,normale,normaly,nto,numer,oder,orgin,orginx,orginy,ot,pard,pres,programm,que,recurrance,rougly,seperator,serie,sinc,strack,substraction,te,thist,thru,tread,uint,unter,vertexes,wallthickness,whitespaces -S ./.git,*.po,*.ts,./ChangeLog.txt,./src/3rdParty,./src/Mod/Assembly/App/opendcm,./src/CXX,./src/zipios++,./src/Base/swig*,./src/Mod/Robot/App/kdl_cp,./src/Mod/Import/App/SCL,./src/WindowsInstaller,./src/Doc/FreeCAD.uml ``` --- src/Mod/Draft/draftguitools/gui_arcs.py | 2 +- src/Mod/Draft/draftguitools/gui_groups.py | 4 ++-- src/Mod/Draft/draftguitools/gui_togglemodes.py | 2 +- src/Mod/Draft/draftobjects/label.py | 2 +- src/Mod/Mesh/App/MeshPy.xml | 2 +- src/Mod/Part/App/AppPartPy.cpp | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Mod/Draft/draftguitools/gui_arcs.py b/src/Mod/Draft/draftguitools/gui_arcs.py index b29766c1f4..eed62a60fb 100644 --- a/src/Mod/Draft/draftguitools/gui_arcs.py +++ b/src/Mod/Draft/draftguitools/gui_arcs.py @@ -139,7 +139,7 @@ class Arc_3Points(gui_base.GuiCommandSimplest): Parameters ---------- point: Base::Vector - The dynamic point pased by the callback + The dynamic point passed by the callback as we move the pointer in the 3D view. info: str diff --git a/src/Mod/Draft/draftguitools/gui_groups.py b/src/Mod/Draft/draftguitools/gui_groups.py index bc790bbf0c..3aadd4e965 100644 --- a/src/Mod/Draft/draftguitools/gui_groups.py +++ b/src/Mod/Draft/draftguitools/gui_groups.py @@ -51,7 +51,7 @@ class AddToGroup(gui_base.GuiCommandNeedsSelection): It adds selected objects to a group, or removes them from any group. - It inherits `GuiCommandNeedsSelection` to only be availbale + It inherits `GuiCommandNeedsSelection` to only be available when there is a document and a selection. See this class for more information. """ @@ -151,7 +151,7 @@ class SelectGroup(gui_base.GuiCommandNeedsSelection): in this case it works in an intuitive manner, selecting only the objects under the group. - It inherits `GuiCommandNeedsSelection` to only be availbale + It inherits `GuiCommandNeedsSelection` to only be available when there is a document and a selection. See this class for more information. """ diff --git a/src/Mod/Draft/draftguitools/gui_togglemodes.py b/src/Mod/Draft/draftguitools/gui_togglemodes.py index 2a6644e5ce..954024be08 100644 --- a/src/Mod/Draft/draftguitools/gui_togglemodes.py +++ b/src/Mod/Draft/draftguitools/gui_togglemodes.py @@ -160,7 +160,7 @@ class ToggleDisplayMode(gui_base.GuiCommandNeedsSelection): Switches the display mode of selected objects from flatlines to wireframe and back. - It inherits `GuiCommandNeedsSelection` to only be availbale + It inherits `GuiCommandNeedsSelection` to only be available when there is a document and a selection. See this class for more information. """ diff --git a/src/Mod/Draft/draftobjects/label.py b/src/Mod/Draft/draftobjects/label.py index 6d6af72010..e025c30703 100644 --- a/src/Mod/Draft/draftobjects/label.py +++ b/src/Mod/Draft/draftobjects/label.py @@ -62,7 +62,7 @@ def make_label(targetpoint=None, target=None, direction=None, ["Horizontal","Vertical","Custom"] distance : Quantity - Lenght of the straight segment of label leader line + Length of the straight segment of label leader line labeltype : String Label type in diff --git a/src/Mod/Mesh/App/MeshPy.xml b/src/Mod/Mesh/App/MeshPy.xml index 974b027428..a43402f289 100644 --- a/src/Mod/Mesh/App/MeshPy.xml +++ b/src/Mod/Mesh/App/MeshPy.xml @@ -469,7 +469,7 @@ an empty dictionary if there is no intersection. getPlanarSegments(dev,[min faces=0]) -> list Get all planes of the mesh as segment. In the worst case each triangle can be regarded as single -plane if none of its neighours is coplanar. +plane if none of its neighbors are coplanar. diff --git a/src/Mod/Part/App/AppPartPy.cpp b/src/Mod/Part/App/AppPartPy.cpp index 1210c81d55..6bbee793bb 100644 --- a/src/Mod/Part/App/AppPartPy.cpp +++ b/src/Mod/Part/App/AppPartPy.cpp @@ -750,7 +750,7 @@ private: p1.Transform(loc.Transformation()); p2.Transform(loc.Transformation()); p3.Transform(loc.Transformation()); - // TODO: verify if tolerence should be hard coded + // TODO: verify if tolerance should be hard coded if (!p1.IsEqual(p2, 0.01) && !p2.IsEqual(p3, 0.01) && !p3.IsEqual(p1, 0.01)) { PyObject *t1 = PyTuple_Pack(3, PyFloat_FromDouble(p1.X()), PyFloat_FromDouble(p1.Y()), PyFloat_FromDouble(p1.Z())); PyObject *t2 = PyTuple_Pack(3, PyFloat_FromDouble(p2.X()), PyFloat_FromDouble(p2.Y()), PyFloat_FromDouble(p2.Z())); From 2ce452c6505f52cd5dfcc0252f69260e3a6f600b Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Mon, 20 Apr 2020 13:34:38 +0200 Subject: [PATCH 141/142] Fixed bad conflict merge in PArtDesign --- src/Mod/PartDesign/SprocketFeature.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Mod/PartDesign/SprocketFeature.py b/src/Mod/PartDesign/SprocketFeature.py index 0cfa0c9adc..95977518cb 100644 --- a/src/Mod/PartDesign/SprocketFeature.py +++ b/src/Mod/PartDesign/SprocketFeature.py @@ -141,13 +141,10 @@ class ViewProviderSprocket: class SprocketTaskPanel: -<<<<<<< HEAD """ The editmode TaskPanel for Sprocket objects """ -======= - '''The editmode TaskPanel for Sprocket objects''' ->>>>>>> 07b2401c1... Converted class names from private to public, per feedback from pull request + def __init__(self,obj,mode): self.obj = obj From e8e67e8c5ebbc9f9ed9ea67aba5b891969595ece Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Mon, 20 Apr 2020 16:22:03 +0200 Subject: [PATCH 142/142] Adding FreeCAD liberapay account --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 09ab5698fd..175d975d86 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -6,7 +6,7 @@ open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username +liberapay: FreeCAD issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ['https://www.patreon.com/yorikvanhavre', 'https://www.patreon.com/kkremitzki', 'https://www.patreon.com/thundereal']