├── .gitattributes ├── .gitignore ├── Init.py ├── InitGui.py ├── LICENSE ├── PartOMagic ├── Base │ ├── Compatibility.py │ ├── Containers.py │ ├── ExpressionParser.py │ ├── FilePlant │ │ ├── Errors.py │ │ ├── FCObject.py │ │ ├── FCProject.py │ │ ├── FCProperty.py │ │ ├── Misc.py │ │ ├── ObjectMaker.py │ │ ├── PropertyExpressionEngine.py │ │ ├── __init__.py │ │ ├── importFCStd.py │ │ ├── readme.md │ │ └── xmls │ │ │ ├── Document_expressions.xml │ │ │ ├── Document_full.xml │ │ │ ├── Document_links.xml │ │ │ ├── Document_links_empty.xml │ │ │ ├── Document_short.xml │ │ │ ├── GuiDocument.xml │ │ │ └── Persistence.xml │ ├── LinkTools.py │ ├── Parameters.py │ ├── Utils.py │ └── __init__.py ├── Features │ ├── AssyFeatures │ │ ├── Instance.py │ │ ├── MuxAssembly.py │ │ └── __init__.py │ ├── Exporter.py │ ├── GenericContainer.py │ ├── Ghost.py │ ├── Module.py │ ├── PDShapeFeature.py │ ├── PartContainer.py │ ├── PartDesign │ │ ├── PDShapeFeature.py │ │ └── __init__.py │ ├── ShapeBinder.py │ ├── ShapeGroup.py │ └── __init__.py ├── Gui │ ├── AACommand.py │ ├── CommandCollection1.py │ ├── Control │ │ ├── OnOff.py │ │ └── __init__.py │ ├── GlobalToolbar.py │ ├── GroupCommand.py │ ├── Icons │ │ ├── Icons.py │ │ ├── Icons.qrc │ │ ├── __init__.py │ │ ├── icons │ │ │ ├── PartOMagic.svg │ │ │ ├── PartOMagic_DisableObserver.svg │ │ │ ├── PartOMagic_Duplicate.svg │ │ │ ├── PartOMagic_EnableObserver.svg │ │ │ ├── PartOMagic_Enter.svg │ │ │ ├── PartOMagic_Ghost.svg │ │ │ ├── PartOMagic_Leave.svg │ │ │ ├── PartOMagic_ListUsages.svg │ │ │ ├── PartOMagic_MUX.svg │ │ │ ├── PartOMagic_MorphContainer.svg │ │ │ ├── PartOMagic_PDShapeFeature_Additive.svg │ │ │ ├── PartOMagic_PDShapeFeature_Subtractive.svg │ │ │ ├── PartOMagic_PauseObserver.svg │ │ │ ├── PartOMagic_Power.svg │ │ │ ├── PartOMagic_ReplaceObject.svg │ │ │ ├── PartOMagic_Select_All.svg │ │ │ ├── PartOMagic_Select_BSwap.svg │ │ │ ├── PartOMagic_Select_Children.svg │ │ │ ├── PartOMagic_Select_ChildrenRecursive.svg │ │ │ ├── PartOMagic_Select_Invert.svg │ │ │ ├── PartOMagic_ShapeGroup.svg │ │ │ ├── PartOMagic_SnapView.svg │ │ │ ├── PartOMagic_TransferObject.svg │ │ │ └── PartOMagic_XRay.svg │ │ └── recompile_rc.bat │ ├── LinkTools │ │ ├── ListUsages.py │ │ ├── ReplaceObject.py │ │ ├── TaskReplace.py │ │ ├── TaskReplace.ui │ │ └── __init__.py │ ├── MonkeyPatches.py │ ├── Observer.py │ ├── TempoVis.py │ ├── Tools │ │ ├── Duplicate.py │ │ ├── LeaveEnter.py │ │ ├── MorphContainer.py │ │ ├── SelectionTools.py │ │ ├── Tip.py │ │ ├── TransferObject.py │ │ └── __init__.py │ ├── Utils.py │ ├── View │ │ ├── SnapView.py │ │ ├── XRay.py │ │ └── __init__.py │ └── __init__.py └── __init__.py ├── README.md └── package.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # ========================= 61 | # Operating System Files 62 | # ========================= 63 | 64 | # OSX 65 | # ========================= 66 | 67 | .DS_Store 68 | .AppleDouble 69 | .LSOverride 70 | 71 | # Thumbnails 72 | ._* 73 | 74 | # Files that might appear in the root of a volume 75 | .DocumentRevisions-V100 76 | .fseventsd 77 | .Spotlight-V100 78 | .TemporaryItems 79 | .Trashes 80 | .VolumeIcon.icns 81 | 82 | # Directories potentially created on remote AFP share 83 | .AppleDB 84 | .AppleDesktop 85 | Network Trash Folder 86 | Temporary Items 87 | .apdisk 88 | 89 | # Windows 90 | # ========================= 91 | 92 | # Windows image file caches 93 | Thumbs.db 94 | ehthumbs.db 95 | 96 | # Folder config file 97 | Desktop.ini 98 | 99 | # Recycle Bin used on file shares 100 | $RECYCLE.BIN/ 101 | 102 | # Windows Installer files 103 | *.cab 104 | *.msi 105 | *.msm 106 | *.msp 107 | 108 | # Windows shortcuts 109 | *.lnk 110 | -------------------------------------------------------------------------------- /Init.py: -------------------------------------------------------------------------------- 1 | FreeCAD.addImportType("FreeCAD file (PoM) (*.FCStdM)","PartOMagic.Base.FilePlant.importFCStd") 2 | FreeCAD.addExportType("FreeCAD file (PoM) (*.FCStdM)","PartOMagic.Base.FilePlant.importFCStd") 3 | -------------------------------------------------------------------------------- /InitGui.py: -------------------------------------------------------------------------------- 1 | 2 | import FreeCAD as App 3 | 4 | import PartOMagic.Base.Parameters as Params 5 | 6 | if Params.EnablePartOMagic.get(): 7 | import PartOMagic.Base.Compatibility as compat 8 | try: 9 | compat.check_POM_compatible() 10 | import PartOMagic 11 | PartOMagic.importAll() 12 | Gui.addModule("PartOMagic") 13 | if Params.EnableObserver.get(): 14 | PartOMagic.Gui.Observer.start() 15 | except compat.CompatibilityError as err: 16 | Params.EnablePartOMagic.set_volatile(False) 17 | App.Console.PrintError(u"Part-o-magic is disabled.\n {err}".format(err= str(err))) 18 | 19 | if Params.EnablePartOMagic.get(): 20 | try: 21 | import Show.Containers 22 | # good tempovis, do not replace. 23 | except ImportError: 24 | # old TempoVis 25 | # substitute TempoVis's isContainer with a more modern one 26 | import Show.TempoVis 27 | Show.TempoVis = PartOMagic.Gui.TempoVis.TempoVis 28 | 29 | if Params.EnablePartOMagic.get(): 30 | # global toolbar - update only if missing 31 | if not PartOMagic.Gui.GlobalToolbar.isRegistered(): 32 | PartOMagic.Gui.GlobalToolbar.registerToolbar() 33 | # PartDesign toolbar - always update 34 | PartOMagic.Gui.GlobalToolbar.registerPDToolbar() 35 | 36 | class PartOMagicWorkbench (Workbench): 37 | MenuText = 'Part-o-magic' 38 | ToolTip = "Part-o-magic: experimental group and Part and Body automation" 39 | 40 | def __init__(self): 41 | # Hack: obtain path to POM by loading a dummy Py module 42 | import os 43 | import PartOMagic 44 | self.__class__.Icon = os.path.dirname(PartOMagic.__file__) + u"/Gui/Icons/icons/PartOMagic.svg".replace("/", os.path.sep) 45 | 46 | def Initialize(self): 47 | import PartOMagic as POM 48 | POM.importAll() 49 | 50 | cmdsControl = ([] 51 | + POM.Gui.Control.exportedCommands() 52 | ) 53 | self.appendMenu('Part-o-Magic', cmdsControl) 54 | cmdsControl.remove('PartOMagic_Power') #don't add this command to toolbar 55 | self.appendToolbar('POMControl', cmdsControl) 56 | 57 | self.appendMenu('Part-o-Magic', ["Separator"]) 58 | 59 | cmdsNewContainers = ([] 60 | + POM.Features.exportedCommands() 61 | + POM.Features.PartDesign.exportedCommands() 62 | ) 63 | self.appendToolbar('POMContainers', cmdsNewContainers) 64 | self.appendMenu('Part-o-Magic', cmdsNewContainers) 65 | 66 | self.appendMenu('Part-o-Magic', ["Separator"]) 67 | 68 | cmdsTools = ([] 69 | + POM.Gui.Tools.exportedCommands() 70 | ) 71 | self.appendToolbar('POMTools', cmdsTools) 72 | self.appendMenu('Part-o-Magic', cmdsTools) 73 | 74 | self.appendMenu('Part-o-Magic', ["Separator"]) 75 | 76 | cmdsLinkTools = ([] 77 | + POM.Gui.LinkTools.exportedCommands() 78 | ) 79 | self.appendToolbar('POMLinkTools', cmdsLinkTools) 80 | self.appendMenu('Part-o-Magic', cmdsLinkTools) 81 | 82 | 83 | self.appendMenu('Part-o-Magic', ["Separator"]) 84 | 85 | cmdsAssy = ([] 86 | + POM.Features.AssyFeatures.exportedCommands() 87 | ) 88 | self.appendToolbar('POMAssembly', cmdsAssy) 89 | self.appendMenu('Part-o-Magic', cmdsAssy) 90 | 91 | 92 | self.appendMenu('Part-o-Magic', ["Separator"]) 93 | 94 | cmdsView = ([] 95 | + POM.Gui.View.exportedCommands() 96 | ) 97 | self.appendToolbar('POMView', cmdsView) 98 | self.appendMenu('Part-o-Magic', cmdsView) 99 | 100 | 101 | def Activated(self): 102 | pass 103 | 104 | if Params.EnablePartOMagic.get(): 105 | Gui.addWorkbench(PartOMagicWorkbench()) 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /PartOMagic/Base/Compatibility.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | 3 | def get_fc_version(): 4 | """returns tuple like (0,18,4,16154) for 0.18.4 release, and (0,19,0,18234) for pre builds""" 5 | # ['0', '18', '4 (GitTag)', 'git://github.com/FreeCAD/FreeCAD.git releases/FreeCAD-0-18', '2019/10/22 16:53:35', 'releases/FreeCAD-0-18', '980bf9060e28555fecd9e3462f68ca74007b70f8'] 6 | # ['0', '19', '18234 (Git)', 'git://github.com/FreeCAD/FreeCAD.git master', '2019/09/15 20:43:17', 'master', '3af5d97e9b2a60823815f662aba25422c4bc45bb'] 7 | # ['0', '21', '0', '32457 (Git)', 'https://github.com/FreeCAD/FreeCAD master', '2023/03/23 00:09:35', 'master', '85216bd12730bbc4c3cbf8f0bc50416ab1556cbb'] 8 | if len(App.Version()) <= 7: 9 | strmaj, strmi, strrev = App.Version()[0:3] 10 | maj, mi = int(strmaj), int(strmi) 11 | submi, rev = 0, 0 12 | elif len(App.Version()) >= 8: 13 | strmaj, strmi, strsubmi, strrev = App.Version()[0:4] 14 | maj, mi, submi = int(strmaj), int(strmi), int(strsubmi) 15 | rev = 0 16 | if '(GitTag)' in strrev: 17 | submi = int(strrev.split(" ")[0]) 18 | elif '(Git)' in strrev: 19 | try: 20 | rev = int(strrev.split(" ")[0]) 21 | except Exception as err: 22 | App.Console.PrintWarning(u"PartOMagic failed to detect FC version number.\n" 23 | " {err}\n".format(err= str(err))) 24 | rev = 32457 #assume fairly modern 25 | if rev < 100: 26 | if mi == 17: 27 | rev = 13544 28 | elif mi == 18: 29 | rev = 16154 30 | elif mi == 19: 31 | rev = 24276 32 | elif mi == 20: 33 | rev = 29177 34 | else: 35 | rev = 32457 #assume fairly modern 36 | App.Console.PrintWarning(u"PartOMagic failed to detect FC version number: revision is zero / too low, minor version is unexpected.") 37 | return (maj, mi, submi, rev) 38 | 39 | 40 | def get_fc_revision_nr(): 41 | return get_fc_version()[3] 42 | 43 | def check_POM_compatible(): 44 | """Raises CompatibilityError if PoM is known to not run""" 45 | try: 46 | rev = get_fc_revision_nr() 47 | except Exception as err: 48 | App.Console.PrintWarning(u"PartOMagic failed to detect FC version number.\n" 49 | " {err}\n".format(err= str(err))) 50 | #keep going, assume the version is good enough... 51 | return 52 | 53 | if rev < 9933: 54 | raise CompatibilityError("Part-o-magic requires FreeCAD at least v0.17.9933. Yours appears to have a rev.{rev}, which is less.".format(rev= rev)) 55 | 56 | def scoped_links_are_supported(): 57 | try: 58 | return get_fc_revision_nr() >= 12027 59 | except Exception as err: 60 | return True #assume good 61 | 62 | def tempovis_is_stacky(): 63 | try: 64 | import Show.SceneDetail 65 | except ImportError: 66 | return False 67 | else: 68 | return True 69 | 70 | class CompatibilityError(RuntimeError): 71 | pass 72 | -------------------------------------------------------------------------------- /PartOMagic/Base/ExpressionParser.py: -------------------------------------------------------------------------------- 1 | __doc__ = "ExpressionParser: minimalistic routines for extracting information from expressions" 2 | 3 | namechars = [chr(c) for c in range(ord('a'), ord('z')+1)] 4 | namechars += [chr(c) for c in range(ord('A'), ord('Z')+1)] 5 | namechars += [chr(c) for c in range(ord('0'), ord('9')+1)] 6 | namechars += ['_'] 7 | namechars = set(namechars) 8 | 9 | def expressionDeps(expr, doc): 10 | """expressionDeps(expr, doc): returns set of objects referenced by the expression, as list of Relations with unfilled linking_object. 11 | expr: expression, as a string 12 | doc: document where to look up objects by names/labels. 13 | If doc is None, all names are assumed valid. A list of tuples is returned instead of list 14 | of relations: [(name_or_label, (start, end_plus_1))]. This may falsely recognize property 15 | self-references as objects, for example in '=Placement.Base.x' will list 'Placement' 16 | as an object. """ 17 | global namechars 18 | startchars = set("+-*/(%^&\[<>;, =") 19 | endchars = set(".") 20 | 21 | if expr is None: 22 | return [] 23 | 24 | ids = [] #list of tuples: (identifier, (start, end_plus_1)) 25 | start = 0 26 | finish = 0 27 | for i in range(len(expr)): 28 | if expr[i] in startchars: 29 | start = i+1 30 | elif expr[i] in endchars: 31 | finish = i 32 | if finish - start > 0: 33 | ids.append((expr[start:finish], (start, finish))) 34 | start = len(expr) 35 | elif expr[i] not in namechars: 36 | finish = i 37 | start = len(expr) 38 | 39 | if doc is None: 40 | #nowhere to look up. Returning the simplified variant: 41 | return ids #list of tuples, [(name_or_label, (start, end_plus_1))] 42 | else: 43 | from .LinkTools import Relation 44 | #look up objects 45 | ret = [] 46 | for id, id_range in ids: 47 | # try by name 48 | obj = doc.getObject(id) 49 | if obj is None: 50 | # try by label 51 | objs = doc.getObjectsByLabel(id) 52 | if len(objs) == 1: 53 | obj = objs[0] 54 | if obj is not None: 55 | ret.append(Relation(None, 'Expression', None, obj, expression_charrange= id_range)) 56 | else: 57 | print(u"identifier in expression not recognized: {id}".format(id= id)) 58 | 59 | return ret 60 | -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/Errors.py: -------------------------------------------------------------------------------- 1 | 2 | class FileplantError(RuntimeError): 3 | pass 4 | class NameCollisionError(FileplantError): 5 | pass 6 | class ObjectNotFoundError(KeyError): 7 | pass 8 | class PropertyNotFoundError(AttributeError): 9 | pass 10 | 11 | 12 | def warn(message): 13 | import sys 14 | freecad = sys.modules.get('FreeCAD', None) 15 | if freecad is None: 16 | print(message) 17 | else: 18 | freecad.Console.PrintWarning(message + '\n') -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/FCObject.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | from xml.etree import ElementTree 3 | import io 4 | 5 | from .Errors import * 6 | from . import FCProperty 7 | 8 | content_base_xml = ( 9 | """ 10 | 11 | """ 12 | ) 13 | 14 | 15 | #objectnode: 16 | # 17 | 18 | #datanode: 19 | # 20 | # 21 | # 22 | # 23 | # 24 | # 25 | # 26 | # 27 | # 28 | 29 | class PropertyContainer(object): 30 | """Either a DocumentObject or ViewProvider""" 31 | datanode = None 32 | objectnode = None 33 | project = None # reference to a project this object is part of 34 | 35 | def __init__(self, objectnode, datanode, project): 36 | self.objectnode = objectnode 37 | self.datanode = datanode 38 | self.project = project 39 | 40 | @property 41 | def Name(self): 42 | """Name: a writable property. Writing to Name will rename the object and its viewprovider, but not update links to the object. To rename and update links, use renameObject method of a Project.""" 43 | return self.datanode.get('name') 44 | 45 | def _rename(self, new_name): 46 | if self.objectnode is not None: 47 | self.objectnode.set('name', new_name) 48 | self.datanode.set('name', new_name) 49 | 50 | def getPropertyNode(self, prop_name): 51 | prop = self.datanode.find('Properties/Property[@name="{propname}"]'.format(propname= prop_name)) 52 | if prop is None: 53 | raise PropertyNotFoundError("Object {obj} has no property named '{prop}'".format(obj= self.Name, prop= prop_name)) 54 | return prop 55 | 56 | @property 57 | def TypeId(self): 58 | return self.objectnode.get("type") 59 | 60 | @TypeId.setter 61 | def TypeId(self, new_type_id): 62 | self.objectnode.set('type', new_type_id) 63 | 64 | def getPropertiesNodes(self): 65 | return self.datanode.findall('Properties/Property') 66 | 67 | @property 68 | def PropertiesList(self): 69 | propsnodes = self.getPropertiesNodes() 70 | return [prop.get('name') for prop in propsnodes] 71 | 72 | def renameProperty(self, old_name, new_name): 73 | node = self.getPropertyNode(old_name) 74 | node.set('name', new_name) 75 | 76 | def Property(self, prop_name): 77 | return FCProperty.CastProperty(self.getPropertyNode(prop_name), self) 78 | 79 | @property 80 | def Properties(self): 81 | #fixme: inefficient! 82 | return [self.Property(prop_name) for prop_name in self.PropertiesList] 83 | 84 | def files(self): 85 | """files(): returns set of filenames used by properties of this object""" 86 | file_set = set() 87 | for prop in self.Properties: 88 | file_set |= prop.files() 89 | return file_set 90 | 91 | def _rename_file(self, rename_dict): 92 | """substitutes file references in properties. does not rename actual files. Returns number of occurrences replaced.""" 93 | n_renamed = 0 94 | for prop in self.Properties: 95 | n_renamed += prop._rename_file(rename_dict) 96 | return n_renamed 97 | 98 | def dumpContent(self, exclude_extensions = False): 99 | rootnode = ElementTree.fromstring(content_base_xml) 100 | rootnode.extend(self.datanode) 101 | if exclude_extensions: 102 | exts = rootnode.find('Extensions') 103 | if exts is not None: 104 | rootnode.remove(exts) 105 | zipdata = io.BytesIO() 106 | with zipfile.ZipFile(zipdata, 'w') as zipout: 107 | fileorder = ['Persistence.xml'] + list(self.files()) 108 | for fn in fileorder: 109 | if fn == 'Persistence.xml': 110 | data = ElementTree.tostring(rootnode, encoding= 'utf-8') 111 | else: 112 | data = self.project.readSubfile(fn) 113 | zipout.writestr(fn, data) 114 | return zipdata.getvalue() 115 | 116 | @property 117 | def Extensions(self): 118 | exts = self.datanode.find('Extensions') 119 | if exts is None: return [] 120 | return [(ext.get('type'), ext.get('name')) for ext in exts if ext.tag == 'Extension'] 121 | 122 | @property 123 | def Label(self): 124 | return self.Property('Label').value 125 | 126 | @Label.setter 127 | def Label(self, new_value): 128 | self.Property('Label').value = new_value 129 | 130 | def fetchAttributes(self): 131 | """fetchAttributes(self): makes object properties accessible as attributes. """ 132 | pass 133 | #doesn't work - descriptors are only applied when the attribute is a class attribute =( 134 | #for prop_name in self.PropertiesList: 135 | # if prop_name != 'Label': #Label is defined explicitly 136 | # self.__dict__[prop_name] = PropertyAsAttribute(prop_name, self) #use instance dictionary instead of setattr, using setattr over a descriptor will do a different thing. 137 | 138 | def replace(self, replace_task): 139 | cnt = 0 140 | for prop in self.Properties: 141 | cnt += prop.replace(replace_task) 142 | return cnt 143 | 144 | def purgeDeadLinks(self): 145 | cnt = 0 146 | for prop in self.Properties: 147 | cnt += prop.purgeDeadLinks() 148 | return cnt 149 | 150 | 151 | #class PropertyAsAttribute(object): 152 | # prop_name = None 153 | # object = None 154 | # def __init__(self, prop_name, obj): 155 | # self.prop_name = prop_name 156 | # self.object = obj 157 | # 158 | # def __get__(self, obj, type = None): 159 | # return self.object.Property(self.prop_name).getAsAttribute() 160 | 161 | class DocumentObject(PropertyContainer): 162 | @property 163 | def ViewObject(self): 164 | return self.project.getViewProvider(self.Name) 165 | 166 | @property 167 | def Name(self): 168 | return super(DocumentObject, self).Name 169 | 170 | @Name.setter 171 | def Name(self, new_name): 172 | vp = self.ViewObject 173 | self._rename(new_name) 174 | if vp: 175 | vp._rename(new_name) 176 | 177 | def rename(self, new_name, update_label = True): 178 | old_name = self.Name 179 | self.Name = new_name 180 | if update_label: 181 | self.Label = self.Label.replace(old_name, new_name) 182 | 183 | def updateFCObject(self, obj, update_expr = False): 184 | exts = self.Extensions 185 | for ext_t, ext_n in exts: 186 | if not obj.hasExtension(ext_t): 187 | obj.addExtension(ext_t) 188 | obj.restoreContent(self.dumpContent(exclude_extensions= True)) 189 | 190 | if update_expr: 191 | self.updateFCObject_expressions(obj) 192 | 193 | vp = obj.ViewObject 194 | if vp: 195 | vp_emu = self.ViewObject 196 | exts = vp_emu.Extensions 197 | for ext_t, ext_n in exts: 198 | if not vp.hasExtension(ext_t): 199 | vp.addExtension(ext_t) 200 | vp.restoreContent(vp_emu.dumpContent(exclude_extensions= True)) 201 | 202 | def updateFCObject_expressions(self, obj): 203 | try: 204 | ee = self.Property('ExpressionEngine') 205 | 206 | #clear existing expressions 207 | for path,expr in obj.ExpressionEngine: 208 | obj.setExpression(path, None) 209 | 210 | for path,expr in ee.value: 211 | obj.setExpression(path, expr) 212 | except PropertyNotFoundError: 213 | pass 214 | 215 | class ViewProvider(PropertyContainer): 216 | @property 217 | def Object(self): 218 | return self.project.getObject(self.Name) 219 | 220 | -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/Misc.py: -------------------------------------------------------------------------------- 1 | def generateNewName(wanted_name, existing_names, existing_names_2 = set()): 2 | """generateNewName(wanted_name, existing_names): returns a unique name (by adding digits to wanted_name). Suitable for file names (but not full paths)""" 3 | import re 4 | split = wanted_name.rsplit('.',1) 5 | title = split[0] 6 | if len(split) == 2: 7 | ext = '.' + split[1] 8 | else: 9 | ext = '' 10 | 11 | match = re.match(r'^(.*?)(\d*)$', title) 12 | title,number = match.groups() 13 | if len(number)>0: 14 | i = int(number) 15 | numlen = len(number) 16 | else: 17 | i = 0 18 | numlen = 3 19 | 20 | f2 = wanted_name 21 | while f2 in existing_names or f2 in existing_names_2: 22 | i += 1 23 | f2 = title + str(i).rjust(numlen, '0') + ext 24 | return f2 25 | 26 | def FC_version(): 27 | try: 28 | import FreeCAD 29 | vertup = FreeCAD.Version() 30 | # ['0', '18', '15518 (Git)', 'git://github.com/FreeCAD/FreeCAD.git master', '2018/12/29 16:41:25', 'master', 'e83c44200ab428b753a1e08a2e4d95 31 | # target format: '0.18R14726 (Git)' 32 | return '{0}.{1}R{2}'.format(*vertup) 33 | except Exception: 34 | return '0.18R14726 (Git)' 35 | 36 | def recursiveNodeIterator(node): 37 | yield node 38 | for child in node: 39 | for it in recursiveNodeIterator(child): 40 | yield it 41 | 42 | class ReplaceTask(object): 43 | """class ReplaceTask: holds a dict of names to replace, along with (optional) name<->label correspondence, to work in expressions. 44 | Constructor: ReplaceTask(name_replacement_dict = None, projects = None). 45 | If you want to remove references to an object, use None as the new name. 46 | Note: if you want to replace a renamed object by label as well, call addObject on both 47 | the original and the renamed versions of the object.""" 48 | 49 | replacements = None # a dict. key = old name, value = new name 50 | labels = None ## a dict. key = label (string). value = list of names 51 | names = None ## a dict. key = name, value = label 52 | 53 | def __init__(self, name_replacement_dict = None, projects = None): 54 | self.names = dict() 55 | self.labels = dict() 56 | 57 | if projects is not None: 58 | try: 59 | iter(projects) 60 | except TypeError: 61 | #not iterable 62 | projects = [projects] 63 | for prj in projects: 64 | self.addProject(prj) 65 | 66 | self.replacements = name_replacement_dict if name_replacement_dict is not None else dict() 67 | 68 | def addProject(self, project): 69 | for obj in project.Objects: 70 | self.addObject(obj.Name, obj.Label) 71 | 72 | 73 | def addObject(self, name, label): 74 | self.labels[name] = label 75 | names = self.names.get(label, []) 76 | if name not in names: 77 | names.append(name) 78 | self.names[label] = names 79 | 80 | 81 | def has(self, name): 82 | return name in self.replacements 83 | 84 | def lookup(self, name): 85 | return self.replacements[name] 86 | 87 | def has_label(self, label): 88 | try: 89 | self.lookup_label(label) 90 | return True 91 | except KeyError: 92 | return False 93 | 94 | def lookup_label(self, label): 95 | names = self.names.get(label, []) 96 | if len(names) == 1: 97 | new_name = self.replacements[names[0]] 98 | if new_name: 99 | return self.labels[new_name] 100 | else: 101 | return None 102 | elif len(names) == 0: 103 | raise KeyError('Label not found') 104 | else: 105 | raise KeyError('Label is not unique') 106 | 107 | #-------------------------dict-like interface------------------------- 108 | def __contains__(self, name): 109 | return self.has(name) 110 | 111 | def __getitem__(self, arg): 112 | return self.lookup(arg) 113 | 114 | def __setitem__(self, arg, value): 115 | self.replacements[arg] = value 116 | 117 | -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/ObjectMaker.py: -------------------------------------------------------------------------------- 1 | def makeObject(doc, type_id, name): 2 | import FreeCAD 3 | obs = Observer(doc) 4 | FreeCAD.addDocumentObserver(obs) 5 | try: 6 | obj = doc.addObject(type_id, name) 7 | finally: 8 | FreeCAD.removeDocumentObserver(obs) 9 | #some objects add extra objects automatically (e.g. Part makes an Origin). Can't prevent their creation. But can delete. 10 | for n in obs.new_objects: 11 | if n != obj.Name: 12 | doc.removeObject(n) 13 | return obj 14 | 15 | 16 | class Observer(object): 17 | new_objects = None 18 | doc = None 19 | 20 | def __init__(self, doc): 21 | self.new_objects = [] 22 | self.doc = doc 23 | 24 | def slotCreatedObject(self, feature): 25 | if feature.Document is self.doc: 26 | self.new_objects.append(feature.Name) 27 | -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/PropertyExpressionEngine.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree 2 | 3 | from . import FCProperty 4 | from PartOMagic.Base import ExpressionParser 5 | 6 | # 7 | # 8 | # 9 | # 10 | # 11 | # 12 | # 13 | 14 | ##fixme: spreadsheet support? 15 | 16 | class PropertyExpression(FCProperty.PropertyLink): 17 | 18 | def getExpressionDeps(self): 19 | """returns list of lists of tuples. tuple corresponds to a single use of object. Tuple is (id, (start, end_plus_1)), where id is a name or a label of the object.""" 20 | return [ExpressionParser.expressionDeps(self._getExpression(v), None) for v in self.value] 21 | 22 | def inputs(self): 23 | depsdeps = self.getExpressionDeps() 24 | ret = [] 25 | doc = self.object.project 26 | for deps in depsdeps: 27 | for id,ch_range in deps: 28 | if doc.getObject(id) is not None: 29 | ret.append(dep.linked_object.Name) 30 | else: 31 | t = doc.getObjectsByLabel(id) 32 | if len(t) == 1: 33 | ret.append(t[0].Name) 34 | return ret 35 | 36 | def replace(self, replace_task): 37 | n_replaced = 0 38 | new_val = [] 39 | val = self.value 40 | depsdeps = self.getExpressionDeps() 41 | for i in range(len(val)): 42 | deps = depsdeps[i] 43 | expr = self._getExpression(val[i]) 44 | if expr is not None: 45 | for id, ch_range in deps[::-1]: 46 | if replace_task.has(id): 47 | new_id = replace_task.lookup(id) 48 | elif replace_task.has_label(id): 49 | new_id = replace_task.lookup_label(id) 50 | else: 51 | new_id = None 52 | if new_id: #new_id is None also if replacing 53 | f,t = ch_range 54 | expr = expr[0:f] + new_id + expr[t:] 55 | n_replaced += 1 56 | new_val.append(self._setExpression(val[i], expr)) 57 | else: 58 | new_val.append(val[i]) 59 | if n_replaced: 60 | self.value = new_val 61 | return n_replaced 62 | 63 | def purgeDeadLinks(self): 64 | #cleaning out references from an expression is not supported anyway. We could wipe out the expression completely... but let's just let FreeCAD to deal with it. 65 | return 0 66 | 67 | class PropertyExpressionEngine(PropertyExpression): 68 | types = ['App::PropertyExpressionEngine'] 69 | 70 | @property 71 | def value(self): 72 | lnn = self.node.find('ExpressionEngine') 73 | return [(it.get('path'), it.get('expression')) for it in lnn if it.tag == 'Expression'] 74 | 75 | @value.setter 76 | def value(self, new_val): 77 | lnn = self.node.find('ExpressionEngine') 78 | lnn.clear() 79 | lnn.set('count', str(len(new_val))) 80 | for path,expr in new_val: 81 | lnn.append(ElementTree.fromstring(''.format(path= path, expr= expr))) 82 | 83 | @staticmethod 84 | def _getExpression(value): 85 | return value[1] 86 | 87 | @staticmethod 88 | def _setExpression(value, expr): 89 | return (value[0], expr) 90 | 91 | 92 | # 93 | # 94 | # 95 | # 96 | # 97 | # 98 | 99 | class PropertyCells(PropertyExpression): 100 | types = ['Spreadsheet::PropertySheet'] 101 | 102 | @property 103 | def value(self): 104 | lnn = self.node.find('Cells') 105 | return [it.attrib for it in lnn if it.tag == 'Cell'] 106 | 107 | @value.setter 108 | def value(self, new_val): 109 | lnn = self.node.find('Cells') 110 | lnn.clear() 111 | lnn.set('Count', str(len(new_val))) 112 | for v in new_val: 113 | cn = ElementTree.fromstring('') 114 | cn.attrib.update(v) 115 | lnn.append(cn) 116 | 117 | @staticmethod 118 | def _getExpression(value): 119 | expr = value['content'] 120 | if not expr.startswith('='): 121 | return None 122 | else: 123 | return expr 124 | 125 | @staticmethod 126 | def _setExpression(value, expr): 127 | value['content'] = expr 128 | return value 129 | 130 | 131 | 132 | FCProperty.register_property_implementation(PropertyExpressionEngine) 133 | FCProperty.register_property_implementation(PropertyCells) -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = [ 4 | 'FCProject', 5 | 'FCObject', 6 | 'FCProperty', 7 | 'PropertyExpressionEngine', 8 | ] 9 | 10 | def importAll(): 11 | from . import FCProject 12 | from . import FCObject 13 | from . import FCProperty 14 | from . import PropertyExpressionEngine 15 | for modstr in __all__: 16 | mod = globals()[modstr] 17 | if hasattr(mod, 'importAll'): 18 | mod.importAll() 19 | 20 | def reloadAll(): 21 | try: #py2-3 compatibility: obtain reload() function 22 | reload 23 | except Exception: 24 | from importlib import reload 25 | 26 | for modstr in __all__: 27 | mod = globals()[modstr] 28 | reload(mod) 29 | if hasattr(mod, 'reloadAll'): 30 | mod.reloadAll() 31 | -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/importFCStd.py: -------------------------------------------------------------------------------- 1 | import FreeCAD 2 | 3 | def insert(filename,docname): 4 | #called when freecad wants to import a file 5 | from . import FCProject 6 | prj = FCProject.load(filename) 7 | try: 8 | doc = FreeCAD.getDocument(docname) 9 | except NameError: 10 | doc = FreeCAD.newDocument(docname) 11 | prj.mergeToFC(doc) 12 | 13 | def export(exportList,filename,tessellation=1): 14 | #called when freecad exports a file 15 | from . import FCProject 16 | prj = FCProject.fromFC(FreeCAD.ActiveDocument,[obj.Name for obj in exportList]) 17 | prj.writeFile(filename) 18 | 19 | def open(filename): 20 | FreeCAD.openDocument(filename) -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/readme.md: -------------------------------------------------------------------------------- 1 | # FilePlant 2 | 3 | ## What? 4 | 5 | FilePlant is a FCStd file parser/reader/writer utility. It is mostly a pure python implementation, with only a few places where FreeCAD is needed, and can potentially be stripped off of Part-o-magic into a standalone python module that doesn't need FreeCAD at all. 6 | 7 | ## Features 8 | 9 | * read an FCStd file and browse objects and properties. 10 | 11 | ~~~~ 12 | #load project "a_Project.FCStd" (it's a file path) and print out label of object named "Sketch" 13 | from PartOMagic.Base.FilePlant import FCProject 14 | prj = FCProject.load("a_Project.FCStd") 15 | print(prj.Object("Sketch").Property("Label").value) 16 | ~~~~ 17 | 18 | * with ViewProvider support 19 | 20 | ~~~~ 21 | #load project "a_Project.FCStd" (it's a file path) and print out if "Sketch" is visible 22 | from PartOMagic.Base.FilePlant import FCProject 23 | prj = FCProject.load("a_Project.FCStd") 24 | print(prj.Object("Sketch").ViewObject.Property("Visibility").value) #doesn't work, but only because there is no parser for boolean property type yet 25 | ~~~~ 26 | 27 | * modify objects in FCStd files 28 | 29 | ~~~~ 30 | #load project "a_Project.FCStd" (it's a file path) and change label of "Sketch" to "The Greatest Sketch ever" 31 | from PartOMagic.Base.FilePlant import FCProject 32 | prj = FCProject.load("a_Project.FCStd") 33 | prj.Object("Sketch").Property("Label").value = "The Greatest Sketch ever" 34 | prj.writeFile("a_Project.FCStd") 35 | ~~~~ 36 | 37 | * write out objects in a currently loaded project as FCStd files 38 | 39 | ~~~~ 40 | #writes out the object named "Sketch" from project opened in FreeCAD, into a file named "Sketch.FCStd". 41 | from PartOMagic.Base.FilePlant import FCProject 42 | prj = FCProject.fromFC(FreeCAD.ActiveDocument,["Sketch"]) 43 | prj.purgeDeadLinks() #optional 44 | prj.writeFile("Sketch.FCStd") 45 | ~~~~ 46 | 47 | * update objects in currently opened FC project using data from an object in an FCStd file 48 | 49 | ~~~~ 50 | #updates "Sketch" in current project with data from "Sketch002" object stored in "AnotherProject.FCStd" file 51 | prj = FCProject.load("AnotherProject.FCStd") 52 | prj.Object("Sketch002").updateFCObject(App.ActiveDocument.Sketch) 53 | ~~~~ 54 | 55 | * merge projects without loading them into FreeCAD: 56 | 57 | ~~~~ 58 | prj1 = FCProject.load("Project1.FCStd") 59 | prj2 = FCProject.load("Project2.FCStd") 60 | prj1.merge(prj2) 61 | prj1.writeFile("Merged.FCStd") 62 | ~~~~ 63 | 64 | * rename objects in FCStd files, taking care to remap links 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/xmls/Document_links_empty.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/xmls/Document_short.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/xmls/GuiDocument.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /PartOMagic/Base/FilePlant/xmls/Persistence.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /PartOMagic/Base/Parameters.py: -------------------------------------------------------------------------------- 1 | 2 | import FreeCAD as App 3 | 4 | class Parameter(object): 5 | path = "" 6 | param = "" 7 | type = "" 8 | default = None 9 | method_dict = {"Bool": ("GetBool", "SetBool")} 10 | def get(self): 11 | if not hasattr(self, "_value"): 12 | self._value = self.get_stored() 13 | return self._value 14 | 15 | def get_stored(self): 16 | return getattr(App.ParamGet(self.path), self.method_dict[self.type][0])(self.param, self.default) 17 | 18 | def set(self, val): 19 | getattr(App.ParamGet(self.path), self.method_dict[self.type][1])(self.param, val) 20 | self._value = val 21 | 22 | def set_volatile(self, val): 23 | "Changes parameter for part-o-magic, but doesn't write it to preferences. It will self-reset on restart." 24 | self._value = val 25 | 26 | class _paramEnableObserver(Parameter): 27 | "Sets if PartOMagic Observer is enabled (Observer sorts new objects to active containers,\n\ 28 | and enables visibility automation for container activation)" 29 | path = "User parameter:BaseApp/Preferences/Mod/PartOMagic" 30 | param = "EnableObserver" 31 | type = "Bool" 32 | default = 1 33 | EnableObserver = _paramEnableObserver() 34 | 35 | class _paramEnablePartOMagic(Parameter): 36 | "Sets if PartOMagic workbench is enabled (if disabled, no commands are added, so no )" 37 | path = "User parameter:BaseApp/Preferences/Mod/PartOMagic" 38 | param = "EnablePartOMagic" 39 | type = "Bool" 40 | default = 1 41 | EnablePartOMagic = _paramEnablePartOMagic() 42 | -------------------------------------------------------------------------------- /PartOMagic/Base/Utils.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import Part 3 | 4 | def shallowCopy(shape, extra_placement = None): 5 | """shallowCopy(shape, extra_placement = None): creates a shallow copy of a shape. The 6 | copy will match by isSame/isEqual/isPartner tests, but will have an independent placement.""" 7 | 8 | copiers = { 9 | "Vertex": lambda sh: sh.Vertexes[0], 10 | "Edge": lambda sh: sh.Edges[0], 11 | "Wire": lambda sh: sh.Wires[0], 12 | "Face": lambda sh: sh.Faces[0], 13 | "Shell": lambda sh: sh.Shells[0], 14 | "Solid": lambda sh: sh.Solids[0], 15 | "CompSolid": lambda sh: sh.CompSolids[0], 16 | "Compound": lambda sh: sh.Compounds[0], 17 | } 18 | copier = copiers.get(shape.ShapeType) 19 | if copier is None: 20 | copier = lambda sh: sh.copy() 21 | App.Console.PrintWarning("PartOMagic: shallowCopy: unexpected shape type '{typ}'. Using deep copy instead.\n".format(typ= shape.ShapeType)) 22 | ret = copier(shape) 23 | if extra_placement is not None: 24 | ret.Placement = extra_placement.multiply(ret.Placement) 25 | return ret 26 | 27 | def deepCopy(shape, extra_placement = None): 28 | """deepCopy(shape, extra_placement = None): Copies all subshapes. The copy will not match by isSame/isEqual/ 29 | isPartner tests.""" 30 | 31 | ret = shape.copy() 32 | if extra_placement is not None: 33 | ret.Placement = extra_placement.multiply(ret.Placement) 34 | return ret 35 | 36 | def transformCopy(shape, extra_placement = None): 37 | """transformCopy(shape, extra_placement = None): creates a deep copy shape with shape's placement applied to 38 | the subelements (the placement of returned shape is zero).""" 39 | 40 | if extra_placement is None: 41 | extra_placement = App.Placement() 42 | ret = shape.copy() 43 | if ret.ShapeType == "Vertex": 44 | # oddly, on Vertex, transformShape behaves strangely. So we'll create a new vertex instead. 45 | ret = Part.Vertex(extra_placement.multVec(ret.Point)) 46 | else: 47 | ret.transformShape(extra_placement.multiply(ret.Placement).toMatrix(), True) 48 | ret.Placement = App.Placement() #reset placement 49 | return ret 50 | 51 | def transformCopy_Smart(shape, feature_placement): 52 | "transformCopy_Smart(shape, feature_placement): unlike transformCopy, creates a shallow copy if possible." 53 | if shape.isNull(): 54 | return shape 55 | if PlacementsFuzzyCompare(shape.Placement, App.Placement()): 56 | sh = shallowCopy(shape) 57 | else: 58 | sh = transformCopy(shape) 59 | sh.Placement = feature_placement 60 | return sh 61 | 62 | def PlacementsFuzzyCompare(plm1, plm2): 63 | pos_eq = (plm1.Base - plm2.Base).Length < 1e-7 # 1e-7 is OCC's Precision::Confusion 64 | 65 | q1 = plm1.Rotation.Q 66 | q2 = plm2.Rotation.Q 67 | # rotations are equal if q1 == q2 or q1 == -q2. 68 | # Invert one of Q's if their scalar product is negative, before comparison. 69 | if q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2] + q1[3]*q2[3] < 0: 70 | q2 = [-v for v in q2] 71 | rot_eq = ( abs(q1[0]-q2[0]) + 72 | abs(q1[1]-q2[1]) + 73 | abs(q1[2]-q2[2]) + 74 | abs(q1[3]-q2[3]) ) < 1e-12 # 1e-12 is OCC's Precision::Angular (in radians) 75 | return pos_eq and rot_eq 76 | 77 | def addProperty(docobj, proptype, propname, group, tooltip, defvalue = None, readonly = False): 78 | """assureProperty(docobj, proptype, propname, defvalue, group, tooltip): adds 79 | a property if one is missing, and sets its value to default. Does nothing if property 80 | already exists. Returns True if property was created, or False if not.""" 81 | 82 | if propname in docobj.PropertiesList: 83 | #todo: check type match 84 | return False 85 | 86 | docobj.addProperty(proptype, propname, group, tooltip) 87 | if defvalue is not None: 88 | setattr(docobj, propname, defvalue) 89 | if readonly: 90 | docobj.setEditorMode(propname, 1) 91 | return True 92 | 93 | def compoundLeaves(compound): 94 | result = [] 95 | if compound.ShapeType != 'Compound': 96 | return compound 97 | for shape in compound.childShapes(): 98 | result.extend(compoundLeaves(shape)) 99 | return result -------------------------------------------------------------------------------- /PartOMagic/Base/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = [ 4 | 'Containers', 5 | 'Parameters', 6 | 'Utils', 7 | 'LinkTools', 8 | 'FilePlant' 9 | ] 10 | 11 | def importAll(): 12 | from . import Containers 13 | from . import Parameters 14 | from . import Utils 15 | from . import LinkTools 16 | from . import FilePlant 17 | for modstr in __all__: 18 | mod = globals()[modstr] 19 | if hasattr(mod, 'importAll'): 20 | mod.importAll() 21 | 22 | def reloadAll(): 23 | try: #py2-3 compatibility: obtain reload() function 24 | reload 25 | except Exception: 26 | from importlib import reload 27 | 28 | for modstr in __all__: 29 | mod = globals()[modstr] 30 | reload(mod) 31 | if hasattr(mod, 'reloadAll'): 32 | mod.reloadAll() 33 | -------------------------------------------------------------------------------- /PartOMagic/Features/AssyFeatures/Instance.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | 3 | from PartOMagic.Gui.Utils import Transaction 4 | 5 | 6 | 7 | def CreateInstance(): 8 | import FreeCADGui as Gui 9 | sel = Gui.Selection.getSelectionEx(App.ActiveDocument.Name) 10 | objs = [selobj.Object for selobj in sel] 11 | with Transaction("Create Instance"): 12 | for obj in objs: 13 | objname = obj.Name 14 | reprname = repr(obj.Name + '_i000') 15 | Gui.doCommand("f = App.ActiveDocument.addObject('App::Link',{reprname})\n" 16 | "f.LinkedObject = App.ActiveDocument.{objname}\n".format(**vars())) 17 | Gui.doCommand("f.Label = u'instance{num} of {obj}'.format(num= f.Name[-3:], obj= f.LinkedObject.Label)") 18 | Gui.doCommand("Gui.Selection.clearSelection()") 19 | 20 | 21 | 22 | from PartOMagic.Gui.AACommand import AACommand, CommandError 23 | commands = [] 24 | class CommandPart(AACommand): 25 | "Command to create an instance of an object" 26 | def GetResources(self): 27 | return {'CommandName': "PartOMagic_Instance", 28 | 'Pixmap' : self.getIconPath("PartOMagic_Instance.svg"), 29 | 'MenuText': "Make instance (realthunder's App::Link)", 30 | 'Accel': "", 31 | 'ToolTip': "Make instance: create a visual clone of selected object."} 32 | def RunOrTest(self, b_run): 33 | import FreeCADGui as Gui 34 | if len(Gui.Selection.getSelectionEx(App.ActiveDocument.Name))==0: 35 | raise CommandError(self, "Please select one or more objects, first. Instances of these objects will be created.") 36 | if b_run: CreateInstance() 37 | commands.append(CommandPart()) 38 | 39 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Features/AssyFeatures/MuxAssembly.py: -------------------------------------------------------------------------------- 1 | 2 | import FreeCAD as App 3 | if App.GuiUp: 4 | import FreeCADGui as Gui 5 | import Part 6 | 7 | from PartOMagic.Features import Ghost as mGhost 8 | Ghost = mGhost.Ghost 9 | from PartOMagic.Base import Containers 10 | from PartOMagic.Base.Utils import shallowCopy 11 | 12 | __title__="MuxAssembly feature (converts assembly into compound)" 13 | __author__ = "DeepSOIC" 14 | __url__ = "" 15 | 16 | def has_property(obj, propname): 17 | try: 18 | obj.getPropertyByName(propname) 19 | except AttributeError: 20 | return False 21 | else: 22 | return True 23 | 24 | def compoundFromAssembly(root, flatten, exclude, recursive = True, visit_set = None): 25 | if visit_set is None: 26 | visit_set = set() 27 | 28 | #recursion guard 29 | if root in visit_set: 30 | raise ValueError("Circular dependency") 31 | visit_set.add(root) 32 | 33 | if has_property(root, 'Shape'): 34 | return root.Shape 35 | else: 36 | children = Containers.getDirectChildren(root) 37 | shapes = [] 38 | for child in children: 39 | if child in exclude: 40 | continue 41 | if child.isDerivedFrom('App::Origin'): 42 | continue #otherwise, origins create empty compounds - undesirable. 43 | if has_property(child, 'Shape'): #since realthunder, Part has Shape attribute, but not Shape property. We want to process Parts ourselves. 44 | if not child.Shape.isNull(): 45 | shapes.append(child.Shape) 46 | elif Containers.isContainer(child) and recursive: 47 | cmp = compoundFromAssembly(child, flatten, exclude, recursive, visit_set) 48 | if flatten: 49 | shapes.extend(cmp.childShapes()) 50 | else: 51 | shapes.append(cmp) 52 | elif hasattr(obj, 'Shape'): #App::Link may have attribute but not property, and we totally want to include it. 53 | if not child.Shape.isNull(): 54 | shapes.append(child.Shape) 55 | transform = root.Placement if hasattr(root, 'Placement') else None 56 | ret = Part.makeCompound(shapes) 57 | if transform is not None: 58 | ret.Placement = transform 59 | return ret 60 | 61 | def make(): 62 | '''make(): makes a MUX object.''' 63 | obj = App.ActiveDocument.addObject('Part::FeaturePython','MUX') 64 | proxy = MUX(obj) 65 | vp_proxy = ViewProviderMUX(obj.ViewObject) 66 | return obj 67 | 68 | class MUX(Ghost): 69 | "MUX object, converts assembly into a compound" 70 | 71 | def __init__(self,selfobj): 72 | Ghost.__init__(self, selfobj) 73 | 74 | selfobj.IAm = 'PartOMagic.MUX' 75 | 76 | selfobj.addProperty('App::PropertyBool','FlattenCompound',"MUX","If true, compound nesting does not follow nesting of Parts. If False, compound nesting follows nesting of parts.") 77 | selfobj.addProperty('App::PropertyLinkListGlobal', 'ExclusionList', "MUX", 'List of objects to exclude from compound') 78 | selfobj.addProperty('App::PropertyEnumeration', 'Traversal', "MUX", 'Sets if to look for shapes in nested containers') 79 | selfobj.Traversal = ['Direct children', 'Recursive'] 80 | selfobj.Traversal = 'Recursive' 81 | 82 | def execute(self,selfobj): 83 | transform = self.getTransform(selfobj) 84 | selfobj.Shape = compoundFromAssembly(selfobj.Base, selfobj.FlattenCompound, selfobj.ExclusionList, recursive= selfobj.Traversal == 'Recursive') 85 | 86 | toleave,toenter = self.path 87 | if True: #(toleave and selfobj.UseForwardPlacements) or (toenter and selfobj.UseInversePlacements): 88 | selfobj.Placement = transform.multiply(selfobj.Base.Placement) 89 | selfobj.setEditorMode('Placement', 1) #read-only 90 | else: 91 | selfobj.setEditorMode('Placement', 0) #editable 92 | 93 | path = '' 94 | for cnt in toenter: 95 | path += '../' 96 | for cnt in toleave: 97 | path += cnt.Name + '/' 98 | labelf = u'{name} {label} from {path}' if toleave or toenter else u'{name} {label}' 99 | selfobj.Label = labelf.format(label= selfobj.Base.Label, name= selfobj.Name, path= path[:-1]) 100 | 101 | 102 | class ViewProviderMUX: 103 | "A View Provider for the MUX object" 104 | 105 | def __init__(self,vobj): 106 | vobj.Proxy = self 107 | 108 | def getIcon(self): 109 | from PartOMagic.Gui.Utils import getIconPath 110 | return getIconPath("PartOMagic_MUX.svg") 111 | 112 | def attach(self, vobj): 113 | self.ViewObject = vobj 114 | self.Object = vobj.Object 115 | 116 | def __getstate__(self): 117 | return None 118 | 119 | def __setstate__(self,state): 120 | return None 121 | 122 | def Create(sel): 123 | from PartOMagic.Gui.Utils import Transaction 124 | with Transaction("Create MUX"): 125 | Gui.addModule("PartOMagic.Features.AssyFeatures.MuxAssembly") 126 | Gui.doCommand("f = PartOMagic.Features.AssyFeatures.MuxAssembly.make()") 127 | Gui.doCommand("f.Base = App.ActiveDocument.{objname}".format(objname= sel.Name)) 128 | Gui.doCommand("Gui.Selection.clearSelection()") 129 | Gui.doCommand("Gui.Selection.addSelection(f)") 130 | Gui.doCommand("App.ActiveDocument.recompute()") 131 | Gui.doCommand("f.Base.ViewObject.hide()") 132 | 133 | 134 | 135 | # -------------------------- Gui command -------------------------------------------------- 136 | from PartOMagic.Gui.AACommand import AACommand, CommandError 137 | commands = [] 138 | class CommandMUX(AACommand): 139 | "Command to create MUX feature" 140 | def GetResources(self): 141 | import PartDesignGui #needed for icon 142 | return {'CommandName': 'PartOMagic_MUXAssembly', 143 | 'Pixmap' : self.getIconPath("PartOMagic_MUX.svg"), 144 | 'MenuText': "MUX assembly (Part to Compound)", 145 | 'Accel': "", 146 | 'ToolTip': "MUX Assembly. Creates a compound from shapes found in selected Part."} 147 | 148 | def RunOrTest(self, b_run): 149 | sel = Gui.Selection.getSelection() 150 | if len(sel)==0 : 151 | raise CommandError(self, "MUX Assembly command. Please select a Part container, then invoke the tool. A mux object will be created, a compound of all features of Part.") 152 | elif len(sel)==1: 153 | sel = sel[0] 154 | ac = Containers.activeContainer() 155 | if b_run: 156 | if sel in (Containers.getAllDependent(ac)+ [ac]): 157 | raise CommandError(self, "Can't create MUX here, because a circular dependency will result.") 158 | Create(sel) 159 | else: 160 | raise CommandError(self, u"MUX Assembly command. You need to select exactly one object (you selected {num}).".format(num= len(sel))) 161 | 162 | commands.append(CommandMUX()) 163 | # -------------------------- /Gui command -------------------------------------------------- 164 | 165 | exportedCommands = AACommand.registerCommands(commands) 166 | -------------------------------------------------------------------------------- /PartOMagic/Features/AssyFeatures/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = [ 4 | "Instance", 5 | "MuxAssembly", 6 | ] 7 | 8 | def importAll(): 9 | from . import Instance 10 | from . import MuxAssembly 11 | 12 | for modstr in __all__: 13 | mod = globals()[modstr] 14 | if hasattr(mod, "importAll"): 15 | mod.importAll() 16 | 17 | def reloadAll(): 18 | try: #py2-3 compatibility: obtain reload() function 19 | reload 20 | except Exception: 21 | from importlib import reload 22 | 23 | for modstr in __all__: 24 | mod = globals()[modstr] 25 | reload(mod) 26 | if hasattr(mod, "reloadAll"): 27 | mod.reloadAll() 28 | 29 | def exportedCommands(): 30 | result = [] 31 | for modstr in __all__: 32 | mod = globals()[modstr] 33 | if not hasattr(mod, 'reloadAll'): #do not add subpackages 34 | if hasattr(mod, "exportedCommands"): 35 | result += mod.exportedCommands() 36 | return result 37 | -------------------------------------------------------------------------------- /PartOMagic/Features/GenericContainer.py: -------------------------------------------------------------------------------- 1 | from PartOMagic.Base import Containers 2 | 3 | class GenericContainer(object): 4 | "Implements default behavior of containers in PoM (mostly aimed at C++ ones)" 5 | selfobj = None 6 | ViewObject = None 7 | 8 | def __init__(self, container): 9 | self.selfobj = container 10 | self.ViewObject = ViewProviderGenericContainer(container) 11 | 12 | def call(self, method, *args, **kwargs): 13 | "call(method, *args): if the object's proxy overrides the method, call the override. Else call standard implementation." 14 | if hasattr(self.selfobj, 'Proxy') and hasattr(self.selfobj.Proxy, method.__name__): 15 | getattr(self.selfobj.Proxy, method.__name__)(self.selfobj, *args, **kwargs) 16 | else: 17 | getattr(self, method.__name__)(*args, **kwargs) 18 | 19 | def advanceTip(self, new_object): 20 | pass 21 | 22 | class ViewProviderGenericContainer(object): 23 | "Implements default behavior of containers in PoM (mostly aimed at C++ ones)" 24 | selfobj = None 25 | selfvp = None 26 | 27 | def __init__(self, container): 28 | self.selfvp = None if not hasattr(container, 'ViewObject') else container.ViewObject 29 | self.selfobj = self.selfvp.Object 30 | 31 | def call(self, method, *args, **kwargs): 32 | "call(method, *args): if the viewprovider's proxy overrides the method, call the override. Else call standard implementation." 33 | if self.selfvp is None: return 34 | if hasattr(self.selfvp, 'Proxy') and hasattr(self.selfvp.Proxy, method.__name__): 35 | getattr(self.selfvp.Proxy, method.__name__)(self.selfvp, *args, **kwargs) 36 | else: 37 | getattr(self, method.__name__)(*args, **kwargs) 38 | 39 | def activationChanged(self, old_active_container, new_active_container, event): 40 | # event: -1 = leaving (active container was self or another container inside, new container is outside) 41 | # +1 = entering (active container was outside, new active container is inside) 42 | self.call(self.doDisplayModeAutomation, old_active_container, new_active_container, event) 43 | self.call(self.doTreeAutomation, old_active_container, new_active_container, event) 44 | 45 | def expandednessChanged(self, old_state, new_state): 46 | ac = Containers.activeContainer() 47 | activeChain = Containers.getContainerChain(ac)+[ac] 48 | self.call(self.doDisplayModeAutomation, ac, ac, +1 if new_state == True else (0 if self.selfobj in activeChain else -1)) 49 | 50 | 51 | def doDisplayModeAutomation(self, old_active_container, new_active_cntainer, event): 52 | # event: -1 = show public stuff 53 | # +1 = show private stuff 54 | o = self.selfobj 55 | if event == +1: 56 | if o.isDerivedFrom('PartDesign::Body'): 57 | dm = "Through" 58 | if o.ViewObject.DisplayModeBody != dm: # check if actual change needed, to avoid potential slowdown 59 | o.ViewObject.DisplayModeBody = dm 60 | o.ViewObject.Visibility = o.ViewObject.Visibility #workaround for bug: http://forum.freecadweb.org/viewtopic.php?f=3&t=15845 61 | elif o.isDerivedFrom('PartDesign::Boolean'): 62 | dm = 'Tools' 63 | if o.ViewObject.Display != dm: # check if actual change needed, to avoid potential slowdown 64 | o.ViewObject.Display = dm 65 | #o.ViewObject.Visibility = o.ViewObject.Visibility #workaround for bug: http://forum.freecadweb.org/viewtopic.php?f=3&t=15845 66 | elif event == -1: 67 | if o.isDerivedFrom('PartDesign::Body'): 68 | dm = "Tip" 69 | if o.ViewObject.DisplayModeBody != dm: # check if actual change needed, to avoid potential slowdown 70 | o.ViewObject.DisplayModeBody = dm 71 | o.ViewObject.Visibility = o.ViewObject.Visibility #workaround for bug: http://forum.freecadweb.org/viewtopic.php?f=3&t=15845 72 | elif o.isDerivedFrom('PartDesign::Boolean'): 73 | dm = 'Result' 74 | if o.ViewObject.Display != dm: # check if actual change needed, to avoid potential slowdown 75 | o.ViewObject.Display = dm 76 | #o.ViewObject.Visibility = o.ViewObject.Visibility #workaround for bug: http://forum.freecadweb.org/viewtopic.php?f=3&t=15845 77 | 78 | def doTreeAutomation(self, old_active_container, new_active_cntainer, event): 79 | import FreeCADGui as Gui 80 | if event == +1: 81 | if not 'Expanded' in self.selfobj.State: 82 | Gui.ActiveDocument.toggleTreeItem(self.selfobj, 2 ) #expand 83 | elif event == -1: 84 | if not (self.selfobj.isDerivedFrom('App::Part') or self.selfobj.isDerivedFrom('App::DocumentObjectGroup')): 85 | if 'Expanded' in self.selfobj.State: 86 | Gui.ActiveDocument.toggleTreeItem(self.selfobj, 1 ) #collapse 87 | 88 | 89 | -------------------------------------------------------------------------------- /PartOMagic/Features/Module.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | if App.GuiUp: 3 | import FreeCADGui as Gui 4 | import Part 5 | 6 | __title__="Module container" 7 | __author__ = "DeepSOIC" 8 | __url__ = "" 9 | 10 | 11 | from PartOMagic.Base.Utils import transformCopy_Smart 12 | 13 | def makeModule(name): 14 | '''makeModule(name): makes a Module object.''' 15 | obj = App.ActiveDocument.addObject("Part::FeaturePython",name) 16 | proxy = _Module(obj) 17 | origin = App.ActiveDocument.addObject('App::Origin', 'Origin') 18 | obj.Origin = origin 19 | vp_proxy = _ViewProviderModule(obj.ViewObject) 20 | return obj 21 | 22 | class _Module: 23 | "The Module object" 24 | def __init__(self,obj): 25 | self.Type = "Module" 26 | obj.addExtension('App::OriginGroupExtensionPython') 27 | try: 28 | obj.addProperty('App::PropertyLinkChild','Tip',"Module","Object to be exposed to the outside") 29 | except Exception: 30 | #for older FC 31 | obj.addProperty('App::PropertyLink','Tip',"Module","Object to be exposed to the outside") 32 | 33 | obj.Proxy = self 34 | 35 | def execute(self,selfobj): 36 | from PartOMagic.Gui.Utils import screen 37 | if selfobj.Tip is not None: 38 | selfobj.Shape = transformCopy_Smart(screen(selfobj.Tip).Shape, selfobj.Placement) 39 | else: 40 | selfobj.Shape = Part.Shape() 41 | 42 | def advanceTip(self, selfobj, new_object): 43 | from PartOMagic.Gui.Utils import screen 44 | old_tip = screen(selfobj.Tip) 45 | new_tip = old_tip 46 | if old_tip is None: 47 | new_tip = new_object 48 | if old_tip in new_object.OutList: 49 | new_tip = new_object 50 | 51 | if new_tip is None: return 52 | if new_tip is old_tip: return 53 | if new_tip.Name.startswith("Clone"): return 54 | if new_tip.Name.startswith("ShapeBinder"): return 55 | if new_tip.Name.startswith("Ghost"): return 56 | selfobj.Tip = new_tip 57 | 58 | def onDocumentRestored(self, selfobj): 59 | import PartOMagic.Base.Compatibility as compat 60 | if compat.scoped_links_are_supported(): 61 | #check that Tip is scoped properly. Recreate the property if not. 62 | if not 'Child' in selfobj.getTypeIdOfProperty('Tip'): 63 | v = selfobj.Tip 64 | t = selfobj.getTypeIdOfProperty('Tip') 65 | g = selfobj.getGroupOfProperty('Tip') 66 | d = selfobj.getDocumentationOfProperty('Tip') 67 | selfobj.removeProperty('Tip') 68 | selfobj.addProperty(t+'Child','Tip', g, d) 69 | selfobj.Tip = v 70 | 71 | def getSubObjects(self, host, reason): 72 | #implementing this fixes Export exporting inner content instead of shape 73 | # in PD Body: 74 | #if(reason==GS_SELECT && !showTip) 75 | # return Part::BodyBase::getSubObjects(reason); 76 | if reason == 1 and host.ViewObject and host.ViewObject.DisplayMode == 'Group': 77 | raise NotImplementedError 78 | return [] 79 | 80 | class _ViewProviderModule: 81 | "A View Provider for the Module object" 82 | 83 | def __init__(self,vobj): 84 | vobj.addExtension("Gui::ViewProviderOriginGroupExtensionPython") 85 | vobj.Proxy = self 86 | 87 | def getIcon(self): 88 | from PartOMagic.Gui.Utils import getIconPath 89 | return getIconPath("PartDesign_Body_Tree.svg") 90 | 91 | def attach(self, vobj): 92 | self.ViewObject = vobj 93 | self.Object = vobj.Object 94 | 95 | def doubleClicked(self, vobj): 96 | from PartOMagic.Gui.Observer import activeContainer, setActiveContainer 97 | ac = activeContainer() 98 | if ac is vobj.Object: 99 | setActiveContainer(vobj.Object.Document) #deactivate self 100 | else: 101 | setActiveContainer(vobj.Object) #activate self 102 | Gui.Selection.clearSelection() 103 | return True 104 | 105 | def doDisplayModeAutomation(self, vobj, old_active_container, new_active_container, event): 106 | # event: -1 = show public stuff 107 | # +1 = show private stuff 108 | if event == +1: 109 | self.oldMode = vobj.DisplayMode 110 | vobj.DisplayMode = 'Group' 111 | elif event == -1: 112 | if self.oldMode == 'Group': 113 | self.oldMode = 'Flat Lines' 114 | vobj.DisplayMode = self.oldMode 115 | 116 | def __getstate__(self): 117 | return None 118 | 119 | def __setstate__(self,state): 120 | return None 121 | 122 | def CreateModule(name): 123 | App.ActiveDocument.openTransaction("Create Module") 124 | Gui.addModule("PartOMagic.Features.Module") 125 | Gui.doCommand("f = PartOMagic.Features.Module.makeModule(name = '"+name+"')") 126 | Gui.doCommand("PartOMagic.Base.Containers.setActiveContainer(f)") 127 | Gui.doCommand("Gui.Selection.clearSelection()") 128 | App.ActiveDocument.commitTransaction() 129 | 130 | 131 | # -------------------------- /common stuff -------------------------------------------------- 132 | 133 | # -------------------------- Gui command -------------------------------------------------- 134 | from PartOMagic.Gui.AACommand import AACommand, CommandError 135 | commands = [] 136 | class CommandModule(AACommand): 137 | "Command to create Module feature" 138 | def GetResources(self): 139 | import PartDesignGui #needed for icon 140 | return {'CommandName': 'PartOMagic_Module', 141 | 'Pixmap' : self.getIconPath("PartDesign_Body_Create_New.svg"), 142 | 'MenuText': "New Module container", 143 | 'Accel': "", 144 | 'ToolTip': "New Module container. Module is like PartDesign Body, but for Part workbench and friends."} 145 | 146 | def RunOrTest(self, b_run): 147 | if b_run: CreateModule(name = "Module") 148 | commands.append(CommandModule()) 149 | # -------------------------- /Gui command -------------------------------------------------- 150 | 151 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Features/PDShapeFeature.py: -------------------------------------------------------------------------------- 1 | #mirror, for not breaking old projects 2 | from .PartDesign.PDShapeFeature import * -------------------------------------------------------------------------------- /PartOMagic/Features/PartContainer.py: -------------------------------------------------------------------------------- 1 | 2 | import FreeCAD as App 3 | if App.GuiUp: 4 | import FreeCADGui as Gui 5 | from PartOMagic.Gui.Utils import Transaction 6 | from PartOMagic.Base import Containers 7 | 8 | 9 | def CreatePart(): 10 | with Transaction("Create Part"): 11 | Gui.doCommand("f = App.ActiveDocument.addObject('App::Part','Part')") 12 | Gui.doCommand("PartOMagic.Gui.Observer.activateContainer(f)") 13 | Gui.doCommand("Gui.Selection.clearSelection()") 14 | 15 | from PartOMagic.Gui.AACommand import AACommand, CommandError 16 | commands = [] 17 | class CommandPart(AACommand): 18 | "Command to create a Part container" 19 | def GetResources(self): 20 | import PartDesignGui 21 | return {'CommandName': "PartOMagic_Part", 22 | 'Pixmap' : self.getIconPath("Tree_Annotation.svg"), 23 | 'MenuText': "New Part container", 24 | 'Accel': "", 25 | 'ToolTip': "New Part container. Creates an empty Part container."} 26 | def RunOrTest(self, b_run): 27 | if b_run: CreatePart() 28 | commands.append(CommandPart()) 29 | 30 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Features/PartDesign/PDShapeFeature.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | if App.GuiUp: 3 | import FreeCADGui as Gui 4 | import Part 5 | 6 | __title__="PDShapeFeature container" 7 | __author__ = "DeepSOIC" 8 | __url__ = "" 9 | 10 | 11 | from PartOMagic.Base.Utils import transformCopy, shallowCopy 12 | from PartOMagic.Base.Utils import PlacementsFuzzyCompare 13 | 14 | def makePDShapeFeature(name, body): 15 | '''makePDShapeFeature(name): makes a PDShapeFeature object.''' 16 | obj = body.newObject('PartDesign::FeaturePython',name) 17 | proxy = PDShapeFeature(obj) 18 | vp_proxy = ViewProviderPDShapeFeature(obj.ViewObject) 19 | return obj 20 | 21 | class PDShapeFeature: 22 | "The PDShapeFeature object" 23 | def __init__(self,obj): 24 | self.Type = 'PDShapeFeature' 25 | obj.addExtension('App::OriginGroupExtensionPython') 26 | 27 | try: 28 | obj.addProperty('App::PropertyLinkChild','Tip',"PartDesign","Object to use to form the feature") 29 | except Exception: 30 | #for older FC 31 | obj.addProperty('App::PropertyLink','Tip',"PartDesign","Object to use to form the feature") 32 | 33 | obj.addProperty('App::PropertyEnumeration', 'AddSubType', "PartDesign", "Feature kind") 34 | obj.addProperty('Part::PropertyPartShape', 'AddSubShape', "PartDesign", "Shape that forms the feature") #TODO: expose PartDesign::AddSub, and use it, instead of mimicking it 35 | obj.AddSubType = ['Additive', 'Subtractive'] 36 | 37 | obj.setEditorMode('Placement', 0) #non-readonly non-hidden 38 | 39 | obj.Proxy = self 40 | 41 | 42 | def execute(self,selfobj): 43 | import Part 44 | selfobj.AddSubShape = shallowCopy(selfobj.Tip.Shape, selfobj.Placement) 45 | base_feature = selfobj.BaseFeature 46 | result_shape = None 47 | if selfobj.AddSubType == 'Additive': 48 | if base_feature is None: 49 | result_shape = selfobj.AddSubShape.Solids[0] 50 | else: 51 | result_shape = base_feature.Shape.fuse(selfobj.AddSubShape).Solids[0] 52 | elif selfobj.AddSubType == 'Subtractive': 53 | result_shape = base_feature.Shape.cut(selfobj.AddSubShape).Solids[0] 54 | else: 55 | raise ValueError("AddSub Type not implemented: {t}".format(t= selfobj.AddSubType)) 56 | if not PlacementsFuzzyCompare(selfobj.Placement, result_shape.Placement): 57 | result_shape = transformCopy(result_shape, selfobj.Placement.inverse()) #the goal is that Placement of selfobj doesn't move the result shape, only the shape being fused up 58 | selfobj.Shape = result_shape 59 | 60 | def advanceTip(self, selfobj, new_object): 61 | from PartOMagic.Gui.Utils import screen 62 | old_tip = screen(selfobj.Tip) 63 | new_tip = old_tip 64 | if old_tip is None: 65 | new_tip = new_object 66 | if old_tip in new_object.OutList: 67 | new_tip = new_object 68 | 69 | if new_tip is None: return 70 | if new_tip is old_tip: return 71 | if new_tip.Name.startswith('Clone'): return 72 | if new_tip.Name.startswith('ShapeBinder'): return 73 | selfobj.Tip = new_tip 74 | 75 | def onDocumentRestored(self, selfobj): 76 | import PartOMagic.Base.Compatibility as compat 77 | if compat.scoped_links_are_supported(): 78 | #check that Tip is scoped properly. Recreate the property if not. 79 | if not 'Child' in selfobj.getTypeIdOfProperty('Tip'): 80 | v = selfobj.Tip 81 | t = selfobj.getTypeIdOfProperty('Tip') 82 | g = selfobj.getGroupOfProperty('Tip') 83 | d = selfobj.getDocumentationOfProperty('Tip') 84 | selfobj.removeProperty('Tip') 85 | selfobj.addProperty(t+'Child','Tip', g, d) 86 | selfobj.Tip = v 87 | 88 | class ViewProviderPDShapeFeature: 89 | "A View Provider for the PDShapeFeature object" 90 | 91 | def __init__(self,vobj): 92 | vobj.addExtension('Gui::ViewProviderGeoFeatureGroupExtensionPython') 93 | vobj.Proxy = self 94 | 95 | def getIcon(self): 96 | from PartOMagic.Gui.Utils import getIconPath 97 | return getIconPath('PartOMagic_PDShapeFeature_{Additive}.svg'.format(Additive= self.Object.AddSubType) ) 98 | 99 | def attach(self, vobj): 100 | self.ViewObject = vobj 101 | self.Object = vobj.Object 102 | 103 | def doubleClicked(self, vobj): 104 | from PartOMagic.Gui.Observer import activeContainer, setActiveContainer 105 | ac = activeContainer() 106 | if ac is vobj.Object: 107 | setActiveContainer(vobj.Object.Document) #deactivate self 108 | else: 109 | setActiveContainer(vobj.Object) #activate self 110 | Gui.Selection.clearSelection() 111 | return True 112 | 113 | def doDisplayModeAutomation(self, vobj, old_active_container, new_active_container, event): 114 | # event: -1 = leaving (active container was self or another container inside, new container is outside) 115 | # +1 = entering (active container was outside, new active container is inside) 116 | if event == +1: 117 | self.oldMode = vobj.DisplayMode 118 | vobj.DisplayMode = 'Group' 119 | elif event == -1: 120 | if self.oldMode == 'Group': 121 | self.oldMode = 'Flat Lines' 122 | vobj.DisplayMode = self.oldMode 123 | 124 | def __getstate__(self): 125 | return None 126 | 127 | def __setstate__(self,state): 128 | return None 129 | 130 | def onDelete(self, viewprovider, subelements): # subelements is a tuple of strings 131 | try: 132 | selfobj = self.Object 133 | import PartOMagic.Base.Containers as Containers 134 | body = Containers.getContainer(selfobj) 135 | if not body.isDerivedFrom('PartDesign::Body'): return 136 | 137 | if self.ViewObject.Visibility and selfobj.BaseFeature: 138 | selfobj.BaseFeature.ViewObject.show() 139 | 140 | body.removeObject(selfobj) 141 | 142 | except Exception as err: 143 | App.Console.PrintError("Error in onDelete: " + err.message) 144 | return True 145 | 146 | 147 | def CreatePDShapeFeature(name, add_sub_type= 'Additive'): 148 | App.ActiveDocument.openTransaction("Create PDShapeFeature") 149 | Gui.addModule('PartOMagic.Features.PDShapeFeature') 150 | Gui.doCommand('body = PartOMagic.Base.Containers.activeContainer()') 151 | Gui.doCommand('f = PartOMagic.Features.PDShapeFeature.makePDShapeFeature(name = {name}, body= body)'.format(name= repr(name))) 152 | Gui.doCommand('if f.BaseFeature:\n' 153 | ' f.BaseFeature.ViewObject.hide()') 154 | Gui.doCommand('f.AddSubType = {t}'.format(t= repr(add_sub_type))) 155 | Gui.doCommand('PartOMagic.Base.Containers.setActiveContainer(f)') 156 | Gui.doCommand('Gui.Selection.clearSelection()') 157 | App.ActiveDocument.commitTransaction() 158 | 159 | 160 | # -------------------------- /common stuff -------------------------------------------------- 161 | 162 | # -------------------------- Gui command -------------------------------------------------- 163 | from PartOMagic.Gui.AACommand import AACommand, CommandError 164 | commands = [] 165 | class CommandPDShapeFeature(AACommand): 166 | "Command to create PDShapeFeature feature" 167 | def GetResources(self): 168 | if self.add_sub_type == 'Additive': 169 | return {'CommandName': 'PartOMagic_PDShapeFeature_Additive', 170 | 'Pixmap' : self.getIconPath('PartOMagic_PDShapeFeature_Additive.svg'), 171 | 'MenuText': "PartDesign addive shape".format(additive= self.add_sub_type), 172 | 'Accel': '', 173 | 'ToolTip': "New PartDesign additive shape container. This allows to insert non-PartDesign things into PartDesign sequence."} 174 | elif self.add_sub_type == 'Subtractive': 175 | return {'CommandName': 'PartOMagic_PDShapeFeature_Subtractive', 176 | 'Pixmap' : self.getIconPath('PartOMagic_PDShapeFeature_Subtractive.svg'), 177 | 'MenuText': "PartDesign subtractive shape".format(additive= self.add_sub_type), 178 | 'Accel': '', 179 | 'ToolTip': "New PartDesign subtractive shape container. This allows to insert non-PartDesign things into PartDesign sequence."} 180 | 181 | def RunOrTest(self, b_run): 182 | from PartOMagic.Base.Containers import activeContainer 183 | ac = activeContainer() 184 | if ac is None: 185 | raise CommandError(self, "No active container!") 186 | if not ac.isDerivedFrom('PartDesign::Body'): 187 | raise CommandError(self, "Active container is not a PartDesign Body. Please activate a PartDesign Body, first.") 188 | if ac.Tip is None and self.add_sub_type != 'Additive': 189 | raise CommandError(self, "There is no material to subtract from. Either use additive shape feature instead, or add some material to the body.") 190 | if b_run: CreatePDShapeFeature('{Additive}Shape'.format(Additive= self.add_sub_type), self.add_sub_type) 191 | 192 | commands.append(CommandPDShapeFeature(add_sub_type= 'Additive')) 193 | commands.append(CommandPDShapeFeature(add_sub_type= 'Subtractive')) 194 | 195 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Features/PartDesign/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = [ 4 | "PDShapeFeature", 5 | ] 6 | 7 | def importAll(): 8 | from . import PDShapeFeature 9 | 10 | def reloadAll(): 11 | try: #py2-3 compatibility: obtain reload() function 12 | reload 13 | except Exception: 14 | from importlib import reload 15 | 16 | for modstr in __all__: 17 | mod = globals()[modstr] 18 | reload(mod) 19 | if hasattr(mod, "reloadAll"): 20 | mod.reloadAll() 21 | 22 | def exportedCommands(): 23 | result = [] 24 | for modstr in __all__: 25 | mod = globals()[modstr] 26 | if hasattr(mod, "exportedCommands"): 27 | result += mod.exportedCommands() 28 | return result 29 | -------------------------------------------------------------------------------- /PartOMagic/Features/ShapeBinder.py: -------------------------------------------------------------------------------- 1 | 2 | import FreeCAD as App 3 | if App.GuiUp: 4 | import FreeCADGui as Gui 5 | from PartOMagic.Gui.Utils import * 6 | from PartOMagic.Base import Containers 7 | 8 | 9 | def CreateShapeBinder(feature): 10 | App.ActiveDocument.openTransaction("Create Shapebinder") 11 | Gui.doCommand("f = App.ActiveDocument.addObject('PartDesign::ShapeBinder','ShapeBinder')") 12 | Gui.doCommand("f.Support = [App.ActiveDocument.{feat},('')]".format(feat= feature.Name)) 13 | Gui.doCommand("f.Label = '{{ref}} ({{selfname}})'.format(selfname= f.Name, ref= App.ActiveDocument.{feat}.Label)".format(feat= feature.Name)) 14 | Gui.doCommand("f.recompute()") 15 | App.ActiveDocument.commitTransaction() 16 | Gui.doCommand("Gui.Selection.clearSelection()") 17 | Gui.doCommand("Gui.Selection.addSelection(f)") 18 | 19 | from PartOMagic.Gui.AACommand import AACommand, CommandError 20 | commands = [] 21 | class CommandShapeBinder(AACommand): 22 | "Command to create a shapebinder" 23 | def GetResources(self): 24 | import PartDesignGui 25 | return {'CommandName': "PartOMagic_ShapeBinder", 26 | 'Pixmap' : self.getIconPath("PartDesign_ShapeBinder.svg"), 27 | 'MenuText': "Shapebinder", 28 | 'Accel': "", 29 | 'ToolTip': "Shapebinder. (create a cross-container shape reference)"} 30 | 31 | def RunOrTest(self, b_run): 32 | sel = Gui.Selection.getSelection() 33 | if len(sel)==0 : 34 | raise CommandError(self, "Shapebinder command. Please select an object to import to active container, first. The object must be geometry.") 35 | elif len(sel)==1: 36 | sel = screen(sel[0]) 37 | ac = Containers.activeContainer() 38 | if sel in Containers.getDirectChildren(ac): 39 | raise CommandError(self, u"{feat} is from active container ({cnt}). Please select an object belonging to another container.".format(feat= sel.Label, cnt= ac.Label)) 40 | if sel in (Containers.getAllDependent(ac)+ [ac]): 41 | raise CommandError(self, "Can't create a shapebinder, because a circular dependency will result.") 42 | if b_run: CreateShapeBinder(sel) 43 | else: 44 | raise CommandError(self, u"Shapebinder command. You need to select exactly one object (you selected {num}).".format(num= len(sel))) 45 | commands.append(CommandShapeBinder()) 46 | 47 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Features/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = [ 4 | "PartContainer", 5 | "Module", 6 | "ShapeGroup", 7 | "ShapeBinder", 8 | "Ghost", 9 | "Exporter", 10 | "PartDesign", 11 | "AssyFeatures" 12 | ] 13 | 14 | def importAll(): 15 | from . import PartContainer 16 | from . import Module 17 | from . import ShapeGroup 18 | from . import ShapeBinder 19 | from . import Ghost 20 | from . import Exporter 21 | from . import PartDesign 22 | from . import AssyFeatures 23 | 24 | for modstr in __all__: 25 | mod = globals()[modstr] 26 | if hasattr(mod, "importAll"): 27 | mod.importAll() 28 | 29 | def reloadAll(): 30 | try: #py2-3 compatibility: obtain reload() function 31 | reload 32 | except Exception: 33 | from importlib import reload 34 | 35 | for modstr in __all__: 36 | mod = globals()[modstr] 37 | reload(mod) 38 | if hasattr(mod, "reloadAll"): 39 | mod.reloadAll() 40 | 41 | def exportedCommands(): 42 | result = [] 43 | for modstr in __all__: 44 | mod = globals()[modstr] 45 | if not hasattr(mod, 'reloadAll'): #do not add subpackages (PartDesign) 46 | if hasattr(mod, "exportedCommands"): 47 | result += mod.exportedCommands() 48 | return result 49 | -------------------------------------------------------------------------------- /PartOMagic/Gui/AACommand.py: -------------------------------------------------------------------------------- 1 | '''AACommand module is here to automate some stuff of command addition within Part-o-magic. 2 | 3 | Example command implementation follows. 4 | 5 | from PartOMagic.Gui.AACommand import AACommand, CommandError 6 | commands = [] 7 | class CommandEnter(AACommand): 8 | "Command to enter a feature" 9 | def GetResources(self): 10 | import SketcherGui #needed for icons 11 | return {'CommandName': 'PartOMagic_Enter', # !!! <----- compared to standard FreeCAD command definition, add this! 12 | 'Pixmap' : self.getIconPath('Sketcher_EditSketch.svg'), 13 | 'MenuText': "Enter object", 14 | 'Accel': "", 15 | 'ToolTip': "Enter object. (activate a container, or open a sketch for editing)"} 16 | 17 | def RunOrTest(self, b_run): 18 | # !!! RunOrTest is called by AACommand from within both IsActive and Activate. 19 | # if b_run is True, the call is from Activated, and you need to perform the action. 20 | # if b_run is False, you should do a dry run: check the conditions, and throw errors, but not do the thing! 21 | # if conditions are not met. 22 | 23 | sel = Gui.Selection.getSelection() 24 | if len(sel)==0 : 25 | raise CommandError(self, "Enter Object command. Please select an object to enter, first. It can be a container, or a sketch.") 26 | #if you raise CommandError, command is inactive. If you throw any other error, 27 | # command is inactive too, but the error is printed to report view. 28 | elif len(sel)==1: 29 | if b_run: Containers.setActiveContainer(sel) # !!! <---- check for b_run is very important! otherwise the command will run every half a second, and screw everything up! 30 | if b_run: Gui.Selection.clearSelection() 31 | else: 32 | raise CommandError(self, u"Enter Object command. You need to select exactly one object (you selected {num}).".format(num= len(sel))) 33 | 34 | # Returning without any error is the signal that the command is active! Return value is ignored. 35 | # Any error thrown when b_run is True will pop up as error message (except CancelError). 36 | 37 | commands.append(CommandEnter()) 38 | 39 | # (add more to commands) 40 | 41 | exportedCommands = AACommand.registerCommands(commands) 42 | 43 | -------------------- 44 | AACommand constructor takes any keyword arguments, and adds them as attributes of instance. 45 | E.g. 46 | cmd = AACommand(my_type= 'additive') 47 | # cmd.my_type == 'additive' 48 | 49 | AACommand is not registered automatically. Use register() method. 50 | AACommand.registerCommands() static method is convenient to register a list of commands, 51 | and make exportedCommands() function at the same time. 52 | ''' 53 | 54 | 55 | 56 | import FreeCAD as App 57 | from PartOMagic.Base.Containers import NoActiveContainerError 58 | 59 | from PartOMagic.Gui.Utils import msgError 60 | 61 | class CommandError(Exception): 62 | def __init__(self, command, message): 63 | self.command = command 64 | try: 65 | self.title = "Part-o-Magic "+ command.GetResources()['MenuText'] 66 | except Exception as err: 67 | self.title = "" 68 | self.message = message 69 | self.show_msg_on_delete = True 70 | 71 | def __del__(self): 72 | if self.show_msg_on_delete: 73 | msgError(self) 74 | 75 | registeredCommands = {} #dict, key = command name, value = instance of command 76 | 77 | class AACommand(object): 78 | "Command class prototype. Any keyword arguments supplied to constructor are added as attributes" 79 | def __define_attributes(self): 80 | self.AA = False #AA stands for "Always Active". If true, IsActive always returns true, and error message is shown if conditions are not met (such as not right selection). 81 | self.command_name = None # string specifying command name 82 | self.command_name_aa = None 83 | self.is_registered = False 84 | self.aa_command_instance = None 85 | 86 | def __init__(self, **kwargs): 87 | self.__define_attributes() 88 | for arg in kwargs: 89 | setattr(self, arg, kwargs[arg]) 90 | 91 | def register(self): 92 | if self.isRegistered(): 93 | import FreeCAD as App 94 | App.Console.PrintWarning(u"Re-registering command {cmd}\n".format(cmd= self.command_name)) 95 | 96 | if self.command_name is None: 97 | self.command_name = self.GetResources()['CommandName'] 98 | 99 | import FreeCADGui as Gui 100 | Gui.addCommand(self.command_name, self) 101 | global registeredCommands 102 | registeredCommands[self.command_name] = self 103 | self.is_registered = True 104 | 105 | #also register an AA version of the command 106 | if not self.AA: 107 | self.command_name_aa = self.command_name + '_AA' 108 | import copy 109 | cpy = copy.copy(self) 110 | cpy.AA = True 111 | cpy.command_name = self.command_name_aa 112 | cpy.is_registered = False #since we copied an already registered command, it thinks it's registered too. 113 | cpy.register() 114 | self.aa_command_instance = cpy 115 | 116 | return self.command_name 117 | 118 | def isRegistered(self): 119 | return self.is_registered 120 | 121 | def RunOrTest(self, b_run): 122 | raise CommandError(self, "command not implemented") 123 | # override this. if b_run, run the actual code. If not, do a dry run and throw CommandErrors if conditions are not met. 124 | 125 | def Activated(self): 126 | # you generally shouldn't override this. Override RunOrTest instead. 127 | try: 128 | self.RunOrTest(b_run= True) 129 | except CommandError as err: 130 | pass 131 | except Exception as err: 132 | msgError(err) 133 | raise 134 | 135 | def IsActive(self): 136 | # you generally shouldn't override this. Override RunOrTest instead. 137 | if not App.ActiveDocument: return False 138 | if self.AA: return True 139 | try: 140 | self.RunOrTest(b_run= False) 141 | return True 142 | except CommandError as err: 143 | err.show_msg_on_delete = False 144 | return False 145 | except NoActiveContainerError as err: 146 | #handling these to prevent error train in report view, when in spreadsheet for example 147 | return False 148 | except Exception as err: 149 | App.Console.PrintError(repr(err)) 150 | return True 151 | 152 | def getIconPath(self, icon_dot_svg): 153 | import PartOMagic.Gui.Icons.Icons 154 | return ":/icons/" + icon_dot_svg 155 | 156 | @staticmethod 157 | def registerCommands(list_of_commands): 158 | 'registerCommands(list_of_commands): registers commands, and returns typical implementation of exportedCommands()' 159 | f_ret = lambda: _exportedCommands(list_of_commands) 160 | if App.GuiUp: 161 | f_ret()# to actually register them 162 | return f_ret 163 | 164 | def _exportedCommands(commands): 165 | if not commands[0].isRegistered(): 166 | for cmd in commands: 167 | cmd.register() 168 | return [cmd.command_name for cmd in commands] 169 | -------------------------------------------------------------------------------- /PartOMagic/Gui/CommandCollection1.py: -------------------------------------------------------------------------------- 1 | 2 | import PartOMagic.Gui as myGui 3 | myGui.Tools.importAll() 4 | import PartOMagic.Features as myFeatures 5 | myFeatures.importAll() 6 | from .GroupCommand import GroupCommand 7 | 8 | import FreeCADGui as Gui 9 | 10 | Gui.addCommand('PartOMagic_Collection1', 11 | GroupCommand( 12 | list_of_commands= myFeatures.exportedCommands() + ['PartOMagic_SetTip'], 13 | menu_text= "PartOMagic collection 1", 14 | tooltip= "" 15 | ) 16 | ) 17 | 18 | def exportedCommands(): 19 | return ['PartOMagic_Collection1'] -------------------------------------------------------------------------------- /PartOMagic/Gui/Control/OnOff.py: -------------------------------------------------------------------------------- 1 | 2 | from PartOMagic.Gui.Utils import msgbox 3 | import PartOMagic.Base.Parameters as Params 4 | 5 | import FreeCAD as App 6 | if App.GuiUp: 7 | import FreeCADGui as Gui 8 | 9 | class CommandTogglePartOMagic: 10 | "Switches Part-O-magic workbench on or off" 11 | def GetResources(self): 12 | from PartOMagic.Gui.Utils import getIconPath 13 | return {'Pixmap' : getIconPath("PartOMagic_Power.svg"), 14 | 'MenuText': "Disable/enable Part-o-magic (DANGER)", 15 | 'Accel': "", 16 | 'ToolTip': "Disable/enable Part-o-magic. (disables the workbench)"} 17 | 18 | def Activated(self): 19 | if Params.EnablePartOMagic.get(): 20 | Params.EnablePartOMagic.set(False) 21 | msgbox("Part-o-Magic", "You've just DISABLED part-o-magic workbench. The workbench will disappear after you restart FreeCAD. \n\n" 22 | "If you disabled it by accident, click this button again to re-enable part-o-magic now. You can re-enable it by deleting configuration file.") 23 | else: 24 | Params.EnablePartOMagic.set(True) 25 | msgbox("Part-o-Magic", "You've just ENABLED part-o-magic workbench.") 26 | 27 | def IsActive(self): 28 | return True 29 | 30 | if App.GuiUp: 31 | Gui.addCommand('PartOMagic_Power', CommandTogglePartOMagic()) 32 | 33 | 34 | 35 | 36 | class CommandEnableObserver: 37 | "Switches Part-O-magic observer on" 38 | def GetResources(self): 39 | from PartOMagic.Gui.Utils import getIconPath 40 | return {'Pixmap' : getIconPath("PartOMagic_EnableObserver.svg"), 41 | 'MenuText': "Enable Observer", 42 | 'Accel': "", 43 | 'ToolTip': "Enable Observer. (enable adding new objects to active containers, and enable visibility automation)"} 44 | 45 | def Activated(self): 46 | import PartOMagic.Gui.Observer as Observer 47 | if not Observer.isRunning(): 48 | Observer.start() 49 | Params.EnableObserver.set(True) 50 | 51 | def IsActive(self): 52 | import PartOMagic.Gui.Observer as Observer 53 | return not Observer.isRunning() 54 | 55 | if App.GuiUp: 56 | Gui.addCommand('PartOMagic_EnableObserver', CommandEnableObserver()) 57 | 58 | 59 | 60 | class CommandPauseObserver: 61 | "Switches Part-O-magic observer off" 62 | def GetResources(self): 63 | from PartOMagic.Gui.Utils import getIconPath 64 | return {'Pixmap' : getIconPath("PartOMagic_PauseObserver.svg"), 65 | 'MenuText': "Pause Observer", 66 | 'Accel': "", 67 | 'ToolTip': "Pause Observer. (temporarily disable)"} 68 | 69 | def Activated(self): 70 | import PartOMagic.Gui.Observer as Observer 71 | if Observer.isRunning(): 72 | Observer.stop() 73 | Params.EnableObserver.set_volatile(False) 74 | 75 | def IsActive(self): 76 | import PartOMagic.Gui.Observer as Observer 77 | return Observer.isRunning() 78 | 79 | if App.GuiUp: 80 | Gui.addCommand('PartOMagic_PauseObserver', CommandPauseObserver()) 81 | 82 | 83 | 84 | class CommandDisableObserver: 85 | "Switches Part-O-magic observer on" 86 | def GetResources(self): 87 | from PartOMagic.Gui.Utils import getIconPath 88 | return {'Pixmap' : getIconPath("PartOMagic_DisableObserver.svg"), 89 | 'MenuText': "Disable Observer", 90 | 'Accel': "", 91 | 'ToolTip': "Disable Observer. (stop adding new objects to active containers, and disable visibility automation)"} 92 | 93 | def Activated(self): 94 | import PartOMagic.Gui.Observer as Observer 95 | if Observer.isRunning(): 96 | Observer.stop() 97 | Params.EnableObserver.set(False) 98 | 99 | def IsActive(self): 100 | return Params.EnableObserver.get_stored() 101 | 102 | if App.GuiUp: 103 | Gui.addCommand('PartOMagic_DisableObserver', CommandDisableObserver()) 104 | 105 | 106 | 107 | 108 | def exportedCommands(): 109 | return ['PartOMagic_Power', 'PartOMagic_EnableObserver', 'PartOMagic_PauseObserver', 'PartOMagic_DisableObserver'] -------------------------------------------------------------------------------- /PartOMagic/Gui/Control/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | __all__ = [ 4 | "OnOff", 5 | ] 6 | 7 | def importAll(): 8 | from . import OnOff 9 | 10 | def reloadAll(): 11 | try: #py2-3 compatibility: obtain reload() function 12 | reload 13 | except Exception: 14 | from importlib import reload 15 | 16 | for modstr in __all__: 17 | mod = globals()[modstr] 18 | reload(mod) 19 | if hasattr(mod, "reloadAll"): 20 | mod.reloadAll() 21 | 22 | def exportedCommands(): 23 | result = [] 24 | for modstr in __all__: 25 | mod = globals()[modstr] 26 | if hasattr(mod, "exportedCommands"): 27 | result += mod.exportedCommands() 28 | return result 29 | -------------------------------------------------------------------------------- /PartOMagic/Gui/GlobalToolbar.py: -------------------------------------------------------------------------------- 1 | 2 | from . import CommandCollection1 3 | from .Tools import LeaveEnter 4 | from .View import SnapView, XRay 5 | from PartOMagic.Features import PartDesign as POMPartDesign 6 | 7 | import FreeCAD as App 8 | 9 | def findToolbar(name, label, workbench, create = False): 10 | """findToolbar(name, label, workbench, create= False): returns tuple "User parameter:BaseApp/Workbench/Global/Toolbar", "toolbar_group_name".""" 11 | tb_root = "User parameter:BaseApp/Workbench/{workbench}/Toolbar".format(workbench= workbench) 12 | pp = App.ParamGet(tb_root) 13 | if pp.HasGroup(name): 14 | return [tb_root, name] 15 | 16 | for i in range(10): 17 | g = 'Custom_'+str(i) 18 | if pp.HasGroup(g) and pp.GetGroup(g).GetString('Name') == label: 19 | return [tb_root, g] 20 | if create: 21 | return [tb_root, name] 22 | return None 23 | 24 | def findGlobalToolbar(name, label, create = False): 25 | return findToolbar(name, label, 'Global', create) 26 | 27 | def findPDToolbar(name, label, create = False): 28 | return findToolbar(name, label, 'PartDesignWorkbench', create) 29 | 30 | def registerToolbar(): 31 | p = App.ParamGet('/'.join(findGlobalToolbar("PartOMagic_3", "Part-o-Magic global v3", create= True))) 32 | p.SetString("Name", "Part-o-Magic global v3") 33 | p.SetString(CommandCollection1.exportedCommands()[0], "FreeCAD") 34 | p.SetString(LeaveEnter.commandEnter.command_name, "FreeCAD") 35 | p.SetString(LeaveEnter.commandLeave.command_name, "FreeCAD") 36 | p.SetString(SnapView.commandSnapView.command_name, "FreeCAD") 37 | p.SetString(XRay.commandXRay.command_name, "FreeCAD") 38 | 39 | 40 | p.SetBool("Active", 1) 41 | 42 | #remove old version of the toolbar 43 | tb = findGlobalToolbar('PartOMagic', "Part-o-Magic global") 44 | if tb: 45 | App.ParamGet(tb[0]).RemGroup(tb[1]) 46 | tb = findGlobalToolbar('PartOMagic_2', "Part-o-Magic global") 47 | if tb: 48 | App.ParamGet(tb[0]).RemGroup(tb[1]) 49 | 50 | 51 | def isRegistered(): 52 | return findGlobalToolbar("PartOMagic_3", "Part-o-Magic global v3") is not None 53 | 54 | def registerPDToolbar(): 55 | creating_anew = not isPDRegistered() 56 | p = App.ParamGet('/'.join(findPDToolbar('PartOMagic',"Part-o-Magic PartDesign", create= True))) 57 | p.SetString("Name", "Part-o-Magic PartDesign") 58 | for cmd in POMPartDesign.exportedCommands(): 59 | p.SetString(cmd, "FreeCAD") 60 | if creating_anew: 61 | p.SetBool("Active", 1) 62 | 63 | def isPDRegistered(): 64 | return findPDToolbar('PartOMagic',"Part-o-Magic PartDesign") 65 | -------------------------------------------------------------------------------- /PartOMagic/Gui/GroupCommand.py: -------------------------------------------------------------------------------- 1 | 2 | #example: 3 | #Gui.addCommand('PartOMagic_Collection1', 4 | # GroupCommand( 5 | # list_of_commands= myFeatures.exportedCommands() + ['PartOMagic_SetTip'], 6 | # menu_text= "PartOMagic collection 1", 7 | # tooltip= "" 8 | # ) 9 | # ) 10 | class GroupCommand(object): 11 | def __init__(self, list_of_commands, menu_text, tooltip, for_edit= False): 12 | self.list_of_commands = [(cmd+'_AA' if cmd.startswith('PartOMagic') else cmd) for cmd in list_of_commands] 13 | self.menu_text = menu_text 14 | self.tooltip = tooltip 15 | 16 | def GetCommands(self): 17 | return tuple(self.list_of_commands) # a tuple of command names that you want to group 18 | 19 | def GetDefaultCommand(self): # return the index of the tuple of the default command. This method is optional and when not implemented '0' is used 20 | return 0 21 | 22 | def GetResources(self): 23 | return { 'MenuText': self.menu_text, 'ToolTip': self.tooltip} 24 | 25 | def IsActive(self): # optional 26 | return True 27 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Icons/Icons.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/PartOMagic.svg 4 | icons/PartOMagic_DisableObserver.svg 5 | icons/PartOMagic_Duplicate.svg 6 | icons/PartOMagic_EnableObserver.svg 7 | icons/PartOMagic_Enter.svg 8 | icons/PartOMagic_Ghost.svg 9 | icons/PartOMagic_Leave.svg 10 | icons/PartOMagic_ListUsages.svg 11 | icons/PartOMagic_MorphContainer.svg 12 | icons/PartOMagic_MUX.svg 13 | icons/PartOMagic_PauseObserver.svg 14 | icons/PartOMagic_Power.svg 15 | icons/PartOMagic_PDShapeFeature_Additive.svg 16 | icons/PartOMagic_PDShapeFeature_Subtractive.svg 17 | icons/PartOMagic_ReplaceObject.svg 18 | icons\PartOMagic_Select_All.svg 19 | icons\PartOMagic_Select_BSwap.svg 20 | icons\PartOMagic_Select_Children.svg 21 | icons\PartOMagic_Select_ChildrenRecursive.svg 22 | icons\PartOMagic_Select_Invert.svg 23 | icons/PartOMagic_ShapeGroup.svg 24 | icons/PartOMagic_SnapView.svg 25 | icons/PartOMagic_TransferObject.svg 26 | icons/PartOMagic_XRay.svg 27 | 28 | 29 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Icons/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "Icons", 3 | ] 4 | 5 | def importAll(): 6 | from . import Icons 7 | 8 | 9 | def reloadAll(): 10 | try: #py2-3 compatibility: obtain reload() function 11 | reload 12 | except Exception: 13 | from importlib import reload 14 | 15 | for modstr in __all__: 16 | mod = globals()[modstr] 17 | reload(mod) 18 | if hasattr(mod, "reloadAll"): 19 | mod.reloadAll() 20 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Icons/icons/PartOMagic_DisableObserver.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 25 | 29 | 33 | 34 | 36 | 40 | 44 | 45 | 47 | 51 | 55 | 56 | 63 | 65 | 69 | 73 | 74 | 85 | 87 | 91 | 95 | 96 | 107 | 109 | 113 | 117 | 118 | 129 | 131 | 135 | 139 | 140 | 151 | 162 | 173 | 182 | 183 | 202 | 204 | 205 | 207 | image/svg+xml 208 | 210 | 211 | 212 | 213 | 214 | 218 | 224 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Icons/icons/PartOMagic_EnableObserver.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 25 | 29 | 33 | 34 | 36 | 40 | 44 | 45 | 47 | 51 | 55 | 56 | 63 | 65 | 69 | 73 | 74 | 85 | 87 | 91 | 95 | 96 | 107 | 109 | 113 | 117 | 118 | 129 | 131 | 135 | 139 | 140 | 151 | 162 | 173 | 183 | 193 | 203 | 204 | 223 | 225 | 226 | 228 | image/svg+xml 229 | 231 | 232 | 233 | 234 | 235 | 239 | 245 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Icons/icons/PartOMagic_Enter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | image/svg+xml 51 | 52 | 53 | 54 | 55 | [wmayer] 56 | 57 | 58 | Sketcher_EditSketch 59 | 2014-07-13 60 | http://www.freecadweb.org/wiki/index.php?title=Artwork 61 | 62 | 63 | FreeCAD 64 | 65 | 66 | FreeCAD/src/Mod/Sketcher/Gui/Resources/icons/Sketcher_EditSketch.svg 67 | 68 | 69 | FreeCAD LGPL2+ 70 | 71 | 72 | https://www.gnu.org/copyleft/lesser.html 73 | 74 | 75 | [agryson] Alexander Gryson 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Icons/icons/PartOMagic_Leave.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | image/svg+xml 51 | 52 | 53 | 54 | 55 | [wmayer] 56 | 57 | 58 | Sketcher_LeaveSketch 59 | 2011-10-10 60 | http://www.freecadweb.org/wiki/index.php?title=Artwork 61 | 62 | 63 | FreeCAD 64 | 65 | 66 | FreeCAD/src/Mod/Sketcher/Gui/Resources/icons/Sketcher_LeaveSketch.svg 67 | 68 | 69 | FreeCAD LGPL2+ 70 | 71 | 72 | https://www.gnu.org/copyleft/lesser.html 73 | 74 | 75 | [agryson] Alexander Gryson 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Icons/icons/PartOMagic_PauseObserver.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 25 | 29 | 33 | 34 | 36 | 40 | 44 | 45 | 47 | 51 | 55 | 56 | 63 | 65 | 69 | 73 | 74 | 85 | 87 | 91 | 95 | 96 | 107 | 109 | 113 | 117 | 118 | 129 | 131 | 135 | 139 | 140 | 151 | 162 | 173 | 183 | 193 | 194 | 213 | 215 | 216 | 218 | image/svg+xml 219 | 221 | 222 | 223 | 224 | 225 | 229 | 235 | 242 | 249 | 250 | 251 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Icons/recompile_rc.bat: -------------------------------------------------------------------------------- 1 | pyside-rcc Icons.qrc -py3 -o Icons.py 2 | echo Should be compiled now =) 3 | pause -------------------------------------------------------------------------------- /PartOMagic/Gui/LinkTools/ListUsages.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import FreeCADGui as Gui 3 | from PartOMagic.Gui.Utils import * 4 | from PartOMagic.Base import Containers 5 | 6 | from PartOMagic.Gui.AACommand import AACommand, CommandError 7 | 8 | commands = [] 9 | class CommandListUsages(AACommand): 10 | "Command to show a list of objects that use the object" 11 | def GetResources(self): 12 | import PartDesignGui 13 | return {'CommandName': 'PartOMagic_ListUsages', 14 | 'Pixmap' : self.getIconPath("PartOMagic_ListUsages.svg"), 15 | 'MenuText': "Used by who?", 16 | 'Accel': "", 17 | 'ToolTip': "Used by who? (lists which objects use selected object, and how)"} 18 | 19 | def RunOrTest(self, b_run): 20 | sel = Gui.Selection.getSelection() 21 | if len(sel)!=1 : 22 | raise CommandError(self, u"Please select one object. Currently selected {n}".format(n= len(sel))) 23 | if b_run: 24 | from PartOMagic.Base import LinkTools as LT 25 | uses = LT.findLinksTo(sel[0]) 26 | uses_str = '\n'.join([ 27 | (rel.linking_object.Name + '.' + rel.linking_property +' (' + rel.kind + ')') for rel in uses ]) 28 | if len(uses_str) == 0: uses_str = "(nothing)" 29 | 30 | links = LT.getDependencies(sel[0]) 31 | links_str = '\n'.join([ 32 | (rel.linked_object.Name + " as " + rel.linking_property +' (' + rel.kind + ')') for rel in links ]) 33 | if len(links_str) == 0: links_str = "(nothing)" 34 | 35 | msg = (u"==== {obj} uses: ====\n" 36 | "{links_str}\n\n" 37 | "====Links to {obj}:====\n" 38 | "{uses_str}").format(obj= sel[0].Label, uses_str= uses_str, links_str= links_str) 39 | 40 | from PySide import QtGui 41 | mb = QtGui.QMessageBox() 42 | mb.setIcon(mb.Icon.Information) 43 | mb.setText(msg) 44 | mb.setWindowTitle("Used by who?") 45 | btnClose = mb.addButton(QtGui.QMessageBox.StandardButton.Close) 46 | btnCopy = mb.addButton("Copy to clipboard", QtGui.QMessageBox.ButtonRole.ActionRole) 47 | btnSelect = mb.addButton("Select dependent objects", QtGui.QMessageBox.ButtonRole.ActionRole) 48 | mb.setDefaultButton(btnClose) 49 | mb.exec_() 50 | 51 | if mb.clickedButton() is btnCopy: 52 | cb = QtGui.QClipboard() 53 | cb.setText(msg) 54 | if mb.clickedButton() is btnSelect: 55 | objs = set([rel.linking_object for rel in uses]) 56 | Gui.Selection.clearSelection() 57 | for obj in objs: 58 | Gui.Selection.addSelection(obj) 59 | 60 | commands.append(CommandListUsages()) 61 | 62 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Gui/LinkTools/ReplaceObject.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import FreeCADGui as Gui 3 | from PartOMagic.Gui.Utils import * 4 | from PartOMagic.Base import Containers 5 | 6 | from PartOMagic.Gui.AACommand import AACommand, CommandError 7 | 8 | commands = [] 9 | class CommandReplaceObject(AACommand): 10 | "Command to replace an object in parametric history with another object" 11 | def GetResources(self): 12 | import PartDesignGui 13 | return {'CommandName': 'PartOMagic_ReplaceObject', 14 | 'Pixmap' : self.getIconPath("PartOMagic_ReplaceObject.svg"), 15 | 'MenuText': "Replace object", 16 | 'Accel': "", 17 | 'ToolTip': "Replace object. Select new, old, and parent. Order matters. Parent is optional."} 18 | 19 | def RunOrTest(self, b_run): 20 | sel = [it.Object for it in Gui.Selection.getSelectionEx()] 21 | if not 2 <= len(sel) <= 3 : 22 | raise CommandError(self, u"Please select two or three objects. Currently selected {n}".format(n= len(sel))) 23 | if b_run: 24 | old = sel[0] 25 | new = sel[1] 26 | parent = sel[2] if len(sel) > 2 else None 27 | 28 | from PartOMagic.Base import LinkTools as LT 29 | rels = LT.findLinksTo(old) 30 | avoid = LT.getAllDependencyObjects(new) 31 | avoid.add(new) 32 | repls = [LT.Replacement(rel, new) for rel in rels] 33 | 34 | n_checked = 0 35 | for repl in repls: 36 | if repl.relation.linking_object in avoid: 37 | repl.disable("dependency loop") 38 | repl.checked = not(repl.disabled) 39 | if parent: 40 | if repl.relation.linking_object is not parent: 41 | repl.checked = False 42 | if repl.relation.kind == 'Child': 43 | repl.checked = False 44 | if repl.checked: 45 | n_checked += 1 46 | 47 | if len(repls) == 0: 48 | raise CommandError(self, u"Nothing depends on {old}, nothing to replace.".format(old= old.Label)) 49 | 50 | if n_checked == 0 and len(repls)>0: 51 | msgbox("Replace", u"No regular replaceable dependencies found, nothing uses {old}. Please pick wanted replacements manually in the dialog.".format(old= old.Label)) 52 | 53 | 54 | from . import TaskReplace 55 | task = TaskReplace.TaskReplace(repls, old.Document, message= u"Replacing {old} with {new}".format(old= old.Label, new= new.Label)) 56 | 57 | commands.append(CommandReplaceObject()) 58 | 59 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Gui/LinkTools/TaskReplace.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | from PySide import QtCore 3 | if App.GuiUp: 4 | import FreeCADGui as Gui 5 | from PySide import QtCore, QtGui 6 | Qt = QtCore.Qt 7 | from FreeCADGui import PySideUic as uic 8 | 9 | from PartOMagic.Base import LinkTools 10 | from PartOMagic.Gui.Utils import msgError, Transaction 11 | from PartOMagic.Base import Containers 12 | 13 | class TaskReplace(QtCore.QObject): 14 | form = None # task widget 15 | replacements = None #usually, a list. None is here instead because it's immutable. 16 | replaced = False 17 | doc = None 18 | 19 | #static 20 | columns = ['State', 'Object', 'Property', 'Kind', 'Value', 'Path'] 21 | column_titles = ["State", "Object", "Property", "Kind", "Value", "Container Path"] 22 | column = {} # lookup ductionary, returns column index given column name. 23 | 24 | def __init__(self, replacements, doc, message= "Replacing..."): 25 | QtCore.QObject.__init__(self) 26 | 27 | import os 28 | self.form = uic.loadUi(os.path.dirname(__file__) + os.path.sep + 'TaskReplace.ui') 29 | self.form.setWindowIcon(QtGui.QIcon(':/icons/PartOMagic_ReplaceObject.svg')) 30 | self.form.setWindowTitle("Replace object") 31 | 32 | self.replacements = replacements 33 | self.form.message.setText(message) 34 | 35 | #debug 36 | global instance 37 | instance = self 38 | 39 | if replacements: 40 | self.openTask() 41 | 42 | def openTask(self): 43 | self.fillList() 44 | Gui.Control.closeDialog() #just in case something else was being shown 45 | Gui.Control.showDialog(self) 46 | Gui.Selection.clearSelection() #otherwise, selection absorbs spacebar press event, interferes with list editing 47 | self.form.treeR.installEventFilter(self) 48 | 49 | def closeTask(self): 50 | self.cleanUp() 51 | Gui.Control.closeDialog() 52 | 53 | def cleanUp(self): 54 | self.form.treeR.removeEventFilter(self) 55 | 56 | def getStandardButtons(self): 57 | return int(QtGui.QDialogButtonBox.Ok) | int(QtGui.QDialogButtonBox.Close) | int(QtGui.QDialogButtonBox.Apply) 58 | 59 | def clicked(self,button): 60 | if button == QtGui.QDialogButtonBox.Apply: 61 | self.apply() 62 | elif button == QtGui.QDialogButtonBox.Close: 63 | self.reject() 64 | 65 | def apply(self): 66 | try: 67 | self.doReplace() 68 | except Exception as err: 69 | msgError(err) 70 | 71 | def accept(self): 72 | if not self.replaced: 73 | success = self.apply() 74 | if not success: return 75 | self.cleanUp() 76 | Gui.Control.closeDialog() 77 | 78 | def reject(self): 79 | self.cleanUp() 80 | Gui.Control.closeDialog() 81 | 82 | def doReplace(self): 83 | if self.replaced: 84 | raise RuntimeError("Already replaced, can't replace again") 85 | 86 | self.replacements = LinkTools.sortForMassReplace(self.replacements) 87 | successcount = 0 88 | failcount = 0 89 | 90 | with Transaction("Replace", self.doc): 91 | for repl in self.replacements: 92 | if repl.gui_item.checkState(0) == Qt.Checked: 93 | try: 94 | repl.replace() 95 | repl.gui_item.setCheckState(0, Qt.Unchecked) 96 | successcount += 1 97 | except Exception as err: 98 | repl.error = err 99 | failcount += 1 100 | import traceback 101 | tb = traceback.format_exc() 102 | App.Console.PrintError(tb+'\n\n') 103 | self.replaced = True 104 | 105 | self.updateList() 106 | if failcount == 0: 107 | self.form.message.setText(u"{successcount} replacements done.".format(successcount= successcount)) 108 | else: 109 | self.form.message.setText(u"{failcount} of {totalcount} replacements failed. See errors in the list.".format(failcount= failcount, totalcount= failcount+successcount)) 110 | self.form.message.setStyleSheet("QLabel { color : red; }") 111 | return failcount > 0 112 | 113 | 114 | def eventFilter(self, widget, event): 115 | # spacebar to toggle checkboxes of selected items 116 | if widget is self.form.treeR: 117 | if event.type() == QtCore.QEvent.KeyPress: 118 | if event.key() == Qt.Key_Space: 119 | sel = self.form.treeR.selectedItems() 120 | if len(sel) > 0: 121 | newstate = Qt.Unchecked if sel[0].checkState(0) == Qt.Checked else Qt.Checked 122 | for it in self.form.treeR.selectedItems(): 123 | it.setCheckState(0, newstate) 124 | return True 125 | return False 126 | 127 | def fillList(self): 128 | lw = self.form.treeR 129 | lw.clear() 130 | lw.setColumnCount(len(self.columns)) 131 | lw.setHeaderLabels(self.column_titles) 132 | for repl in self.replacements: 133 | item = QtGui.QTreeWidgetItem(lw) 134 | 135 | item.setText(self.column['Kind'], repl.relation.kind) 136 | item.setText(self.column['Object'], repl.relation.linking_object.Label) 137 | item.setText(self.column['Property'], repl.relation.linking_property) 138 | try: 139 | chain = Containers.getContainerChain(repl.relation.linking_object)[1:] + [repl.relation.linking_object] #[1:] strips off project name, to unclutter the column. 140 | path = '.'.join([cnt.Name for cnt in chain]) 141 | except Exception as err: 142 | import traceback 143 | tb = traceback.format_exc() 144 | App.Console.PrintError(tb+'\n\n') 145 | path = "!" + str(err) 146 | item.setText(self.column['Path'], path) 147 | item.setText(self.column['Value'], str(repl.relation.value_repr)) 148 | 149 | repl.gui_item = item 150 | item.setData(0,256,repl) 151 | flags = Qt.ItemIsUserCheckable | Qt.ItemIsSelectable 152 | if not repl.disabled: 153 | flags = flags | Qt.ItemIsEnabled 154 | item.setFlags(flags) 155 | item.setCheckState(0, Qt.Checked if repl.checked else Qt.Unchecked) 156 | if repl.disabled: 157 | item.setText(self.column['State'], repl.disabled_reason) 158 | 159 | def updateList(self): 160 | """updates status fields of previously filled list""" 161 | redbrush = QtGui.QBrush(QtGui.QColor(255,128,128)) 162 | for repl in self.replacements: 163 | if repl.replaced: 164 | state = "replaced" 165 | else: 166 | if hasattr(repl, 'error'): 167 | state = "! "+str(repl.error) 168 | for icol in range(len(self.columns)): 169 | repl.gui_item.setBackground(icol, redbrush) 170 | else: 171 | continue #to preserve status of disabled replacements 172 | repl.gui_item.setText(self.column['State'], state) 173 | 174 | TaskReplace.column = {TaskReplace.columns[i]:i for i in range(len(TaskReplace.columns))} 175 | 176 | class CancelError(RuntimeError): 177 | pass -------------------------------------------------------------------------------- /PartOMagic/Gui/LinkTools/TaskReplace.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PartDesignGui::TaskDatumParameters 4 | 5 | 6 | 7 | 0 8 | 0 9 | 271 10 | 604 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Replacing X with Y 21 | 22 | 23 | Qt::AlignCenter 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | Replacements: 34 | 35 | 36 | 37 | 38 | 39 | 40 | QAbstractItemView::ExtendedSelection 41 | 42 | 43 | true 44 | 45 | 46 | true 47 | 48 | 49 | 50 | 1 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 10 62 | 63 | 64 | 10 65 | 66 | 67 | true 68 | 69 | 70 | true 71 | 72 | 73 | true 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /PartOMagic/Gui/LinkTools/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "ListUsages", 3 | "ReplaceObject", 4 | ] 5 | 6 | def importAll(): 7 | from . import ListUsages 8 | from . import ReplaceObject 9 | 10 | def reloadAll(): 11 | try: #py2-3 compatibility: obtain reload() function 12 | reload 13 | except Exception: 14 | from importlib import reload 15 | 16 | for modstr in __all__: 17 | mod = globals()[modstr] 18 | reload(mod) 19 | if hasattr(mod, "reloadAll"): 20 | mod.reloadAll() 21 | 22 | def exportedCommands(): 23 | result = [] 24 | for modstr in __all__: 25 | mod = globals()[modstr] 26 | if hasattr(mod, "exportedCommands"): 27 | result += mod.exportedCommands() 28 | return result 29 | -------------------------------------------------------------------------------- /PartOMagic/Gui/MonkeyPatches.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | from PartOMagic.Base import Parameters 3 | 4 | def move_input_objects(self, objects): 5 | """move_input_objects has been monkeypatched by Part-o-magic""" 6 | targetGroup = None 7 | for obj in objects: 8 | obj.Visibility = False 9 | parent = obj.getParent() 10 | if parent: 11 | # parent.removeObject(obj) 12 | targetGroup = parent 13 | return None #targetGroup 14 | 15 | def monkeypatch_Part(): 16 | try: 17 | from BOPTools import BOPFeatures 18 | except ImportError: 19 | App.Console.PrintLog("Part-o-magic: BOPFeatures missing, skipping monkeypatching\n") 20 | else: 21 | if hasattr(BOPFeatures.BOPFeatures, 'move_input_objects'): 22 | BOPFeatures.BOPFeatures.move_input_objects = move_input_objects 23 | App.Console.PrintLog("Part-o-magic: monkeypatching Part.BOPTools.BOPFeatures done\n") 24 | else: 25 | App.Console.PrintWarning("Part-o-magic: monkeypatching Part.BOPTools.BOPFeatures failed - move_input_objects() method is missing\n") 26 | 27 | def monkeypatch_all(): 28 | try: 29 | monkeypatch_Part() 30 | except Exception as err: 31 | import sys 32 | b_tb = err is sys.exc_info()[1] 33 | if b_tb: 34 | import traceback 35 | tb = traceback.format_exc() 36 | App.Console.PrintError(tb+'\n') 37 | 38 | if Parameters.EnableObserver.get() and Parameters.EnablePartOMagic.get(): 39 | monkeypatch_all() -------------------------------------------------------------------------------- /PartOMagic/Gui/Tools/Duplicate.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import FreeCADGui as Gui 3 | from PartOMagic.Base import Containers 4 | from PartOMagic.Base.FilePlant import FCProject 5 | 6 | from PartOMagic.Gui.AACommand import AACommand, CommandError 7 | from PartOMagic.Gui.Utils import Transaction 8 | from PartOMagic.Gui import Observer 9 | 10 | 11 | def duplicateObjects(objects, top_objects, target_cnt): 12 | with Transaction("PoM Duplicate"): 13 | keeper = Observer.suspend() 14 | 15 | objset = set(objects) 16 | doc = objects[0].Document 17 | namelist = [obj.Name for obj in doc.TopologicalSortedObjects if obj in objset][::-1] 18 | 19 | tmp_prj = FCProject.fromFC(doc, namelist) 20 | map = tmp_prj.mergeToFC(doc) 21 | for obj in top_objects: 22 | new_top_obj = doc.getObject(map[obj.Name]) 23 | Containers.addObjectTo(target_cnt, new_top_obj, b_advance_tip= True) 24 | keeper.release() 25 | 26 | 27 | 28 | commands = [] 29 | class CommandDuplicateObject(AACommand): 30 | "Command duplicate objects" 31 | def GetResources(self): 32 | import PartDesignGui 33 | return {'CommandName': 'PartOMagic_Duplicate', 34 | 'Pixmap' : self.getIconPath("PartOMagic_Duplicate.svg"), 35 | 'MenuText': "Duplicate objects", 36 | 'Accel': "", 37 | 'ToolTip': "Duplicate objects (container-aware). (Copy selected objects and all their children; don't copy all dependent objects)"} 38 | 39 | def RunOrTest(self, b_run): 40 | sel = Gui.Selection.getSelection() 41 | if len(sel)==0 : 42 | raise CommandError(self,"No object selected. Please select objects to duplicate, first.") 43 | else: 44 | if b_run: 45 | (full_list, top_list, implicit_list) = Containers.expandList(sel) 46 | duplicateObjects(full_list, top_list, Containers.activeContainer()) 47 | commands.append(CommandDuplicateObject()) 48 | 49 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Gui/Tools/LeaveEnter.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import FreeCADGui as Gui 3 | from PartOMagic.Gui.Utils import screen 4 | from PartOMagic.Base import Containers 5 | from PartOMagic.Gui.AACommand import AACommand, CommandError 6 | 7 | commands = [] 8 | 9 | class _CommandEnter(AACommand): 10 | "Command to enter a feature" 11 | def GetResources(self): 12 | import SketcherGui #needed for icons 13 | return {'CommandName': 'PartOMagic_Enter', 14 | 'Pixmap' : self.getIconPath("PartOMagic_Enter.svg"), 15 | 'MenuText': "Enter object", 16 | 'Accel': "", 17 | 'ToolTip': "Enter object. (activate a container, or open a sketch for editing)"} 18 | 19 | def RunOrTest(self, b_run): 20 | if Gui.ActiveDocument: 21 | in_edit = Gui.ActiveDocument.getInEdit() 22 | if in_edit is not None: 23 | raise CommandError(self, u"{object} is currently being edited. Can't enter anything.".format(object= in_edit.Object.Label)) 24 | sel = Gui.Selection.getSelection() 25 | if len(sel)==0 : 26 | raise CommandError(self, "Enter Object command. Please select an object to enter, first. It can be a container, or a sketch.") 27 | elif len(sel)==1: 28 | sel = screen(sel[0]) 29 | ac = Containers.activeContainer() 30 | if Containers.isContainer(sel): 31 | if sel in Containers.getContainerChain(ac) + [ac]: 32 | raise CommandError(self, "Already inside this object") 33 | if b_run: Containers.setActiveContainer(sel) 34 | if b_run: Gui.Selection.clearSelection() 35 | else: 36 | cnt = Containers.getContainer(sel) 37 | if ac is cnt: 38 | if b_run: Gui.ActiveDocument.setEdit(sel) 39 | else: 40 | if b_run: Containers.setActiveContainer(cnt) 41 | else: 42 | raise CommandError(self, u"Enter Object command. You need to select exactly one object (you selected {num}).".format(num= len(sel))) 43 | commandEnter = _CommandEnter() 44 | commands.append(commandEnter) 45 | 46 | 47 | 48 | class _CommandLeave(AACommand): 49 | "Command to leave editing or a container" 50 | def GetResources(self): 51 | import SketcherGui #needed for icons 52 | return {'CommandName': 'PartOMagic_Leave', 53 | 'Pixmap' : self.getIconPath("PartOMagic_Leave.svg"), 54 | 'MenuText': "Leave object", 55 | 'Accel': "", 56 | 'ToolTip': "Leave object. (close sketch editing, or close task dialog, or leave a container).", 57 | 'CmdType': "ForEdit"} 58 | 59 | def RunOrTest(self, b_run): 60 | if Gui.ActiveDocument.getInEdit() is not None: 61 | if b_run: 62 | Gui.ActiveDocument.resetEdit() 63 | App.ActiveDocument.recompute() 64 | App.ActiveDocument.commitTransaction() 65 | elif Gui.Control.activeDialog(): 66 | if b_run: 67 | Gui.Control.closeDialog() 68 | App.ActiveDocument.recompute() 69 | App.ActiveDocument.commitTransaction() 70 | else: 71 | ac = Containers.activeContainer() 72 | if ac.isDerivedFrom("App::Document"): 73 | raise CommandError(self, "Nothing to leave.") 74 | if b_run: Containers.setActiveContainer(Containers.getContainer(ac)) 75 | if b_run: Gui.Selection.clearSelection() 76 | if b_run: Gui.Selection.addSelection(ac) 77 | if b_run: App.ActiveDocument.recompute() #fixme: scoped recompute, maybe? 78 | commandLeave = _CommandLeave() 79 | commands.append(commandLeave) 80 | 81 | exportedCommands = AACommand.registerCommands(commands) 82 | 83 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Tools/SelectionTools.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import FreeCADGui as Gui 3 | from PartOMagic.Base import Containers 4 | 5 | from PartOMagic.Gui.AACommand import AACommand, CommandError 6 | from PartOMagic.Gui.GroupCommand import GroupCommand 7 | from PySide import QtCore, QtGui 8 | 9 | def select(objects): 10 | km = QtGui.QApplication.keyboardModifiers() 11 | ctrl_is_down = bool(km & QtCore.Qt.ControlModifier) 12 | if not ctrl_is_down: 13 | Gui.Selection.clearSelection() 14 | for obj in objects: 15 | Gui.Selection.addSelection(obj) 16 | 17 | 18 | commands = [] 19 | class CommandSelectExpand(AACommand): 20 | 21 | def GetResources(self): 22 | import PartDesignGui 23 | return {'CommandName': 'PartOMagic_Select_ChildrenRecursive', 24 | 'Pixmap' : self.getIconPath("PartOMagic_Select_ChildrenRecursive.svg"), 25 | 'MenuText': "Select children (recursive)", 26 | 'Accel': "", 27 | 'ToolTip': "Select children (recursive). (add all children of selected objects to selection, recursively; only container childship is considered)"} 28 | 29 | def RunOrTest(self, b_run): 30 | sel = Gui.Selection.getSelection() 31 | if len(sel)==0 : 32 | raise CommandError(self,"No object selected. Please select something, first.") 33 | else: 34 | if b_run: 35 | (full_list, top_list, implicit_list) = Containers.expandList(sel) 36 | select(implicit_list) 37 | commands.append(CommandSelectExpand()) 38 | 39 | class CommandSelectChildren(AACommand): 40 | 41 | def GetResources(self): 42 | import PartDesignGui 43 | return {'CommandName': 'PartOMagic_Select_Children', 44 | 'Pixmap' : self.getIconPath("PartOMagic_Select_Children.svg"), 45 | 'MenuText': "Select children", 46 | 'Accel': "", 47 | 'ToolTip': "Select children. (selects children of selected container)"} 48 | 49 | def RunOrTest(self, b_run): 50 | sel = Gui.Selection.getSelection() 51 | if len(sel)==0 : 52 | raise CommandError(self,"No object selected. Please select something, first.") 53 | else: 54 | if b_run: 55 | new_sel = [] 56 | for obj in sel: 57 | if Containers.isContainer(obj): 58 | new_sel.extend(Containers.getDirectChildren(obj)) 59 | else: 60 | new_sel.extend(obj.ViewObject.claimChildren()) 61 | select(new_sel) 62 | commands.append(CommandSelectChildren()) 63 | 64 | class CommandSelectAll(AACommand): 65 | 66 | def GetResources(self): 67 | import PartDesignGui 68 | return {'CommandName': 'PartOMagic_Select_All', 69 | 'Pixmap' : self.getIconPath("PartOMagic_Select_All.svg"), 70 | 'MenuText': "Select all (in active container)", 71 | 'Accel': "", 72 | 'ToolTip': "Select all (in active container). (select all objects in active container)"} 73 | 74 | def RunOrTest(self, b_run): 75 | if b_run: 76 | children = Containers.getDirectChildren(Containers.activeContainer()) 77 | select(children) 78 | commands.append(CommandSelectAll()) 79 | 80 | class CommandSelectInvert(AACommand): 81 | 82 | def GetResources(self): 83 | import PartDesignGui 84 | return {'CommandName': 'PartOMagic_Select_Invert', 85 | 'Pixmap' : self.getIconPath("PartOMagic_Select_Invert.svg"), 86 | 'MenuText': "Invert selection (in active container)", 87 | 'Accel': "", 88 | 'ToolTip': "Select all (in active container). (select all objects in active container)"} 89 | 90 | def RunOrTest(self, b_run): 91 | if b_run: 92 | sel = set(Gui.Selection.getSelection()) 93 | children = Containers.getDirectChildren(Containers.activeContainer()) 94 | for child in children: 95 | if child in sel: 96 | Gui.Selection.removeSelection(child) 97 | else: 98 | Gui.Selection.addSelection(child) 99 | commands.append(CommandSelectInvert()) 100 | 101 | class CommandSelectMem(AACommand): 102 | buffer = [] 103 | def GetResources(self): 104 | import PartDesignGui 105 | return {'CommandName': 'PartOMagic_Select_BSwap', 106 | 'Pixmap' : self.getIconPath("PartOMagic_Select_BSwap.svg"), 107 | 'MenuText': "Selection buffer swap", 108 | 'Accel': "", 109 | 'ToolTip': "Selection buffer swap. (restores previously remembered selection, and remembers current selection)"} 110 | 111 | def RunOrTest(self, b_run): 112 | if b_run: 113 | sel = Gui.Selection.getSelectionEx() 114 | buf = self.buffer 115 | Gui.Selection.clearSelection() 116 | for it in buf: 117 | try: 118 | it.Object #throws if the object has been deleted 119 | except Exception: 120 | continue 121 | subs = it.SubElementNames 122 | pts = it.PickedPoints 123 | if subs: 124 | for isub in range(len(subs)): 125 | Gui.Selection.addSelection(it.Object, subs[isub], *pts[isub]) 126 | else: 127 | Gui.Selection.addSelection(it.Object) 128 | self.buffer = sel 129 | commands.append(CommandSelectMem()) 130 | AACommand.registerCommands(commands) 131 | 132 | 133 | Gui.addCommand('PartOMagic_SelectGroupCommand', 134 | GroupCommand( 135 | list_of_commands= [cmd.command_name for cmd in commands], 136 | menu_text= "Select", 137 | tooltip= "" 138 | ) 139 | ) 140 | 141 | exportedCommands = lambda:['PartOMagic_SelectGroupCommand'] -------------------------------------------------------------------------------- /PartOMagic/Gui/Tools/Tip.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import FreeCADGui as Gui 3 | from PartOMagic.Gui.Utils import * 4 | from PartOMagic.Base import Containers 5 | 6 | from PartOMagic.Gui.AACommand import AACommand, CommandError 7 | 8 | 9 | def MoveTip(container, new_tip): 10 | App.ActiveDocument.openTransaction("Set Tip") 11 | Gui.doCommand("cnt = App.ActiveDocument.{cnt}".format(cnt= container.Name)) 12 | Gui.doCommand("cnt.Tip = App.ActiveDocument.{tip}".format(cnt= container.Name, tip= new_tip.Name)) 13 | App.ActiveDocument.commitTransaction() 14 | Gui.doCommand("App.ActiveDocument.recompute()") 15 | 16 | commands = [] 17 | class CommandSetTip(AACommand): 18 | "Command to set tip feature of a module/body" 19 | def GetResources(self): 20 | import PartDesignGui 21 | return {'CommandName': 'PartOMagic_SetTip', 22 | 'Pixmap' : self.getIconPath("PartDesign_MoveTip.svg"), 23 | 'MenuText': "Set as Tip", 24 | 'Accel': "", 25 | 'ToolTip': "Set as Tip. (mark this object as final shape of containing module/body)"} 26 | 27 | def RunOrTest(self, b_run): 28 | sel = Gui.Selection.getSelection() 29 | if len(sel)==0 : 30 | ac = Containers.activeContainer() 31 | if not hasattr(ac, "Tip"): 32 | raise CommandError(self,u"{cnt} can't have Tip object (it is not a module or a body).".format(cnt= ac.Label)) 33 | if type(ac.Tip) is list: 34 | if Gui.ActiveDocument.getInEdit() is not None: 35 | raise CommandError(self,"Please leave editing mode.") 36 | if b_run: Gui.ActiveDocument.setEdit(ac, 0) 37 | return 38 | raise CommandError(self, "Set as Tip command. Please select an object to become Tip, first. The object must be geometry. ") 39 | elif len(sel)==1: 40 | sel = screen(sel[0]) 41 | ac = Containers.getContainer(sel) 42 | if not hasattr(ac, "Tip"): 43 | raise CommandError(self,u"{cnt} can't have Tip object (it is not a module or a body).".format(cnt= ac.Label)) 44 | if type(ac.Tip) is list: 45 | if Gui.ActiveDocument.getInEdit() is not None: 46 | raise CommandError(self,"Please leave editing mode.") 47 | if b_run: Gui.ActiveDocument.setEdit(ac, 0) 48 | else: 49 | if not sel in Containers.getDirectChildren(ac): 50 | raise CommandError(self, u"{feat} is not from active container ({cnt}). Please select an object belonging to active container.".format(feat= sel.Label, cnt= ac.Label)) 51 | if screen(ac.Tip) is sel: 52 | raise CommandError(self, u"{feat} is already a Tip of ({cnt}).".format(feat= sel.Label, cnt= ac.Label)) 53 | if b_run: ac.Tip = sel 54 | else: 55 | raise CommandError(self, u"Set as Tip command. You need to select exactly one object (you selected {num}).".format(num= len(sel))) 56 | commands.append(CommandSetTip()) 57 | 58 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Gui/Tools/TransferObject.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | import FreeCADGui as Gui 3 | from PartOMagic.Base import Containers 4 | 5 | from PartOMagic.Gui.AACommand import AACommand, CommandError 6 | from PartOMagic.Gui.Utils import Transaction 7 | 8 | 9 | def TransferObject(objects, target_cnt): 10 | with Transaction("Transfer object"): 11 | for obj in objects: 12 | Containers.moveObjectTo(obj, target_cnt) 13 | 14 | commands = [] 15 | class CommandTransferObject(AACommand): 16 | "Command to transfer an object into active container" 17 | def GetResources(self): 18 | import PartDesignGui 19 | return {'CommandName': 'PartOMagic_TransferObject', 20 | 'Pixmap' : self.getIconPath("PartOMagic_TransferObject.svg"), 21 | 'MenuText': "Transfer object", 22 | 'Accel': "", 23 | 'ToolTip': "Transfer object. (withdraw selected object from its container into active container)"} 24 | 25 | def RunOrTest(self, b_run): 26 | sel = Gui.Selection.getSelection() 27 | if len(sel)==0 : 28 | raise CommandError(self,"No object selected. Please select an object from another container, first.") 29 | elif len(sel)==1: 30 | sel = sel[0] 31 | cnt = Containers.getContainer(sel) 32 | if cnt is Containers.activeContainer(): 33 | raise CommandError(self, "This object is already in active container. Please select an object that is not in active container, or activate another container.") 34 | if b_run: TransferObject([sel], Containers.activeContainer()) 35 | else: 36 | # multiple selection. Checking is involved. Let's just assume it's correct. 37 | if b_run: TransferObject(sel, Containers.activeContainer()) 38 | commands.append(CommandTransferObject()) 39 | 40 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Gui/Tools/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "LeaveEnter", 3 | "Tip", 4 | "MorphContainer", 5 | "TransferObject", 6 | "Duplicate", 7 | "SelectionTools", 8 | ] 9 | 10 | def importAll(): 11 | from . import LeaveEnter 12 | from . import Tip 13 | from . import MorphContainer 14 | from . import TransferObject 15 | from . import Duplicate 16 | from . import SelectionTools 17 | 18 | def reloadAll(): 19 | try: #py2-3 compatibility: obtain reload() function 20 | reload 21 | except Exception: 22 | from importlib import reload 23 | 24 | for modstr in __all__: 25 | mod = globals()[modstr] 26 | reload(mod) 27 | if hasattr(mod, "reloadAll"): 28 | mod.reloadAll() 29 | 30 | def exportedCommands(): 31 | result = [] 32 | for modstr in __all__: 33 | mod = globals()[modstr] 34 | if hasattr(mod, "exportedCommands"): 35 | result += mod.exportedCommands() 36 | return result 37 | -------------------------------------------------------------------------------- /PartOMagic/Gui/Utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | #TODO: remove this: 4 | def getIconPath(icon_dot_svg): 5 | import PartOMagic.Gui.Icons.Icons 6 | return ":/icons/" + icon_dot_svg 7 | 8 | def msgbox(title, text): 9 | from PySide import QtGui 10 | mb = QtGui.QMessageBox() 11 | mb.setIcon(mb.Icon.Information) 12 | mb.setText(text) 13 | mb.setWindowTitle(title) 14 | mb.exec_() 15 | 16 | def msgError(err = None, message = u'{errmsg}'): 17 | if err is None: 18 | err = sys.exc_info()[1] 19 | if type(err) is CancelError: return 20 | 21 | # can we get a traceback? 22 | b_tb = err is sys.exc_info()[1] 23 | if b_tb: 24 | import traceback 25 | tb = traceback.format_exc() 26 | import FreeCAD as App 27 | App.Console.PrintError(tb+'\n') 28 | 29 | #make messagebox object 30 | from PySide import QtGui 31 | mb = QtGui.QMessageBox() 32 | mb.setIcon(mb.Icon.Warning) 33 | 34 | #fill in message 35 | errmsg = '' 36 | if hasattr(err,'message'): 37 | if isinstance(err.message, dict): 38 | errmsg = err.message['swhat'] 39 | elif len(err.message) > 0: 40 | errmsg = err.message 41 | else: 42 | errmsg = str(err) 43 | else: 44 | errmsg = str(err) 45 | mb.setText(message.format(errmsg= errmsg, err= err)) 46 | 47 | # fill in title 48 | if hasattr(err, "title"): 49 | mb.setWindowTitle(err.title) 50 | else: 51 | mb.setWindowTitle("Error") 52 | 53 | #add traceback button 54 | if b_tb: 55 | btnClose = mb.addButton(QtGui.QMessageBox.StandardButton.Close) 56 | btnCopy = mb.addButton("Copy traceback", QtGui.QMessageBox.ButtonRole.ActionRole) 57 | mb.setDefaultButton(btnClose) 58 | 59 | mb.exec_() 60 | if b_tb: 61 | if mb.clickedButton() is btnCopy: 62 | cb = QtGui.QClipboard() 63 | cb.setText(tb) 64 | 65 | class CancelError(Exception): 66 | pass 67 | 68 | def screen(feature): 69 | """screen(feature): protects link properties from being overwritten. 70 | This is to be used as workaround for a bug where modifying an object accessed through 71 | a link property of another object results in the latter being touched. 72 | 73 | returns: feature""" 74 | if not hasattr(feature,"isDerivedFrom"): 75 | return feature 76 | if not feature.isDerivedFrom("App::DocumentObject"): 77 | return feature 78 | if feature.Document is None: 79 | return feature 80 | feature = getattr(feature.Document, feature.Name) 81 | return feature 82 | 83 | class DelayedExecute(object): 84 | "DelayedExecute(func, delay = 30): sets up a timer, executes func, and self-destructs." 85 | def defineAttributes(self): 86 | self.func = None # function to run 87 | self.timer = None # the timer 88 | self.self = None #self-reference, to keep self alive until timer fires 89 | self.delay = 0 # not really needed, for convenience/debug 90 | self.is_done = False 91 | 92 | #self._freeze() 93 | 94 | def __init__(self, func, delay= 30): 95 | self.defineAttributes() 96 | self.func = func 97 | self.delay = delay 98 | from PySide import QtCore 99 | timer = QtCore.QTimer(); self.timer = timer 100 | timer.setInterval(delay) 101 | timer.setSingleShot(True) 102 | timer.connect(QtCore.SIGNAL("timeout()"), self.timeout) 103 | timer.start() 104 | self.self = self 105 | self.is_done = False 106 | 107 | def timeout(self): 108 | self.timer = None 109 | self.self = None 110 | try: 111 | self.func() 112 | finally: 113 | self.is_done = True 114 | 115 | class Transaction(object): 116 | """Transaction object is to be used in a 'with' block. If an error is thrown in the with block, the transaction is undone automatically.""" 117 | def __init__(self, title, doc= None): 118 | if doc is None: 119 | import FreeCAD as App 120 | doc = App.ActiveDocument 121 | self.title = title 122 | self.document = doc 123 | 124 | def __enter__(self): 125 | self.document.openTransaction(self.title) 126 | 127 | def __exit__(self, exc_type, exc_value, exc_traceback): 128 | if exc_value is None: 129 | self.document.commitTransaction() 130 | else: 131 | self.document.abortTransaction() 132 | -------------------------------------------------------------------------------- /PartOMagic/Gui/View/SnapView.py: -------------------------------------------------------------------------------- 1 | import FreeCAD as App 2 | Plm = App.Placement 3 | Rot = App.Rotation 4 | V = App.Vector 5 | 6 | 7 | 8 | def snapRot(rot): 9 | """snapRot(rot): returns tuple (new_rotation, action_done). 10 | action_done is either 'untilt', 'std', or 'nothing'.""" 11 | view_dirs = [ 12 | V( 1, 0,0), 13 | V( 1, 1,0), 14 | V( 0, 1,0), 15 | V(-1, 1,0), 16 | V(-1, 0,0), 17 | V(-1,-1,0), 18 | V( 0,-1,0), 19 | V( 1,-1,0), 20 | 21 | V( 1, 0,1), 22 | V( 1, 1,1), 23 | V( 0, 1,1), 24 | V(-1, 1,1), 25 | V(-1, 0,1), 26 | V(-1,-1,1), 27 | V( 0,-1,1), 28 | V( 1,-1,1), 29 | 30 | V( 1, 0,-1), 31 | V( 1, 1,-1), 32 | V( 0, 1,-1), 33 | V(-1, 1,-1), 34 | V(-1, 0,-1), 35 | V(-1,-1,-1), 36 | V( 0,-1,-1), 37 | V( 1,-1,-1), 38 | 39 | V(0,0,1), 40 | V(0,0,-1) 41 | ] 42 | 43 | for v in view_dirs: 44 | v.normalize() 45 | 46 | view_dir = rot.multVec(V(0,0,-1)) #current view direction 47 | 48 | rot_upright = Rot(V(),V(0,0,1),view_dir*(-1), "ZYX") 49 | 50 | if rots_equal(rot, rot_upright): 51 | # view is already upright. Align it to nearest standard view. 52 | v_nearest = V() 53 | for v in view_dirs: 54 | if (v - view_dir).Length < (v_nearest - view_dir).Length: 55 | v_nearest = v 56 | 57 | new_view_rot = Rot(V(),V(),v_nearest*(-1)) 58 | changed = not rots_equal(new_view_rot, rot) 59 | return (new_view_rot, 'std' if changed else 'nothing') 60 | else: 61 | # view is not upright (tilted). Remove the tilt, keeping view direction. 62 | return (rot_upright, 'untilt') 63 | 64 | orig_rot = None 65 | last_seen_rot = None 66 | 67 | def snapView(): 68 | global orig_rot 69 | global last_seen_rot 70 | 71 | import FreeCADGui as Gui 72 | rot = Gui.ActiveDocument.ActiveView.getCameraOrientation() 73 | rotated_by_user = last_seen_rot is None or not rots_equal(rot, last_seen_rot) 74 | 75 | new_rot, act = snapRot(rot) 76 | if act == 'nothing': 77 | if orig_rot is not None: 78 | new_rot = orig_rot 79 | orig_rot = None 80 | else: 81 | if rotated_by_user: 82 | orig_rot = rot 83 | Gui.ActiveDocument.ActiveView.setCameraOrientation(new_rot) 84 | last_seen_rot = new_rot if act != 'nothing' else None 85 | 86 | def rots_equal(rot1, rot2): 87 | q1 = rot1.Q 88 | q2 = rot2.Q 89 | # rotations are equal if q1 == q2 or q1 == -q2. 90 | # Invert one of Q's if their scalar product is negative, before comparison. 91 | if q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2] + q1[3]*q2[3] < 0: 92 | q2 = [-v for v in q2] 93 | rot_eq = ( abs(q1[0]-q2[0]) + 94 | abs(q1[1]-q2[1]) + 95 | abs(q1[2]-q2[2]) + 96 | abs(q1[3]-q2[3]) ) < 1e-4 # 1e-4 is a made-up number. This much camera rotation should be unnoticeable. 97 | return rot_eq 98 | 99 | 100 | # =================command==================== 101 | 102 | 103 | from PartOMagic.Gui.AACommand import AACommand, CommandError 104 | commands = [] 105 | 106 | class CommandSnapView(AACommand): 107 | "Command to straighten up view" 108 | def GetResources(self): 109 | return {'CommandName': 'PartOMagic_SnapView', 110 | 'Pixmap' : self.getIconPath("PartOMagic_SnapView.svg"), 111 | 'MenuText': "Straighten camera (tri-state)", 112 | 'Accel': "", 113 | 'ToolTip': "Straighten camera (tri-state). First click aligns camera upright without changing view direction. Second click snaps to nearest standard view. Third click restores original view.", 114 | 'CmdType': 'ForEdit'} 115 | 116 | def RunOrTest(self, b_run): 117 | import FreeCADGui as Gui 118 | if Gui.ActiveDocument is None: 119 | raise CommandError(self, "No open project") 120 | if not hasattr(Gui.ActiveDocument.ActiveView, 'getCameraOrientation'): 121 | raise CommandError(self, "Not 3d view") 122 | if b_run: 123 | Gui.addModule('PartOMagic.Gui.View.SnapView') 124 | Gui.doCommand('PartOMagic.Gui.View.SnapView.snapView()') 125 | 126 | commandSnapView = CommandSnapView() 127 | commands.append(commandSnapView) 128 | 129 | 130 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Gui/View/XRay.py: -------------------------------------------------------------------------------- 1 | library = {} #dict. Key = object, Value = TempoVis_instance 2 | 3 | 4 | def XRay(obj): 5 | import FreeCAD as App 6 | import Show 7 | 8 | global library 9 | if obj is None: 10 | for itobj in library: 11 | tv = library[itobj] 12 | tv.restore() 13 | library = {} 14 | return 15 | if obj in library: 16 | tv = library.pop(obj) 17 | tv.restore() 18 | else: 19 | tv = Show.TempoVis(App.ActiveDocument) 20 | failed = False 21 | try: 22 | tv.modifyVPProperty(obj, 'Transparency', 80) 23 | except Exception: 24 | failed = True 25 | else: 26 | try: 27 | tv.modifyVPProperty(obj, 'DisplayMode', 'Shaded') 28 | except Exception: 29 | failed = True 30 | if failed: 31 | tv.forget() # workaround for tv saving the value and then failing to restore it 32 | tv = Show.TempoVis(App.ActiveDocument) 33 | tv.hide(obj) 34 | else: 35 | tv.setUnpickable(obj) 36 | library[obj] = tv 37 | 38 | 39 | from PartOMagic.Gui.AACommand import AACommand, CommandError 40 | commands = [] 41 | 42 | class CommandXRay(AACommand): 43 | "Command to select through object" 44 | def GetResources(self): 45 | return {'CommandName': 'PartOMagic_XRay', 46 | 'Pixmap' : self.getIconPath("PartOMagic_XRay.svg"), 47 | 'MenuText': "X-ray selected object", 48 | 'Accel': "", 49 | 'ToolTip': "X-ray: makes an object transparent and click-through.", 50 | 'CmdType': 'ForEdit'} 51 | 52 | def RunOrTest(self, b_run): 53 | import FreeCADGui as Gui 54 | sel = Gui.Selection.getSelectionEx() 55 | global library 56 | if len(sel) == 0 and len(library) == 0 : 57 | raise CommandError(self, "Please select an object to make transparent, and invoke this command.") 58 | if b_run: 59 | if len(sel) == 0: 60 | XRay(None) 61 | else: 62 | for selobj in sel: 63 | XRay(selobj.Object) 64 | Gui.Selection.clearSelection() 65 | Gui.Selection.clearPreselection() 66 | 67 | 68 | commandXRay = CommandXRay() 69 | commands.append(commandXRay) 70 | 71 | 72 | exportedCommands = AACommand.registerCommands(commands) -------------------------------------------------------------------------------- /PartOMagic/Gui/View/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "SnapView", 3 | "XRay" 4 | ] 5 | 6 | def importAll(): 7 | from . import SnapView 8 | from . import XRay 9 | 10 | def reloadAll(): 11 | try: #py2-3 compatibility: obtain reload() function 12 | reload 13 | except Exception: 14 | from importlib import reload 15 | 16 | for modstr in __all__: 17 | mod = globals()[modstr] 18 | reload(mod) 19 | if hasattr(mod, "reloadAll"): 20 | mod.reloadAll() 21 | 22 | def exportedCommands(): 23 | result = [] 24 | for modstr in __all__: 25 | mod = globals()[modstr] 26 | if hasattr(mod, "exportedCommands"): 27 | result += mod.exportedCommands() 28 | return result 29 | -------------------------------------------------------------------------------- /PartOMagic/Gui/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "Utils", 3 | "Observer", 4 | "TempoVis", 5 | "AACommand", 6 | "Control", 7 | "Tools", 8 | "LinkTools", 9 | "Icons", 10 | "CommandCollection1", 11 | "GlobalToolbar", 12 | "View", 13 | "MonkeyPatches", 14 | ] 15 | 16 | def importAll(): 17 | from . import Utils 18 | from . import Observer 19 | from . import TempoVis 20 | from . import AACommand 21 | from . import Control 22 | from . import Tools 23 | from . import LinkTools 24 | from . import Icons 25 | from . import CommandCollection1 26 | from . import GlobalToolbar 27 | from . import View 28 | from . import MonkeyPatches 29 | for modstr in __all__: 30 | mod = globals()[modstr] 31 | if hasattr(mod, "importAll"): 32 | mod.importAll() 33 | 34 | 35 | def reloadAll(): 36 | try: #py2-3 compatibility: obtain reload() function 37 | reload 38 | except Exception: 39 | from importlib import reload 40 | 41 | for modstr in __all__: 42 | mod = globals()[modstr] 43 | reload(mod) 44 | if hasattr(mod, "reloadAll"): 45 | mod.reloadAll() 46 | -------------------------------------------------------------------------------- /PartOMagic/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = "Part-o-magic package" 2 | __doc__ = """Experimental container automation for FreeCAD""" 3 | 4 | 5 | __all__ = [ 6 | "Base", 7 | "Gui", 8 | "Features", 9 | ] 10 | 11 | def importAll(): 12 | "importAll(): imports all modules of Part-o-magic" 13 | from . import Base 14 | from . import Gui 15 | from . import Features 16 | for modstr in __all__: 17 | mod = globals()[modstr] 18 | if hasattr(mod, "importAll"): 19 | mod.importAll() 20 | 21 | def reloadAll(): 22 | "reloadAll(): reloads all modules of Part-o-magic. Useful for debugging." 23 | 24 | try: #py2-3 compatibility: obtain reload() function 25 | reload 26 | except Exception: 27 | from importlib import reload 28 | 29 | for modstr in __all__: 30 | mod = globals()[modstr] 31 | reload(mod) 32 | if hasattr(mod, "reloadAll"): 33 | mod.reloadAll() 34 | 35 | import FreeCAD 36 | if FreeCAD.GuiUp: 37 | addCommands() 38 | 39 | def addCommands(): 40 | pass 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Part-o-magic 2 | 3 | ![figure](https://raw.githubusercontent.com/wiki/DeepSOIC/Part-o-magic/pictures/rotating-plate.png) 4 | 5 | PoM is an experimental add-on module, originally developed for FreeCAD v0.17-v0.20. 6 | It changes behavior across whole FreeCAD to embrace Active Container, allowing one to easily focus on a piece of a large project. 7 | It also contains an assortment of tools to help dealing with complex muti-part designs. 8 | 9 | PoM's main features are: 10 | 11 | * all new objects are added to active Part/Body/other_container. From all workbenches. 12 | 13 | * visibility automation for active container. 14 | When you activate a Body, PoM automatically hides everything else. 15 | When you are finished and deactivate it, everything is shown back again, while the inner things of the body such as sketches and datum planes are hidden. 16 | 17 | * Module container, which is PartDesign Body but for Part workbench (and Draft, and Sketcher, and Lattice2, and so on). 18 | 19 | * container-aware duplication tool 20 | 21 | * object replacement tool 22 | 23 | * saving a subset of objects as a FreeCAD file 24 | 25 | **Beware.** 26 | Part-o-magic is an epic hack. 27 | It will collide (collides already) with similar functionality in FreeCAD as it is introduced. 28 | If you experience problems: switch to Part-o-magic workbench, and disable Observer. 29 | This turns off Part-o-magic's functions that affect the whole FreeCAD, but lets you still recompute your project with PoM features in it. 30 | 31 | # Status 32 | 33 | FreeCAD's evolution (since v0.20) has somewhat diverged from Part-o-magic's concept of how things should work. But Part-o-magic way of modeling still works in 0.21 and 1.0, and a bunch of tools offered by PoM are still valuable. 34 | 35 | There are known limitations, as of FreeCAD v1.0: 36 | 37 | * PoM's FCStd file exporting, importing, and object duplication tools cannot deal with Link and XLink objects, and may not handle the toponaming data correctly. 38 | 39 | Part-o-magic's tools to disable Observer were made to allow you continue to use projects you made with PoM, even when FreeCAD progress renders PoM obsolete. So you can at least be a little bit confident that ShapeGroup feature won't quickly go bust. 40 | 41 | 42 | # Install 43 | 44 | ## Via Addon Manager: 45 | 46 | If you are on FreeCAD 0.19 and older: don't use addon manager, install [release-1.0.0](https://github.com/DeepSOIC/Part-o-magic/releases/tag/v1.0.0) manually. 47 | 48 | On FreeCAD 1.0, install master branch of Part-o-magic with addon manager: 49 | 50 | 1. Launch FreeCAD. 51 | 2. In menu, pick Tools->Addon Manager. 52 | 3. Select Part-o-magic in the list, and click Install. 53 | 4. Restart FreeCAD. 54 | 55 | After restart, you should notice: 56 | * Part-o-magic workbench should appear in the workbench selector. 57 | * A small global toolbar with a selection of Part-o-magic tools should appear. 58 | * Behavior of PartDesign workbench should change drastically - that is due to Observer running. 59 | 60 | ## ... or manually: 61 | 62 | 1. download source code zip file 63 | 2. unpack the content of `Part-o-magic-x.x.x` folder within the zip into a folder named `Part-o-magic` in where FreeCAD can pick it up as a module (for example, on Windows, it's `%appdata%/FreeCAD/Mod`) 64 | 3. Done! Run FreecAD, you should see Part-o-Magic workbench in the list, and the global toolbar. 65 | 4. (to verify you have the right PoM version) create a new project, and create a Module container from Part-o-magic workbench. If it is created without errors, you are running the correct version. 66 | 67 | 68 | # Uninstall 69 | 70 | Just use Addon Manager. 71 | 72 | BTW, if you just find the invasive nature of PoM unacceptable, you can just disable PoM Observer instead of completely uninstalling the workbench. 73 | Switch to PoM and press Stop button on the toolbar. 74 | This turns off all the invasive automation stuff, but features of part-o-magic can still be recomputed in your projects that have them. 75 | 76 | If you completely uninstall the workbench (delete Mod/Part-o-magic folder), part-o-magic features you used in your projects will stop working. 77 | 78 | **Important**. 79 | Part-o-magic messes with "DisplayModeBody" properties of PartDesign Body objects. 80 | If you uninstall Part-o-magic, or disable Observer, it will cause somewhat unusual behavior of any projects that were saved with part-o-magic enabled and had any container objects present. 81 | You can reset them manually with property editor, or run this simple snippet in Py console: 82 | 83 | for obj in App.ActiveDocument.Objects: 84 | if hasattr(obj.ViewObject, "DisplayModeBody"): 85 | obj.ViewObject.DisplayModeBody = "Through" 86 | if hasattr(obj.ViewObject, "Selectable"): 87 | obj.ViewObject.Selectable = True 88 | 89 | # list of features 90 | 91 | ## "Observer" 92 | 93 | ### Active container everywhere 94 | 95 | In PartDesign, all new features are automatically added to Body. Observer expands this to all workbenches of FreeCAD: new objects, made in any workbench, are automatically added to active container. 96 | 97 | That works (well, it should) in absolutely every workbench, including add-on workbenches and macros! 98 | 99 | ### Visibility automation 100 | 101 | When you activate a container, the container is switched into Through mode, so you see individual contained objects. 102 | When you leave it, you see only the final shape (Tip), but not contained objects. Also, when you activate a container, anything outside of it is automatically hidden, so that you can focus on editing the piece. 103 | 104 | ### Tree automation 105 | 106 | when you activate a container, it is automatically expanded in tree. When deactivated, it is automatically collapsed. With the aim to show you only the features that make up the container, so that you can focus on editing the piece. 107 | 108 | ### Editing automation 109 | 110 | When you try to edit a feature, PoM will check if the container of the feature is active, and activate it. (as of now, it has to cancel the editing, so please invoke the edititng again). If the right container is not activated, you may not even see the feature. 111 | 112 | ## Containers 113 | 114 | ### Module container 115 | 116 | It's an analog of PartDesign Body, but for Part and other workbenches. It groups together features that were used to create a final shape, and exposes the final shape (Tip) as its own. 117 | 118 | When you enter module, you can edit it - add new features from any workbench, including PartDesign Bodies, in order to arrive to the final result shape (typically a solid, but it can be any other type of b-rep shape). When you leave Module, you see Module as the final result. The final result is shape copied from Tip object. Tip object can be assigned with Set Tip tool in PoM. 119 | 120 | ### ShapeGroup container 121 | 122 | ShapeGroup similar to Module, but it can expose multiple objects to the outside. 123 | 124 | It is somewhat similar to what is called a "group" in vector graphics software. You can group up existing objects, and enter a group to modify its contents (as well as add new objects). 125 | 126 | It also can do an operation between objects to be exposed, for example fusing them together into one. 127 | 128 | ### PartDesign Additive shape, PartDesign Subtractive shape containers 129 | 130 | These are just like Modules, but they integrate themselves as PartDesign features. They allow to integrate other workbench tools into PartDesign workflow. 131 | 132 | ### Ghost (obsolete) 133 | 134 | Obsolete! Ghost tool still works, but is obsolete, because 1) FreeCAD's Shapebinder supports it too now; 2) FreeCAD's Subshape Link does that too. 135 | 136 | Ghost is a placement-aware version of shapebinder. It is a tool to bring a copy of a shape from one container into another, applying a proper transform. 137 | 138 | Ghost supports both extracting an object from a container, and importing an object from a higher-level container. The latter is somewhat limited: it will not work properly if Placement of the container the Ghost is in, changes as a result of a recompute (for example if there is an expression bound to the placement). 139 | 140 | ### Morph container tool 141 | 142 | Created a Module, but later realized you want ShapeGroup instead? Of course, you can create a new container, drag-drop stuff... The "Morph container" tool is for simplifying the process. It takes care of moving stuff, redirecting links, and deletion of the remaining old empty container. 143 | 144 | ## Other features 145 | 146 | * Set Tip. Works on PartDesign Bodies and Module-like things. 147 | 148 | * Enter and Leave. Work on almost everything (can be used to enter/leave containers, and edit objects (e.g. open a sketch)). 149 | 150 | * Exporter feature, for keeping exported files (like STL) up to date with project. 151 | 152 | * an advanced Object Replacement tool, with container support, and UI to pick specific replacements. 153 | 154 | * X-ray tool. For getting through objects to select concealed objects. 155 | 156 | * Align View, a one-button replacement for standard view buttons. 157 | 158 | * Object duplication tool that can duplicate containers without causing a mess. 159 | 160 | * container-aware selection tools like "select all children" 161 | 162 | 163 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Part-o-magic Workbench 4 | Experiment on FreeCAD-wide automation of Part container management 5 | 1.1.0 6 | 2023-03-29 7 | DeepSOIC 8 | LGPL-2.0-or-later 9 | https://github.com/DeepSOIC/Part-o-magic 10 | https://github.com/DeepSOIC/Part-o-magic/issues 11 | PartOMagic/Gui/Icons/icons/PartOMagic.svg 12 | 13 | 14 | 15 | PartOMagicWorkbench 16 | ./ 17 | 18 | 19 | 20 | 21 | 22 | 23 | --------------------------------------------------------------------------------