From a09b83ec0d4740420858a0dba3b5bd08a0f50883 Mon Sep 17 00:00:00 2001 From: DeepSOIC Date: Sat, 24 Aug 2019 02:23:39 +0300 Subject: [PATCH] Show: Tempovis stack support Improvents: * Allows using several tempovis instances in arbitrary order * plugin system (SceneDetail), ease of extending * Mild restore feature - more respect for changes made by user * workbench switching support * undo saveable changes for the time of writing a file Regressions: * Removed support for pickling (with hopes to reintroduce later) --- src/Mod/Show/CMakeLists.txt | 18 +- src/Mod/Show/Containers.py | 18 +- src/Mod/Show/DepGraphTools.py | 22 - src/Mod/Show/SceneDetail.py | 78 ++ src/Mod/Show/SceneDetails/Camera.py | 48 ++ src/Mod/Show/SceneDetails/ClipPlane.py | 64 ++ src/Mod/Show/SceneDetails/ObjectClipPlane.py | 107 +++ src/Mod/Show/SceneDetails/Pickability.py | 81 ++ src/Mod/Show/SceneDetails/VProperty.py | 48 ++ .../Workbench.py} | 33 +- src/Mod/Show/SceneDetails/__init__.py | 0 src/Mod/Show/ShowUtils.py | 37 + src/Mod/Show/TVObserver.py | 48 ++ src/Mod/Show/TVStack.py | 192 +++++ src/Mod/Show/TempoVis.py | 712 ++++++++++-------- 15 files changed, 1144 insertions(+), 362 deletions(-) create mode 100644 src/Mod/Show/SceneDetail.py create mode 100644 src/Mod/Show/SceneDetails/Camera.py create mode 100644 src/Mod/Show/SceneDetails/ClipPlane.py create mode 100644 src/Mod/Show/SceneDetails/ObjectClipPlane.py create mode 100644 src/Mod/Show/SceneDetails/Pickability.py create mode 100644 src/Mod/Show/SceneDetails/VProperty.py rename src/Mod/Show/{FrozenClass.py => SceneDetails/Workbench.py} (73%) create mode 100644 src/Mod/Show/SceneDetails/__init__.py create mode 100644 src/Mod/Show/ShowUtils.py create mode 100644 src/Mod/Show/TVObserver.py create mode 100644 src/Mod/Show/TVStack.py diff --git a/src/Mod/Show/CMakeLists.txt b/src/Mod/Show/CMakeLists.txt index 94b668229b..b786d8bdd0 100644 --- a/src/Mod/Show/CMakeLists.txt +++ b/src/Mod/Show/CMakeLists.txt @@ -1,10 +1,20 @@ SET(Show_SRCS __init__.py - DepGraphTools.py - FrozenClass.py - TempoVis.py Containers.py + DepGraphTools.py + SceneDetail.py + TempoVis.py + TVObserver.py + TVStack.py + ShowUtils.py + SceneDetails/__init__.py + SceneDetails/Camera.py + SceneDetails/ClipPlane.py + SceneDetails/ObjectClipPlane.py + SceneDetails/Pickability.py + SceneDetails/VProperty.py + SceneDetails/Workbench.py ) SOURCE_GROUP("" FILES ${Show_SRCS}) @@ -19,4 +29,4 @@ INSTALL( FILES ${Show_SRCS} DESTINATION Mod/Show -) \ No newline at end of file +) diff --git a/src/Mod/Show/Containers.py b/src/Mod/Show/Containers.py index c5c4917a5d..f6874ef0d1 100644 --- a/src/Mod/Show/Containers.py +++ b/src/Mod/Show/Containers.py @@ -34,7 +34,7 @@ class Container(object): def self_check(self): if self.Object is None: raise ValueError("Null!") - if not isAContainer(self.Object): + if not isAContainer(self.Object, links_too= True): raise NotAContainerError(self.Object) def getAllChildren(self): @@ -58,7 +58,7 @@ class Container(object): return container.OriginFeatures elif container.hasExtension('App::GroupExtension'): return [] - elif container.hasChildElement(): + elif container.hasChildElement(): # Link return [] raise RuntimeError("getStaticChildren: unexpected container type!") @@ -102,7 +102,7 @@ class Container(object): return True #Document is a special thing... is it a CS or not is a matter of coding convenience. elif container.hasExtension('App::GeoFeatureGroupExtension'): return True - elif container.hasElement(): + elif container.hasChildElement(): # Link return True else: return False @@ -118,7 +118,7 @@ class Container(object): return True elif container.isDerivedFrom('App::Origin'): return True - elif container.hasChildElement(): + elif container.hasChildElement(): # Link return True else: return False @@ -180,11 +180,13 @@ def _getMetacontainerChildren(container, isrightcontainer_func): -def isAContainer(obj): - '''isAContainer(obj): returns True if obj is an object container, such as +def isAContainer(obj, links_too = False): + '''isAContainer(obj, links_too): returns True if obj is an object container, such as Group, Part, Body. The important characterisic of an object being a container is that it can be activated to receive new objects. Documents - are considered containers, too.''' + are considered containers, too. + If links_too, App::Link objects are considered containers, too. Then, container tree + isn't necessarily a tree.''' if obj.isDerivedFrom('App::Document'): return True @@ -193,7 +195,7 @@ def isAContainer(obj): if obj.isDerivedFrom('App::Origin'): return True if obj.hasChildElement(): - return True + return True if links_too else False return False #from Part-o-magic... diff --git a/src/Mod/Show/DepGraphTools.py b/src/Mod/Show/DepGraphTools.py index ad1a91faa5..41f4cb2777 100644 --- a/src/Mod/Show/DepGraphTools.py +++ b/src/Mod/Show/DepGraphTools.py @@ -64,25 +64,3 @@ def getAllDependent(feat): 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 visibility nesting.''' - - if obj.hasChildElement(): - return True - if obj.hasExtension('App::OriginGroupExtension'): - return True - if obj.hasExtension('App::GroupExtension'): - return True - if obj.isDerivedFrom('App::Origin'): - return True - return False - -def isContainer(obj): - if _isContainer(obj): - return True - linked = obj.getLinkedObject() - return linked and linked!=obj and _isContainer(linked) - diff --git a/src/Mod/Show/SceneDetail.py b/src/Mod/Show/SceneDetail.py new file mode 100644 index 0000000000..73333589d2 --- /dev/null +++ b/src/Mod/Show/SceneDetail.py @@ -0,0 +1,78 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2019 * +# * * +# * 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 * +# * * +# ***************************************************************************/ + +class SceneDetail(object): + """SceneDetail class: abstract class for tempovis scene save/restore plug-in. An implementation must provide: + * data storage (as "data" attribute of the object) + * constructor (preferably, with value for stored data as optional argument) + * methods to apply values to actual scene (apply_data), + * ...and to read out the state of the detail in the actual scene (scene_value) + * keying, for identifying two detail instances that affect the exact same thing + * class_id string, which is required for keying + * copying + * test for equality, that checks if .data attributes of two SceneDetail instances are equal + * info on if the modification affects what is saved to disk, and thus should be undone temporarily for file writing. + """ + class_id = '' + + data = None + doc = None + + ## Storage field for TV. Mild restore means that the saved state won't be restored + # if the current state differs from the state requested via last TempoVis.modify(...) call. + # This is a default, it may be changed per-instance. + mild_restore = False + + def set_doc(self, doc): + self.doc = doc + + # + key = None #a string or something alike to use to store/find the entry in TempoVis. For example, a string "{object_name}.{property_name}". + affects_persistence = False #True indicate that the changes will be recorded if the doc is saved, and that this detail should be restored for saving + + def scene_value(self): + """scene_value(): returns the value from the scene""" + raise NotImplementedError() + + def apply_data(self, val): + """apply a value to scene""" + raise NotImplementedError() + + ## Equality test. Override if data attributes can't be directly compared + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.data == other.data and self.data is not None + else: + raise TypeError('{self} can\'t be compared with {other}' + .format(self= repr(self), other= repr(other))) + # + + # + @property + def full_key(self): + return (self.class_id, self.doc.Name if self.doc else None, self.key) + + + def __ne__(self, other): + return not self.__eq__(other) + \ No newline at end of file diff --git a/src/Mod/Show/SceneDetails/Camera.py b/src/Mod/Show/SceneDetails/Camera.py new file mode 100644 index 0000000000..1b2325fc43 --- /dev/null +++ b/src/Mod/Show/SceneDetails/Camera.py @@ -0,0 +1,48 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2019 * +# * * +# * 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 Show.SceneDetail import SceneDetail + +import FreeCADGui + +class Camera(SceneDetail): + """Camera(doc): TempoVis plugin for saving and restoring camera.""" + class_id = 'SDCamera' + + def __init__(self, doc): + self.doc = doc + self.key = 'the_cam' + + def _viewer(self): + gdoc = FreeCADGui.getDocument(self.doc.Name) + v = gdoc.activeView() + if not hasattr(v, 'getCamera'): + v = gdoc.mdiViewsOfType('Gui::View3DInventor')[0] + return v + + def scene_value(self): + return self._viewer().getCamera() + + def apply_data(self, val): + self._viewer().setCamera(val) + diff --git a/src/Mod/Show/SceneDetails/ClipPlane.py b/src/Mod/Show/SceneDetails/ClipPlane.py new file mode 100644 index 0000000000..0d661b62f7 --- /dev/null +++ b/src/Mod/Show/SceneDetails/ClipPlane.py @@ -0,0 +1,64 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2019 * +# * * +# * 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 Show.SceneDetail import SceneDetail +import FreeCAD as App +import FreeCADGui as Gui + +class ClipPlane(SceneDetail): + """ClipPlane(document, enable = None, placement = None, offset = 0.0): + TempoVis plugin for applying clipping plane to whole project except edit root. + enable can be 0 for disable, 1 for enable, and -1 for toggle + (FIXME: toggle value support is a hack for while we can't read out the current state).""" + + class_id = 'SDClipPlane' + key = '' + + def __init__(self, document, enable = None, placement = None, offset = 0.0): + self.doc = document + if enable is not None: + if placement is not None and offset != 0.0: + placement = placement.copy() + dir = placement.multVec(App.Vector(0,0,1)) + placement.Base = placement.Base + dir*offset + self.data = (enable, placement) + + def scene_value(self): + return (0, None) #hack. For until there is a way to easily query the plane, this should be good enough. + + def apply_data(self, val): + enable, pla = val + if enable != 0: + self._viewer().toggleClippingPlane(enable, pla= pla) + else: + self._viewer().toggleClippingPlane(enable) + + def _viewer(self): + if self.doc is None: + gdoc = Gui.editDocument() + else: + gdoc = Gui.getDocument(self.doc.Name) + v = gdoc.activeView() + if not hasattr(v, 'toggleClippingPlane'): + v = gdoc.mdiViewsOfType('Gui::View3DInventor')[0] + return v diff --git a/src/Mod/Show/SceneDetails/ObjectClipPlane.py b/src/Mod/Show/SceneDetails/ObjectClipPlane.py new file mode 100644 index 0000000000..569e5edc0a --- /dev/null +++ b/src/Mod/Show/SceneDetails/ObjectClipPlane.py @@ -0,0 +1,107 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2019 * +# * * +# * 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 Show.SceneDetail import SceneDetail + +import FreeCAD as App + +class ObjectClipPlane(SceneDetail): + """ObjectClipPlane(object, enable = None, placement = None, offset = 0.0): + Plugin for TempoVis for adding clipping planes to individual objects.""" + class_id = 'SDObjectClipPlane' + propname = '' + objname = '' + + def __init__(self, object, enable = None, placement = None, offset = 0.0): + self.objname = object.Name + self.doc = object.Document + self.key = self.objname + if enable is not None: + self.data = self.val(enable, placement, offset) + + def scene_value(self): + vp = self.doc.getObject(self.objname).ViewObject + cp = getClipPlaneNode(vp, make_if_missing= False) + if cp is None: + return self.val(False) + else: + enable = cp.on.getValue() + pln = cp.plane + D = pln.getDistanceFromOrigin() + normal = tuple(pln.getNormal()) + return enable, (normal, D) + + def apply_data(self, val): + enable, pldef = val + vp = self.doc.getObject(self.objname).ViewObject + cp = getClipPlaneNode(vp, make_if_missing= True if enable else False) + if cp is None and not enable: + return + if enable: + from pivy import coin + v, d = pldef + cp.plane.setValue(coin.SbPlane(coin.SbVec3f(*v), d)) + cp.on.setValue(enable) + + def val(self, enable, placement = None, offset = 0.0): + """val(enable, placement = None, offset = 0.0): constructs a value from convenient + parameters. Placement is in global CS. The cutting will be by XY plane of Placement; + the stuff in negative Z is visible and the stuff in positive Z is invisible.""" + + pldef = None + if enable: + obj = self.doc.getObject(self.objname) + plm_cs = obj.getGlobalPlacement().multiply(obj.Placement.inverse()) # placement of CS the object is in + plm_plane = plm_cs.inverse().multiply(placement) + pldef = placement2plane(plm_plane, offset) + return (enable, pldef if enable else None) + +def getClipPlaneNode(viewprovider, make_if_missing = True): + from pivy import coin + sa = coin.SoSearchAction() + sa.setType(coin.SoClipPlane.getClassTypeId()) + sa.setName('TVClipPlane') + sa.traverse(viewprovider.RootNode) + if sa.isFound() and sa.getPath().getLength() == 1: + return sa.getPath().getTail() + elif not sa.isFound(): + if not make_if_missing: + return None + clipplane = coin.SoClipPlane() + viewprovider.RootNode.insertChild(clipplane, 0) + clipplane.setName('TVClipPlane') + clipplane.on.setValue(False) #make sure the plane is not activated by default + return clipplane + +def placement2plane(placement, offset): + """returns tuple (normal, D) for making coin plane.""" + normal = placement.Rotation.multVec(App.Vector(0,0,-1)) + D = placement.Base * normal - offset + return tuple(normal), D + +def clipPlane(obj, enable, placement = None, offset = 0, tv = None): + if tv is None: + from Show.TempoVis import TempoVis + tv = TempoVis(obj.Document) + tv.modify(ClipPlane(obj, enable, placement, offset)) + return tv diff --git a/src/Mod/Show/SceneDetails/Pickability.py b/src/Mod/Show/SceneDetails/Pickability.py new file mode 100644 index 0000000000..0dd668b1b9 --- /dev/null +++ b/src/Mod/Show/SceneDetails/Pickability.py @@ -0,0 +1,81 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2019 * +# * * +# * 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 Show.SceneDetail import SceneDetail + +class Pickability(SceneDetail): + """Pickability(object, pickstyle = None):Plugin for TempoVis for altering pick style + of objects (i.e., selectability). + pickstyle may be: + PS_REGULAR = 0 # selectable + PS_BOUNDBOX = 1 # selectable, but faster hit testing using bounding box + PS_UNPICKABLE = 2 # not selectable and not obstructing.""" + class_id = 'SDPickability' + propname = '' + objname = '' + + def __init__(self, object, pickstyle = None): + self.objname = object.Name + self.doc = object.Document + self.key = self.objname + if pickstyle is not None: + self.data = pickstyle + + def scene_value(self): + return getPickStyle(self.doc.getObject(self.objname).ViewObject) + + def apply_data(self, val): + setPickStyle(self.doc.getObject(self.objname).ViewObject, val) + +PS_REGULAR = 0 +PS_BOUNDBOX = 1 +PS_UNPICKABLE = 2 + +def getPickStyleNode(viewprovider, make_if_missing = True): + from pivy import coin + sa = coin.SoSearchAction() + sa.setType(coin.SoPickStyle.getClassTypeId()) + sa.traverse(viewprovider.RootNode) + if sa.isFound() and sa.getPath().getLength() == 1: + return sa.getPath().getTail() + else: + if not make_if_missing: + return None + pick_style = coin.SoPickStyle() + pick_style.style.setValue(coin.SoPickStyle.SHAPE) + viewprovider.RootNode.insertChild(pick_style, 0) + return pick_style + + +def getPickStyle(viewprovider): + ps = getPickStyleNode(viewprovider, make_if_missing= False) + if ps is not None: + return ps.style.getValue() + else: + return PS_REGULAR + +def setPickStyle(viewprovider, pickstyle): + ps = getPickStyleNode(viewprovider, make_if_missing= pickstyle != 0) #coin.SoPickStyle.SHAPE + if ps is not None: + return ps.style.setValue(pickstyle) + diff --git a/src/Mod/Show/SceneDetails/VProperty.py b/src/Mod/Show/SceneDetails/VProperty.py new file mode 100644 index 0000000000..d404a71275 --- /dev/null +++ b/src/Mod/Show/SceneDetails/VProperty.py @@ -0,0 +1,48 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2019 * +# * * +# * 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 Show.SceneDetail import SceneDetail + +class VProperty(SceneDetail): + """VProperty(object, propname, val = None): plugin for TempoVis to alter ViewProvider properties""" + + class_id = 'SDVProperty' + affects_persistence = True + propname = '' + objname = '' + mild_restore = True + + def __init__(self, object, propname, val = None): + self.objname = object.Name + self.propname = propname + self.doc = object.Document + self.key = self.objname + '.' + self.propname + self.data = val + if propname == 'LinkVisibility': #seems to not be a property + self.affects_persistence = False + + def scene_value(self): + return getattr(self.doc.getObject(self.objname).ViewObject, self.propname) + + def apply_data(self, val): + setattr(self.doc.getObject(self.objname).ViewObject, self.propname, val) diff --git a/src/Mod/Show/FrozenClass.py b/src/Mod/Show/SceneDetails/Workbench.py similarity index 73% rename from src/Mod/Show/FrozenClass.py rename to src/Mod/Show/SceneDetails/Workbench.py index 1fa76fb5fc..11929d6f70 100644 --- a/src/Mod/Show/FrozenClass.py +++ b/src/Mod/Show/SceneDetails/Workbench.py @@ -1,6 +1,6 @@ #/*************************************************************************** # * Copyright (c) Victor Titov (DeepSOIC) * -# * (vv.titov@gmail.com) 2016 * +# * (vv.titov@gmail.com) 2019 * # * * # * This file is part of the FreeCAD CAx development system. * # * * @@ -21,17 +21,24 @@ # * * # ***************************************************************************/ -# 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) +from Show.SceneDetail import SceneDetail - def _freeze(self): - self.__isfrozen = True +import FreeCADGui + +class Workbench(SceneDetail): + """Workbench(wb = None): Plugin for TempoVis for changing active workbench. + wb: string, a name of a workbench (e.g. 'SketcherWorkbench')""" + class_id = 'SDWorkbench' + mild_restore = True + + def __init__(self, wb = None): + self.key = 'workbench' + if wb is not None: + self.data = wb + + def scene_value(self): + return FreeCADGui.activeWorkbench().name() + + def apply_data(self, val): + FreeCADGui.activateWorkbench(val) - def _unfreeze(self): - self.__isfrozen = False diff --git a/src/Mod/Show/SceneDetails/__init__.py b/src/Mod/Show/SceneDetails/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Mod/Show/ShowUtils.py b/src/Mod/Show/ShowUtils.py new file mode 100644 index 0000000000..de22e24b38 --- /dev/null +++ b/src/Mod/Show/ShowUtils.py @@ -0,0 +1,37 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2019 * +# * * +# * 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 * +# * * +# ***************************************************************************/ + +def is3DObject(obj): + """is3DObject(obj): tests if the object has some 3d geometry. + TempoVis is made only for objects in 3d view, so all objects that don't pass this check are ignored by TempoVis.""" + + # See "Gui Problem Sketcher and TechDraw" https://forum.freecadweb.org/viewtopic.php?f=3&t=22797 + + # observation: all viewproviders have transform node, then a switch node. If that switch node contains something, the object has something in 3d view. + try: + from pivy import coin + return obj.ViewObject.SwitchNode.getNumChildren()>0 + except Exception as err: + import FreeCAD as App + App.Console.PrintWarning(u"Show.TempoVis.is3DObject error: {err}\n".format(err= str(err))) + return True #assume. diff --git a/src/Mod/Show/TVObserver.py b/src/Mod/Show/TVObserver.py new file mode 100644 index 0000000000..1f7ccd0a8c --- /dev/null +++ b/src/Mod/Show/TVObserver.py @@ -0,0 +1,48 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2019 * +# * * +# * 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 . import TVStack +import FreeCAD + +class TVObserver(object): + def __init__(self): + FreeCAD.addDocumentObserver(self) + + def stop(self): + FreeCAD.removeDocumentObserver(self) + + def slotStartSaveDocument(self, doc, filepath): + TVStack._slotStartSaveDocument(doc) + + def slotFinishSaveDocument(self, doc, filepath): + TVStack._slotFinishSaveDocument(doc) + + def slotDeletedDocument(self, doc): + from . import TVStack + TVStack._slotDeletedDocument(doc) + +#handle module reload +if 'observer_singleton' in vars(): + observer_singleton.stop() + +observer_singleton = TVObserver() diff --git a/src/Mod/Show/TVStack.py b/src/Mod/Show/TVStack.py new file mode 100644 index 0000000000..5b536546d7 --- /dev/null +++ b/src/Mod/Show/TVStack.py @@ -0,0 +1,192 @@ +#/*************************************************************************** +# * Copyright (c) Victor Titov (DeepSOIC) * +# * (vv.titov@gmail.com) 2019 * +# * * +# * 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 . import TempoVis +from . import TVObserver #import to start the observer + +import weakref + +global_stacks = {} # dict of TVStacks. key = document name. + +class TVStack(object): + index_LUT = None # Key = id(tempovis_instance). Value = index in the stack. + stack = None #list of weakrefs to TV instances. Using weakrefs, so that TempoVis can self-destruct if forgotten. + document = None + + _rewind_tv = None + + def __init__(self, document): + self.document = None + self.index_LUT = {} + self.stack = [] + + def insert(self, tv, index = None): + if index is None: + index = len(self.stack) + idtv = id(tv) + ref = weakref.ref(tv, (lambda _, idtv=idtv, self=self : self._destruction(idtv))) + self.stack.insert(index, ref) + + self.rebuild_index(index) + + tv._inserted(self, index) + + def _destruction(self, idtv): + # the tempovis itself is destroyed already. Purge it from the stack (it should have withdrawn itself, but just in case). + try: + index = self.index_LUT.get(idtv) + except KeyError: + #already withdrawn + pass + else: + self.stack.pop(index) + self.index_LUT.pop(idtv) + self.rebuild_index(index) + + def withdraw(self, tv): + idtv = id(tv) + index = self.index_LUT[idtv] + ref = self.stack.pop(index) + self.index_LUT.pop(idtv) + + self.rebuild_index(index) + + tv = ref() + if tv: + tv._withdrawn(self, index) + + def value_after(self, tv, detail): + """value_after(tv, detail): returns tuple (tv1, detail), or None. + Here, tv1 is the tv that remembers the value, and detail is reference to recorded + data in tv1. None is returned, if no TVs in the stack after the provided one have + recorded a change to this detail. + + tv can be None, then, the function returns the original value of the detail, or + None, if the current value matches the original.""" + + index = self.index_LUT[id(tv)] if tv is not None else -1 + for tvref in self.stack[index + 1 : ]: + tv = tvref() + if tv.state == TempoVis.S_ACTIVE: + if tv.has(detail): + return (tv, tv.data[detail.full_key]) + return None + + def rebuild_index(self, start = 0): + if start == 0: + self.index_LUT = {} + for i in range(start, len(self.stack)): + self.index_LUT[id(self.stack[i]())] = i + + + def purge_dead(self): + """removes dead TV instances from the stack""" + n = 0 + for i in reversed(range(len(self.stack))): + if self.stack[i]() is None: + self.stack.pop(i) + n += 1 + if n > 0: + self.rebuild_index() + return n + + def dissolve(self): + """silently cleans all TVs, so that they won't restore.""" + for ref in self.stack: + if ref() is not None: + ref().forget() + + def unwindForSaving(self): + self.rewindAfterSaving() #just in case there was a failed save before. + + details = {} #dict of detail original values. Key = detail key; value = detail instance with data representing the original value + for ref in self.stack: + tv = ref() + for key, detail in tv.data.items(): + if not key in details: + if detail.affects_persistence: + details[detail.full_key] = detail + + self._rewind_tv = TempoVis.TempoVis(self.document, None) + for key, detail in details.items(): + self._rewind_tv.modify(detail) + + def rewindAfterSaving(self): + if self._rewind_tv is not None: + self._rewind_tv.restore() + self._rewind_tv = None + + def getSplitSequence(self, tv): + """getSplitSequence(tv): returns (list_before, list_after), neither list includes tv.""" + index = self.index_LUT[id(tv)] + def deref(lst): + return [ref() for ref in lst] + return deref(self.stack[0:index]), deref(self.stack[index+1:]) + + def __getitem__(self, index): + return self.stack[index]() + + def __len__(self): + return len(self.stack) + + def __iter__(self): + for ref in self.stack: + yield ref() + + def __reversed__(self): + for ref in reversed(self.stack): + yield ref() + + def restoreAll(self): + for ref in reversed(self.stack): + ref().restore() + + def byTag(self, tag): + return [ref() for ref in self.stack if ref().tag == tag] + + +def mainStack(document, create_if_missing = True): + """mainStack(document, create_if_missing = True):returns the main TVStack instance for provided document""" + docname = document.Name + + if create_if_missing: + if not docname in global_stacks: + global_stacks[docname] = TVStack(document) + + return global_stacks.get(docname, None) + +def _slotDeletedDocument(document): + docname = document.Name + stk = global_stacks.pop(docname, None) + if stk is not None: + stk.dissolve() + +def _slotStartSaveDocument(doc): + stk = mainStack(doc, create_if_missing= False) + if stk is not None: + stk.unwindForSaving() + +def _slotFinishSaveDocument(doc): + stk = mainStack(doc, create_if_missing= False) + if stk is not None: + stk.rewindAfterSaving() diff --git a/src/Mod/Show/TempoVis.py b/src/Mod/Show/TempoVis.py index 54179b9d39..dd063f6009 100644 --- a/src/Mod/Show/TempoVis.py +++ b/src/Mod/Show/TempoVis.py @@ -21,96 +21,269 @@ # * * # ***************************************************************************/ -import FreeCAD as App -if App.GuiUp: - import FreeCADGui as Gui -from .FrozenClass import FrozenClass - -from .DepGraphTools import getAllDependencies, getAllDependent, isContainer +from .DepGraphTools import getAllDependencies, getAllDependent +from .ShowUtils import is3DObject from . import Containers Container = Containers.Container -class TempoVis(FrozenClass): +from . import TVStack + +import FreeCAD as App +if App.GuiUp: + import FreeCADGui as Gui +Wrn = lambda msg: App.Console.PrintWarning(msg + "\n") +Err = lambda msg: App.Console.PrintError(msg + "\n") +Log = lambda msg: App.Console.PrintLog(msg + "\n") + +from copy import copy + +S_EMPTY = 0 # TV is initialized, but no changes were done through it +S_ACTIVE = 1 # TV has something to be undone +S_RESTORED = 2 # TV has been restored +S_INTERNAL = 3 # TV instance is being used by another TV instance as a redo data storage + +def _printTraceback(err): + import sys + if err is sys.exc_info()[1]: + import traceback + tb = traceback.format_exc() + Log(tb) + +class MAINSTACK(object): + '''it's just a default value definition for TV constructor''' + pass +class JUST_SAVE(object): + '''it's just a default value meaning "save current scene value but don't modify anything"''' + pass + +class TempoVis(object): '''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.''' + TempoVis(document, stack = MAINSTACK, **kwargs): creates a new TempoVis. + + document: required. Objects not belonging to the document can't be modified via TempoVis. + + stack: optional. Which stack to insert this new TV into. Can be: + a TVStack instance (then, the new TV is added to the top of the stack), + MAINSTACK special value (a global stack for the document will be used), or + None (then, the TV is not in any stack, and can be manually instertd into one if desired). + + Any additional keyword args are assigned as attributes. You can use it to immediately set a tag, for example.''' - def __define_attributes(self): - self.data = {} # dict. key = ("Object","Property"), value = original value of the property - self.data_pickstyle = {} # dict. key = "Object", value = original value of pickstyle - self.data_clipplane = {} # dict. key = "Object", value = original state of plane-clipping - self.clippingDoc = None + document = None + stack = None # reference to stack this TV is in - self.sketch_clipplane_on = False #True if some clipping planes are active + data = None # dict. key = ("class_id","key"), value = instance of SceneDetail + data_requested = None #same as data, but stores (wanted) values passed to modify() + + state = S_EMPTY + + tag = '' #stores any user-defined string for identification purposes + + def _init_attrs(self): + '''initialize member variables to empty values (needed because we can't use mutable initial values when initializing member variables in class definition)''' + self.data = {} + self.data_requested = {} - self.cam_string = "" # inventor ASCII string representing the camera - self.viewer = None # viewer the camera is saved from - - self.document = None - self.restore_on_delete = False # if true, restore() gets called upon object deletion. It becomes False after explicit call to Restore, and set to true by many methods. - - self.links_are_lost = False # set to true after restore from JSON. Indicates to attempt to use ActiveDocument/ActiveViewer instead. - - self._freeze() - - def __init__(self, document): - self.__define_attributes() - + # + def __init__(self, document, stack = MAINSTACK, **kwargs): + self._init_attrs() self.document = document - - @staticmethod - def is3DObject(obj): - """is3DObject(obj): tests if the object has some 3d geometry. - TempoVis is made only for objects in 3d view, so all objects that don't pass this check are ignored by TempoVis.""" - # See "Gui Problem Sketcher and TechDraw" https://forum.freecadweb.org/viewtopic.php?f=3&t=22797 - - # observation: all viewproviders have transform node, then a switch node. If that switch node contains something, the object has something in 3d view. - try: - from pivy import coin - return obj.ViewObject.SwitchNode.getNumChildren()>0 - except Exception as err: - App.Console.PrintWarning(u"Show.TempoVis.isIn3DObject error: {err}\n".format(err= str(err))) - return True #assume. + if stack is MAINSTACK: + stack = TVStack.mainStack(document) + + if stack is None: + pass + else: + stack.insert(self) + + for key,val in kwargs.items(): + setattr(self, key, val) - def modifyVPProperty(self, doc_obj_or_list, prop_names, new_value): - '''modifyVPProperty(self, doc_obj_or_list, prop_name, new_value): modifies + def __del__(self): + if self.state == S_ACTIVE: + self.restore(ultimate= True) + + def has(self, detail): + '''has(self, detail): returns True if this TV has this detail value saved. + example: tv.has(VProperty(obj, "Visibility"))''' + return detail.full_key in self.data + + def stored_val(self, detail): + '''stored_val(self, detail): returns value of detail remembered by this TV. If not, raises KeyError.''' + return self.data[detail.full_key].data + + def save(self, detail, mild_restore = False): + '''save(detail, mild_restore = False):saves the scene detail to be restored. + The detail is saved only once; repeated calls are ignored. + mild_restore: internal, do not use.''' + self._change() + if not detail.full_key in self.data: + #not saved yet + tv1, curr = self._value_after(detail, query_scene= True) + self.data[detail.full_key] = copy(curr) + self.data[detail.full_key].mild_restore = mild_restore + else: + #saved already. Change restore policy, if necessary. + stored_dt = self.data[detail.full_key] + if not mild_restore: + stored_dt.mild_restore = False + + def modify(self, detail, mild_restore = None): + '''modify(detail, mild_restore = True): modifies scene detail through this TV. + The value is provided as an instance of SceneDetail implementation. + The procedure takes care to account for the stack - that is, if in a TV applied + later than this one this detail was changed too, the value saved therein is altered, + rather than applied to the scene. + + mild_restore: if True, when restoring later, checks if the value was changed + by user after last call to modify(), and doesn't restore if it was changed. + + Example: tv.modify(VProperty(obj, "Visibility", True))''' + + self._change() + + if mild_restore is not None: + detail.mild_restore = mild_restore + + # save current + self.save(detail, detail.mild_restore) + + # apply + tv1, curr = self._value_after(detail) + if tv1 is not None: + tv1.data[detail.full_key].data = detail.data + else: + detail.apply_data(detail.data) + + # and record. + if detail.mild_restore: + self.data_requested[detail.full_key] = copy(detail) + + def restoreDetail(self, detail, ultimate = False): + '''restoreDetail(detail, ultimate = False): restores a specific scene detail. + ultimate: if true, the saved value is cleaned out. + If the detail is not found, nothing is done. + ''' + if not self.has(detail): + return + self._restore_detail(detail) + if ultimate: + self.forgetDetail(detail) + + + def forgetDetail(self, detail): + '''forgetDetail(detail): ditches a saved detail value, making the change done through this TV permanent.''' + self.data.pop(detail.full_key, None) + self.data_requested.pop(detail.full_key, None) + + def forget(self): + '''forget(self): clears this TV, making all changes done through it permanent. + Also, withdraws the TV from the stack.''' + self.state = S_EMPTY + self.data = {} + if self.is_in_stack: + self.stack.withdraw(self) + + def restore(self, ultimate = True): + '''restore(ultimate = True): undoes all changes done through this tempovis / restores saved scene details. + ultimate: if true, the saved values are cleaned out, and the TV is withdrawn from + the stack. If false, the TV will still remember stuff, and resore can be called again. + ''' + if self.state == S_RESTORED: + return + + if self.state != S_INTERNAL and ultimate: + self.state = S_RESTORED + + for key, detail in self.data.items(): + try: + self._restoreDetail(detail) + except Exception as err: + Err("TempoVis.restore: failed to restore detail {key}: {err}".format(key= key, err= str(err))) + _printTraceback(err) + if ultimate: + self.data = {} + if self.is_in_stack: + self.stack.withdraw(self) + + # + + # + def _inserted(self, stack, index): + '''calles when this tv is inserted into a stack''' + self.stack = stack + def _withdrawn(self, stack, index): + '''calles when this tv is withdrawn from a stack''' + self.stack = None + @property + def is_in_stack(self): + return self.stack is not None + # + + # + def modifyVPProperty(self, doc_obj_or_list, prop_names, new_value = JUST_SAVE, mild_restore = None): + '''modifyVPProperty(doc_obj_or_list, prop_names, new_value = JUST_SAVE, mild_restore = None): 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().''' + TempoVis deletion, or call to restore(). + + mild_restore: test if user changed the value manually when restoring the TV.''' + + if self.state == S_RESTORED: + warn("Attempting to use a TV that has been restored. There must be a problem with code.") + return - if App.GuiUp: - if not hasattr(doc_obj_or_list, '__iter__'): - doc_obj_or_list = [doc_obj_or_list] - if not isinstance(prop_names,(list,tuple)): - prop_names = [prop_names] - for doc_obj in doc_obj_or_list: - if not self.is3DObject(doc_obj): + if not hasattr(doc_obj_or_list, '__iter__'): + doc_obj_or_list = [doc_obj_or_list] + if not isinstance(prop_names,(list,tuple)): + prop_names = [prop_names] + for doc_obj in doc_obj_or_list: + for prop_name in prop_names: + if not hasattr(doc_obj.ViewObject, prop_name): + warn("TempoVis: object {obj} has no attribute {attr}. Skipped." + .format(obj= doc_obj.Name, attr= prop_name)) continue - for prop_name in prop_names: - if not hasattr(doc_obj.ViewObject, prop_name): - App.Console.PrintWarning("TempoVis: object {obj} has no attribute {attr}. Skipped.\n" - .format(obj= doc_obj.Name, attr= prop_name)) - continue # silently ignore if object doesn't have the property... - # Because the introduction of external objects, we shall now - # accept objects from all opening documents. - # - # 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) - if new_value is not None: - setattr(doc_obj.ViewObject, prop_name, new_value) - self.restore_on_delete = True - key = (doc_obj.Name,prop_name,doc_obj.Document.Name) - self.data.setdefault(key,oldval) + # Because the introduction of external objects, we shall now + # accept objects from all opened documents. + # + # 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.") + from .SceneDetails.VProperty import VProperty + if new_value is JUST_SAVE: + if mild_restore: + Wrn("TempoVis: can't just save a value for mild restore. Saving for hard restore.") + self.save(VProperty(doc_obj, prop_name, new_value)) + else: + self.modify(VProperty(doc_obj, prop_name, new_value), mild_restore) - def saveBodyVisibleFeature(self,doc_obj_or_list): + def restoreVPProperty(self, doc_obj_or_list, prop_names): + '''restoreVPProperty(doc_obj_or_list, prop_name, new_value): restores specific property changes.''' + from .SceneDetails.VProperty import VProperty + + if not hasattr(doc_obj_or_list, '__iter__'): + doc_obj_or_list = [doc_obj_or_list] + if not isinstance(prop_names,(tuple,list)): + prop_names = [prop_names] + for doc_obj in doc_obj_or_list: + for prop_name in prop_names: + try: + self.restoreDetail(VProperty(doc_obj, prop_name)) + except Exception as err: + Err("TempoVis.restore: failed to restore detail {key}: {err}".format(key= key, err= str(err))) + _printTraceback(err) + + + def saveBodyVisibleFeature(self, doc_obj_or_list): + """saveBodyVisibleFeature(self, doc_obj_or_list): saves Visibility of currently + visible feature, for every body of PartDesign features in the provided list.""" if not hasattr(doc_obj_or_list, '__iter__'): doc_obj_or_list = [doc_obj_or_list] objs = [] @@ -123,173 +296,76 @@ class TempoVis(FrozenClass): feature = getattr(body,'VisibleFeature',None) if feature: objs.append(feature) - self.modifyVPProperty(objs, 'Visibility',None) + self.modifyVPProperty(objs, 'Visibility', JUST_SAVE) + return objs - 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.saveBodyVisibleFeature(doc_obj_or_list) - self.modifyVPProperty(doc_obj_or_list, ('Visibility','LinkVisibility'),True) + def show(self, doc_obj_or_list, links_too = True, mild_restore = None): + '''show(doc_obj_or_list, links_too = True): shows objects (sets their Visibility to True). + doc_obj_or_list can be a document object, or a list of document objects. + If links_too is True, all Links of the objects are also hidden, by setting LinkVisibility attribute of each object.''' + doc_obj_or_list = self._3D_objects(doc_obj_or_list) + self.saveBodyVisibleFeature(doc_obj_or_list) #fix implicit hiding of other features by PartDesign not being recorded to TV + self.modifyVPProperty(doc_obj_or_list, 'Visibility', True, mild_restore) + if links_too: + self.modifyVPProperty(doc_obj_or_list, 'LinkVisibility', True, mild_restore) - def hide(self, doc_obj_or_list): + def hide(self, doc_obj_or_list, links_too = True, mild_restore = None): '''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.saveBodyVisibleFeature(doc_obj_or_list) - self.modifyVPProperty(doc_obj_or_list, ('Visibility',"LinkVisibility"), False) + doc_obj_or_list = self._3D_objects(doc_obj_or_list) + # no need to saveBodyVisibleFeature here, as no implicit showing will happen + self.modifyVPProperty(doc_obj_or_list, 'Visibility', False, mild_restore) + if links_too: + self.modifyVPProperty(doc_obj_or_list, 'LinkVisibility', False, mild_restore) - def get_all_dependent(self, doc_obj, subname=None): - '''get_all_dependent(doc_obj): gets all objects that depend on doc_obj. Containers of the object are excluded from the list.''' + def get_all_dependent(self, doc_obj, subname = None): + '''get_all_dependent(doc_obj, subname = None): gets all objects that depend on doc_obj. Containers and Links (if subname) required for visibility of the object are excluded from the list.''' + from Containers import isAContainer if subname: + # a link-path was provided. doc_obj has nothing to do with the object we want + # to collect dependencies from. So, replace it with the one pointed by link-path. cnt_chain = doc_obj.getSubObjectList(subname) doc_obj = cnt_chain[-1].getLinkedObject() + # cnt_chain can either end with the object (e.g. if a sketch is in a part, and + # a link is to a part), or it may be a Link object (if we have a straight or + # even nested Link to the sketch). + # + # I don't know why do we need that isAContainer check here, but I'm leaving it, + # realthunder must be knowing his business --DeepSOIC cnt_chain = [ o for o in cnt_chain - if o==cnt_chain[-1] or isContainer(o) ] + if o==cnt_chain[-1] or isAContainer(o, links_too= True) ] else: cnt_chain = Containers.ContainerChain(doc_obj) return [o for o in getAllDependent(doc_obj) if not o in cnt_chain] 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( self.get_all_dependent(doc_obj) ) + self.hide(self._3D_objects(self.get_all_dependent(doc_obj))) 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) ) + self.show(self._3D_objects(getAllDependent(doc_obj))) def restore_all_dependent(self, doc_obj): '''show_all_dependent(doc_obj): restores original visibilities of all dependent objects.''' - self.restoreVPProperty( getAllDependent(doc_obj), ('Visibility','LinkVisibility') ) + self.restoreVPProperty( getAllDependent(doc_obj), ('Visibility', 'LinkVisibility') ) 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) ) + self.hide(self._3D_objects(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) ) + self.show(self._3D_objects(getAllDependencies(doc_obj))) - def saveCamera(self): - vw = Gui.ActiveDocument.ActiveView - self.cam_string = vw.getCamera() - self.viewer = vw + def saveCamera(self, vw = None): + self._change() + from .SceneDetails.Camera import Camera + self.save(Camera(self.document)) - self.restore_on_delete = True - - def restoreCamera(self): - if not self.cam_string: - return - vw = self.viewer - if self.links_are_lost: # can happen after save-restore - import FreeCADGui as Gui - vw = Gui.ActiveDocument.ActiveView - - vw.setCamera(self.cam_string) - - def restore(self): - '''restore(): restore all ViewProvider properties modified via TempoVis to their - original values, and saved camera, if any. Called automatically when instance is - destroyed, unless it was called explicitly. Should not raise exceptions.''' - - if self.links_are_lost: - self.document = App.ActiveDocument - self.viewer = Gui.ActiveDocument.ActiveView - self.links_are_lost = False - - for (obj_name, prop_name, doc_name), value in self.data.items(): - try: - setattr(App.getDocument(doc_name).getObject(obj_name).ViewObject,prop_name,value) - except Exception as err: - App.Console.PrintWarning("TempoVis: failed to restore {obj}.{prop}. {err}\n" - .format(err= err.message, - obj= obj_name, - prop= prop_name)) - - self.restoreUnpickable() - self.restoreClipPlanes() - - try: - self.restoreCamera() - except Exception as err: - App.Console.PrintWarning("TempoVis: failed to restore camera. {err}\n" - .format(err= err.message)) - self.restore_on_delete = False - - def restoreVPProperty(self, object_or_list, prop_names): - """restoreVPProperty(object_or_list, prop_name): restores original values of certain property for certain objects.""" - if App.GuiUp: - if not hasattr(object_or_list, '__iter__'): - object_or_list = [object_or_list] - if not isinstance(prop_names,(tuple,list)): - prop_names = [prop_names] - for doc_obj in object_or_list: - if not self.is3DObject(doc_obj): - continue - for prop_name in prop_names: - key = (doc_obj.Name, prop_name, doc_obj.Document.Name) - if key in self.data: - try: - setattr(doc_obj.ViewObject, prop_name, self.data[key]) - except Exception as err: - App.Console.PrintWarning("TempoVis: failed to restore {obj}.{prop}. {err}\n" - .format(err= err.message, - obj= doc_obj.Name, - prop= prop_name)) - - - def forget(self): - '''forget(): resets TempoVis''' - self.data = {} - self.data_pickstyle = {} - self.data_clipplane = {} - - self.cam_string = "" - self.viewer = None - - self.restore_on_delete = False - - def __del__(self): - if self.restore_on_delete: - self.restore() - - def __getstate__(self): - return (list(self.data.items()), - self.cam_string, - self.restore_on_delete) - - def __setstate__(self, state): - self.__define_attributes() - - items, self.cam_string, self.restore_on_delete = state - - # need to convert keys to tuples (dict doesn't accept list as key; tuples are converted to lists by json) - items = [(tuple(item[0]), item[1]) for item in items] - self.data = dict(items) - self.links_are_lost = True - - def _getPickStyleNode(self, viewprovider, make_if_missing = True): - from pivy import coin - sa = coin.SoSearchAction() - sa.setType(coin.SoPickStyle.getClassTypeId()) - sa.traverse(viewprovider.RootNode) - if sa.isFound() and sa.getPath().getLength() == 1: - return sa.getPath().getTail() - else: - if not make_if_missing: - return None - pick_style = coin.SoPickStyle() - pick_style.style.setValue(coin.SoPickStyle.SHAPE) - viewprovider.RootNode.insertChild(pick_style, 0) - return pick_style - - def _getPickStyle(self, viewprovider): - ps = self._getPickStyleNode(viewprovider, make_if_missing= False) - if ps is not None: - return ps.style.getValue() - else: - return 0 #coin.SoPickStyle.SHAPE - - def _setPickStyle(self, viewprovider, pickstyle): - ps = self._getPickStyleNode(viewprovider, make_if_missing= pickstyle != 0) #coin.SoPickStyle.SHAPE - if ps is not None: - return ps.style.setValue(pickstyle) + def restoreCamera(self, ultimate = False): + from .SceneDetails.Camera import Camera + dt = Camera(self.document) + self.restoreDetail(dt, ultimate) def setUnpickable(self, doc_obj_or_list, actual_pick_style = 2): #2 is coin.SoPickStyle.UNPICKABLE '''setUnpickable(doc_obj_or_list, actual_pick_style = 2): sets object unpickable (transparent to clicks). @@ -302,72 +378,16 @@ class TempoVis(FrozenClass): inserted as the very first node, and remains there even after restore()/deleting tempovis. ''' - if App.GuiUp: - if not hasattr(doc_obj_or_list, '__iter__'): - doc_obj_or_list = [doc_obj_or_list] - for doc_obj in doc_obj_or_list: - if not self.is3DObject(doc_obj): - continue - 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 = self._getPickStyle(doc_obj.ViewObject) - if actual_pick_style != oldval: - self._setPickStyle(doc_obj.ViewObject, actual_pick_style) - self.restore_on_delete = True - if doc_obj.Name not in self.data_pickstyle: - self.data_pickstyle[doc_obj.Name] = oldval - - def restoreUnpickable(self): - for obj_name in self.data_pickstyle: - try: - self._setPickStyle(self.document.getObject(obj_name).ViewObject, self.data_pickstyle[obj_name]) - except Exception as err: - App.Console.PrintWarning("TempoVis: failed to restore pickability of {obj}. {err}\n" - .format(err= err.message, - obj= obj_name)) - - def _getClipplaneNode(self, viewprovider, make_if_missing = True): - from pivy import coin - sa = coin.SoSearchAction() - sa.setType(coin.SoClipPlane.getClassTypeId()) - sa.setName('TVClipPlane') - sa.traverse(viewprovider.RootNode) - if sa.isFound() and sa.getPath().getLength() == 1: - return sa.getPath().getTail() - elif not sa.isFound(): - if not make_if_missing: - return None - clipplane = coin.SoClipPlane() - viewprovider.RootNode.insertChild(clipplane, 0) - clipplane.setName('TVClipPlane') - clipplane.on.setValue(False) #make sure the plane is not activated by default - return clipplane - - def _enableClipPlane(self, obj, enable, placement = None, offset = 0.0): - """Enables or disables clipping for an object. Placement specifies the plane (plane - is placement's XY plane), and should be in global CS. - Offset shifts the plane; positive offset reveals more material, negative offset - hides more material.""" - if not hasattr(obj, 'getGlobalPlacement'): - print(" {obj} has no attribute 'getGlobalPlacement'".format(obj= obj.Name)) - return #can't clip these yet... skip them for now. Example object: Draft Label - - node = self._getClipplaneNode(obj.ViewObject, make_if_missing= enable) - if node is None: - if enable: - App.Console.PrintError("TempoVis: failed to set clip plane to {obj}.\n".format(obj= obj.Name)) - return - if placement is not None: - from pivy import coin - plm_local = obj.getGlobalPlacement().multiply(obj.Placement.inverse()) # placement of CS the object is in - plm_plane = plm_local.inverse().multiply(placement) - normal = plm_plane.Rotation.multVec(App.Vector(0,0,-1)) - basepoint = plm_plane.Base + normal * (-offset) - normal_coin = coin.SbVec3f(*tuple(normal)) - basepoint_coin = coin.SbVec3f(*tuple(basepoint)) - node.plane.setValue(coin.SbPlane(normal_coin,basepoint_coin)) - node.on.setValue(enable) + from .SceneDetails.Pickability import Pickability + if not hasattr(doc_obj_or_list, '__iter__'): + doc_obj_or_list = [doc_obj_or_list] + for doc_obj in doc_obj_or_list: + if not is3DObject(doc_obj): + continue + dt = Pickability(doc_obj, actual_pick_style) + self.modify(dt) + def clipPlane(self, doc_obj_or_list, enable, placement, offset = 0.02): '''clipPlane(doc_obj_or_list, enable, placement, offset): slices off the object with a clipping plane. doc_obj_or_list: object or list of objects to alter (App) @@ -379,34 +399,20 @@ class TempoVis(FrozenClass): of this type as direct child, one is used. Otherwise, new one is created and inserted as the very first node. The node is left, but disabled when tempovis is restoring.''' - if App.GuiUp: - if not hasattr(doc_obj_or_list, '__iter__'): - doc_obj_or_list = [doc_obj_or_list] - for doc_obj in doc_obj_or_list: - if not self.is3DObject(doc_obj): - continue - # 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.") - print("Clipping {obj}".format(obj= doc_obj.Name)) - self._enableClipPlane(doc_obj, enable, placement, offset) - self.restore_on_delete = True - if doc_obj.Name not in self.data_pickstyle: - self.data_clipplane[doc_obj.Name] = False - - def restoreClipPlanes(self): - self.sketchClipPlane(None,False) - for obj_name in self.data_clipplane: - try: - self._enableClipPlane(self.document.getObject(obj_name), self.data_clipplane[obj_name]) - except Exception as err: - App.Console.PrintWarning("TempoVis: failed to remove clipplane for {obj}. {err}\n" - .format(err= err.message, - obj= obj_name)) - + from .SceneDetails.ObjectClipPlane import ObjectClipPlane + + if not hasattr(doc_obj_or_list, '__iter__'): + doc_obj_or_list = [doc_obj_or_list] + for doc_obj in doc_obj_or_list: + if not is3DObject(doc_obj): + continue + dt = ObjectClipPlane(doc_obj, enable, placement, offset) + self.modify(dt) + @staticmethod def allVisibleObjects(aroundObject): - """allVisibleObjects(aroundObject): returns list of objects that have to be toggled invisible for only aroundObject to remain. - If a whole container can be made invisible, it is returned, instead of its child objects.""" + '''allVisibleObjects(aroundObject): returns list of objects that have to be toggled invisible for only aroundObject to remain. + If a whole container can be made invisible, it is returned, instead of its child objects.''' chain = Containers.VisGroupChain(aroundObject) result = [] @@ -415,7 +421,7 @@ class TempoVis(FrozenClass): cnt_next = chain[i+1] if i+1 < len(chain) else aroundObject container = Container(cnt) for obj in container.getVisGroupChildren(): - if not TempoVis.is3DObject(obj): + if not is3DObject(obj): continue if obj is not cnt_next: if container.isChildVisible(obj): @@ -423,24 +429,100 @@ class TempoVis(FrozenClass): return result def sketchClipPlane(self, sketch, enable = None): - """sketchClipPlane(sketch, enable = None): Clips all objects by plane of sketch. - If enable argument is omitted, calling the routine repeatedly will toggle clipping plane.""" + '''sketchClipPlane(sketch, enable = None): Clips all objects by plane of sketch. + If enable argument is omitted, calling the routine repeatedly will toggle clipping plane.''' - editDoc = self.clippingDoc - if not editDoc: - editDoc = Gui.editDocument() - if editDoc: - self.clippingDoc = editDoc - toggle = 1 if enable else -1 if enable is None else 0 - editDoc.ActiveView.toggleClippingPlane( - toggle, pla=App.Placement(editDoc.EditingTransform)) - return - elif not sketch: + from .SceneDetails.ClipPlane import ClipPlane + + editDoc = Gui.editDocument() + if editDoc is None: + doc = sketch.Document + pla = sketch.getGlobalPlacement() + else: + doc = editDoc.Document + pla = App.Placement(editDoc.EditingTransform) + toggle = {False: 0, True: 1, None: -1}[enable] + + self.modify(ClipPlane(doc, toggle, pla, 0.02)) + + def activateWorkbench(self, wb_name): + from .SceneDetails.Workbench import Workbench + self.modify(Workbench(wb_name)) + + # + + # + def _restoreDetail(self, detail): + p = self.data[detail.full_key] + tv1, curr = self._value_after(detail, query_scene= p.mild_restore) + if p.mild_restore: + if self.data_requested[detail.full_key] != curr: + #the value on the scene doesn't match what was requested through TV. User probably changed it. We don't want to mess it up. + self._purge_milds(detail) + return + if tv1 is None: + # no other TV has changed this detail later, apply to the scene + detail.apply_data(p.data) + else: + #modify saved detail of higher TV + tv1.data[detail.full_key].data = p.data + + def _purge_milds(self, detail): + """_purge_milds(detail): wipes out detail from earlier TVs if the detail is mild-restore.""" + if not self.is_in_stack: return + seq_before, seq_after = self.stack.getSplitSequence(self) + for tv in reversed(seq_before): + if tv.has(detail): + if tv.data[detail.full_key].mild_restore: + tv.forgetDetail(detail) + else: + #hard-restoring value encountered, stop + break - if enable is None: - enable = not self.sketch_clipplane_on - self.sketch_clipplane_on = enable - self.clipPlane(self.allVisibleObjects(sketch), enable, sketch.getGlobalPlacement(), 0.02) + def _change(self): + '''to be called whenever anything is done that is to be restored later.''' + if self.state == S_EMPTY: + self.state = S_ACTIVE + if self.state == S_RESTORED: + Wrn("Attempting to use a TV that has been restored. There must be a problem with code.") + self.tv_redo = None + def _value_after(self, detail, query_scene = False): + '''_value_current(detail): returns (tv, detail1). SceneDetail instance holds "current" value of + scene detail (current from the context of this TV; i.e. either the current scene + status, or the saved state from upper TVs). + If no upper TV has saved the detail value, returns either (None, None), or + (None, detail1) if query_scene is True, where detail1 holds value from the scene.''' + def scene_value(): + if query_scene: + cpy = copy(detail) + cpy.data = cpy.scene_value() + return (None, cpy) + else: + return (None, None) + + if self.is_in_stack: + va = self.stack.value_after(self, detail) + if va is None: + return scene_value() + else: + return va + else: + return scene_value() + + def _3D_objects(self, doc_obj_or_list): + """_3D_objects(doc_obj_or_list): returns list of objects that are in 3d view.""" + if not hasattr(doc_obj_or_list, '__iter__'): + doc_obj_or_list = [doc_obj_or_list] + + return [obj for obj in doc_obj_or_list if is3DObject(obj)] + + def __getstate__(self): + return None + + def __setstate__(self, state): + self._init_attrs() + +