From 7eccda34d090ab641b29fa4be24c515cfa39c601 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Thu, 11 Jul 2019 19:37:10 -0300 Subject: [PATCH] Arch: Lightweight mode for Arch References --- src/Mod/Arch/ArchBuildingPart.py | 103 ++++++++++----- src/Mod/Arch/ArchReference.py | 210 ++++++++++++++++++++++++++----- src/Mod/Draft/WorkingPlane.py | 4 +- 3 files changed, 248 insertions(+), 69 deletions(-) diff --git a/src/Mod/Arch/ArchBuildingPart.py b/src/Mod/Arch/ArchBuildingPart.py index 3f053fbd7d39..9f68cf511581 100644 --- a/src/Mod/Arch/ArchBuildingPart.py +++ b/src/Mod/Arch/ArchBuildingPart.py @@ -22,7 +22,14 @@ #* * #*************************************************************************** -import FreeCAD,Draft,ArchCommands,DraftVecUtils,sys,ArchIFC +import FreeCAD +import Draft +import ArchCommands +import DraftVecUtils +import sys +import ArchIFC +import tempfile +import os if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore, QtGui @@ -35,7 +42,6 @@ def translate(ctxt,txt): def QT_TRANSLATE_NOOP(ctxt,txt): return txt # \endcond -import sys if sys.version_info.major >= 3: unicode = str @@ -335,6 +341,9 @@ def setProperties(self,obj): obj.addProperty("App::PropertyString","Tag","Component",QT_TRANSLATE_NOOP("App::Property","An optional tag for this component")) if not "Shape" in pl: obj.addProperty("Part::PropertyPartShape","Shape","BuildingPart",QT_TRANSLATE_NOOP("App::Property","The shape of this object")) + if not "SavedInventor" in pl: + obj.addProperty("App::PropertyFileIncluded","SavedInventor","BuildingPart",QT_TRANSLATE_NOOP("App::Property","This property stores an inventor representation for this object")) + obj.setEditorMode("SavedInventor",2) self.Type = "BuildingPart" @@ -368,7 +377,7 @@ def onChanged(self,obj,prop): elif prop == "Placement": if hasattr(self,"oldPlacement"): - if self.oldPlacement: + if self.oldPlacement and (self.oldPlacement != obj.Placement): deltap = obj.Placement.Base.sub(self.oldPlacement.Base) if deltap.Length == 0: deltap = None @@ -399,8 +408,14 @@ def execute(self,obj): # gather all the child shapes into a compound shapes = self.getShapes(obj) if shapes: + f = [] + for s in shapes: + f.extend(s.Faces) + #print("faces before compound:",len(f)) import Part - obj.Shape = Part.makeCompound(shapes) + obj.Shape = Part.makeCompound(f) + #print("faces after compound:",len(obj.Shape.Faces)) + #print("recomputing ",obj.Label) obj.Area = self.getArea(obj) def getArea(self,obj): @@ -421,18 +436,9 @@ def getShapes(self,obj): "recursively get the shapes of objects inside this BuildingPart" shapes = [] - if obj.isDerivedFrom("Part::Feature") and obj.Shape and (not obj.Shape.isNull()): - shapes.append(obj.Shape) - if hasattr(obj,"Group"): - for child in obj.Group: - shapes.extend(self.getShapes(child)) - for i in obj.InList: - if hasattr(i,"Hosts"): - if obj in i.Hosts: - shapes.extend(self.getShapes(i)) - elif hasattr(i,"Host"): - if obj == i.Host: - shapes.extend(self.getShapes(i)) + for child in Draft.getGroupContents(obj): + if child.isDerivedFrom("Part::Feature"): + shapes.extend(child.Shape.Faces) return shapes def getSpaces(self,obj): @@ -514,7 +520,6 @@ def setProperties(self,vobj): vobj.ChildrenLineColor = (float((c>>24)&0xFF)/255.0,float((c>>16)&0xFF)/255.0,float((c>>8)&0xFF)/255.0,0.0) if not "ChildrenTransparency" in pl: vobj.addProperty("App::PropertyPercent","ChildrenTransparency","Children",QT_TRANSLATE_NOOP("App::Property","The transparency of child objects")) - if not "CutView" in pl: vobj.addProperty("App::PropertyBool","CutView","Clip",QT_TRANSLATE_NOOP("App::Property","Cut the view above this level")) if not "CutMargin" in pl: @@ -522,6 +527,8 @@ def setProperties(self,vobj): vobj.CutMargin = 1600 if not "AutoCutView" in pl: vobj.addProperty("App::PropertyBool","AutoCutView","Clip",QT_TRANSLATE_NOOP("App::Property","Turn cutting on when activating this level")) + if not "SaveInventor" in pl: + vobj.addProperty("App::PropertyBool","SaveInventor","BuildingPart",QT_TRANSLATE_NOOP("App::Property","If this is enabled, the inventor representation of this object will be saved in the FreeCAD file, allowing to reference it in other file sin lightweight mode.")) def onDocumentRestored(self,vobj): @@ -591,6 +598,9 @@ def updateData(self,obj,prop): if len(colors) == len(obj.Shape.Faces): if colors != obj.ViewObject.DiffuseColor: obj.ViewObject.DiffuseColor = colors + self.writeInventor(obj) + #else: + #print("color mismatch:",len(colors),"colors,",len(obj.Shape.Faces),"faces") elif prop == "Group": self.onChanged(obj.ViewObject,"ChildrenOverride") elif prop == "Label": @@ -601,23 +611,14 @@ def getColors(self,obj): "recursively get the colors of objects inside this BuildingPart" colors = [] - if obj.isDerivedFrom("Part::Feature") and obj.Shape and (not obj.Shape.isNull()): - if hasattr(obj.ViewObject,"DiffuseColor") and (len(obj.ViewObject.DiffuseColor) == len(obj.Shape.Faces)): - colors.extend(obj.ViewObject.DiffuseColor) - elif hasattr(obj.ViewObject,"ShapeColor"): - c = obj.ViewObject.ShapeColor[:3]+(obj.ViewObject.Transparency/100.0,) - for i in range(len(obj.Shape.Faces)): - colors.append(c) - if hasattr(obj,"Group"): - for child in obj.Group: - colors.extend(self.getColors(child)) - for i in obj.InList: - if hasattr(i,"Hosts"): - if obj in i.Hosts: - colors.extend(self.getColors(i)) - elif hasattr(i,"Host"): - if obj == i.Host: - colors.extend(self.getColors(i)) + for child in Draft.getGroupContents(obj): + if child.isDerivedFrom("Part::Feature"): + if len(child.ViewObject.DiffuseColor) == len(child.Shape.Faces): + colors.extend(child.ViewObject.DiffuseColor) + else: + c = child.ViewObject.ShapeColor[:3]+(child.ViewObject.Transparency/100.0,) + for i in range(len(child.Shape.Faces)): + colors.append(c) return colors def onChanged(self,vobj,prop): @@ -728,6 +729,8 @@ def onChanged(self,vobj,prop): # turn clipping off when turning the object off if hasattr(vobj,"Visibility") and not(vobj.Visibility) and hasattr(vobj,"CutView"): vobj.CutView = False + elif prop == "SaveInventor": + self.writeInventor(vobj.Object) def onDelete(self,vobj,subelements): @@ -737,6 +740,7 @@ def onDelete(self,vobj,subelements): for o in Draft.getGroupContents(vobj.Object.Group,walls=True): if hasattr(o.ViewObject,"Lighting"): o.ViewObject.Lighting = "Two side" + return True def doubleClicked(self,vobj): @@ -865,5 +869,36 @@ def __getstate__(self): def __setstate__(self,state): return None + def writeInventor(self,obj): + + def callback(match): + return next(callback.v) + + if hasattr(obj.ViewObject,"SaveInventor") and obj.ViewObject.SaveInventor: + if obj.Shape and obj.Shape.Faces and hasattr(obj,"SavedInventor"): + colors = obj.ViewObject.DiffuseColor + if len(colors) != len(obj.Shape.Faces): + print("Debug: Colors mismatch in",obj.Label) + colors = None + iv = self.Object.Shape.writeInventor() + import re + if colors: + if len(re.findall("IndexedFaceSet",iv)) == len(obj.Shape.Faces): + # convert colors to iv representations + colors = ["Material { diffuseColor "+str(color[0])+" "+str(color[1])+" "+str(color[2])+"}\n IndexedFaceSet" for color in colors] + # replace + callback.v=iter(colors) + iv = re.sub("IndexedFaceSet",callback,iv) + else: + print("Debug: IndexedFaceSet mismatch in",obj.Label) + # save embedded file + tf = tempfile.mkstemp(prefix=obj.Name,suffix=".iv")[1] + f = open(tf,"w") + f.write(iv) + f.close() + obj.SavedInventor = tf + os.remove(tf) + + if FreeCAD.GuiUp: FreeCADGui.addCommand('Arch_BuildingPart',CommandBuildingPart()) diff --git a/src/Mod/Arch/ArchReference.py b/src/Mod/Arch/ArchReference.py index ff5edabf8642..c0bddce152c7 100644 --- a/src/Mod/Arch/ArchReference.py +++ b/src/Mod/Arch/ArchReference.py @@ -25,7 +25,11 @@ __url__ = "http://www.freecadweb.org" -import FreeCAD,os,zipfile,re,sys +import FreeCAD +import os +import zipfile +import re +import sys if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore, QtGui @@ -91,14 +95,23 @@ def setProperties(self,obj): obj.addProperty("App::PropertyFile","File","Reference",QT_TRANSLATE_NOOP("App::Property","The base file this component is built upon")) if not "Part" in pl: obj.addProperty("App::PropertyString","Part","Reference",QT_TRANSLATE_NOOP("App::Property","The part to use from the base file")) - if not "TransientReference" in pl: - obj.addProperty("App::PropertyBool","TransientReference","Reference",QT_TRANSLATE_NOOP("App::Property","If True, the shape will be discarded when turning visibility off, resulting in a lighter file, but with an additional loading time when turning the object back on")) + if not "ReferenceMode" in pl: + obj.addProperty("App::PropertyEnumeration","ReferenceMode","Reference",QT_TRANSLATE_NOOP("App::Property","The way the referenced objects are included in the current document. 'Normal' includes the shape, 'Transient' discards the shape when the object is switched off (smaller filesize), 'Lightweight' does not import the shape but only the OpenInventor representation")) + obj.ReferenceMode = ["Normal","Transient","Lightweight"] + if "TransientReference" in pl: + if obj.TransientReference: + obj.ReferenceMode = "Transient" + obj.removeProperty("TransientReference") + FreeCAD.Console.PrintMessage("Upgrading "+obj.Label+" TransientReference property to ReferenceMode\n") self.Type = "Reference" def onDocumentRestored(self,obj): ArchReference.setProperties(self,obj) self.reload = False + if obj.ReferenceMode == "Lightweight": + if obj.ViewObject and obj.ViewObject.Proxy: + obj.ViewObject.Proxy.loadInventor(obj) def __getstate__(self): @@ -112,26 +125,31 @@ def onChanged(self,obj,prop): if prop in ["File","Part"]: self.reload = True - elif prop == "TransientReference": - if obj.TransientReference: + elif prop == "ReferenceMode": + if obj.ReferenceMode == "Normal": + if obj.ViewObject and obj.ViewObject.Proxy: + obj.ViewObject.Proxy.unloadInventor(obj) if (not obj.Shape) or obj.Shape.isNull(): self.reload = True obj.touch() - else: - if obj.ViewObject: - obj.ViewObject.Visibility = False - else: - self.reload = False - import Part - pl = obj.Placement - obj.Shape = Part.Shape() - obj.Placement = pl + elif obj.ReferenceMode == "Transient": + if obj.ViewObject and obj.ViewObject.Proxy: + obj.ViewObject.Proxy.unloadInventor(obj) + self.reload = False + elif obj.ReferenceMode == "Lightweight": + self.reload = False + import Part + pl = obj.Placement + obj.Shape = Part.Shape() + obj.Placement = pl + if obj.ViewObject and obj.ViewObject.Proxy: + obj.ViewObject.Proxy.loadInventor(obj) def execute(self,obj): pl = obj.Placement filename = self.getFile(obj) - if filename and obj.Part and self.reload: + if filename and obj.Part and self.reload and obj.ReferenceMode in ["Normal","Transient"]: self.parts = self.getPartsList(obj) if self.parts: zdoc = zipfile.ZipFile(filename) @@ -175,7 +193,7 @@ def getFile(self,obj,filename=None): else: # search for subpaths in current folder altfile = None - subdirs = splitall(os.path.dirname(filename)) + subdirs = self.splitall(os.path.dirname(filename)) for i in range(len(subdirs)): subpath = [currentdir]+subdirs[-i:]+[basename] altfile = os.path.join(*subpath) @@ -185,6 +203,8 @@ def getFile(self,obj,filename=None): return filename def getPartsList(self,obj,filename=None): + + "returns a list of Part-based objects in a FCStd file" parts = {} filename = self.getFile(obj,filename) @@ -226,6 +246,8 @@ def getPartsList(self,obj,filename=None): def getColors(self,obj): + "returns the DiffuseColor of the referenced object" + filename = self.getFile(obj) if not filename: return None @@ -269,6 +291,24 @@ def getColors(self,obj): return colors return None + def splitall(self,path): + + "splits a path between its components" + + allparts = [] + while 1: + parts = os.path.split(path) + if parts[0] == path: # sentinel for absolute paths + allparts.insert(0, parts[0]) + break + elif parts[1] == path: # sentinel for relative paths + allparts.insert(0, parts[1]) + break + else: + path = parts[0] + allparts.insert(0, parts[1]) + return allparts + class ViewProviderArchReference: @@ -380,7 +420,7 @@ def onChanged(self,vobj,prop): vobj.Object.Proxy.reload = True vobj.Object.Proxy.execute(vobj.Object) else: - if hasattr(vobj.Object,"TransientReference") and vobj.Object.TransientReference: + if hasattr(vobj.Object,"ReferenceMode") and vobj.Object.ReferenceMode == "Transient": vobj.Object.Proxy.reload = False import Part pl = vobj.Object.Placement @@ -420,11 +460,127 @@ def onOpen(self): if self.Object.File: FreeCAD.openDocument(self.Object.File) + def loadInventor(self,obj): + + "loads an openinventor file and replace the root node of this object" + + # check inventor contents + ivstring = self.getInventorString(obj) + if not ivstring: + FreeCAD.Console.PrintWarning("Unable to get lightWeight node for object referenced in "+obj.Label+"\n") + return + from pivy import coin + inputnode = coin.SoInput() + inputnode.setBuffer(ivstring) + lwnode = coin.SoDB.readAll(inputnode) + if not isinstance(lwnode,coin.SoSeparator): + FreeCAD.Console.PrintError("Invalid lightWeight node for object referenced in "+obj.Label+"\n") + return + if lwnode.getNumChildren() < 2: + FreeCAD.Console.PrintError("Invalid lightWeight node for object referenced in "+obj.Label+"\n") + return + flatlines = lwnode + shaded = lwnode.getChild(0) + wireframe = lwnode.getChild(1) + + # check node contents + rootnode = obj.ViewObject.RootNode + if rootnode.getNumChildren() < 3: + FreeCAD.Console.PrintError("Invalid root node in "+obj.Label+"\n") + return + switch = rootnode.getChild(2) + if switch.getNumChildren() != 4: + FreeCAD.Console.PrintError("Invalid root node in "+obj.Label+"\n") + return + + # keep a copy of the original nodes + self.orig_flatlines = switch.getChild(0).copy() + self.orig_shaded = switch.getChild(1).copy() + self.orig_wireframe = switch.getChild(2).copy() + + # replace root node of object + switch.replaceChild(0,flatlines) + switch.replaceChild(1,shaded) + switch.replaceChild(2,wireframe) + + def unloadInventor(self,obj): + + "restore original nodes" + + if (not hasattr(self,"orig_flatlines")) or (not self.orig_flatlines): + return + if (not hasattr(self,"orig_shaded")) or (not self.orig_shaded): + return + if (not hasattr(self,"orig_wireframe")) or (not self.orig_wireframe): + return + + # check node contents + rootnode = obj.ViewObject.RootNode + if rootnode.getNumChildren() < 3: + FreeCAD.Console.PrintError("Invalid root node in "+obj.Label+"\n") + return + switch = rootnode.getChild(2) + if switch.getNumChildren() != 4: + FreeCAD.Console.PrintError("Invalid root node in "+obj.Label+"\n") + return + + # replace root node of object + switch.replaceChild(0,self.orig_flatlines) + switch.replaceChild(1,self.orig_shaded) + switch.replaceChild(2,self.orig_wireframe) + + # discard old content + self.orig_flatlines = None + self.orig_shaded = None + self.orig_wireframe = None + + def getInventorString(self,obj): + + "locates and loads an iv file saved together with an object, if existing" + + filename = obj.Proxy.getFile(obj) + if not filename: + return None + part = obj.Part + if not obj.Part: + return None + zdoc = zipfile.ZipFile(filename) + if not "Document.xml" in zdoc.namelist(): + return None + ivfile = None + with zdoc.open("Document.xml") as docf: + writemode1 = False + writemode2 = False + for line in docf: + if sys.version_info.major >= 3: + line = line.decode("utf8") + if ("= 3: + buf = buf.decode("utf8") + f.close() + buf = buf.replace("lineWidth 2","lineWidth "+str(int(obj.ViewObject.LineWidth))) + return buf + class ArchReferenceTaskPanel: - '''The editmode TaskPanel for Axis objects''' + '''The editmode TaskPanel for Reference objects''' def __init__(self,obj): @@ -539,21 +695,7 @@ def Activated(self): FreeCAD.ActiveDocument.commitTransaction() FreeCADGui.doCommand("obj.ViewObject.Document.setEdit(obj.ViewObject, 0)") -if FreeCAD.GuiUp: - FreeCADGui.addCommand('Arch_Reference', ArchReferenceCommand()) -def splitall(path): - allparts = [] - while 1: - parts = os.path.split(path) - if parts[0] == path: # sentinel for absolute paths - allparts.insert(0, parts[0]) - break - elif parts[1] == path: # sentinel for relative paths - allparts.insert(0, parts[1]) - break - else: - path = parts[0] - allparts.insert(0, parts[1]) - return allparts +if FreeCAD.GuiUp: + FreeCADGui.addCommand('Arch_Reference', ArchReferenceCommand()) diff --git a/src/Mod/Draft/WorkingPlane.py b/src/Mod/Draft/WorkingPlane.py index dea7f036b6c3..7a1c598df0d6 100644 --- a/src/Mod/Draft/WorkingPlane.py +++ b/src/Mod/Draft/WorkingPlane.py @@ -207,7 +207,9 @@ def alignToPointAndAxis_SVG(self, point, axis, offset): def alignToCurve(self, shape, offset): - if shape.ShapeType == 'Edge': + if shape.isNull(): + return False + elif shape.ShapeType == 'Edge': #??? TODO: process curve here. look at shape.edges[0].Curve return False elif shape.ShapeType == 'Wire':