269 lines
11 KiB
Python
269 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()
|