Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A FreeCAD exporter for Blender <= 2.80
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {
"name": "FreeCAD Exporter",
"category": "Import-Export",
"author": "Yorik van Havre",
"version": (1, 0, 0),
"blender": (2, 80, 0),
"location": "File > Export > FreeCAD",
"description": "Exports a FreeCAD FCStd file",
"warning": "This addon needs FreeCAD installed on your system.",
}
# DESCRIPTION
# This script exports the selected objects or the whole model as
# a FreeCAD .FCStd file.
# The development of this addon happens on the FreeCAD forum
# at https://forum.freecadweb.org (no thread yet, please
# create one ;) !)
# WARNING
# This addon requires FreeCAD to be installed on your system.
# A word of warning, your version of FreeCAD must be compiled
# with the same version of python as Blender. The first two
# numbers of the python version must be the same. For example,
# if Blender is using Pyhon 3.7.2, your version of FreeCAD must
# use Python 3.7 too (the third number after 3.7
#
# Once you have a Python3 version of FreeCAD installed, the FreeCAD
# Python module must be known to Blender. There are several 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
import sys, bpy, xml.sax, zipfile, os
import time
#from bpy_extras.node_shader_utils import PrincipledBSDFWrapper
SKIP_REGIONS_MAX = 100 # the max number of faces in a n object for region finding
def export_fcstd(filename,
skiphidden=True,
selected=False,
scale=1.0,
report=None):
"""Creates a FreeCAD .FCStd file"""
# start time
start = time.time()
# import the FreeCAD module
try:
# append the FreeCAD path specified in addon preferences
user_preferences = bpy.context.preferences
addon_prefs = user_preferences.addons[__name__].preferences
path = addon_prefs.filepath
if path:
if os.path.isfile(path):
path = os.path.dirname(path)
print("Configured FreeCAD path:",path)
sys.path.append(path)
else:
print("FreeCAD path is not configured in preferences")
import FreeCAD
except:
print("Unable to import the FreeCAD Python module. Make sure it is installed on your system")
print("and compiled with Python3 (same version as Blender).")
print("It must also be found by Python, you might need to set its path in this Addon preferences")
print("(User preferences->Addons->expand this addon).")
if report:
report({'ERROR'},"Unable to import the FreeCAD Python module. Check Addon preferences.")
return {'CANCELLED'}
# gather a list of objects
scene = bpy.context.scene
objects = [obj for obj in scene.objects if obj.type == 'MESH']
if selected:
objects = [obj for obj in objects if obj.select_get()]
# remove invisible objects if needed
if skiphidden:
objects = [obj for obj in objects if obj.visible_get()]
# init color dict to store face colors
colors = {}
# create FreeCAD document
docname = os.path.splitext(os.path.basename(bpy.data.filepath))[0]
if not docname:
docname = "Unnamed"
doc = FreeCAD.newDocument(docname)
# build shapes
for obj in objects:
mesh = obj.data
name = obj.name
# create regions of connected flat faces with same mat id
regions = []
faces = list(mesh.polygons)
if len(faces) <= SKIP_REGIONS_MAX:
while faces:
if not regions:
regions.append([faces.pop()])
for region in regions:
found = False
for regface in region:
regedges = regface.edge_keys
for face in faces:
for edge in face.edge_keys:
if (edge in regedges) or (edge[::-1] in regedges):
# this face shares an edge with a face already in regions
if face.normal == regface.normal:
if face.material_index == regface.material_index:
# these two faces are part of a same region
region.append(face)
faces.remove(face)
found = True
# modifying a list while looping in it is dangerous, so we better break now
break
if found:
break
else:
# no face found to add to existing regions, starting a new region
regions.append([faces.pop()])
else:
# too many faces... Leave them alone
regions = [[face] for face in faces]
elapsed = time.time() - start
#print("Building object",name,"with",len(regions),"regions,",elapsed,"sec")
import Part, DraftGeomUtils
# build FreeCAD faces from regions
faces = []
for region in regions:
# build list of border edges
edges = {}
for face in region:
for edge in face.edge_keys:
if (edge in edges):
edges[edge] = edges[edge] + 1
elif (edge[::-1] in edges):
edges[edge[::-1]] = edges[edge[::-1]] + 1
else:
edges[edge] = 1
borders = []
for key,val in edges.items():
if val == 1:
borders.append(key)
# build FreeCAD edges
fedges = []
for border in borders:
p1 = FreeCAD.Vector(list(obj.data.vertices[border[0]].co))
p2 = FreeCAD.Vector(list(obj.data.vertices[border[1]].co))
fedges.append(Part.LineSegment(p1,p2).toShape())
# sort by wires
wires = DraftGeomUtils.findWires(fedges)
if wires:
for wire in wires:
if not wire.isClosed():
print("Open wires in",name)
break
else:
# TODO do this better
faces.append(Part.Face(wires))
else:
print("Unable to build border wires for",name)
# upgrade shape to highest possible level
if faces:
try:
shape = Part.Shell(faces)
if shape.isClosed():
try:
shape = Part.Solid(shape)
except Part.OCCError:
pass
except Part.OCCError:
shape = Part.makeCompound(faces)
# add the object to the FreeCAD document
fobj = doc.addObject("Part::Feature",name)
fobj.Shape = shape
mod = obj.rotation_mode
# need to switch otherwise quaternion is not properly set...
obj.rotation_mode = "QUATERNION"
rot = obj.rotation_quaternion
obj.rotation_mode = mod
# FreeCAD Quaternion is XYZW while Blender is WXYZ
rot = FreeCAD.Rotation(rot[1],rot[2],rot[3],rot[0])
loc = obj.location
loc = FreeCAD.Vector(loc[0],loc[1],loc[2])
fobj.Placement = FreeCAD.Placement(loc,rot)
# build color data
coldata = []
for region in regions:
i = region[0].material_index
if i < len(obj.data.materials):
if hasattr(obj.data.materials[i],"diffuse_color"):
coldata.append(tuple(list(obj.data.materials[i].diffuse_color)[:3]))
continue
# no material
coldata.append(tuple(list(obj.color)[:3]))
colors[name] = coldata
#print(name,len(coldata),"colors,",len(shape.Faces),"faces")
# TODO the OfflineRenderingUtils module doesn't support face colors yet.. So for now each object will have only one color
# build OpenInventor camera data
pc = "PerspectiveCamera {&#10; viewportMapping ADJUST_CAMERA&#10; position --pos--&#10; orientation --rot-- 1.0&#10; nearDistance 0.0001&#10; farDistance 1000000&#10; aspectRatio 1&#10; focalDistance 15.0&#10; heightAngle 30.0&#10;&#10;}&#10;"
oc = "OrthographicCamera {&#10; viewportMapping ADJUST_CAMERA&#10; position --pos--&#10; orientation --rot-- 1.0&#10; nearDistance 0.0001&#10; farDistance 1000000&#10; aspectRatio 1&#10; focalDistance 15.0&#10; height 30.0&#10;&#10;}&#10;"
camera = None
# find the first available 3D window
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == "VIEW_3D":
for space in area.spaces:
if space.type == "VIEW_3D":
if space.region_3d.view_perspective == "PERSP":
camera = pc
else:
camera = oc
pos = camera_position(space.region_3d.view_matrix)
pos = str(pos[0])+" "+str(pos[1])+" "+str(pos[2])
camera = camera.replace("--pos--",pos)
rot = space.region_3d.view_rotation
rot = FreeCAD.Rotation(rot[1],rot[2],rot[3],rot[0])
rot = rot.multVec(FreeCAD.Vector(0,0,1))
rot = str(rot[0])+" "+str(rot[1])+" "+str(rot[2])
camera = camera.replace("--rot--",rot)
#print("camera:",camera)
# all done! save the doc with its colors dict
import OfflineRenderingUtils
OfflineRenderingUtils.save(doc,filename,guidata=None,colors=colors,camera=camera)
FreeCAD.closeDocument(docname)
elapsed = time.time() - start
print("Export finished without errors in",elapsed,"sec")
return {'FINISHED'}
def camera_position(matrix):
""" From 4x4 matrix, calculate camera location """
# https://stackoverflow.com/questions/9028398/change-viewport-angle-in-blender-using-python
t = (matrix[0][3], matrix[1][3], matrix[2][3])
r = (
(matrix[0][0], matrix[0][1], matrix[0][2]),
(matrix[1][0], matrix[1][1], matrix[1][2]),
(matrix[2][0], matrix[2][1], matrix[2][2])
)
rp = (
(-r[0][0], -r[1][0], -r[2][0]),
(-r[0][1], -r[1][1], -r[2][1]),
(-r[0][2], -r[1][2], -r[2][2])
)
output = (
rp[0][0] * t[0] + rp[0][1] * t[1] + rp[0][2] * t[2],
rp[1][0] * t[0] + rp[1][1] * t[1] + rp[1][2] * t[2],
rp[2][0] * t[0] + rp[2][1] * t[1] + rp[2][2] * t[2],
)
return output
#==============================================================================
# Blender Operator class
#==============================================================================
class EXPORT_OT_FreeCAD(bpy.types.Operator):
"""Exports the scene to a FreeCAD .FCStd file"""
bl_idname = 'export_fcstd.export_freecad'
bl_label = 'Export FreeCAD FCStd file'
bl_options = {'REGISTER', 'UNDO'}
# ExportHelper mixin class uses this
filename_ext = ".fcstd"
# Properties assigned by the file selection window.
directory : bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN'})
files : bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN'})
filepath : bpy.props.StringProperty(name="File path", maxlen=1024, default="", subtype='FILE_PATH')
filter_folder : bpy.props.BoolProperty(default=True, options={'HIDDEN'})
filter_glob : bpy.props.StringProperty(default="*.FCStd", options={'HIDDEN'})
option_skiphidden : bpy.props.BoolProperty(name="Skip hidden objects", default=True,
description="Only export objects that are visible in Blender"
)
option_selected : bpy.props.BoolProperty(name="Selected only", default=True,
description="Only export selected objects"
)
option_scale : bpy.props.FloatProperty(name="Scaling value", default=1.0,
description="A scaling value to apply to exported objects. Default value of 1 means one Blender unit = 1 millimeter in FreeCAD"
)
# invoke is called when the user picks our Export 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(".blend"):
filestr = filestr.replace(".blend",".FCStd")
if filestr.lower().endswith(".fcstd"):
return export_fcstd(filename=dir+filestr,
skiphidden=self.option_skiphidden,
selected=self.option_selected,
scale=self.option_scale,
report=self.report)
return {'FINISHED'}
class EXPORT_OT_FreeCAD_Preferences(bpy.types.AddonPreferences):
"""A preferences settings 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 must be installed on your system, and its path set below. Make sure both FreeCAD and Blender use the same Python version (check their Python console)")
layout.prop(self, "filepath")
#==============================================================================
# Register plugin with Blender
#==============================================================================
classes = (
EXPORT_OT_FreeCAD,
EXPORT_OT_FreeCAD_Preferences,
)
# needed if you want to add into a dynamic menu
def menu_func_export(self, context):
self.layout.operator(EXPORT_OT_FreeCAD.bl_idname, text="FreeCAD (.FCStd)")
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
if __name__ == "__main__":
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.