-
-
Save newdigate/c1157cb606c4f6c7134a36f90027541b to your computer and use it in GitHub Desktop.
Blender FreeCAD importer stub
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
bl_info = { | |
"name": "FreeCAD Importer", | |
"category": "Import-Export", | |
"author": "Yorik van Havre", | |
"version": (1, 0, 0), | |
"blender": (2, 79, 0), | |
"location": "File > Import > FreeCAD", | |
"description": "Imports a .FCStd file from FreeCAD", | |
"warning": "You need a version of FreeCAD compiled with the same Python version as Blender. Only Part- and Mesh-based objects are supported at the moment", | |
} | |
# DESCRIPTION | |
# This script imports FreeCAD .FCStd files into Blender. This is a work in | |
# progress, so not all geometry elements of FreeCAD might be suported at | |
# this point. The development of this addon happens on the FreeCAD forum | |
# at https://forum.freecadweb.org/XXXXXXXXXXXXX (not yet!) | |
# WARNING | |
# This addon requires FreeCAD to be installed on your system. | |
# The default versions of FreeCAD that you can download from the FreeCAD | |
# website or from your distribution's repositories is usually compiled | |
# with Python2. However, Blender supporting only Python3, this addon | |
# requires a version of FreeCAD that is compiled with Python3. This might | |
# require you to compile FreeCAD yourself at the moment, or find a kind | |
# soul (maybe on the FreeCAD forum above...) who will be willing to | |
# compile a Python3 version for your system. | |
# | |
# Once you have a Python3 version of FreeCAD installed, the FreeCAD | |
# Python module must be known to Blender. There are three ways to obtain | |
# this: | |
# | |
# 1) Set the correct path to FreeCAD.so (or FreeCAD.pyd on windows) in | |
# the Addons preferences in user settings, there is a setting for | |
# that under the addon panel | |
# | |
# 2) Copy or symlink FreeCAD.so (or FreeCAD.pyd on windows) to one of the | |
# directories from the list you get when doing this in a Python console: | |
# | |
# import sys; print(sys.path) | |
# | |
# On Debian/Ubuntu and most Linux systems, an easy way to do this is is | |
# to symlink FreeCAD.so to your local (user) python modules folder: | |
# | |
# ln -s /path/to/FreeCAD.so /home/YOURUSERNAME/.local/lib/python3.6/site-packages | |
# | |
# (make sure to use the same python version your blender is using instead | |
# of 3.6) | |
# | |
# 3) A more brutal way if the others fail is to uncomment the following line | |
# and set the correct path to where your FreeCAD.so or FreeCAD.pyd resides: | |
# | |
# import sys; sys.path.append("/path/to/FreeCAD.so") | |
# | |
# A simple way to test if everything is OK is to enter the following line | |
# in the Python console of Blender. If no error message appears, | |
# everything is fine: | |
# | |
# import FreeCAD | |
# TODO | |
# support clones + hires | |
# support texts, dimensions, etc (non-part/mesh objects) | |
# support materials | |
# HISTORY | |
# v1.0.0 - 12 june 2018 - initial release - basically working | |
# v2.0.0 - 21 june 2018 - option to turn cycles mat on/off, per-face material support, | |
# use of polygons when possible, shared materials | |
import sys, bpy, xml.sax, zipfile | |
TRIANGULATE = False # set to True to triangulate all faces (will loose multimaterial info) | |
class FreeCADGuiHandler(xml.sax.ContentHandler): | |
"A XML handler to process the FreeCAD GUI xml data" | |
# this creates a dictionary where each key is a FC object name, | |
# and each value is a dictionary of property:value pairs | |
def __init__(self): | |
self.guidata = {} | |
self.current = None | |
self.properties = {} | |
self.currentprop = None | |
self.currentval = None | |
# Call when an element starts | |
def startElement(self, tag, attributes): | |
if tag == "ViewProvider": | |
self.current = attributes["name"] | |
elif tag == "Property": | |
name = attributes["name"] | |
if name in ["Visibility","ShapeColor","Transparency","DiffuseColor"]: | |
self.currentprop = 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 == "Float": | |
self.currentval = float(attributes["value"]) | |
elif tag == "ColorList": | |
self.currentval = attributes["file"] | |
# 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 != None): | |
self.properties[self.currentprop] = self.currentval | |
self.currentprop = None | |
self.currentval = None | |
def import_fcstd(filename,update=True,placement=True,tessellation=1.0,skiphidden=True,scale=1.0,enablenodes=True,sharemats=True): | |
"imports a FreeCAD .FCStd file" | |
try: | |
# add the path specified in addon preferences, any | |
user_preferences = bpy.context.user_preferences | |
addon_prefs = user_preferences.addons[__name__].preferences | |
path = addon_prefs.filepath | |
if path: | |
sys.path.append(path) | |
import FreeCAD | |
except: | |
print("Unable to import the FreeCAD Python module. Make sure it is installed on your system and compiled with Python3.") | |
print("It must also be found by Python, you might need to set its path in this Addon preferences (User preferences->Addons->expand this addon).") | |
return {'CANCELLED'} | |
# check if we have a GUI document | |
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 | |
if "DiffuseColor" in properties: | |
#print ("opening:",guidata[key]["DiffuseColor"]) | |
df = zdoc.open(guidata[key]["DiffuseColor"]) | |
buf = df.read() | |
#print (buf," length ",len(buf)) | |
df.close() | |
cols = [] | |
for i in range(1,int(len(buf)/4)): | |
cols.append((buf[i*4+3],buf[i*4+2],buf[i*4+1],buf[i*4])) | |
guidata[key]["DiffuseColor"] = cols | |
zdoc.close() | |
#print ("guidata:",guidata) | |
doc = FreeCAD.open(filename) | |
docname = doc.Name | |
if not doc: | |
print("Unable to open the given FreeCAD file") | |
return {'CANCELLED'} | |
#print ("Transferring",len(doc.Objects),"objects to Blender") | |
# import some FreeCAD modules needed below. After "import FreeCAD" these modules become available | |
import Part | |
def hascurves(shape): | |
for e in shape.Edges: | |
if not isinstance(e.Curve,(Part.Line,Part.LineSegment)): | |
return True | |
return False | |
matdatabase = {} # to store reusable materials | |
for obj in doc.Objects: | |
#print("Importing",obj.Label) | |
if skiphidden: | |
if obj.Name in guidata: | |
if "Visibility" in guidata[obj.Name]: | |
if guidata[obj.Name]["Visibility"] == False: | |
#print(obj.Label,"is invisible. Skipping.") | |
continue | |
verts = [] | |
edges = [] | |
faces = [] | |
matindex = [] # face to material relationship | |
plac = None | |
faceedges = [] # a placeholder to store edges that belong to a face | |
name = "Unnamed" | |
if obj.isDerivedFrom("Part::Feature"): | |
# create mesh from shape | |
shape = obj.Shape | |
if placement: | |
placement = obj.Placement | |
shape = obj.Shape.copy() | |
shape.Placement = placement.inverse().multiply(shape.Placement) | |
if shape.Faces: | |
if TRIANGULATE: | |
# triangulate and make faces | |
rawdata = shape.tessellate(tessellation) | |
for v in rawdata[0]: | |
verts.append([v.x,v.y,v.z]) | |
for f in rawdata[1]: | |
faces.append(f) | |
for face in shape.Faces: | |
for e in face.Edges: | |
faceedges.append(e.hashCode()) | |
else: | |
# write FreeCAD faces as polygons when possible | |
for face in shape.Faces: | |
if (len(face.Wires) > 1) or (not isinstance(face.Surface,Part.Plane)) or hascurves(face): | |
# face has holes or is curved, so we need to triangulate it | |
rawdata = face.tessellate(tessellation) | |
for v in rawdata[0]: | |
vl = [v.x,v.y,v.z] | |
if not vl in verts: | |
verts.append(vl) | |
for f in rawdata[1]: | |
nf = [] | |
for vi in f: | |
nv = rawdata[0][vi] | |
nf.append(verts.index([nv.x,nv.y,nv.z])) | |
faces.append(nf) | |
matindex.append(len(rawdata[1])) | |
else: | |
f = [] | |
ov = face.OuterWire.OrderedVertexes | |
for v in ov: | |
vl = [v.X,v.Y,v.Z] | |
if not vl in verts: | |
verts.append(vl) | |
f.append(verts.index(vl)) | |
# FreeCAD doesn't care about verts order. Make sure our loop goes clockwise | |
c = face.CenterOfMass | |
v1 = ov[0].Point.sub(c) | |
v2 = ov[1].Point.sub(c) | |
n = face.normalAt(0,0) | |
if (v1.cross(v2)).getAngle(n) > 1.57: | |
f.reverse() # inverting verts order if the direction is couterclockwise | |
faces.append(f) | |
matindex.append(1) | |
for e in face.Edges: | |
faceedges.append(e.hashCode()) | |
for edge in shape.Edges: | |
# Treat remaining edges (that are not in faces) | |
if not (edge.hashCode() in faceedges): | |
if hascurves(edge): | |
dv = edge.discretize(9) #TODO use tessellation value | |
for i in range(len(dv)-1): | |
dv1 = [dv[i].x,dv[i].y,dv[i].z] | |
dv2 = [dv[i+1].x,dv[i+1].y,dv[i+1].z] | |
if not dv1 in verts: | |
verts.append(dv1) | |
if not dv2 in verts: | |
verts.append(dv2) | |
edges.append([verts.index(dv1),verts.index(dv2)]) | |
else: | |
e = [] | |
for vert in edge.Vertexes: | |
# TODO discretize non-linear edges | |
v = [vert.X,vert.Y,vert.Z] | |
if not v in verts: | |
verts.append(v) | |
e.append(verts.index(v)) | |
edges.append(e) | |
elif obj.isDerivedFrom("Mesh::Feature"): | |
# convert freecad mesh to blender mesh | |
mesh = obj.Mesh | |
if placement: | |
placement = obj.Placement | |
mesh = obj.Mesh.copy() # in meshes, this zeroes the placement | |
t = mesh.Topology | |
verts = [[v.x,v.y,v.z] for v in t[0]] | |
faces = t[1] | |
if verts and (faces or edges): | |
# create or update object with mesh and material data | |
bobj = None | |
bmat = None | |
if update: | |
# locate existing object (mesh with same name) | |
for o in bpy.data.objects: | |
if o.data.name == obj.Name: | |
bobj = o | |
print("Replacing existing object:",obj.Label) | |
bmesh = bpy.data.meshes.new(name=obj.Name) | |
bmesh.from_pydata(verts, edges, faces) | |
bmesh.update() | |
if bobj: | |
# update only the mesh of existing object. Don't touch materials | |
bobj.data = bmesh | |
else: | |
# create new object | |
bobj = bpy.data.objects.new(obj.Label, bmesh) | |
if placement: | |
#print ("placement:",placement) | |
bobj.location = placement.Base.multiply(scale) | |
m = bobj.rotation_mode | |
bobj.rotation_mode = 'QUATERNION' | |
if placement.Rotation.Angle: | |
# FreeCAD Quaternion is XYZW while Blender is WXYZ | |
q = (placement.Rotation.Q[3],)+placement.Rotation.Q[:3] | |
bobj.rotation_quaternion = (q) | |
bobj.rotation_mode = m | |
bobj.scale = (scale,scale,scale) | |
if obj.Name in guidata: | |
if matindex and ("DiffuseColor" in guidata[obj.Name]) and (len(matindex) == len(guidata[obj.Name]["DiffuseColor"])): | |
# we have per-face materials. Create new mats and attribute faces to them | |
fi = 0 | |
objmats = [] | |
for i in range(len(matindex)): | |
# DiffuseColor stores int values, Blender use floats | |
rgba = tuple([float(x)/255.0 for x in guidata[obj.Name]["DiffuseColor"][i]]) | |
# FreeCAD stores transparency, not alpha | |
alpha = 1.0 | |
if rgba[3] > 0: | |
alpha = 1.0/rgba[3] | |
rgba = rgba[:3]+(alpha,) | |
bmat = None | |
if sharemats: | |
if rgba in matdatabase: | |
bmat = matdatabase[rgba] | |
if not rgba in objmats: | |
objmats.append(rgba) | |
bobj.data.materials.append(bmat) | |
if not bmat: | |
if rgba in objmats: | |
bmat = bobj.data.materials[objmats.index(rgba)] | |
if not bmat: | |
bmat = bpy.data.materials.new(name=obj.Name+str(len(objmats))) | |
bmat.diffuse_color = rgba[:3] | |
bmat.alpha = alpha | |
if enablenodes: | |
bmat.use_nodes = True | |
else: | |
bmat.use_nodes = False | |
diffshader = bmat.node_tree.nodes.get("Diffuse BSDF") | |
diffshader.inputs['Color'].default_value = rgba | |
# TODO add Cycles node to handle transparency | |
objmats.append(rgba) | |
bobj.data.materials.append(bmat) | |
if sharemats: | |
matdatabase[rgba] = bmat | |
for fj in range(matindex[i]): | |
bobj.data.polygons[fi+fj].material_index = objmats.index(rgba) | |
fi += matindex[i] | |
else: | |
# one material for the whole object | |
alpha = 1.0 | |
rgb = (0.5,0.5,0.5) | |
if "Transparency" in guidata[obj.Name]: | |
if guidata[obj.Name]["Transparency"] > 0: | |
alpha = 1.0/guidata[obj.Name]["Transparency"] | |
if "ShapeColor" in guidata[obj.Name]: | |
rgb = guidata[obj.Name]["ShapeColor"] | |
rgba = rgb+(alpha,) | |
bmat = None | |
if sharemats: | |
if rgba in matdatabase: | |
bmat = matdatabase[rgba] | |
else: | |
print("not found in db:",rgba,"in",matdatabase) | |
if not bmat: | |
bmat = bpy.data.materials.new(name=obj.Name) | |
bmat.diffuse_color = rgb | |
bmat.alpha = alpha | |
if enablenodes: | |
bmat.use_nodes = True | |
else: | |
bmat.use_nodes = False | |
# A default Cycles node is created automatically. Set its color | |
diffshader = bmat.node_tree.nodes.get("Diffuse BSDF") | |
if diffshader: | |
diffshader.inputs['Color'].default_value = rgba | |
# TODO add Cycles node to handle transparency | |
if sharemats: | |
matdatabase[rgba] = bmat | |
bobj.data.materials.append(bmat) | |
bpy.context.scene.objects.link(bobj) | |
#bpy.context.scene.objects.active = obj | |
#obj.select = True | |
FreeCAD.closeDocument(docname) | |
for area in bpy.context.screen.areas: | |
if area.type == 'VIEW_3D': | |
for region in area.regions: | |
if region.type == 'WINDOW': | |
override = {'area': area, 'region': region, 'edit_object': bpy.context.edit_object} | |
bpy.ops.view3d.view_all(override) | |
print("Import finished without errors") | |
return {'FINISHED'} | |
#============================================================================== | |
# Blender Operator class | |
#============================================================================== | |
class IMPORT_OT_FreeCAD(bpy.types.Operator): | |
"""Imports the contents of a FreeCAD .FCStd file""" | |
bl_idname = 'import.freecad' | |
bl_label = 'Import FreeCAD FCStd file' | |
bl_options = {'REGISTER', 'UNDO'} | |
# Properties assigned by the file selection window. | |
directory = bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) | |
files = bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) | |
option_skiphidden = bpy.props.BoolProperty(name="Skip hidden objects", default=True, | |
description="Only import objects that are visible in FreeCAD" | |
) | |
option_update = bpy.props.BoolProperty(name="Update existing objects", default=True, | |
description="Keep objects with same names in current scene and their materials, only replace the geometry" | |
) | |
option_placement = bpy.props.BoolProperty(name="Use Placements", default=True, | |
description="Set Blender pivot points to the FreeCAD placements" | |
) | |
option_tessellation = bpy.props.FloatProperty(name="Tessellation value", default=1.0, | |
description="The tessellation value to apply when triangulating shapes" | |
) | |
option_scale = bpy.props.FloatProperty(name="Scaling value", default=0.001, | |
description="A scaling value to apply to imported objects. Default value of 0.001 means one Blender unit = 1 meter" | |
) | |
option_nodes = bpy.props.BoolProperty(name="Enable Cycles", default=True, | |
description="Enable Cycles materials (nodes)" | |
) | |
option_sharemats = bpy.props.BoolProperty(name="Share similar materials", default=True, | |
description="Objects with same color/transparency will use the same material" | |
) | |
# invoke is called when the user picks our Import menu entry. | |
def invoke(self, context, event): | |
context.window_manager.fileselect_add(self) | |
return {'RUNNING_MODAL'} | |
# execute is called when the user is done using the modal file-select window. | |
def execute(self, context): | |
dir = self.directory | |
for file in self.files: | |
filestr = str(file.name) | |
if filestr.lower().endswith(".fcstd"): | |
return import_fcstd(filename=dir+filestr, | |
update=self.option_update, | |
placement=self.option_placement, | |
tessellation=self.option_tessellation, | |
skiphidden=self.option_skiphidden, | |
scale=self.option_scale, | |
enablenodes=self.option_nodes, | |
sharemats=self.option_sharemats) | |
return {'FINISHED'} | |
class FreeCADImporterPreferences(bpy.types.AddonPreferences): | |
"""A dialog to set the path to the FreeCAD module""" | |
bl_idname = __name__ | |
filepath = bpy.props.StringProperty( | |
name="Path to FreeCAD.so (Mac/Linux) or FreeCAD.pyd (Windows)", | |
subtype='FILE_PATH', | |
) | |
def draw(self, context): | |
layout = self.layout | |
layout.label(text="FreeCAD python module") | |
layout.prop(self, "filepath") | |
class IMPORT_OT_FreeCAD_prefs(bpy.types.Operator): | |
"""Displays addon preferences""" | |
bl_idname = "import.freecad_prefs" | |
bl_label = "FreeCAD importer preferences" | |
bl_options = {'REGISTER', 'UNDO'} | |
def execute(self, context): | |
user_preferences = context.user_preferences | |
addon_prefs = user_preferences.addons[__name__].preferences | |
info = ("Path: %s" % (addon_prefs.filepath)) | |
self.report({'INFO'}, info) | |
print(info) | |
return {'FINISHED'} | |
#============================================================================== | |
# Register plugin with Blender | |
#============================================================================== | |
def import_freecad_button(self, context): | |
self.layout.operator(IMPORT_OT_FreeCAD.bl_idname, | |
text="FreeCAD (.FCStd)") | |
def register(): | |
bpy.utils.register_module(__name__) | |
bpy.types.INFO_MT_file_import.append(import_freecad_button) | |
#bpy.utils.register_class(IMPORT_OT_FreeCAD_prefs) | |
#bpy.utils.register_class(FreeCADImporterPreferences) | |
def unregister(): | |
bpy.utils.unregister_module(__name__) | |
bpy.types.INFO_MT_file_import.remove(import_freecad_button) | |
#bpy.utils.unregister_class(IMPORT_OT_FreeCAD_prefs) | |
#bpy.utils.unregister_class(FreeCADImporterPreferences) | |
if __name__ == '__main__': | |
register() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment