# SPDX-License-Identifier: LGPL-2.1-or-later # /*************************************************************************** # * Copyright (c) 2016 Victor Titov (DeepSOIC) * # * * # * 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 * # * * # ***************************************************************************/ # module is named mTempoVis, because Show.TimpoVis exposes the class as its member, and hides the module TempoVis.py from . import Containers 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, 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. """ document = None stack = None # reference to stack this TV is in 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 = {} # def __init__(self, document, stack=MAINSTACK, **kwargs): self._init_attrs() self.document = document 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 __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 restore can be called again. """ if self.state == S_RESTORED: return if self.state != S_INTERNAL and ultimate: self.state = S_RESTORED for key, detail in self.data.items(): try: self._restoreDetail(detail) except Exception as err: Err( "TempoVis.restore: failed to restore detail {key}: {err}".format( key=key, err=str(err) ) ) _printTraceback(err) if ultimate: self.data = {} if self.is_in_stack: self.stack.withdraw(self) # # def _inserted(self, stack, index): """called when this tv is inserted into a stack""" self.stack = stack def _withdrawn(self, stack, index): """called when this tv is withdrawn from a stack""" self.stack = None @property def is_in_stack(self): return self.stack is not None # # def modifyVPProperty(self, doc_obj_or_list, prop_names, new_value=JUST_SAVE, mild_restore=None): """modifyVPProperty(doc_obj_or_list, prop_names, new_value = JUST_SAVE, mild_restore = None): modifies prop_name property of ViewProvider of doc_obj_or_list, and remembers original value of the property. Original values will be restored upon TempoVis deletion, or call to restore(). mild_restore: test if user changed the value manually when restoring the TV.""" if self.state == S_RESTORED: Wrn("Attempting to use a TV that has been restored. There must be a problem with code.") return 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): Wrn( "TempoVis: object {obj} has no attribute {attr}. Skipped.".format( obj=doc_obj.Name, attr=prop_name ) ) continue # 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 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 = [] bodies = set() for obj in doc_obj_or_list: body = getattr(obj, "_Body", None) if not body or body in bodies: continue bodies.add(body) feature = getattr(body, "VisibleFeature", None) if feature: objs.append(feature) self.modifyVPProperty(objs, "Visibility", JUST_SAVE) return objs 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, 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""" 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, 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 . import Containers from .Containers import isAContainer from .DepGraphTools import getAllDependencies, getAllDependent 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 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._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.""" from .DepGraphTools import getAllDependencies, getAllDependent 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.""" from .DepGraphTools import getAllDependencies, getAllDependent 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).""" from .DepGraphTools import getAllDependencies, getAllDependent 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.""" from .DepGraphTools import getAllDependencies, getAllDependent self.show(self._3D_objects(getAllDependencies(doc_obj))) def saveCamera(self, vw=None): self._change() from .SceneDetails.Camera import Camera self.save(Camera(self.document)) 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). doc_obj_or_list: object or list of objects to alter (App) actual_pick_style: optional parameter, specifying the actual pick style: 0 = regular, 1 = bounding box, 2 (default) = unpickable. Implementation detail: uses SoPickStyle 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, and remains there even after restore()/deleting tempovis.""" from .SceneDetails.Pickability import Pickability from .ShowUtils import is3DObject 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) 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. """ from .SceneDetails.ObjectClipPlane import ObjectClipPlane from .ShowUtils import is3DObject 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.""" from .ShowUtils import is3DObject from . import Containers chain = Containers.VisGroupChain(aroundObject) result = [] for i in range(len(chain)): cnt = chain[i] cnt_next = chain[i + 1] if i + 1 < len(chain) else aroundObject container = Containers.Container(cnt) for obj in container.getVisGroupChildren(): if not is3DObject(obj): continue if obj is not cnt_next: if container.isChildVisible(obj): result.append(obj) return result def sketchClipPlane(self, sketch, enable=None, reverted=False): """sketchClipPlane(sketch, enable = None): Clips all objects by plane of sketch. If enable argument is omitted, calling the routine repeatedly will toggle clipping plane.""" 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] if reverted: pla = pla * App.Rotation(0, 1, 0, 0) if enable: # clip plane shall be disabled so new placement can be applied self.modify(ClipPlane(doc, 0)) self.modify(ClipPlane(doc, toggle, pla, 0.001)) sketch.ViewObject.SectionView = ( enable if enable is not None else not sketch.ViewObject.SectionView ) def activateWorkbench(self, wb_name): from .SceneDetails.Workbench import Workbench self.modify(Workbench(wb_name)) # # def _restoreDetail(self, detail): p = self.data[detail.full_key] tv1, curr = self._value_after(detail, query_scene=p.mild_restore) if p.mild_restore: if self.data_requested[detail.full_key] != curr: # the value on the scene doesn't match what was requested through TV. User probably changed it. We don't want to mess it up. self._purge_milds(detail) return if tv1 is None: # no other TV has changed this detail later, apply to the scene detail.apply_data(p.data) else: # modify saved detail of higher TV tv1.data[detail.full_key].data = p.data def _purge_milds(self, detail): """_purge_milds(detail): wipes out detail from earlier TVs if the detail is mild-restore.""" if not self.is_in_stack: return seq_before, seq_after = self.stack.getSplitSequence(self) for tv in reversed(seq_before): if tv.has(detail): if tv.data[detail.full_key].mild_restore: tv.forgetDetail(detail) else: # hard-restoring value encountered, stop break 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.""" from .ShowUtils import is3DObject 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 dumps(self): return None def loads(self, state): self._init_attrs()