TempoVis: Clip Plane Support
This commit is contained in:
committed by
Yorik van Havre
parent
e4cea7f3ca
commit
1bf9da8a2a
174
src/Mod/Show/Containers.py
Normal file
174
src/Mod/Show/Containers.py
Normal file
@@ -0,0 +1,174 @@
|
||||
#/***************************************************************************
|
||||
# * Copyright (c) Victor Titov (DeepSOIC) *
|
||||
# * (vv.titov@gmail.com) 2018 *
|
||||
# * *
|
||||
# * 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 *
|
||||
# * *
|
||||
# ***************************************************************************/
|
||||
|
||||
#This is a temporary replacement for C++-powered Container class that should be eventually introduced into FreeCAD
|
||||
|
||||
class Container(object):
|
||||
"""Container class: a unified interface for container objects, such as Group, Part, Body, or Document.
|
||||
This is a temporary implementation"""
|
||||
Object = None #DocumentObject or Document, the actual container
|
||||
|
||||
def __init__(self, obj):
|
||||
self.Object = obj
|
||||
|
||||
def self_check(self):
|
||||
if self.Object is None:
|
||||
raise ValueError("Null!")
|
||||
if not isAContainer(self.Object):
|
||||
raise NotAContainerError(self.Object)
|
||||
|
||||
def getAllChildren(self):
|
||||
return self.getStaticChildren() + self.getDynamicChildren()
|
||||
|
||||
def getStaticChildren(self):
|
||||
self.self_check()
|
||||
container = self.Object
|
||||
|
||||
if container.hasExtension("App::OriginGroupExtension"):
|
||||
if container.Origin is not None:
|
||||
return [container.Origin]
|
||||
elif container.isDerivedFrom("App::Origin"):
|
||||
return container.OriginFeatures
|
||||
|
||||
def getDynamicChildren(self):
|
||||
self.self_check()
|
||||
container = self.Object
|
||||
|
||||
if container.isDerivedFrom("App::Document"):
|
||||
# find all objects not contained by any Part or Body
|
||||
result = set(container.Objects)
|
||||
for obj in container.Objects:
|
||||
if isAContainer(obj):
|
||||
children = set(getAllChildren(obj))
|
||||
result = result - children
|
||||
return result
|
||||
elif container.hasExtension("App::GroupExtension"):
|
||||
result = container.Group
|
||||
if container.hasExtension('App::GeoFeatureGroupExtension'):
|
||||
#geofeaturegroup's group contains all objects within the CS, we don't want that
|
||||
result = [obj for obj in result if obj.getParentGroup() is not container]
|
||||
return result
|
||||
elif container.isDerivedFrom("App::Origin"):
|
||||
return []
|
||||
raise RuntimeError("getDynamicChildren: unexpected container type!")
|
||||
|
||||
def isACS(self):
|
||||
"""isACS(): returns true if the container forms internal coordinate system."""
|
||||
self.self_check()
|
||||
container = self.Object
|
||||
|
||||
if container.isDerivedFrom("App::Document"):
|
||||
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
|
||||
else:
|
||||
return False
|
||||
|
||||
def getCSChildren(self):
|
||||
if not self.isACS():
|
||||
raise TypeError("Container is not a coordinate system")
|
||||
container = self.Object
|
||||
|
||||
if container.isDerivedFrom("App::Document"):
|
||||
result = set(container.Objects)
|
||||
for obj in container.Objects:
|
||||
if isAContainer(obj) and Container(obj).isACS():
|
||||
children = set(Container(obj).getCSChildren())
|
||||
result = result - children
|
||||
return result
|
||||
elif container.hasExtension('App::GeoFeatureGroupExtension'):
|
||||
return container.Group + self.getStaticChildren()
|
||||
else:
|
||||
assert(False)
|
||||
|
||||
def hasObject(self, obj):
|
||||
return obj in self.getAllChildren()
|
||||
|
||||
def isAContainer(obj):
|
||||
'''isAContainer(obj): 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.'''
|
||||
|
||||
if obj.isDerivedFrom('App::Document'):
|
||||
return True
|
||||
if obj.hasExtension('App::GroupExtension'):
|
||||
return True
|
||||
if obj.isDerivedFrom('App::Origin'):
|
||||
return True
|
||||
return False
|
||||
|
||||
#from Part-o-magic...
|
||||
def ContainerOf(obj):
|
||||
"""ContainerOf(obj): returns the container that immediately has obj."""
|
||||
cnt = None
|
||||
for dep in obj.InList:
|
||||
if isAContainer(dep):
|
||||
if Container(dep).hasObject(obj):
|
||||
if cnt is not None and dep is not cnt:
|
||||
raise ContainerTreeError("Container tree is not a tree")
|
||||
cnt = dep
|
||||
if cnt is None:
|
||||
return obj.Document
|
||||
return cnt
|
||||
|
||||
#from Part-o-magic... over-engineered, but proven to work
|
||||
def ContainerChain(feat):
|
||||
'''ContainerChain(feat): return a list of containers feat is in.
|
||||
Last container directly contains the feature.
|
||||
Example of output: [<document>,<SuperPart>,<Part>,<Body>]'''
|
||||
|
||||
if feat.isDerivedFrom('App::Document'):
|
||||
return []
|
||||
|
||||
list_traversing_now = [feat]
|
||||
set_of_deps = set()
|
||||
list_of_deps = []
|
||||
|
||||
while len(list_traversing_now) > 0:
|
||||
list_to_be_traversed_next = []
|
||||
for feat in list_traversing_now:
|
||||
for dep in feat.InList:
|
||||
if isAContainer(dep) and Container(dep).hasObject(feat):
|
||||
if not (dep in set_of_deps):
|
||||
set_of_deps.add(dep)
|
||||
list_of_deps.append(dep)
|
||||
list_to_be_traversed_next.append(dep)
|
||||
if len(list_to_be_traversed_next) > 1:
|
||||
raise ContainerTreeError("Container tree is not a tree")
|
||||
list_traversing_now = list_to_be_traversed_next
|
||||
|
||||
return [feat.Document] + list_of_deps[::-1]
|
||||
|
||||
def CSChain(feat):
|
||||
cnt_chain = ContainerChain(feat)
|
||||
return [cnt for cnt in cnt_chain if Container(cnt).isACS()]
|
||||
|
||||
|
||||
class ContainerError(RuntimeError):
|
||||
pass
|
||||
class NotAContainerError(ContainerError):
|
||||
def __init__(self):
|
||||
ContainerError.__init__(self, u"{obj} is not recognized as container".format(obj.Name))
|
||||
class ContainerTreeError(ContainerError):
|
||||
pass
|
||||
@@ -25,9 +25,12 @@ import FreeCAD as App
|
||||
if App.GuiUp:
|
||||
import FreeCADGui as Gui
|
||||
|
||||
from Show.FrozenClass import FrozenClass
|
||||
from .FrozenClass import FrozenClass
|
||||
|
||||
from Show.DepGraphTools import getAllDependencies, getAllDependent, isContainer
|
||||
from .DepGraphTools import getAllDependencies, getAllDependent, isContainer
|
||||
|
||||
from . import Containers
|
||||
Container = Containers.Container
|
||||
|
||||
class TempoVis(FrozenClass):
|
||||
'''TempoVis - helper object to save visibilities of objects before doing
|
||||
@@ -40,6 +43,7 @@ class TempoVis(FrozenClass):
|
||||
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.cam_string = "" # inventor ASCII string representing the camera
|
||||
self.viewer = None # viewer the camera is saved from
|
||||
@@ -88,8 +92,9 @@ class TempoVis(FrozenClass):
|
||||
self.modifyVPProperty(doc_obj_or_list, "Visibility", False)
|
||||
|
||||
def get_all_dependent(self, doc_obj):
|
||||
'''get_all_dependent(doc_obj): gets all objects that depend on doc_obj. Groups, Parts and Bodies are not hidden by this.'''
|
||||
return [o for o in getAllDependent(doc_obj) if not isContainer(o)]
|
||||
'''get_all_dependent(doc_obj): gets all objects that depend on doc_obj. Containers of the object are excluded from the list.'''
|
||||
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.'''
|
||||
@@ -144,6 +149,7 @@ class TempoVis(FrozenClass):
|
||||
prop= prop_name))
|
||||
|
||||
self.restoreUnpickable()
|
||||
self.restoreClipPlanes()
|
||||
|
||||
try:
|
||||
self.restoreCamera()
|
||||
@@ -156,6 +162,7 @@ class TempoVis(FrozenClass):
|
||||
'''forget(): resets TempoVis'''
|
||||
self.data = {}
|
||||
self.data_pickstyle = {}
|
||||
self.data_clipplane = {}
|
||||
|
||||
self.cam_string = ""
|
||||
self.viewer = None
|
||||
@@ -241,3 +248,89 @@ class TempoVis(FrozenClass):
|
||||
.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.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.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.
|
||||
Offest shifts the plane; positive offset reveals more material, negative offset
|
||||
hides more material."""
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
enable: True if you want clipping, False if you want to remove clipping:
|
||||
placement: XY plane of local coordinates of the placement is the clipping plane. The placement must be in document's global coordinate system.
|
||||
offset: shifts the plane. Positive offset reveals more of the object.
|
||||
|
||||
Implementation detail: uses SoClipPlane node. If viewprovider already has a node
|
||||
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 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.")
|
||||
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):
|
||||
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))
|
||||
|
||||
def allVisibleObjects(self, aroundObject):
|
||||
"""allVisibleObjects(self, 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.CSChain(aroundObject)
|
||||
result = []
|
||||
for i in range(len(chain)):
|
||||
cnt = chain[i]
|
||||
cnt_next = chain[i+1] if i+1 < len(chain) else aroundObject
|
||||
for obj in Container(cnt).getCSChildren():
|
||||
if obj is not cnt_next:
|
||||
if obj.ViewObject.Visibility:
|
||||
result.append(obj)
|
||||
return result
|
||||
|
||||
def sketchClipPlane(self, sketch, enable = True):
|
||||
"""Clips all objects by plane of sketch"""
|
||||
self.clipPlane(self.allVisibleObjects(sketch), enable, sketch.getGlobalPlacement(), 0.02)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__doc__ = "Show module: helper code for visibility automation."
|
||||
|
||||
from Show.TempoVis import TempoVis
|
||||
import Show.DepGraphTools as DepGraphTools
|
||||
from .TempoVis import TempoVis
|
||||
from . import DepGraphTools
|
||||
Reference in New Issue
Block a user