From fe597c19337522831d78e48634671d83fe8023fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20=C3=89corchard?= Date: Fri, 10 Jan 2025 09:15:43 +0100 Subject: [PATCH] BIM: Colors in DAE import + code improvements (#18965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * BIM: Fix color support for DAE import There was some mention of color in the original code but it was not working on my system. Signed-off-by: Gaël Écorchard * BIM: Use `BIM` rather than `Arch` for translation context * BIM: remove Python 2 compatibility * BIM: improve style of importDAE.py Signed-off-by: Gaël Écorchard --------- Signed-off-by: Gaël Écorchard Co-authored-by: Gaël Écorchard --- src/Mod/BIM/importers/importDAE.py | 446 +++++++++++++++++------------ 1 file changed, 261 insertions(+), 185 deletions(-) diff --git a/src/Mod/BIM/importers/importDAE.py b/src/Mod/BIM/importers/importDAE.py index 4c979061fd..d3749559da 100644 --- a/src/Mod/BIM/importers/importDAE.py +++ b/src/Mod/BIM/importers/importDAE.py @@ -19,14 +19,23 @@ #* * #*************************************************************************** -import FreeCAD, Mesh, os, numpy, MeshPart, Arch, Draft +import os +from typing import Optional + +import numpy as np + from draftutils import params +import Arch +import Draft +import FreeCAD +import Mesh +import MeshPart if FreeCAD.GuiUp: from draftutils.translate import translate else: # \cond - def translate(context,text): + def translate(context, text): return text # \endcond @@ -42,54 +51,55 @@ __url__ = "https://www.freecad.org" DEBUG = True -try: - # Python 2 forward compatibility - range = xrange -except NameError: - pass -def checkCollada(): +def check_collada_import() -> bool: + """Return True if the `collada` module is available. - "checks if collada if available" + Also imports the module. + + """ global collada - COLLADA = None try: import collada except ImportError: - FreeCAD.Console.PrintError(translate("Arch","pycollada not found, collada support is disabled.")+"\n") + FreeCAD.Console.PrintError(translate("BIM", "pycollada not found, collada support is disabled.") + "\n") return False else: return True def triangulate(shape): - - "triangulates the given face" + """Triangulate the given shape.""" mesher = params.get_param_arch("ColladaMesher") tessellation = params.get_param_arch("ColladaTessellation") grading = params.get_param_arch("ColladaGrading") - segsperedge = params.get_param_arch("ColladaSegsPerEdge") - segsperradius = params.get_param_arch("ColladaSegsPerRadius") - secondorder = params.get_param_arch("ColladaSecondOrder") + segs_per_edge = params.get_param_arch("ColladaSegsPerEdge") + segs_per_radius = params.get_param_arch("ColladaSegsPerRadius") + second_order = params.get_param_arch("ColladaSecondOrder") optimize = params.get_param_arch("ColladaOptimize") - allowquads = params.get_param_arch("ColladaAllowQuads") + allow_quads = params.get_param_arch("ColladaAllowQuads") if mesher == 0: return shape.tessellate(tessellation) elif mesher == 1: - return MeshPart.meshFromShape(Shape=shape,MaxLength=tessellation).Topology + return MeshPart.meshFromShape(Shape=shape, MaxLength=tessellation).Topology else: - return MeshPart.meshFromShape(Shape=shape,GrowthRate=grading,SegPerEdge=segsperedge, - SegPerRadius=segsperradius,SecondOrder=secondorder,Optimize=optimize, - AllowQuad=allowquads).Topology + return MeshPart.meshFromShape( + Shape=shape, + GrowthRate=grading, + SegPerEdge=segs_per_edge, + SegPerRadius=segs_per_radius, + SecondOrder=second_order, + Optimize=optimize, + AllowQuad=allow_quads, + ).Topology def open(filename): + """Called when FreeCAD wants to open a file.""" - "called when freecad wants to open a file" - - if not checkCollada(): + if not check_collada_import(): return docname = os.path.splitext(os.path.basename(filename))[0] doc = FreeCAD.newDocument(docname) @@ -99,11 +109,10 @@ def open(filename): return doc -def insert(filename,docname): +def insert(filename, docname): + """Called when FreeCAD wants to import a file.""" - "called when freecad wants to import a file" - - if not checkCollada(): + if not check_collada_import(): return try: doc = FreeCAD.getDocument(docname) @@ -115,197 +124,264 @@ def insert(filename,docname): def read(filename): + """Read a DAE file.""" - "reads a DAE file" - - global col col = collada.Collada(filename, ignore=[collada.common.DaeUnsupportedError]) - # Read the unitmeter info from dae file and compute unit to convert to mm + # Read the unitmeter info from DAE file and compute unit to convert to mm. unitmeter = col.assetInfo.unitmeter or 1 unit = unitmeter / 0.001 - #for geom in col.geometries: - #for geom in col.scene.objects('geometry'): + # for geom in col.geometries: + # for geom in col.scene.objects("geometry"): for node in col.scene.nodes: - if list(node.objects("geometry")): - color = None - # retrieving material - if "}" in node.xmlnode.tag: - bt = node.xmlnode.tag.split("}")[0]+"}" - gnode = node.xmlnode.find(bt+"instance_geometry") - if gnode is not None: - bnode = gnode.find(bt+"bind_material") - if bnode is not None: - tnode = bnode.find(bt+"technique_common") - if tnode is not None: - mnode = tnode.find(bt+"instance_material") - if mnode is not None: - if "target" in mnode: - mname = mnode.get("target").strip("#") - for m in col.materials: - if m.id == mname: - e = m.effect - if isinstance(e.diffuse,tuple): - color = e.diffuse - for geom in node.objects("geometry"): - for prim in geom.primitives(): - #print(prim, dir(prim)) - meshdata = [] - if hasattr(prim,"triangles"): - tset = prim.triangles() - elif hasattr(prim,"triangleset"): - tset = prim.triangleset() - else: - tset = [] - for tri in tset: - face = [] - for v in tri.vertices: - v = [x * unit for x in v] - face.append([v[0],v[1],v[2]]) - meshdata.append(face) - #print(meshdata) - newmesh = Mesh.Mesh(meshdata) - #print(newmesh) - obj = FreeCAD.ActiveDocument.addObject("Mesh::Feature","Mesh") - obj.Mesh = newmesh - if color and FreeCAD.GuiUp: + for child in node.children: + if not isinstance(child, collada.scene.GeometryNode): + continue + geom: collada.scenes.GeometryNode = child.geometry + mat_symbols: list[str] = [m.symbol for m in child.materials] + for prim in geom.primitives: + meshdata = [] + for tri in prim: + # tri.vertices is a numpy array. + meshdata.append((tri.vertices * unit).tolist()) + mesh = Mesh.Mesh(meshdata) + try: + name = geom.name + except AttributeError: + name = geom.id + obj = FreeCAD.ActiveDocument.addObject("Mesh::Feature", name) + obj.Label = name + obj.Mesh = mesh + if FreeCAD.GuiUp: + try: + mat_index = mat_symbols.index(prim.material) + material = child.materials[mat_index].target + color = material.effect.diffuse obj.ViewObject.ShapeColor = color + except ValueError: + # Material not found. + pass + except TypeError: + # color is not a tuple but a texture. + pass + + # Print the errors that occurred during reading. + if col.errors: + FreeCAD.Console.PrintWarning(translate("BIM", "File was read but some errors occured:") + "\n") + for e in col.errors: + FreeCAD.Console.PrintWarning(str(e) + "\n") -def export(exportList,filename,tessellation=1,colors=None): +def export( + exports: list[FreeCAD.DocumentObject], + filename: str, + tessellation: int = 1, + colors: Optional[dict[str, tuple]] = None, +): + """Export FreeCAD contents to a DAE file. - """export(exportList,filename,tessellation=1,colors=None) -- exports FreeCAD contents to a DAE file. - colors is an optional dictionary of objName:shapeColorTuple or objName:diffuseColorList elements - to be used in non-GUI mode if you want to be able to export colors. Tessellation is used when breaking - curved surfaces into triangles.""" + Parameters + ---------- + - tessellation is used when breaking curved surfaces into triangles. + - colors is an optional dictionary of {objName: shapeColorTuple} or + {objName: diffuseColorList} elements to be used in non-GUI + mode if you want to be able to export colors. - if not checkCollada(): return + """ + + if not check_collada_import(): + return + if colors is None: + colors = {} scale = params.get_param_arch("ColladaScalingFactor") - scale = scale * 0.001 # from millimeters (FreeCAD) to meters (Collada) - defaultcolor = Draft.get_rgba_tuple(params.get_param_view("DefaultShapeColor"))[:3] - colmesh = collada.Collada() - colmesh.assetInfo.upaxis = collada.asset.UP_AXIS.Z_UP - # authoring info - cont = collada.asset.Contributor() + scale = scale * 0.001 # from millimeters (FreeCAD) to meters (Collada) + default_color = Draft.get_rgba_tuple(params.get_param_view("DefaultShapeColor"))[:3] + col_mesh = collada.Collada() + col_mesh.assetInfo.upaxis = collada.asset.UP_AXIS.Z_UP + # Authoring info. + col_contributor = collada.asset.Contributor() try: author = FreeCAD.ActiveDocument.CreatedBy except UnicodeEncodeError: author = FreeCAD.ActiveDocument.CreatedBy.encode("utf8") - author = author.replace("<","") - author = author.replace(">","") - cont.author = author + author = author.replace("<", "") + author = author.replace(">", "") + col_contributor.author = author ver = FreeCAD.Version() - appli = "FreeCAD v" + ver[0] + "." + ver[1] + " build" + ver[2] + "\n" - cont.authoring_tool = appli - #print(author, appli) - colmesh.assetInfo.contributors.append(cont) - colmesh.assetInfo.unitname = "meter" - colmesh.assetInfo.unitmeter = 1.0 - defaultmat = None - objind = 0 - scenenodes = [] - objectslist = Draft.get_group_contents(exportList, walls=True, - addgroups=True) - objectslist = Arch.pruneIncluded(objectslist, strict=True) - for obj in objectslist: - findex = numpy.array([]) - m = None + appli = f"FreeCAD v{ver[0]}.{ver[1]} build {ver[2]}" + col_contributor.authoring_tool = appli + col_mesh.assetInfo.contributors.append(col_contributor) + col_mesh.assetInfo.unitname = "meter" + col_mesh.assetInfo.unitmeter = 1.0 + default_mat = None + obj_ind = 0 + scene_nodes = [] + objects = Draft.get_group_contents( + exports, + walls=True, + addgroups=True, + ) + objects = Arch.pruneIncluded(objects, strict=True) + for obj in objects: + findex = np.array([]) + m: Optional[Mesh.Mesh] = None if obj.isDerivedFrom("Part::Feature"): - print("exporting object ",obj.Name, obj.Shape) + FreeCAD.Console.PrintMessage(f"Exporting shape of object {obj.Name} (\"{obj.Label}\")" + "\n") new_shape = obj.Shape.copy() new_shape.Placement = obj.getGlobalPlacement() m = Mesh.Mesh(triangulate(new_shape)) elif obj.isDerivedFrom("Mesh::Feature"): - print("exporting object ",obj.Name, obj.Mesh) + FreeCAD.Console.PrintMessage(f"Exporting mesh of object {obj.Name} (\"{obj.Label}\")" + "\n") m = obj.Mesh elif obj.isDerivedFrom("App::Part"): for child in obj.OutList: - objectslist.append(child) + objects.append(child) continue else: continue if m: - Topology = m.Topology - Facets = m.Facets + topology = m.Topology + facets = m.Facets - # vertex indices - vindex = numpy.empty(len(Topology[0]) * 3) - for i in range(len(Topology[0])): - v = Topology[0][i] - vindex[list(range(i*3, i*3+3))] = (v.x*scale,v.y*scale,v.z*scale) + # Vertex indices. + vindex = np.empty(len(topology[0]) * 3) + for i in range(len(topology[0])): + v = topology[0][i] + vindex[list(range(i*3, i*3+3))] = (v.x*scale, v.y*scale, v.z*scale) - # normals - nindex = numpy.empty(len(Facets) * 3) - for i in range(len(Facets)): - n = Facets[i].Normal + # Normals. + nindex = np.empty(len(facets) * 3) + for i in range(len(facets)): + n = facets[i].Normal nindex[list(range(i*3, i*3+3))] = (n.x,n.y,n.z) - # face indices - findex = numpy.empty(len(Topology[1]) * 6, numpy.int64) - for i in range(len(Topology[1])): - f = Topology[1][i] - findex[list(range(i*6, i*6+6))] = (f[0],i,f[1],i,f[2],i) + # Face indices. + findex = np.empty(len(topology[1]) * 6, np.int64) + for i in range(len(topology[1])): + f = topology[1][i] + findex[list(range(i*6, i*6+6))] = (f[0], i, f[1], i, f[2], i) - print(len(vindex), " vert indices, ", len(nindex), " norm indices, ", len(findex), " face indices.") - vert_src = collada.source.FloatSource("cubeverts-array"+str(objind), vindex, ('X', 'Y', 'Z')) - normal_src = collada.source.FloatSource("cubenormals-array"+str(objind), nindex, ('X', 'Y', 'Z')) - geom = collada.geometry.Geometry(colmesh, "geometry"+str(objind), obj.Name, [vert_src, normal_src]) + vert_src = collada.source.FloatSource(f"cubeverts-array{obj_ind}", vindex, ("X", "Y", "Z")) + normal_src = collada.source.FloatSource(f"cubenormals-array{obj_ind}", nindex, ("X", "Y", "Z")) + geom = collada.geometry.Geometry( + collada=col_mesh, + id=f"geometry{obj_ind}", + name=obj.Name, + sourcebyid=[vert_src, normal_src], + ) input_list = collada.source.InputList() - input_list.addInput(0, 'VERTEX', "#cubeverts-array"+str(objind)) - input_list.addInput(1, 'NORMAL', "#cubenormals-array"+str(objind)) - matnode = None - matref = "materialref" - if hasattr(obj,"Material"): - if obj.Material: - if hasattr(obj.Material,"Material"): - if "DiffuseColor" in obj.Material.Material: - kd = tuple([float(k) for k in obj.Material.Material["DiffuseColor"].strip("()").split(",")]) - effect = collada.material.Effect("effect_"+obj.Material.Name, [], "phong", diffuse=kd, specular=(1,1,1)) - mat = collada.material.Material("mat_"+obj.Material.Name, obj.Material.Name, effect) - colmesh.effects.append(effect) - colmesh.materials.append(mat) - matref = "ref_"+obj.Material.Name - matnode = collada.scene.MaterialNode(matref, mat, inputs=[]) - if not matnode: - if colors: - if obj.Name in colors: - color = colors[obj.Name] - if color: - if isinstance(color[0],tuple): - # this is a diffusecolor. For now, use the first color - #TODO: Support per-face colors - color = color[0] - #print("found color for obj",obj.Name,":",color) - kd = color[:3] - effect = collada.material.Effect("effect_"+obj.Name, [], "phong", diffuse=kd, specular=(1,1,1)) - mat = collada.material.Material("mat_"+obj.Name, obj.Name, effect) - colmesh.effects.append(effect) - colmesh.materials.append(mat) - matref = "ref_"+obj.Name - matnode = collada.scene.MaterialNode(matref, mat, inputs=[]) + input_list.addInput(0, "VERTEX", f"#cubeverts-array{obj_ind}") + input_list.addInput(1, "NORMAL", f"#cubenormals-array{obj_ind}") + mat_node: Optional[collada.scene.MaterialNode] = None + mat_ref = "materialref" + if ( + hasattr(obj, "Material") + and obj.Material + and hasattr(obj.Material, "Material") + and ("DiffuseColor" in obj.Material.Material) + ): + kd = tuple([float(k) for k in obj.Material.Material["DiffuseColor"].strip("()").split(",")]) + effect = collada.material.Effect( + id=f"effect_{obj.Material.Name}", + params=[], + shadingtype="phong", + diffuse=kd, + specular=(1, 1, 1), + ) + mat = collada.material.Material( + id=f"mat_{obj.Material.Name}", + name=obj.Material.Name, + effect=effect, + ) + col_mesh.effects.append(effect) + col_mesh.materials.append(mat) + mat_ref = f"ref_{obj.Material.Name}" + mat_node = collada.scene.MaterialNode( + symbol=mat_ref, + target=mat, + inputs=[], + ) + if not mat_node: + if obj.Name in colors: + color = colors[obj.Name] + if color: + if isinstance(color[0], tuple): + # This is a diffusecolor. For now, use the first color. + # TODO: Support per-face colors + color = color[0] + kd = color[:3] + effect = collada.material.Effect( + id=f"effect_{obj.Name}", + params=[], + shadingtype="phong", + diffuse=kd, + specular=(1, 1, 1), + ) + mat = collada.material.Material( + id=f"mat_{obj.Name}", + name=obj.Name, + effect=effect, + ) + col_mesh.effects.append(effect) + col_mesh.materials.append(mat) + mat_ref = "ref_" + obj.Name + mat_node = collada.scene.MaterialNode( + symbol=mat_ref, + target=mat, + inputs=[], + ) elif FreeCAD.GuiUp: - if hasattr(obj.ViewObject,"ShapeColor"): + if hasattr(obj.ViewObject, "ShapeColor"): kd = obj.ViewObject.ShapeColor[:3] - effect = collada.material.Effect("effect_"+obj.Name, [], "phong", diffuse=kd, specular=(1,1,1)) - mat = collada.material.Material("mat_"+obj.Name, obj.Name, effect) - colmesh.effects.append(effect) - colmesh.materials.append(mat) - matref = "ref_"+obj.Name - matnode = collada.scene.MaterialNode(matref, mat, inputs=[]) - if not matnode: - if not defaultmat: - effect = collada.material.Effect("effect_default", [], "phong", diffuse=defaultcolor, specular=(1,1,1)) - defaultmat = collada.material.Material("mat_default", "default_material", effect) - colmesh.effects.append(effect) - colmesh.materials.append(defaultmat) - matnode = collada.scene.MaterialNode(matref, defaultmat, inputs=[]) - triset = geom.createTriangleSet(findex, input_list, matref) + effect = collada.material.Effect( + id=f"effect_{obj.Name}", + params=[], + shadingtype="phong", + diffuse=kd, + specular=(1, 1, 1), + ) + mat = collada.material.Material( + id=f"mat_{obj.Name}", + name=obj.Name, + effect=effect, + ) + col_mesh.effects.append(effect) + col_mesh.materials.append(mat) + mat_ref = f"ref_{obj.Name}" + mat_node = collada.scene.MaterialNode( + symbol=mat_ref, + target=mat, + inputs=[], + ) + if not mat_node: + if not default_mat: + effect = collada.material.Effect( + id="effect_default", + params=[], + shadingtype="phong", + diffuse=default_color, + specular=(1, 1, 1), + ) + default_mat = collada.material.Material( + id="mat_default", + name="default_material", + effect=effect, + ) + col_mesh.effects.append(effect) + col_mesh.materials.append(default_mat) + mat_node = collada.scene.MaterialNode( + symbol=mat_ref, + target=default_mat, + inputs=[], + ) + triset = geom.createTriangleSet(indices=findex, inputlist=input_list, materialid=mat_ref) geom.primitives.append(triset) - colmesh.geometries.append(geom) - geomnode = collada.scene.GeometryNode(geom, [matnode]) - node = collada.scene.Node("node"+str(objind), children=[geomnode]) - scenenodes.append(node) - objind += 1 - myscene = collada.scene.Scene("myscene", scenenodes) - colmesh.scenes.append(myscene) - colmesh.scene = myscene - colmesh.write(filename) - FreeCAD.Console.PrintMessage(translate("Arch","file %s successfully created.") % filename + "\n") + col_mesh.geometries.append(geom) + geom_node = collada.scene.GeometryNode(geom, [mat_node]) + node = collada.scene.Node(id=f"node{obj_ind}", children=[geom_node]) + scene_nodes.append(node) + obj_ind += 1 + scene = collada.scene.Scene("scene", scene_nodes) + col_mesh.scenes.append(scene) + col_mesh.scene = scene + col_mesh.write(filename) + FreeCAD.Console.PrintMessage(translate("BIM", f'file "{filename}" successfully created.' + "\n"))