# -*- coding: utf-8 -*- # Original code by (C) 2019 yorikvanhavre # Copyright (C) 2023 Ilia Kurochkin # # 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()