diff --git a/src/Mod/Show/Containers.py b/src/Mod/Show/Containers.py new file mode 100644 index 0000000000..a00dd8e201 --- /dev/null +++ b/src/Mod/Show/Containers.py @@ -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: [,,,]''' + + 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 diff --git a/src/Mod/Show/TempoVis.py b/src/Mod/Show/TempoVis.py index 5b00c8693f..c666ecba12 100644 --- a/src/Mod/Show/TempoVis.py +++ b/src/Mod/Show/TempoVis.py @@ -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) diff --git a/src/Mod/Show/__init__.py b/src/Mod/Show/__init__.py index 44b15b8d02..9456140364 100644 --- a/src/Mod/Show/__init__.py +++ b/src/Mod/Show/__init__.py @@ -1,4 +1,4 @@ __doc__ = "Show module: helper code for visibility automation." -from Show.TempoVis import TempoVis -import Show.DepGraphTools as DepGraphTools \ No newline at end of file +from .TempoVis import TempoVis +from . import DepGraphTools \ No newline at end of file