Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Blender FreeCAD importer stub
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": "",
}
# 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
# 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 two ways to obtain
# this: 1) Copy 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)
# 2) A second way 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 a Python console. If no error message appears, all is fine:
#
# import FreeCAD
# TODO
# implement options
# replace existing
# tessellation value
# apply placement
# support clones
# support texts, dimensions, etc?
import sys, bpy, xml.sax, zipfile
class FreeCADGuiHandler(xml.sax.ContentHandler):
"A XML handler to process the FreeCAD GUI xml data"
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"]:
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"])
# 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):
"imports a FreeCAD .FCStd file"
try:
# First, try system-wide location
import FreeCAD
except:
try:
sys.path.append("/home/yorik/Sources/build/fcpy3/lib")
# TODO allow to set the correct path in Blender
import FreeCAD
except:
print("Unable to import FreeCAD. Make sure it is installed on your system and compiled with Python3.")
print("It must also be known to Python, you might need to set the path manually in this script.")
return
# 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
zdoc.close()
doc = FreeCAD.open(filename)
if not doc:
print("Unable to open the given FreeCAD file")
return
for obj in doc.Objects:
if skiphidden:
if obj.Name in guidata:
if "Visibility" in guidata[obj.Name]:
if guidata[obj.Name]["Visibility"] == False:
continue
verts = []
edges = []
faces = []
plac = None
faceedges = [] # a placeholder to store edges that belong to a face
name = "Unnamed"
if obj.isDerivedFrom("Part::Feature"):
shape = obj.Shape
if placement:
placement = obj.Placement
shape = obj.Shape
if shape.Faces:
# First triangulate and make faces (edges are created automatically)
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())
for edge in shape.Edges:
# Treat remaining edges (that are not in faces)
if not (edge.hashCode() in faceedges):
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"):
if placement:
placement = obj.Placement
for face in obj.Mesh.Facets:
f = []
for v in f.Points:
if not v in verts:
verts.append(v)
f.append(verts.index(v))
faces.append(f)
if verts and (faces or edges):
# create object
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
bmesh = bpy.data.meshes.new(name=obj.Name)
bmesh.from_pydata(verts, edges, faces)
bmesh.update()
if bobj:
bmat = bobj.data.materials
bobj.data = bmesh
else:
bobj = bpy.data.objects.new(obj.Label, bmesh)
if placement:
bobj.location = placement.Base
bobj.rotation = placement.Rotation.Q
if not bmat:
if obj.Name in guidata:
bmat = bpy.data.materials.new(name=obj.Name)
if "ShapeColor" in guidata[obj.Name]:
bmat.diffuse_color = guidata[obj.Name]["ShapeColor"]
bmat.use_nodes = True
# A default Cycles node is created automatically. Set its color
diffshader = bmat.node_tree.nodes.get("Diffuse BSDF")
diffshader.inputs['Color'].default_value = guidata[obj.Name]["ShapeColor"]
if "Transparency" in guidata[obj.Name]:
bmat.alpha = 1.0/guidata[obj.Name]["Transparency"]
# TODO add Cycles node
if bmat:
bobj.data.materials = bmat
bpy.context.scene.objects.link(obj)
#bpy.context.scene.objects.active = obj
#obj.select = True
#==============================================================================
# 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"
)
# 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.endswith(".fcstd"):
import_fcstd(filename=dir+filestr,
update=self.option_update,
placement=self.option_placement,
tessellation=self.option_tessellation,
skiphidden=self.option_skiphidden)
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="Path to FreeCAD.so (Mac/Linux) or FreeCAD.pyd (Windows)")
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