915 lines
35 KiB
Python
915 lines
35 KiB
Python
# -*- coding: utf-8 -*-
|
|
#***************************************************************************
|
|
#* Copyright (c) 2019 Yorik van Havre <yorik@uncreated.net> *
|
|
#* *
|
|
#* 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
|
|
|
|
from importers import importOBJ
|
|
importOBJ.export(doc.Objects,baseFileName+".obj",colors=colors)
|
|
|
|
# export to DAE
|
|
|
|
from importers import importDAE
|
|
importDAE.export(doc.Objects,baseFileName+".dae",colors=colors)
|
|
|
|
# export to IFC
|
|
|
|
from importers 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 ShapeAppearaance, 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.freecad.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 ShapeAppeaaraance. 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("unable 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 quarter
|
|
from PySide2 import QtWidgets
|
|
|
|
app = QtWidgets.QApplication([])
|
|
# Create a Qt widget, which will be our window.
|
|
win = quarter.QuarterWidget()
|
|
|
|
if win is None:
|
|
print("Unable to create a Quarter 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()
|
|
|
|
# Put our scene into viewer, change the title
|
|
win.setSceneGraph(scene)
|
|
win.setWindowTitle("Coin viewer")
|
|
|
|
# IMPORTANT!!!!! Windows are hidden by default.
|
|
win.show()
|
|
# Start the event loop.
|
|
app.exec_()
|
|
|
|
|
|
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.freecad.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 = "<?xml version='1.0' encoding='utf-8'?>\n"
|
|
guidoc += "<!--\n"
|
|
guidoc += " FreeCAD Document, see https://www.freecad.org for more information...\n"
|
|
guidoc += "-->\n"
|
|
guidoc += "<Document SchemaVersion=\"1\">\n"
|
|
|
|
colfiles = []
|
|
|
|
vps = [obj for obj in document.Objects if obj.Name in colors]
|
|
if not vps:
|
|
return None
|
|
guidoc += " <ViewProviderData Count=\""+str(len(vps))+"\">\n"
|
|
for vp in vps:
|
|
guidoc += " <ViewProvider name=\""+vp.Name+"\" expanded=\"0\">\n"
|
|
guidoc += " <Properties Count=\"2\">\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 += " <Property name=\"DiffuseColor\" type=\"App::PropertyColorList\">\n"
|
|
guidoc += " <ColorList file=\""+name+"\"/>\n"
|
|
guidoc += " </Property>\n"
|
|
else:
|
|
guidoc += " <Property name=\"ShapeColor\" type=\"App::PropertyColor\">\n"
|
|
guidoc += " <PropertyColor value=\""+getUnsigned(vpcol)+"\"/>\n"
|
|
guidoc += " </Property>\n"
|
|
guidoc += " <Property name=\"Visibility\" type=\"App::PropertyBool\">\n"
|
|
guidoc += " <Bool value=\"true\"/>\n"
|
|
guidoc += " </Property>\n"
|
|
if hasattr(vp,"Proxy"):
|
|
# if this is a python feature, store the view provider class if possible
|
|
m = getViewProviderClass(vp)
|
|
if m:
|
|
guidoc += " <Property name=\"Proxy\" type=\"App::PropertyPythonObject\">\n"
|
|
guidoc += " <Python value=\"bnVsbA==\" encoded=\"yes\" module=\""+m[0]+"\" class=\""+m[1]+"\"/>\n"
|
|
guidoc += " </Property>\n"
|
|
guidoc += " </Properties>\n"
|
|
guidoc += " </ViewProvider>\n"
|
|
|
|
guidoc +=" </ViewProviderData>\n"
|
|
guidoc +=" <Camera settings=\" " + camera + " \"/>\n"
|
|
guidoc += "</Document>"
|
|
|
|
# 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 = "<?xml version='1.0' encoding='utf-8'?>\n"
|
|
guidoc += "<!--\n"
|
|
guidoc += " FreeCAD Document, see https://www.freecad.org for more information...\n"
|
|
guidoc += "-->\n"
|
|
guidoc += "<Document SchemaVersion=\"1\">\n"
|
|
|
|
vps = [obj for obj in document.Objects if obj.Name in guidata]
|
|
if not vps:
|
|
return None
|
|
guidoc += " <ViewProviderData Count=\""+str(len(vps))+"\">\n"
|
|
for vp in vps:
|
|
properties = guidata[vp.Name]
|
|
guidoc += " <ViewProvider name=\""+vp.Name+"\" expanded=\"0\">\n"
|
|
guidoc += " <Properties Count=\""+str(len(properties))+"\">\n"
|
|
for name,prop in properties.items():
|
|
guidoc += " <Property name=\""+name+"\" type=\""+prop["type"]+"\">\n"
|
|
if prop["type"] in ["App::PropertyString","PropertyFont"]:
|
|
guidoc += " <String value=\""+prop["value"]+"\"/>\n"
|
|
elif prop["type"] in ["App::PropertyAngle","App::PropertyFloat","App::PropertyFloatConstraint","App::PropertyDistance","App::PropertyLength"]:
|
|
guidoc += " <Float value=\""+str(prop["value"])+"\"/>\n"
|
|
elif prop["type"] in ["App::PropertyInteger","App::PropertyPercent"]:
|
|
guidoc += " <Integer value=\""+str(prop["value"])+"\"/>\n"
|
|
elif prop["type"] in ["App::PropertyBool"]:
|
|
guidoc += " <Bool value=\""+str(prop["value"]).lower()+"\"/>\n"
|
|
elif prop["type"] in ["App::PropertyEnumeration"]:
|
|
if isinstance(prop["value"],int):
|
|
guidoc += " <Integer value=\""+str(prop["value"])+"\"/>\n"
|
|
elif isinstance(prop["value"],list):
|
|
guidoc += " <Integer value=\""+str(prop["value"][0])+"\" CustomEnum=\"true\"/>\n"
|
|
guidoc += " <CustomEnumList count=\""+str(len(prop["value"])-1)+"\">\n"
|
|
for v in prop["value"][1:]:
|
|
guidoc += " <Enum value=\""+v+"\"/>\n"
|
|
guidoc += " </CustomEnumList>\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 += " <ColorList file=\""+tempcolorname+"\"/>\n"
|
|
files.append((tempcolorfile,tempcolorname))
|
|
elif prop["type"] in ["App::PropertyMaterial"]:
|
|
guidoc += " <PropertyMaterial ambientColor=\""+str(prop["value"][0])
|
|
guidoc += "\" diffuseColor=\""+str(prop["value"][1])+"\" specularColor=\""+str(prop["value"][2])
|
|
guidoc += "\" emissiveColor=\""+str(prop["value"][3])+"\" shininess=\""+str(prop["value"][4])
|
|
guidoc += "\" transparency=\""+str(prop["value"][5])+"\"/>\n"
|
|
elif prop["type"] in ["App::PropertyPythonObject"]:
|
|
guidoc += " <Python value=\""+str(prop["value"][0])+"\" encoded=\""
|
|
guidoc += str(prop["value"][1])+"\" module=\""+str(prop["value"][2])+"\" class=\""
|
|
guidoc += str(prop["value"][3])+"\"/>\n"
|
|
elif prop["type"] in ["App::PropertyColor"]:
|
|
guidoc += " <PropertyColor value=\""+str(getUnsigned(prop["value"]))+"\"/>\n"
|
|
guidoc += " </Property>\n"
|
|
guidoc += " </Properties>\n"
|
|
guidoc += " </ViewProvider>\n"
|
|
guidoc +=" </ViewProviderData>\n"
|
|
if "GuiCameraSettings" in guidata:
|
|
guidoc +=" <Camera settings=\" " + guidata["GuiCameraSettings"] + " \"/>\n"
|
|
guidoc += "</Document>\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()
|
|
|
|
|
|
|
|
## @}
|