diff --git a/src/Base/Interpreter.cpp b/src/Base/Interpreter.cpp index d5b2a5152..21d78b292 100644 --- a/src/Base/Interpreter.cpp +++ b/src/Base/Interpreter.cpp @@ -92,49 +92,49 @@ void PyException::ReportException (void) const SystemExitException::SystemExitException() { - // Set exception message and code based upon the pthon sys.exit() code and/or message - // based upon the the following sys.exit() call semantics. - // - // Invocation | _exitCode | _sErrMsg - // ---------------- + --------- + -------- - // sys.exit(int#) | int# | "System Exit" - // sys.exit(string) | 1 | string - // sys.exit() | 1 | "System Exit" - - long int errCode = 1; - std::string errMsg = "System exit"; - PyObject *type, *value, *traceback, *code; - - PyGILStateLocker locker; - PyErr_Fetch(&type, &value, &traceback); - PyErr_NormalizeException(&type, &value, &traceback); - - if (value) { - code = PyObject_GetAttrString(value, "code"); - if (code != NULL && value != Py_None) { - Py_DECREF(value); - value = code; - } - - if (PyInt_Check(value)) { - errCode = PyInt_AsLong(value); - } - else { - const char *str = PyString_AsString(value); - if (str) - errMsg = errMsg + ": " + str; - } - } - + // Set exception message and code based upon the pthon sys.exit() code and/or message + // based upon the the following sys.exit() call semantics. + // + // Invocation | _exitCode | _sErrMsg + // ---------------- + --------- + -------- + // sys.exit(int#) | int# | "System Exit" + // sys.exit(string) | 1 | string + // sys.exit() | 1 | "System Exit" + + long int errCode = 1; + std::string errMsg = "System exit"; + PyObject *type, *value, *traceback, *code; + + PyGILStateLocker locker; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + + if (value) { + code = PyObject_GetAttrString(value, "code"); + if (code != NULL && value != Py_None) { + Py_DECREF(value); + value = code; + } + + if (PyInt_Check(value)) { + errCode = PyInt_AsLong(value); + } + else { + const char *str = PyString_AsString(value); + if (str) + errMsg = errMsg + ": " + str; + } + } + _sErrMsg = errMsg; - _exitCode = errCode; + _exitCode = errCode; } SystemExitException::SystemExitException(const SystemExitException &inst) : Exception(inst), _exitCode(inst._exitCode) { } - + // --------------------------------------------------------- @@ -214,6 +214,30 @@ std::string InterpreterSingleton::runString(const char *sCmd) } } +Py::Object InterpreterSingleton::runString_returnObject(const char *sCmd) +{ + PyObject *module, *dict, *presult; /* "exec code in d, d" */ + + PyGILStateLocker locker; + module = PP_Load_Module("__main__"); /* get module, init python */ + if (module == NULL) + throw PyException(); /* not incref'd */ + dict = PyModule_GetDict(module); /* get dict namespace */ + if (dict == NULL) + throw PyException(); /* not incref'd */ + + + presult = PyRun_String(sCmd, Py_eval_input, dict, dict); /* eval direct */ + if (!presult) { + if (PyErr_ExceptionMatches(PyExc_SystemExit)) + throw SystemExitException(); + else + throw PyException(); + } + + return Py::Object(presult, true); +} + void InterpreterSingleton::systemExit(void) { /* This code is taken from the original Python code */ @@ -314,22 +338,22 @@ void InterpreterSingleton::runFile(const char*pxFileName, bool local) Py_INCREF(dict); // avoid to further distinguish between local and global dict } - if (PyDict_GetItemString(dict, "__file__") == NULL) { - PyObject *f = PyString_FromString(pxFileName); - if (f == NULL) { - fclose(fp); - Py_DECREF(dict); - return; - } - if (PyDict_SetItemString(dict, "__file__", f) < 0) { - Py_DECREF(f); - fclose(fp); - Py_DECREF(dict); - return; - } - Py_DECREF(f); - } - + if (PyDict_GetItemString(dict, "__file__") == NULL) { + PyObject *f = PyString_FromString(pxFileName); + if (f == NULL) { + fclose(fp); + Py_DECREF(dict); + return; + } + if (PyDict_SetItemString(dict, "__file__", f) < 0) { + Py_DECREF(f); + fclose(fp); + Py_DECREF(dict); + return; + } + Py_DECREF(f); + } + PyObject *result = PyRun_File(fp, pxFileName, Py_file_input, dict, dict); fclose(fp); Py_DECREF(dict); diff --git a/src/Base/Interpreter.h b/src/Base/Interpreter.h index 2b88071dd..c0793a433 100644 --- a/src/Base/Interpreter.h +++ b/src/Base/Interpreter.h @@ -34,6 +34,8 @@ #include +#include + #ifdef FC_OS_MACOSX #undef toupper @@ -159,6 +161,8 @@ public: //@{ /// Run a statement on the python interpreter and gives back a string with the representation of the result. std::string runString(const char *psCmd); + /// Runs a string (expression) and returns object returned by expression. + Py::Object runString_returnObject(const char *sCmd); /// Run a statement on the python interpreter and gives back a string with the representation of the result. void runInteractiveString(const char *psCmd); /// Run file (script) on the python interpreter diff --git a/src/Mod/Part/App/CMakeLists.txt b/src/Mod/Part/App/CMakeLists.txt index 333557ad6..18cb8f799 100644 --- a/src/Mod/Part/App/CMakeLists.txt +++ b/src/Mod/Part/App/CMakeLists.txt @@ -287,6 +287,13 @@ SET(Part_Scripts TestPartApp.py MakeBottle.py JoinFeatures.py + AttachmentEditor/__init__.py + AttachmentEditor/Commands.py + AttachmentEditor/DepGraphTools.py + AttachmentEditor/FrozenClass.py + AttachmentEditor/TaskAttachmentEditor.py + AttachmentEditor/TaskAttachmentEditor.ui + AttachmentEditor/TempoVis.py ) add_library(Part SHARED ${Part_SRCS}) diff --git a/src/Mod/Part/AttachmentEditor/Commands.py b/src/Mod/Part/AttachmentEditor/Commands.py new file mode 100644 index 000000000..7da6579d6 --- /dev/null +++ b/src/Mod/Part/AttachmentEditor/Commands.py @@ -0,0 +1,100 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2016 * +# * * +# * 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 * +# * * +# ***************************************************************************/ + +from __future__ import absolute_import + +import FreeCAD as App +from PySide import QtCore + +def editAttachment(feature = None, + take_selection = False, + create_transaction = True, + callback_OK = None, + callback_Cancel = None, + callback_Apply = None): + '''Opens attachment editing dialog. + editAttachment(feature = None, + take_selection = False, + create_transaction = True, + callback_OK = None, + callback_Cancel = None, + callback_Apply = None) + feature: object to attach/modify. If None is supplied, the object is taken from + selection. + take_selection: if True, current selection is filled into first references (but only + if object to be attached doesn't have any references already) + create_transaction = if False, no undo transation operations will be done by the + dialog (consequently, canceling the dialog will not reset the feature to original + state). + callback_OK: function to be called upon OK. Invoked after writing values to feature, + committing transaction and closing the dialog. + callback_Cancel: called after closing the dialog and aborting transaction. + callback_Apply: invoked after writing values to feature.''' + + import AttachmentEditor.TaskAttachmentEditor as TaskAttachmentEditor + global taskd # exposing to outside, for ease of debugging + if feature is None: + feature = Gui.Selection.getSelectionEx()[0].Object + + try: + taskd = TaskAttachmentEditor.AttachmentEditorTaskPanel(feature, + take_selection= take_selection, + create_transaction= create_transaction, + callback_OK= callback_OK, + callback_Cancel= callback_Cancel, + callback_Apply= callback_Apply) + Gui.Control.showDialog(taskd) + except TaskAttachmentEditor.CancelError: + pass + + +class CommandEditAttachment: + 'Command to edit attachment' + def GetResources(self): + return {'MenuText': QtCore.QT_TRANSLATE_NOOP("AttachmentEditor","Attachment..."), + 'Accel': "", + 'ToolTip': QtCore.QT_TRANSLATE_NOOP("AttachmentEditor","Edit attachment of selected object.")} + + def Activated(self): + try: + editAttachment() + except Exception as err: + from PySide import QtGui + mb = QtGui.QMessageBox() + mb.setIcon(mb.Icon.Warning) + mb.setText(err.message) + mb.setWindowTitle("Error") + mb.exec_() + + def IsActive(self): + sel = Gui.Selection.getSelectionEx() + if len(sel) == 1: + if hasattr(sel[0].Object,"Placement"): + return True + return False + +if App.GuiUp: + global command_instance + import FreeCADGui as Gui + command_instance = CommandEditAttachment() + Gui.addCommand('Part_EditAttachment', command_instance) diff --git a/src/Mod/Part/AttachmentEditor/DepGraphTools.py b/src/Mod/Part/AttachmentEditor/DepGraphTools.py new file mode 100644 index 000000000..f5d36b541 --- /dev/null +++ b/src/Mod/Part/AttachmentEditor/DepGraphTools.py @@ -0,0 +1,82 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2016 * +# * * +# * 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 * +# * * +# ***************************************************************************/ + +import FreeCAD as App + +def getAllDependencies(feat): + '''getAllDependencies(feat): gets all features feat depends on, directly or indirectly. + Returns a list, with deepest dependencies last. feat is not included in the list, except + if the feature depends on itself (dependency loop).''' + list_traversing_now = [feat] + set_of_deps = set() + list_of_deps = [] + + while len(list_traversing_now) > 0: + list_to_be_traversed_next = [] + for feat in list_traversing_now: + for dep in feat.OutList: + if not (dep in set_of_deps): + set_of_deps.add(dep) + list_of_deps.append(dep) + list_to_be_traversed_next.append(dep) + + list_traversing_now = list_to_be_traversed_next + + return list_of_deps + +def getAllDependent(feat): + '''getAllDependent(feat): gets all features that depend on feat, directly or indirectly. + Returns a list, with deepest dependencies last. feat is not included in the list, except + if the feature depends on itself (dependency loop).''' + list_traversing_now = [feat] + set_of_deps = set() + list_of_deps = [] + + while len(list_traversing_now) > 0: + list_to_be_traversed_next = [] + for feat in list_traversing_now: + for dep in feat.InList: + if not (dep in set_of_deps): + set_of_deps.add(dep) + list_of_deps.append(dep) + list_to_be_traversed_next.append(dep) + + list_traversing_now = list_to_be_traversed_next + + return list_of_deps + +def isContainer(obj): + '''isContainer(obj): returns True if obj is an object container, such as + Group, Part, Body. The important characterisic of an object being a + container is its action on visibility of linked objects. E.g. a + Part::Compound is not a group, because it does not affect visibility + of originals. Documents are considered containers, too.''' + + if obj.isDerivedFrom("App::DocumentObjectGroup"): + return True + if obj.isDerivedFrom("PartDesign::Body"): + return True + if obj.isDerivedFrom("App::Origin"): + return True + if obj.isDerivedFrom('App::Document'): + return True diff --git a/src/Mod/Part/AttachmentEditor/FrozenClass.py b/src/Mod/Part/AttachmentEditor/FrozenClass.py new file mode 100644 index 000000000..1fa76fb5f --- /dev/null +++ b/src/Mod/Part/AttachmentEditor/FrozenClass.py @@ -0,0 +1,37 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2016 * +# * * +# * 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 * +# * * +# ***************************************************************************/ + +# adapted from http://stackoverflow.com/a/3603824/6285007 +class FrozenClass(object): + '''FrozenClass: prevents adding new attributes to class outside of __init__''' + __isfrozen = False + def __setattr__(self, key, value): + if self.__isfrozen and not hasattr(self, key): + raise TypeError( "{cls} has no attribute {attr}".format(cls= self.__class__.__name__, attr= key) ) + object.__setattr__(self, key, value) + + def _freeze(self): + self.__isfrozen = True + + def _unfreeze(self): + self.__isfrozen = False diff --git a/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py new file mode 100644 index 000000000..894600934 --- /dev/null +++ b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py @@ -0,0 +1,557 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2016 * +# * * +# * 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 * +# * * +# ***************************************************************************/ + +from __future__ import absolute_import + +import FreeCAD as App +import Part +from FreeCAD import Units +from Units import MilliMetre as mm +from Units import Degree as deg +from Units import Quantity as Q + +from AttachmentEditor.FrozenClass import FrozenClass +from AttachmentEditor.TempoVis import TempoVis +from AttachmentEditor.DepGraphTools import getAllDependent + +if App.GuiUp: + import FreeCADGui as Gui + from PySide import QtCore, QtGui + from FreeCADGui import PySideUic as uic + +#-------------------------- translation-related code ---------------------------------------- +#Thanks, yorik! (see forum thread "A new Part tool is being born... JoinFeatures!" +#http://forum.freecadweb.org/viewtopic.php?f=22&t=11112&start=30#p90239 ) +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) +#--------------------------/translation-related code ---------------------------------------- + + +def StrFromLink(feature, subname): + return feature.Name+ ((':'+subname) if subname else '') + +def LinkFromStr(strlink, document): + if len(strlink) == 0: + return None + pieces = strlink.split(':') + + feature = document.getObject(pieces[0]) + + subname = '' + if feature is None: + raise ValueError(_translate('AttachmentEditor',"No object named {name}",None).format(name= pieces[0])) + if len(pieces) == 2: + subname = pieces[1] + elif len(pieces) > 2: + raise ValueError(_translate('AttachmentEditor',"Failed to parse link (more than one colon encountered)",None)) + + return (feature,str(subname)) #wrap in str to remove unicode, which confuses assignment to PropertyLinkSubList. + +def StrListFromRefs(references): + '''input: PropertyLinkSubList. Output: list of strings for UI.''' + return [StrFromLink(feature,subelement) for (feature, subelement) in references] + +def RefsFromStrList(strings, document): + '''input: strings as from UI. Output: list of tuples that can be assigned to PropertyLinkSubList.''' + refs = [] + for st in strings: + lnk = LinkFromStr(st, document) + if lnk is not None: + refs.append(lnk) + return refs + +def GetSelectionAsLinkSubList(): + sel = Gui.Selection.getSelectionEx() + result = [] + for selobj in sel: + for subname in selobj.SubElementNames: + result.append((selobj, subname)) + if len(selobj.SubElementNames) == 0: + result.append((selobj, '')) + return result + +def PlacementsFuzzyCompare(plm1, plm2): + pos_eq = (plm1.Base - plm2.Base).Length < 1e-7 # 1e-7 is OCC's Precision::Confusion + + q1 = plm1.Rotation.Q + q2 = plm2.Rotation.Q + # rotations are equal if q1 == q2 or q1 == -q2. + # Invert one of Q's if their scalar product is negative, before comparison. + if q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2] + q1[3]*q2[3] < 0: + q2 = [-v for v in q2] + rot_eq = ( abs(q1[0]-q2[0]) + + abs(q1[1]-q2[1]) + + abs(q1[2]-q2[2]) + + abs(q1[3]-q2[3]) ) < 1e-12 # 1e-12 is OCC's Precision::Angular (in radians) + return pos_eq and rot_eq + +class CancelError(Exception): + def __init__(self): + self.message = 'Canceled by user' + self.isCancelError = True + +class AttachmentEditorTaskPanel(FrozenClass): + '''The editmode TaskPanel for attachment editing''' + KEYmode = QtCore.Qt.ItemDataRole.UserRole # Key to use in Item.data(key) to obtain a mode associated with list item + KEYon = QtCore.Qt.ItemDataRole.UserRole + 1 # Key to use in Item.data(key) to obtain if the mode is valid + + def __define_attributes(self): + self.obj = None #feature being attached + self.attacher = None #AttachEngine that is being actively used by the dialog. Its parameters are constantly and actively kept in sync with the dialog. + self.obj_is_attachable = True # False when editing non-attachable objects (alignment, not attachment) + + self.last_sugr = None #result of last execution of suggestor + + self.form = None #Qt widget of dialog interface + self.block = False #when True, event handlers return without doing anything (instead of doing-undoing blockSignals to everything) + self.refLines = [] #reference lineEdit widgets, packed into a list for convenience + self.refButtons = [] #buttons next to reference lineEdits + self.superPlacementEdits = [] #all edit boxes related to superplacement + self.i_active_ref = -1 #index of reference being selected (-1 means no reaction to selecting) + self.auto_next = False #if true, references being selected are appended ('Selecting' state is automatically advanced to next button) + + self.tv = None #TempoVis class instance + + self.create_transaction = True # if false, dialog doesn't mess with transactions. + self.callback_OK = None + self.callback_Cancel = None + self.callback_Apply = None + + self._freeze() + + def __init__(self, obj_to_attach, + take_selection = False, + create_transaction = True, + callback_OK = None, + callback_Cancel = None, + callback_Apply = None): + + self.__define_attributes() + + self.create_transaction = create_transaction + self.callback_OK = callback_OK + self.callback_Cancel = callback_Cancel + self.callback_Apply = callback_Apply + + self.obj = obj_to_attach + if hasattr(obj_to_attach,'Attacher'): + self.attacher = obj_to_attach.Attacher + elif hasattr(obj_to_attach,'AttacherType'): + self.attacher = Part.AttachEngine(obj_to_attach.AttacherType) + else: + movable = True + if not hasattr(self.obj, "Placement"): + movable = False + if 'Hidden' in self.obj.getEditorMode("Placement") or 'ReadOnly' in self.obj.getEditorMode("Placement"): + movable = False + if not movable: + if self.callback_Cancel: + self.callback_Cancel() + raise ValueError(_translate('AttachmentEditor',"Object {name} is neither movable nor attachable, can't edit attachment",None) + .format(name= self.obj.Label)) + + self.obj_is_attachable = False + self.attacher = Part.AttachEngine() + + mb = QtGui.QMessageBox() + mb.setIcon(mb.Icon.Warning) + mb.setText(_translate('AttachmentEditor', + "{obj} is not attachable. You can still use attachment editor dialog to align the object, but the attachment won't be parametic." + ,None) + .format(obj= obj_to_attach.Label)) + mb.setWindowTitle(_translate('AttachmentEditor',"Attachment",None)) + btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort) + btnOK = mb.addButton(_translate('AttachmentEditor',"Continue",None),QtGui.QMessageBox.ButtonRole.ActionRole) + mb.setDefaultButton(btnOK) + mb.exec_() + if mb.clickedButton() is btnAbort: + if self.callback_Cancel: + self.callback_Cancel() + raise CancelError() + + import os + self.form=uic.loadUi(os.path.dirname(__file__) + os.path.sep + 'TaskAttachmentEditor.ui') + # self.form.setWindowIcon(QtGui.QIcon(':/icons/PartDesign_InternalExternalGear.svg')) + self.form.setWindowTitle(_translate('AttachmentEditor',"Attachment",None)) + + self.refLines = [self.form.lineRef1, + self.form.lineRef2, + self.form.lineRef3, + self.form.lineRef4] + self.refButtons = [self.form.buttonRef1, + self.form.buttonRef2, + self.form.buttonRef3, + self.form.buttonRef4] + self.superPlacementEdits = [self.form.superplacementX, + self.form.superplacementY, + self.form.superplacementZ, + self.form.superplacementYaw, + self.form.superplacementPitch, + self.form.superplacementRoll] + + self.block = False + + for i in range(len(self.refLines)): + QtCore.QObject.connect(self.refLines[i], QtCore.SIGNAL('textEdited(QString)'), lambda txt, i=i: self.lineRefChanged(i,txt)) + + for i in range(len(self.refLines)): + QtCore.QObject.connect(self.refButtons[i], QtCore.SIGNAL('clicked()'), lambda i=i: self.refButtonClicked(i)) + + for i in range(len(self.superPlacementEdits)): + QtCore.QObject.connect(self.superPlacementEdits[i], QtCore.SIGNAL('valueChanged(double)'), lambda val, i=i: self.superplacementChanged(i,val)) + + QtCore.QObject.connect(self.form.checkBoxFlip, QtCore.SIGNAL('clicked()'), self.checkBoxFlipClicked) + + QtCore.QObject.connect(self.form.listOfModes, QtCore.SIGNAL('itemSelectionChanged()'), self.modeSelected) + + if self.create_transaction: + self.obj.Document.openTransaction(_translate('AttachmentEditor',"Edit attachment of {feat}",None).format(feat= self.obj.Name)) + + + self.readParameters() + + + if len(self.attacher.References) == 0 and take_selection: + sel = GetSelectionAsLinkSubList() + for i in range(len(sel))[::-1]: + if sel[i][0] is obj_to_attach: + sel.pop(i) + self.attacher.References = sel + if len(self.attacher.References) == 0: + self.i_active_ref = 0 + self.auto_next = True + else: + self.i_active_ref = -1 + self.auto_next = False + + Gui.Selection.addObserver(self) + + self.updatePreview() + self.updateRefButtons() + + self.tv = TempoVis(self.obj.Document) + self.tv.hide_all_dependent(self.obj) + self.tv.show(self.obj) + self.tv.show([obj for (obj,subname) in self.attacher.References]) + + # task dialog handling + 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: + if self.obj_is_attachable: + self.writeParameters() + self.updatePreview() + if self.callback_Apply: + self.callback_Apply() + + def accept(self): + if self.obj_is_attachable: + self.writeParameters() + if self.create_transaction: + self.obj.Document.commitTransaction() + self.cleanUp() + Gui.Control.closeDialog() + if self.callback_OK: + self.callback_OK() + + def reject(self): + if self.create_transaction: + self.obj.Document.abortTransaction() + self.cleanUp() + Gui.Control.closeDialog() + if self.callback_Cancel: + self.callback_Cancel() + + + #selectionObserver stuff + def addSelection(self,docname,objname,subname,pnt): + i = self.i_active_ref + if i < 0: + #not selecting any reference + return + if i > 0 and self.auto_next: + prevref = LinkFromStr( self.refLines[i-1].text(), self.obj.Document ) + if prevref[0].Name == objname and subname == '': + # whole object was selected by double-clicking + # its subelement was already written to line[i-1], so we decrease i to overwrite the lineRefChanged + i -= 1 + if i > len(self.refLines)-1: + # all 4 references have been selected, finish + assert(self.auto_next) + self.i_active_ref = -1 + self.updateRefButtons() + return + if i > -1: + # assign the selected reference + if objname == self.obj.Name: + self.form.message.setText(_translate('AttachmentEditor',"Ignored. Can't attach object to itself!",None)) + return + if App.getDocument(docname).getObject(objname) in getAllDependent(self.obj): + self.form.message.setText(_translate('AttachmentEditor',"{obj1} depends on object being attached, can't use it for attachment",None).format(obj1= objname)) + return + + self.refLines[i].setText( StrFromLink(App.getDocument(docname).getObject(objname), subname) ) + self.lineRefChanged(i,'') + if self.auto_next: + i += 1 + self.i_active_ref = i + self.updateRefButtons() + + # slots + + def superplacementChanged(self, index, value): + if self.block: + return + plm = self.attacher.SuperPlacement + pos = plm.Base + if index==0: + pos.x = Q(self.form.superplacementX.text()).getValueAs(mm) + if index==1: + pos.y = Q(self.form.superplacementY.text()).getValueAs(mm) + if index==2: + pos.z = Q(self.form.superplacementZ.text()).getValueAs(mm) + if index >= 0 and index <= 2: + plm.Base = pos + + rot = plm.Rotation; + (yaw, pitch, roll) = rot.toEuler() + if index==3: + yaw = Q(self.form.superplacementYaw.text()).getValueAs(deg) + if index==4: + pitch = Q(self.form.superplacementPitch.text()).getValueAs(deg) + if index==5: + roll = Q(self.form.superplacementRoll.text()).getValueAs(deg) + if index >= 3 and index <= 5: + rot = App.Rotation(yaw,pitch,roll) + plm.Rotation = rot + + self.attacher.SuperPlacement = plm + + self.updatePreview() + + def checkBoxFlipClicked(self): + if self.block: + return + self.attacher.Reverse = self.form.checkBoxFlip.isChecked() + self.updatePreview() + + def lineRefChanged(self, index, value): + if self.block: + return + # not parsing links here, because doing it in updatePreview will display error message + self.updatePreview() + + def refButtonClicked(self, index): + if self.block: + return + if self.i_active_ref == index: + #stop selecting + self.i_active_ref = -1 + else: + #start selecting + self.i_active_ref = index + self.auto_next = False + self.updateRefButtons() + + def modeSelected(self): + if self.block: + return + self.attacher.Mode = self.getCurrentMode() + self.updatePreview() + + #internal methods + def writeParameters(self): + 'Transfer from the dialog to the object' + self.attacher.writeParametersToFeature(self.obj) + + def readParameters(self): + 'Transfer from the object to the dialog' + if self.obj_is_attachable: + self.attacher.readParametersFromFeature(self.obj) + + plm = self.attacher.SuperPlacement + try: + old_selfblock = self.block + self.block = True + self.form.superplacementX.setText ((plm.Base.x * mm).UserString) + self.form.superplacementY.setText ((plm.Base.y * mm).UserString) + self.form.superplacementZ.setText ((plm.Base.z * mm).UserString) + self.form.superplacementYaw.setText ((plm.Rotation.toEuler()[0] * deg).UserString) + self.form.superplacementPitch.setText((plm.Rotation.toEuler()[1] * deg).UserString) + self.form.superplacementRoll.setText ((plm.Rotation.toEuler()[2] * deg).UserString) + + self.form.checkBoxFlip.setChecked(self.attacher.Reverse) + + strings = StrListFromRefs(self.attacher.References) + if len(strings) < len(self.refLines): + strings.extend(['']*(len(self.refLines) - len(strings))) + for i in range(len(self.refLines)): + self.refLines[i].setText(strings[i]) + finally: + self.block = old_selfblock + + def parseAllRefLines(self): + self.attacher.References = RefsFromStrList([le.text() for le in self.refLines], self.obj.Document) + + def updateListOfModes(self): + '''needs suggestor to have been called, and assigned to self.last_sugr''' + try: + old_selfblock = self.block + self.block = True + list_widget = self.form.listOfModes + list_widget.clear() + sugr = self.last_sugr + # add valid modes + for m in sugr['allApplicableModes']: + item = QtGui.QListWidgetItem() + txt = self.attacher.getModeInfo(m)['UserFriendlyName'] + item.setText(txt) + item.setData(self.KEYmode,m) + item.setData(self.KEYon,True) + if m == sugr['bestFitMode']: + f = item.font() + f.setBold(True) + item.setFont(f) + list_widget.addItem(item) + item.setSelected(self.attacher.Mode == m) + # add potential modes + for m in sugr['reachableModes'].keys(): + item = QtGui.QListWidgetItem() + txt = self.attacher.getModeInfo(m)['UserFriendlyName'] + listlistrefs = sugr['reachableModes'][m] + if len(listlistrefs) == 1: + listrefs_userfriendly = [self.attacher.getRefTypeInfo(t)['UserFriendlyName'] for t in listlistrefs[0]] + txt = _translate('AttachmentEditor',"{mode} (add {morerefs})",None).format(mode= txt, + morerefs= u"+".join(listrefs_userfriendly)) + else: + txt = _translate('AttachmentEditor',"{mode} (add more references)",None).format(mode= txt) + item.setText(txt) + item.setData(self.KEYmode,m) + item.setData(self.KEYon,True) + if m == sugr['bestFitMode']: + f = item.font() + f.setBold(True) + item.setFont(f) + + #disable this item + f = item.flags() + f = f & ~(QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable) + item.setFlags(f) + + list_widget.addItem(item) + + # re-scan the list to fill in tooltips + for item in list_widget.findItems('', QtCore.Qt.MatchContains): + m = item.data(self.KEYmode) + on = item.data(self.KEYon) + + mi = self.attacher.getModeInfo(m) + cmb = [] + for refstr in mi['ReferenceCombinations']: + refstr_userfriendly = [self.attacher.getRefTypeInfo(t)['UserFriendlyName'] for t in refstr] + cmb.append(u", ".join(refstr_userfriendly)) + tip = _translate('AttachmentEditor',"{docu}\n\nReference combinations:\n{combinations}",None).format(docu=mi['BriefDocu'], combinations= u"\n".join(cmb) ) + + item.setToolTip(tip) + + finally: + self.block = old_selfblock + + + def updateRefButtons(self): + try: + old_selfblock = self.block + self.block = True + for i in range(len(self.refButtons)): + btn = self.refButtons[i] + btn.setCheckable(True) + btn.setChecked(self.i_active_ref == i) + typ = _translate('AttachmentEditor',"Reference{i}",None).format(i= str(i+1)) + if self.last_sugr is not None: + typestr = self.last_sugr['references_Types'] + if i < len(typestr): + typ = self.attacher.getRefTypeInfo(typestr[i])['UserFriendlyName'] + btn.setText(_translate('AttachmentEditor',"Selecting...",None) if self.i_active_ref == i else typ) + finally: + self.block = old_selfblock + + def getCurrentMode(self): + list_widget = self.form.listOfModes + sel = list_widget.selectedItems() + if len(sel) == 1: + if sel[0].data(self.KEYon): + return str(sel[0].data(self.KEYmode)) # data() returns unicode, which confuses attacher + # nothing selected in list. Return suggested + if self.last_sugr is not None: + if self.last_sugr['message'] == 'OK': + return self.last_sugr['bestFitMode'] + # no suggested mode. Return current, so it doesn't change + return self.attacher.Mode + + def updatePreview(self): + new_plm = None + + try: + self.parseAllRefLines() + self.last_sugr = self.attacher.suggestModes() + if self.last_sugr['message'] == 'LinkBroken': + raise ValueError(_translate('AttachmentEditor',"Failed to resolve links. {err}",None).format(err= self.last_sugr['error'])) + + self.updateListOfModes() + + self.attacher.Mode = self.getCurrentMode() + + new_plm = self.attacher.calculateAttachedPlacement(self.obj.Placement) + if new_plm is None: + self.form.message.setText(_translate('AttachmentEditor',"Not attached",None)) + else: + self.form.message.setText( _translate('AttachmentEditor',"Attached with mode {mode}",None) + .format( mode= self.attacher.getModeInfo(self.getCurrentMode())['UserFriendlyName'] ) ) + if PlacementsFuzzyCompare(self.obj.Placement, new_plm) == False: + # assign only if placement changed. this avoids touching the object + # when entering and extiting dialog without changing anything + self.obj.Placement = new_plm + except Exception as err: + self.form.message.setText(_translate('AttachmentEditor',"Error: {err}",None).format(err= err.message)) + + if new_plm is not None: + self.form.groupBox_superplacement.setTitle(_translate('AttachmentEditor',"Extra placement:",None)) + else: + self.form.groupBox_superplacement.setTitle(_translate('AttachmentEditor',"Extra placement (inactive - not attached):",None)) + + def cleanUp(self): + '''stuff that needs to be done when dialog is closed.''' + Gui.Selection.removeObserver(self) + self.tv.restore() \ No newline at end of file diff --git a/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.ui b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.ui new file mode 100644 index 000000000..ec2d894e2 --- /dev/null +++ b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.ui @@ -0,0 +1,385 @@ + + + PartDesignGui::TaskDatumParameters + + + + 0 + 0 + 271 + 604 + + + + Form + + + + + + Selection accepted + + + Qt::AlignCenter + + + true + + + + + + + + + Reference 1 + + + true + + + + + + + + + + + + + + Reference 2 + + + true + + + + + + + + + + + + + + Reference 3 + + + true + + + + + + + + + + + + + + Reference 4 + + + true + + + + + + + + + + + + Attachment mode: + + + + + + + QAbstractItemView::SingleSelection + + + + + + + + 0 + 0 + + + + Mirror of superPlacement property. Extra placement is expressed in local space of object being attached. + + + Extra placement: + + + + + + + 0 + 0 + + + + X: + + + labelOffset + + + + + + + + 0 + 0 + + + + Y: + + + + + + + + 0 + 0 + + + + + 0 + 5 + + + + + + + + + 0 + 0 + + + + Z: + + + + + + + + 0 + 0 + + + + + 0 + 5 + + + + + + + + + 0 + 0 + + + + Yaw: + + + + + + + + 0 + 0 + + + + Pitch: + + + + + + + + 0 + 0 + + + + Roll: + + + + + + + + 0 + 0 + + + + + 0 + 5 + + + + + + + + + 0 + 0 + + + + deg + + + -360.000000000000000 + + + 360.000000000000000 + + + 0.000000000000000 + + + + + + + + 0 + 0 + + + + deg + + + -360.000000000000000 + + + 360.000000000000000 + + + 0.000000000000000 + + + + + + + + 0 + 0 + + + + deg + + + -360.000000000000000 + + + 360.000000000000000 + + + 0.000000000000000 + + + + + + + + + + Flip sides + + + + + + + + Gui::InputField + QLineEdit +
Gui/InputField.h
+
+ + Gui::QuantitySpinBox + QWidget +
Gui/QuantitySpinBox.h
+
+ + +
+ + buttonRef1 + lineRef1 + buttonRef2 + lineRef2 + buttonRef3 + lineRef3 + buttonRef4 + lineRef4 + listOfModes + superplacementX + superplacementY + superplacementZ + superplacementYaw + superplacementPitch + superplacementRoll + checkBoxFlip + + + + + + 10 + + + 10 + + + true + + + true + + + true + + +
diff --git a/src/Mod/Part/AttachmentEditor/TempoVis.py b/src/Mod/Part/AttachmentEditor/TempoVis.py new file mode 100644 index 000000000..39b1031d7 --- /dev/null +++ b/src/Mod/Part/AttachmentEditor/TempoVis.py @@ -0,0 +1,110 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2016 * +# * * +# * 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 * +# * * +# ***************************************************************************/ + +import FreeCAD as App +if App.GuiUp: + import FreeCADGui as Gui + +from FrozenClass import FrozenClass + +from DepGraphTools import getAllDependencies, getAllDependent, isContainer + +class TempoVis(FrozenClass): + '''TempoVis - helper object to save visibilities of objects before doing + some GUI editing, hiding or showing relevant stuff during edit, and + then restoring all visibilities after editing. + + Constructors: + TempoVis(document): creates a new TempoVis. Supplying document is mandatory. Objects not belonging to the document can't be modified via TempoVis.''' + + def __define_attributes(self): + self.data = {} # dict. key = ("Object","Property"), value = original value of the property + self.document = None + self.restore_on_delete = False + + self._freeze() + + def __init__(self, document): + self.__define_attributes() + + self.document = document + + def modifyVPProperty(self, doc_obj_or_list, prop_name, new_value): + '''modifyVPProperty(self, doc_obj_or_list, prop_name, new_value): modifies + prop_name property of ViewProvider of doc_obj_or_list, and remembers + original value of the property. Original values will be restored upon + TempoVis deletion, or call to restore().''' + + if App.GuiUp: + if type(doc_obj_or_list) is not list: + doc_obj_or_list = [doc_obj_or_list] + for doc_obj in doc_obj_or_list: + if doc_obj.Document is not self.document: #ignore objects from other documents + raise ValueError("Document object to be modified does not belong to document TempoVis was made for.") + oldval = getattr(doc_obj.ViewObject, prop_name) + setattr(doc_obj.ViewObject, prop_name, new_value) + #assert(getattr(doc_obj.ViewObject, prop_name)==new_value) + if not self.data.has_key((doc_obj.Name,prop_name)): + self.data[(doc_obj.Name,prop_name)] = oldval + self.restore_on_delete = True + + def show(self, doc_obj_or_list): + '''show(doc_obj_or_list): shows objects (sets their Visibility to True). doc_obj_or_list can be a document object, or a list of document objects''' + self.modifyVPProperty(doc_obj_or_list, "Visibility", True) + + def hide(self, doc_obj_or_list): + '''hide(doc_obj_or_list): hides objects (sets their Visibility to False). doc_obj_or_list can be a document object, or a list of document objects''' + self.modifyVPProperty(doc_obj_or_list, "Visibility", False) + + def hide_all_dependent(self, doc_obj): + '''hide_all_dependent(doc_obj): hides all objects that depend on doc_obj. Groups, Parts and Bodies are not hidden by this.''' + self.hide( [o for o in getAllDependent(doc_obj) if not isContainer(o)]) + + def show_all_dependent(self, doc_obj): + '''show_all_dependent(doc_obj): shows all objects that depend on doc_obj. This method is probably useless.''' + self.show( getAllDependent(doc_obj) ) + + def hide_all_dependencies(self, doc_obj): + '''hide_all_dependencies(doc_obj): hides all objects that doc_obj depends on (directly and indirectly).''' + self.hide( getAllDependencies(doc_obj) ) + + def show_all_dependencies(self, doc_obj): + '''show_all_dependencies(doc_obj): shows all objects that doc_obj depends on (directly and indirectly). This method is probably useless.''' + self.show( getAllDependencies(doc_obj) ) + + def restore(self): + '''restore(): restore all ViewProvider properties modified via TempoVis to their original values. Called automatically when instance is destroyed, unless it was called explicitly.''' + for obj_name, prop_name in self.data: + setattr(self.document.getObject(obj_name).ViewObject, prop_name, self.data[(obj_name, prop_name)]) + self.restore_on_delete = False + + def forget(self): + '''forget(): resets TempoVis''' + self.data = {} + self.restore_on_delete = False + + def __del__(self): + if self.restore_on_delete: + self.restore() + + \ No newline at end of file diff --git a/src/Mod/Part/AttachmentEditor/__init__.py b/src/Mod/Part/AttachmentEditor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/Mod/Part/CMakeLists.txt b/src/Mod/Part/CMakeLists.txt index b08e551f5..7ed515703 100644 --- a/src/Mod/Part/CMakeLists.txt +++ b/src/Mod/Part/CMakeLists.txt @@ -15,3 +15,16 @@ INSTALL( DESTINATION Mod/Part ) + +INSTALL( + FILES + AttachmentEditor/__init__.py + AttachmentEditor/Commands.py + AttachmentEditor/DepGraphTools.py + AttachmentEditor/FrozenClass.py + AttachmentEditor/TaskAttachmentEditor.py + AttachmentEditor/TaskAttachmentEditor.ui + AttachmentEditor/TempoVis.py + DESTINATION + Mod/Part/AttachmentEditor + ) diff --git a/src/Mod/Part/Gui/AppPartGui.cpp b/src/Mod/Part/Gui/AppPartGui.cpp index c9913200c..8f49e1cac 100644 --- a/src/Mod/Part/Gui/AppPartGui.cpp +++ b/src/Mod/Part/Gui/AppPartGui.cpp @@ -126,7 +126,6 @@ PyMODINIT_FUNC initPartGui() Py_INCREF(pAttachEngineTextsModule); PyModule_AddObject(partGuiModule, "AttachEngineResources", pAttachEngineTextsModule); - PartGui::SoBrepFaceSet ::initClass(); PartGui::SoBrepEdgeSet ::initClass(); PartGui::SoBrepPointSet ::initClass(); @@ -182,6 +181,13 @@ PyMODINIT_FUNC initPartGui() CreatePartCommands(); CreateSimplePartCommands(); CreateParamPartCommands(); + try{ + Py::Object ae = Base::Interpreter().runString_returnObject("__import__('AttachmentEditor.Commands').Commands"); + Py::Module(partGuiModule).setAttr(std::string("AttachmentEditor"),ae); + } catch (Base::PyException &err){ + err.ReportException(); + } + // register preferences pages (void)new Gui::PrefPageProducer ( QT_TRANSLATE_NOOP("QObject","Part design") );