framework/cg/blender/import_fcstd/importer.py

268 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# Original code by (C) 2019 yorikvanhavre <yorik@uncreated.net>
# Copyright (C) 2023 Ilia Kurochkin <brothermechanic@gmail.com>
#
# 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 3 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.
import FreeCAD
import logging
import xml
import sys
import xml.sax
import zipfile
import os
import bpy
from bpy_extras.node_shader_utils import PrincipledBSDFWrapper
from import_fcstd.handler import FreeCAD_xml_handler
from import_fcstd.materials import set_fem_mat
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
def importer(filename,
update=False,
placement=True,
tessellation=10.0,
skiphidden=True,
scale=0.001,
select=True,
report=None):
"""Reads a FreeCAD .FCStd file and creates Blender objects"""
#path = '/usr/lib64/freecad/lib64'
TRIANGULATE = False # set to True to triangulate all faces (will loose multimaterial info)
'''
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)
logger.debug("Configured FreeCAD path:", path)
sys.path.append(path)
else:
logger.debug("FreeCAD path is not configured in preferences")
import FreeCAD
except:
logger.debug("Unable to import the FreeCAD Python module. Make sure it is installed on your system")
logger.debug("and compiled with Python3 (same version as Blender).")
logger.debug("It must also be found by Python, you might need to set its path in this Addon preferences")
logger.debug("(User preferences->Addons->expand this addon).")
if report:
report({'ERROR'},"Unable to import the FreeCAD Python module. Check Addon preferences.")
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 = FreeCAD_xml_handler()
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:
#logger.debug ("opening:",guidata[key]["DiffuseColor"])
df = zdoc.open(guidata[key]["DiffuseColor"])
buf = df.read()
#logger.debug (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()
#logger.debug ("guidata:",guidata)
doc = FreeCAD.open(filename)
docname = doc.Name
if not doc:
logger.debug("Unable to open the given FreeCAD file")
if report:
report({'ERROR'},"Unable to open the given FreeCAD file")
return {'CANCELLED'}
#logger.debug ("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
fcstd_collection = bpy.data.collections.new("FreeCAD import")
bpy.context.scene.collection.children.link(fcstd_collection)
for obj in doc.Objects:
# logger.debug("Importing",obj.Label)
if skiphidden:
if obj.Name in guidata:
if "Visibility" in guidata[obj.Name]:
if guidata[obj.Name]["Visibility"] == False:
# logger.debug(obj.Label,"is invisible. Skipping.")
# TODO add parent visibility check
continue
verts = []
edges = []
faces = []
matindex = [] # face to material relationship
faceedges = [] # a placeholder to store edges that belong to a face
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
logger.debug("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:
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:
# one material for the whole object
for fem_mat in doc.Objects:
set_fem_mat(obj, bobj, fem_mat)
fcstd_collection.objects.link(bobj)
if select:
bpy.context.view_layer.objects.active = bobj
bobj.select_set(True)
FreeCAD.closeDocument(docname)
# TODO
# update do not dork
logger.info("Import freecad scene finished without errors")
return {'FINISHED'}
if __name__ == '__main__':
importer()