# -*- coding: utf-8 -*- #*************************************************************************** #* Copyright (c) 2019 Yorik van Havre * #* * #* This program is free software; you can redistribute it and/or modify * #* it under the terms of the GNU Lesser General Public License (LGPL) * #* as published by the Free Software Foundation; either version 2 of * #* the License, or (at your option) any later version. * #* for detail see the LICENCE text file. * #* * #* This program is distributed in the hope that it will be useful, * #* but WITHOUT ANY WARRANTY; without even the implied warranty of * #* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * #* GNU Library General Public License for more details. * #* * #* You should have received a copy of the GNU Library General Public * #* License along with this program; if not, write to the Free Software * #* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * #* USA * #* * #*************************************************************************** ## \defgroup OFFLINERENDERINGUTILS OfflineRenderingUtils # \ingroup UTILITIES # \brief Utility functions to work with FreeCAD files in console mode # # Offline rendering utilities ## \addtogroup OFFLINERENDERINGUTILS # @{ """@package docstring OfflineRenderingUtils - Utilities to help producing files with colors from FreeCAD in non-GUI mode Example usage: The examples below extract the colors from an existing file, but you can also create your own while working, as a simple dictionary of "objectName":(r,g,b) pairs (r, g, b as floats) import os import sys import FreeCAD import OfflineRenderingUtils # build full filepaths freecadFile = os.path.join(os.getcwd(),"testfile.FCStd") baseFileName = os.path.splitext(freecadFile)[0] # open FreeCAD file doc = FreeCAD.open(freecadFile) # build color dict # setting nodiffuse=True (optional) discards per-face colors, and only sets one color per object # only the STEP exporter accepts per-face colors. The others would consider the first color in the per-face colors list as # the object color, which might be not what we want, so it's best to turn it off here. colors = OfflineRenderingUtils.getColors(freecadFile,nodiffuse=True) # get the camera data from the file (used in some functions below) camera = OfflineRenderingUtils.getCamera(freecadFile) # export to OBJ import importOBJ importOBJ.export(doc.Objects,baseFileName+".obj",colors=colors) # export to DAE import importDAE importDAE.export(doc.Objects,baseFileName+".dae",colors=colors) # export to IFC import importIFC importIFC.export(doc.Objects,baseFileName+".ifc",colors=colors) # export to STEP # The STEP exporter accepts different kind of data than the above # exporters. Use the function below to reformat it the proper way stepdata = OfflineRenderingUtils.getStepData(doc.Objects,colors) import Import Import.export(stepdata,baseFileName+".stp") # export to PNG scene = OfflineRenderingUtils.buildScene(doc.Objects,colors) OfflineRenderingUtils.render(baseFileName+".png",scene,camera,width=800,height=600) # view the scene in a standalone coin viewer OfflineRenderingUtils.viewer(scene) # file saving OfflineRenderingUtils.save(doc,filename=baseFileName+"_exported.FCStd",colors=colors,camera=camera) """ import sys import os import xml.sax import zipfile import tempfile import inspect import binascii class FreeCADGuiHandler(xml.sax.ContentHandler): "A XML handler to process the FreeCAD GUI xml data, used by getGuiData()" # this creates a dictionary where each key is a FC object name, # and each value is a dictionary of property:value pairs, # plus a GuiCameraSettings key that contains an iv repr of a coin camera def __init__(self): super().__init__() self.guidata = {} self.current = None self.properties = {} self.currentprop = None self.currentval = None self.currenttype = None # Call when an element starts def startElement(self, tag, attributes): if tag == "ViewProvider": self.current = attributes["name"] elif tag == "Property": self.currenttype = attributes["type"] self.currentprop = attributes["name"] elif tag == "Bool": if attributes["value"] == "true": self.currentval = True else: self.currentval = False elif tag == "PropertyColor": c = int(attributes["value"]) r = float((c>>24)&0xFF)/255.0 g = float((c>>16)&0xFF)/255.0 b = float((c>>8)&0xFF)/255.0 self.currentval = (r,g,b) elif tag == "Integer": self.currentval = int(attributes["value"]) elif tag == "String": self.currentval = attributes["value"] elif tag == "Float": self.currentval = float(attributes["value"]) elif tag == "ColorList": self.currentval = attributes["file"] elif tag == "PropertyMaterial": a = int(attributes["ambientColor"]) d = int(attributes["diffuseColor"]) s = int(attributes["specularColor"]) e = int(attributes["emissiveColor"]) i = float(attributes["shininess"]) t = float(attributes["transparency"]) self.currentval = (a,d,s,e,i,t) elif tag == "Enum": if isinstance(self.currentval,int): self.currentval = [self.currentval,attributes["value"]] elif isinstance(self.currentval,list): self.currentval.append(attributes["value"]) elif tag == "Python": if "module" in attributes: self.currentval = (attributes["value"],attributes["encoded"],attributes["module"],attributes["class"]) elif tag == "Camera": self.guidata["GuiCameraSettings"] = attributes["settings"] # Call when an elements ends def endElement(self, tag): if tag == "ViewProvider": if self.current and self.properties: self.guidata[self.current] = self.properties self.current = None self.properties = {} elif tag == "Property": if self.currentprop and (self.currentval is not None): self.properties[self.currentprop] = {"type":self.currenttype,"value":self.currentval} self.currentprop = None self.currentval = None self.currenttype = None def getGuiData(filename): """getGuiData(filename): Extract visual data from a saved FreeCAD file. Returns a dictionary ["objectName:dict] where dict contains properties keys like ShapeColor, Transparency, DiffuseColor or Visibility. If found, also contains a GuiCameraSettings key with an iv repr of a coin camera""" guidata = {} zdoc = zipfile.ZipFile(filename) if zdoc: if "GuiDocument.xml" in zdoc.namelist(): gf = zdoc.open("GuiDocument.xml") guidata = gf.read() gf.close() Handler = FreeCADGuiHandler() xml.sax.parseString(guidata, Handler) guidata = Handler.guidata for key,properties in guidata.items(): # open each diffusecolor files and retrieve values # first 4 bytes are the array length, then each group of 4 bytes is abgr # https://forum.freecadweb.org/viewtopic.php?t=29382 if isinstance(properties,dict): for propname in properties.keys(): if properties[propname]["type"] == "App::PropertyColorList": df = zdoc.open(guidata[key][propname]["value"]) buf = df.read() df.close() cols = [] for i in range(1,int(len(buf)/4)): cols.append((buf[i*4+3]/255.0,buf[i*4+2]/255.0,buf[i*4+1]/255.0,buf[i*4]/255.0)) guidata[key][propname]["value"] = cols zdoc.close() #print ("guidata:",guidata) return guidata def saveDiffuseColor(colorlist): """saveDiffuseColor(colorlist): Saves the given list or tuple of color tuples to a temp file, suitable to include in a DiffuseColor property. Returns the path to the created temp file""" def tochr(i): return bytes((i,)) # if too many colors, bail out and use only the first one for now... if len(colorlist) > 254: colorlist = colorlist[:1] print("debug: too many colors, reducing") output = tochr(len(colorlist))+3*tochr(0) allfloats = True for color in colorlist: for d in color: if d > 1: allfloats = False break for color in colorlist: if len(color) < 4: output += tochr(0) for d in reversed(color): if allfloats: output += tochr(int(d*255)) else: output += tochr(int(d)) colfile = tempfile.mkstemp(prefix="DiffuseColor")[-1] f = open(colfile,"wb") f.write(output) f.close() return colfile def getColors(filename,nodiffuse=False): """getColors(filename,nodiffuse): Extracts the colors saved in a FreeCAD file Returns a dictionary containing ["objectName":colors] pairs. colrs can be either a 3-element tuple representing an RGB color, if the object has no per-face colors (DiffuseColor) defined, or a list of tuples if per-face colors are available. In case of DiffuseColors, tuples can have 4 values (RGBT) (T = transparency, inverse of alpha) This is a reduced version of getGuiData(), which returns more information. If nodiffuse = True, DiffuseColor info is discarded, only ShapeColor is read.""" guidata = getGuiData(filename) colors = {} for k,v in guidata.items(): if ("DiffuseColor" in v) and (not nodiffuse): if len(v["DiffuseColor"]["value"]) == 1: # only one color in DiffuseColor: used for the whole object colors[k] = v["DiffuseColor"]["value"][0] else: colors[k] = v["DiffuseColor"]["value"] elif "ShapeColor" in v: colors[k] = v["ShapeColor"]["value"] return colors def getStepData(objects,colors): """getStepData(objects,colors): transforms the given list of objects and colors dictionary into a list of tuples acceptable by the STEP exporter of FreeCAD's Import module""" # The STEP exporter accepts two kinds of data: [obj1,obj2,...] list, or # [(obj1,DiffuseColor),(obj2,DiffuseColor),...] list. # we need to reformat a bit the data we send to the exporter # since, differently than the others, it wants (object,DiffuseColor) tuples data = [] for obj in objects: if obj.Name in colors: color = colors[obj.Name] if isinstance(color,tuple): # this is a ShapeColor. We reformat as a list so it works as a DiffuseColor, # which is what the exporter expects. DiffuseColor can have either one color, # or the same number of colors as the number of faces color = [color] data.append((obj,color)) else: # no color information. This object will be exported without colors data.append(obj) return data def render(outputfile,scene=None,camera=None,zoom=False,width=400,height=300,background=(1.0,1.0,1.0),lightdir=None): """render(outputfile,scene=None,camera=None,zoom=False,width=400,height=300,background=(1.0,1.0,1.0),lightdir=None): Renders a PNG image of given width and height and background color from the given coin scene, using the given coin camera (ortho or perspective). If zoom is True the camera will be resized to fit all objects. The outputfile must be a file path to save a png image. Optionally a light direction as a (x,y,z) tuple can be given. In this case, a directional light will be added and shadows will be turned on. This might not work with some 3D drivers.""" # On Linux, the X server must have indirect rendering enabled in order to be able to do offline # PNG rendering. Unfortunately, this is turned off by default on most recent distros. The easiest # way I found is to edit (or create if inexistent) /etc/X11/xorg.conf and add this: # # Section "ServerFlags" # Option "AllowIndirectGLX" "on" # Option "IndirectGLX" "on" # EndSection # # But there are other ways, google of GLX indirect rendering from pivy import coin if isinstance(camera,str): camera = getCoinCamera(camera) print("Starting offline renderer") # build an offline scene root separator root = coin.SoSeparator() if not lightdir: # add one light (mandatory) light = coin.SoDirectionalLight() root.addChild(light) if not camera: # create a default camera if none was given camera = coin.SoPerspectiveCamera() cameraRotation = coin.SbRotation.identity() cameraRotation *= coin.SbRotation(coin.SbVec3f(1,0,0),1.0) cameraRotation *= coin.SbRotation(coin.SbVec3f(0,0,1),0.4) camera.orientation = cameraRotation # make sure all objects get in the view later zoom = True root.addChild(camera) if scene: root.addChild(scene) else: # no scene was given, add a simple cube cube = coin.SoCube() root.addChild(cube) if lightdir: root = embedLight(root,lightdir) vpRegion = coin.SbViewportRegion(width,height) if zoom: camera.viewAll(root,vpRegion) print("Creating viewport") offscreenRenderer = coin.SoOffscreenRenderer(vpRegion) offscreenRenderer.setBackgroundColor(coin.SbColor(background[0],background[1],background[2])) print("Ready to render") # ref ensures that the node will not be garbage-collected during rendering root.ref() ok = offscreenRenderer.render(root) root.unref() if ok: offscreenRenderer.writeToFile(outputfile,"PNG") print("Rendering",outputfile,"done") else: print("Error rendering image") def buildScene(objects,colors=None): """buildScene(objects,colors=None): builds a coin node from a given list of FreeCAD objects. Optional colors argument can be a dictionary of objName:ShapeColorTuple or obj:DiffuseColorList pairs.""" from pivy import coin root = coin.SoSeparator() for o in objects: buf = None if hasattr(o,'Shape') and o.Shape and (not o.Shape.isNull()): # writeInventor of shapes needs tessellation values buf = o.Shape.writeInventor(2,0.01) elif o.isDerivedFrom("Mesh::Feature"): buf = o.Mesh.writeInventor() if buf: # 3 lines below are the standard way to produce a coin node from a string # Part and Mesh objects always have a containing SoSeparator inp = coin.SoInput() inp.setBuffer(buf) node = coin.SoDB.readAll(inp) if colors: if o.Name in colors: # insert a material node at 1st position, before the geometry color = colors[o.Name] if isinstance(color,list): # DiffuseColor, not supported here color = color[0] color = color[:3] mat = coin.SoMaterial() mat.diffuseColor = color node.insertChild(mat,0) root.addChild(node) return root def getCamera(filepath): """getCamera(filepath): Returns a string representing a coin camera node from a given FreeCAD file, or None if none was found inside""" guidata = getGuiData(filepath) if "GuiCameraSettings" in guidata: return guidata["GuiCameraSettings"].strip() print("No camera found in file") return None def getCoinCamera(camerastring): """getCoinCamera(camerastring): Returns a coin camera node from a string""" from pivy import coin if camerastring: inp = coin.SoInput() inp.setBuffer(camerastring) node = coin.SoDB.readAll(inp) # this produces a SoSeparator containing the camera for child in node.getChildren(): if ("SoOrthographicCamera" in str(child)) or ("SoPerspectiveCamera" in str(child)): return child print("unnable to build a camera node from string:",camerastring) return None def viewer(scene=None,background=(1.0,1.0,1.0),lightdir=None): """viewer(scene=None,background=(1.0,1.0,1.0),lightdir=None): starts a standalone coin viewer with the contents of the given scene. You can give a background color, and optionally a light direction as a (x,y,z) tuple. In this case, a directional light will be added and shadows will be turned on. This might not work with some 3D drivers.""" # Initialize Coin. This returns a main window to use from pivy import coin from pivy import sogui win = sogui.SoGui.init() if win is None: print("Unable to create a SoGui window") return win.setBackgroundColor(coin.SbColor(background[0],background[1],background[2])) if not scene: # build a quick default scene mat = coin.SoMaterial() mat.diffuseColor = (1.0, 0.0, 0.0) # Make a scene containing a red cone scene = coin.SoSeparator() scene.addChild(mat) scene.addChild(coin.SoCone()) if lightdir: scene = embedLight(scene,lightdir) # ref the scene so it doesn't get garbage-collected scene.ref() # Create a viewer in which to see our scene graph viewer = sogui.SoGuiExaminerViewer(win) # Put our scene into viewer, change the title viewer.setSceneGraph(scene) viewer.setTitle("Coin viewer") viewer.show() sogui.SoGui.show(win) # Display main window sogui.SoGui.mainLoop() # Main Coin event loop def embedLight(scene,lightdir): """embedLight(scene,lightdir): embeds a given coin node inside a shadow group with directional light with the given direction (x,y,z) tuple. Returns the final coin node""" from pivy import coin # buggy - no SoShadowGroup in pivy? #sgroup = coin.SoShadowGroup() #sgroup.quality = 1 #sgroup.precision = 1 #slight = SoShadowDirectionalLight() #slight.direction = lightdir #slight.intensity = 20.0 buf = """ #Inventor V2.1 ascii ShadowGroup { quality 1 precision 1 ShadowDirectionalLight { direction """+str(lightdir[0])+" "+str(lightdir[1])+" "+str(lightdir[2])+""" intensity 200.0 # enable this to reduce the shadow view distance # maxShadowDistance 200 } }""" inp = coin.SoInput() inp.setBuffer(buf) sgroup = coin.SoDB.readAll(inp) sgroup.addChild(scene) return sgroup def save(document,filename=None,guidata=None,colors=None,camera=None): """save(document,filename=None,guidata=None,colors=None,camera=None): Saves the current document. If no filename is given, the filename stored in the document (document.FileName) is used. You can provide a guidata dictionary, which can be obtained by the getGuiData() function, and has the form: { "objectName" : { "propertyName" : { "type" : "App::PropertyString", "value" : "My Value" } } } The type of the "value" contents depends on the type (int, string, float,tuple...) see inside the FreeCADGuiHandler class to get an idea. If guidata is provided, colors and camera attributes are discarded. Alternatively, a color dictionary of objName:ShapeColorTuple or obj:DiffuseColorList pairs.can be provided, in that case the objects will keep their colors when opened in the FreeCAD GUI. If given, camera is a string representing a coin camera node.""" if filename: print("Saving as",filename) document.saveAs(filename) else: if document.FileName: filename = document.FileName document.save() else: print("Unable to save this document. Please provide a file name") return if guidata: guidocs = buildGuiDocumentFromGuiData(document,guidata) if guidocs: zf = zipfile.ZipFile(filename, mode='a') for guidoc in guidocs: zf.write(guidoc[0],guidoc[1]) zf.close() # delete the temp files for guidoc in guidocs: os.remove(guidoc[0]) elif colors: guidoc = buildGuiDocumentFromColors(document,colors,camera) if guidoc: zf = zipfile.ZipFile(filename, mode='a') zf.write(guidoc[0],'GuiDocument.xml') for colorfile in guidoc[1:]: zf.write(colorfile,os.path.basename(colorfile)) zf.close() # delete the temp files for tfile in guidoc: os.remove(tfile) def getUnsigned(color): """getUnsigned(color): returns an unsigned int from a (r,g,b) color tuple""" if (color[0] <= 1) and (color[1] <= 1) and (color[2] <= 1): # 0->1 float colors, convert to 0->255 color = (color[0]*255.0,color[1]*255.0,color[2]*255.0) # ensure everything is int otherwise bit ops below don't work color = (int(color[0]),int(color[1]),int(color[2])) # https://forum.freecadweb.org/viewtopic.php?t=19074 return str(color[0] << 24 | color[1] << 16 | color[2] << 8) def buildGuiDocumentFromColors(document,colors,camera=None): """buildGuiDocumentFromColors(document,colors,camera=None): Returns the path to a temporary GuiDocument.xml for the given document. Colors is a color dictionary of objName:ShapeColorTuple or obj:DiffuseColorList. Camera, if given, is a string representing a coin camera. You must delete the temporary file after using it.""" if not camera: camera = "OrthographicCamera { viewportMapping ADJUST_CAMERA position 0 -0 20000 orientation 0.0, 0.8939966636005564, 0.0, -0.44807361612917324 nearDistance 7561.228 farDistance 63175.688 aspectRatio 1 focalDistance 35368.102 height 2883.365 }" guidoc = "\n" guidoc += "\n" guidoc += "\n" colfiles = [] vps = [obj for obj in document.Objects if obj.Name in colors] if not vps: return None guidoc += " \n" for vp in vps: guidoc += " \n" guidoc += " \n" vpcol = colors[vp.Name] if isinstance(vpcol[0],tuple): # distinct diffuse colors colfile = saveDiffuseColor(vpcol) name = os.path.basename(colfile) colfiles.append(colfile) guidoc += " \n" guidoc += " \n" guidoc += " \n" else: guidoc += " \n" guidoc += " \n" guidoc += " \n" guidoc += " \n" guidoc += " \n" guidoc += " \n" if hasattr(vp,"Proxy"): # if this is a python feature, store the view provider class if possible m = getViewProviderClass(vp) if m: guidoc += " \n" guidoc += " \n" guidoc += " \n" guidoc += " \n" guidoc += " \n" guidoc +=" \n" guidoc +=" \n" guidoc += "" # although the zipfile module has a writestr() function that should allow us to write the # string above directly to the zip file, I couldn't manage to make it work.. So we rather # use a temp file here, which works. #print(guidoc) tempxml = tempfile.mkstemp(suffix=".xml")[-1] f = open(tempxml,"w") f.write(guidoc) f.close() return [tempxml]+colfiles def buildGuiDocumentFromGuiData(document,guidata): """buildGuiDocumentFromColors(document,guidata): Returns the path to a temporary GuiDocument.xml for the given document. GuiData is a dictionary, which can be obtained by the getGuiData() function, and has the form: { "objectName" : { "propertyName" : { "type" : "App::PropertyString", "value" : "My Value" } } } This function returns a list of (filepath,name) tuples, the first one named GuiDocument.xml and the next ones being color files """ files = [] colorindex = 1 guidoc = "\n" guidoc += "\n" guidoc += "\n" vps = [obj for obj in document.Objects if obj.Name in guidata] if not vps: return None guidoc += " \n" for vp in vps: properties = guidata[vp.Name] guidoc += " \n" guidoc += " \n" for name,prop in properties.items(): guidoc += " \n" if prop["type"] in ["App::PropertyString","PropertyFont"]: guidoc += " \n" elif prop["type"] in ["App::PropertyAngle","App::PropertyFloat","App::PropertyFloatConstraint","App::PropertyDistance","App::PropertyLength"]: guidoc += " \n" elif prop["type"] in ["App::PropertyInteger","App::PropertyPercent"]: guidoc += " \n" elif prop["type"] in ["App::PropertyBool"]: guidoc += " \n" elif prop["type"] in ["App::PropertyEnumeration"]: if isinstance(prop["value"],int): guidoc += " \n" elif isinstance(prop["value"],list): guidoc += " \n" guidoc += " \n" for v in prop["value"][1:]: guidoc += " \n" guidoc += " \n" elif prop["type"] in ["App::PropertyColorList"]: # DiffuseColor: first 4 bytes of file tells number of Colors # then rest of bytes represent colors value where each color # is of 4 bytes in abgr order. # convert number of colors into hexadecimal representation hex_repr = hex(len(prop["value"]))[2:] # if len of `hex_repr` is odd, then add 0 padding. # `hex_repr` must contain an even number of hex digits # as `binascii.unhexlify` only works of even hex str. if len(hex_repr) % 2 != 0: hex_repr = "0" + hex_repr buf = binascii.unhexlify(hex_repr) if len(hex_repr) > 8: raise NotImplementedError( "Number of colors ({}) is greater than 4 bytes and in DiffuseColor file we only " "specify number of colors in 4 bytes.".format(len(prop["value"])) ) elif len(hex_repr) == 2: # `hex_repr` == 1 byte # fill 3 other bytes (the number of colors occupies 4 bytes) buf += binascii.unhexlify(hex(0)[2:].zfill(2)) buf += binascii.unhexlify(hex(0)[2:].zfill(2)) buf += binascii.unhexlify(hex(0)[2:].zfill(2)) elif len(hex_repr) == 4: # `hex_repr` == 2 bytes buf += binascii.unhexlify(hex(0)[2:].zfill(2)) buf += binascii.unhexlify(hex(0)[2:].zfill(2)) elif len(hex_repr) == 6: # `hex_repr` == 3 bytes buf += binascii.unhexlify(hex(0)[2:].zfill(2)) # fill colors in abgr order for color in prop["value"]: if len(color) >= 4: buf += binascii.unhexlify(hex(int(color[3]*255))[2:].zfill(2)) else: buf += binascii.unhexlify(hex(0)[2:].zfill(2)) buf += binascii.unhexlify(hex(int(color[2]*255))[2:].zfill(2)) buf += binascii.unhexlify(hex(int(color[1]*255))[2:].zfill(2)) buf += binascii.unhexlify(hex(int(color[0]*255))[2:].zfill(2)) tempcolorfile = tempfile.mkstemp(suffix=".xml")[-1] f = open(tempcolorfile,"wb") f.write(buf) f.close() tempcolorname = "ColorFile" + str(colorindex) colorindex += 1 guidoc += " \n" files.append((tempcolorfile,tempcolorname)) elif prop["type"] in ["App::PropertyMaterial"]: guidoc += " \n" elif prop["type"] in ["App::PropertyPythonObject"]: guidoc += " \n" elif prop["type"] in ["App::PropertyColor"]: guidoc += " \n" guidoc += " \n" guidoc += " \n" guidoc += " \n" guidoc +=" \n" if "GuiCameraSettings" in guidata: guidoc +=" \n" guidoc += "\n" # although the zipfile module has a writestr() function that should allow us to write the # string above directly to the zip file, I couldn't manage to make it work.. So we rather # use a temp file here, which works. #print(guidoc) tempxml = tempfile.mkstemp(suffix=".xml")[-1] f = open(tempxml,"w") f.write(guidoc) f.close() files.insert(0,(tempxml,"GuiDocument.xml")) return files def getViewProviderClass(obj): """getViewProviderClass(obj): tries to identify the associated view provider for a given python object. Returns a (modulename,classname) tuple if found, or None""" if not hasattr(obj,"Proxy"): return None if not obj.Proxy: return None mod = obj.Proxy.__module__ objclass = obj.Proxy.__class__.__name__ classes = [] for name, mem in inspect.getmembers(sys.modules[mod]): if inspect.isclass(mem): classes.append(mem.__name__) # try to find a matching ViewProvider class if objclass.startswith("_"): wantedname = "_ViewProvider"+objclass[1:] else: wantedname = "ViewProvider"+objclass if wantedname in classes: #print("Found matching view provider for",mod,objclass,wantedname) return (mod,wantedname,) # use the default Draft VP if this is a Draft object if mod == "Draft": return(mod,"_ViewProviderDraft") print("Found no matching view provider for",mod,objclass) return None def extract(filename,inputpath,outputpath=None): """extract(filename,inputpath,outputpath=None): extracts 'inputpath' which is a filename stored in filename (a FreeCAD or zip file). If outputpath is given, the file is saved as outputpath and nothing is returned. If not, the contents of the inputfile are returned and nothing is saved.""" zdoc = zipfile.ZipFile(filename) if zdoc: if inputpath in zdoc.namelist(): gf = zdoc.open(inputpath) data = gf.read() gf.close() if data: if outputpath: if isinstance(data,str): of = open(outputpath,"w") else: of = open(outputpath,"wb") of.write(data) of.close() else: return data def openiv(filename): """openiv(filename): opens an .iv file and returns a coin node from it""" from pivy import coin f = open(filename,"r") buf = f.read() f.close() inp = coin.SoInput() inp.setBuffer(buf) node = coin.SoDB.readAll(inp) return node def saveiv(scene,filename): """saveiv(scene,filename): saves an .iv file with the contents of the given coin node""" from pivy import coin wa=coin.SoWriteAction() wa.getOutput().openFile(filename) wa.getOutput().setBinary(False) wa.apply(sc) wa.getOutput().closeFile() ## @}