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)
This commit is contained in:
DeepSOIC
2019-08-24 02:23:39 +03:00
committed by Yorik van Havre
parent 7969979686
commit 6dcdc7fe22
15 changed files with 1144 additions and 362 deletions

View File

@@ -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
)
)

View File

@@ -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...

View File

@@ -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)

View File

@@ -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
# <interface>
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)))
# </interface>
# <utility>
@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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

37
src/Mod/Show/ShowUtils.py Normal file
View File

@@ -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.

View File

@@ -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()

192
src/Mod/Show/TVStack.py Normal file
View File

@@ -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()

View File

@@ -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()
#<core interface>
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)
#</core interface>
#<stack interface>
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
#</stack interface>
#<convenience functions>
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))
#</convenience functions>
#<internals>
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()