TempoVis: Clip Plane Support

This commit is contained in:
DeepSOIC
2018-05-12 21:31:29 +03:00
committed by Yorik van Havre
parent e4cea7f3ca
commit 1bf9da8a2a
3 changed files with 273 additions and 6 deletions

174
src/Mod/Show/Containers.py Normal file
View 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

View File

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

View File

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