From c32628c61c906dcdd3dead0a41acd04fb3907c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20=C3=89corchard?= Date: Thu, 25 Sep 2025 14:28:05 +0200 Subject: [PATCH] BIM: fix regression in DAE import, support for polylists (#24031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * BIM: fix regression in DAE import, support for polylists The support for non-triangular faces was remove in commit 346c7581d40150e2. Now, the support for them is restored, by triangulating them during the import (with a warning and changing the object name). Signed-off-by: Gaël Écorchard * BIM: Remove translation calls console Use raw strings for console messages, as they are usually not translated. --------- Signed-off-by: Gaël Écorchard Co-authored-by: Gaël Écorchard --- src/Mod/BIM/importers/importDAE.py | 229 +++++++++++++++++++++-------- 1 file changed, 170 insertions(+), 59 deletions(-) diff --git a/src/Mod/BIM/importers/importDAE.py b/src/Mod/BIM/importers/importDAE.py index 8d787d2ed5..92248014f4 100644 --- a/src/Mod/BIM/importers/importDAE.py +++ b/src/Mod/BIM/importers/importDAE.py @@ -32,7 +32,9 @@ __url__ = "https://www.freecad.org" # # This module provides tools to import and export Collada (.dae) files. +from dataclasses import dataclass import os +import string from typing import Optional from xml.sax.saxutils import escape as sax_escape @@ -76,7 +78,7 @@ def import_collada() -> bool: try: import collada except ImportError: - FreeCAD.Console.PrintError(translate("BIM", "pycollada not found, collada support is disabled.") + "\n") + FreeCAD.Console.PrintError("pycollada not found, collada support is disabled.\n") return False return True @@ -148,66 +150,18 @@ def read(filename): for bound_geom in col.scene.objects('geometry'): prim: collada.primitive.BoundPrimitive for prim in bound_geom.primitives(): - if not isinstance(prim, collada.triangleset.BoundTriangleSet): - # e.g. a BoundLineSet, which is not supported yet. - continue - # Get the materials and associated vertices. - meshes: dict[collada.scene.MaterialNode, list] = {} - tri: collada.triangleset.Triangle - for tri in prim: - material_node: collada.material.Material = tri.material - if material_node not in meshes: - # Not yet in the dict, create a new entry. - meshes[material_node] = [] - if len(tri.vertices) != 3: - msg = ( - f"Warning: triangle with {len(tri.vertices)} vertices found" - f" in {bound_geom.original.name}, expected 3. Skipping this triangle." - ) - FreeCAD.Console.PrintWarning(msg + "\n") - continue - # tri.vertices is a numpy array. - meshes[material_node].append((tri.vertices * unit).tolist()) - # Create a mesh for each material node. - for material_node, vertices in meshes.items(): - mesh = Mesh.Mesh(vertices) - name = bound_geom.original.name - if not name: - name = bound_geom.original.id - obj = doc.addObject("Mesh::Feature", name) - obj.Label = name - obj.Mesh = mesh - if not material_node: - continue - if FreeCAD.GuiUp: - fc_mat = FreeCAD.Material() - # We do not import transparency because it is often set - # wrongly (transparency mistaken for opacity). - # TODO: Ask whether to import transparency. - field_map = { - "ambient": "AmbientColor", - "diffuse": "DiffuseColor", - "emission": "EmissiveColor", - "specular": "SpecularColor", - "shininess": "Shininess", - # "transparency": "Transparency", - } - for col_field, fc_field in field_map.items(): - try: - # Implementation note: using floats, so values must - # be within [0, 1]. OK. - setattr(fc_mat, fc_field, getattr(material_node.effect, col_field)) - except ValueError: - # The collada value is not compatible with FreeCAD. - pass - except TypeError: - # color is not a tuple but a texture. - pass - obj.ViewObject.ShapeAppearance = (fc_mat,) + if isinstance(prim, collada.triangleset.BoundTriangleSet): + _read_bound_triangle_set(doc, bound_geom, prim, unit) + if isinstance(prim, collada.polylist.BoundPolylist): + FreeCAD.Console.PrintWarning( + f"Warning: triangulating polylist primitive in {_name_from_bound_geom(bound_geom)}\n", + ) + _read_bound_polylist(doc, bound_geom, prim, unit) + # e.g. a BoundLineSet, which is not supported yet. # Print the errors that occurred during reading. if col.errors: - FreeCAD.Console.PrintWarning(translate("BIM", "File was read but some errors occurred:") + "\n") + FreeCAD.Console.PrintWarning("File was read but some errors occurred:\n") for e in col.errors: FreeCAD.Console.PrintWarning(str(e) + "\n") if FreeCAD.GuiUp: @@ -427,4 +381,161 @@ def export( col_mesh.scenes.append(scene) col_mesh.scene = scene col_mesh.write(filename) - FreeCAD.Console.PrintMessage(translate("BIM", f'file "{filename}" successfully created.' + "\n")) + FreeCAD.Console.PrintMessage(f'file "{filename}" successfully created.\n') + + +@dataclass +class _TriangleSet: + """The result of the processing of BoundTriangleSet and BoundPolylist.""" + + # List of triangles, each triangle is a list of 3 vertices, + # each vertex is a list of 3 floats. + triangles: list[list[float]] + + # The material associated to these triangles, or None. + material: Optional["collada.material.Material"] + + +def _read_bound_triangle_set( + doc: FreeCAD.Document, + bound_geom: "collada.geometry.BoundGeometry", + prim: "collada.primitive.BoundPrimitive", + unit: float, +) -> list[FreeCAD.DocumentObject]: + """ + Read a BoundTriangleSet primitive. + + Return the generated objects. + + Parameters + ---------- + - bound_geom is the BoundGeometry containing the primitive. + - prim is the BoundTriangleSet primitive to read. + - unit is the factor to convert from the Collada unit to mm. + """ + if not isinstance(prim, collada.triangleset.BoundTriangleSet): + raise TypeError("Expected a BoundTriangleSet") + triangle_sets = _get_triangle_sets_from_triangle(prim, unit) + return _add_meshes_to_doc(doc, triangle_sets, _name_from_bound_geom(bound_geom)) + + +def _read_bound_polylist( + doc: FreeCAD.Document, + bound_geom: "collada.geometry.BoundGeometry", + prim: "collada.primitive.BoundPrimitive", + unit: float, +) -> list[FreeCAD.DocumentObject]: + """ + Read a BoundPolylist primitive. + + Return the generated objects. + + Parameters + ---------- + - bound_geom is the BoundGeometry containing the primitive. + - prim is the BoundPolylist primitive to read. + - unit is the factor to convert from the Collada unit to mm. + """ + if not isinstance(prim, collada.polylist.BoundPolylist): + raise TypeError("Expected a BoundPolylist") + triangle_sets = _get_triangle_sets_from_polylist(prim, unit) + return _add_meshes_to_doc(doc, triangle_sets, _name_from_bound_geom(bound_geom) + " (triangulated)") + + +def _get_triangle_sets_from_triangle( + prim: "collada.triangleset.BoundTriangleSet", + unit: float, +) -> list[_TriangleSet]: + """Return triangles from the given BoundTriangleSet.""" + # Get the materials and associated triangles. + material_map: dict[collada.scene.MaterialNode, list] = {} + tri: collada.triangleset.Triangle + for tri in prim: + material_node: "collada.material.Material" = tri.material + if material_node not in material_map: + # Not yet in the dict, create a new entry. + material_map[material_node] = [] + # tri.vertices is a numpy array. + material_map[material_node].append((tri.vertices * unit).tolist()) + return [_TriangleSet(triangles=t, material=m) for m, t in material_map.items()] + + +def _get_triangle_sets_from_polylist( + prim: "collada.triangleset.BoundPolylist", + unit: float, +) -> list[_TriangleSet]: + """Return triangles from the given BoundPolylist.""" + # Get the materials and associated triangles. + material_map: dict[collada.scene.MaterialNode, list] = {} + poly: collada.triangleset.Triangle + for poly in prim: + material_node: "collada.material.Material" = poly.material + if material_node not in material_map: + # Not yet in the dict, create a new entry. + material_map[material_node] = [] + for tri in poly.triangles(): + # tri.vertices is a numpy array. + material_map[material_node].append((tri.vertices * unit).tolist()) + return [_TriangleSet(triangles=t, material=m) for m, t in material_map.items()] + + +def _name_from_bound_geom( + bound_geom: "collada.geometry.BoundGeometry", +) -> str: + """Return `name` or `id` of the original geometry.""" + name = bound_geom.original.name + if not name: + name = bound_geom.original.id + return name + + +def _add_meshes_to_doc( + doc: FreeCAD.Document, + triangle_sets: list[_TriangleSet], + name: str, +) -> list[FreeCAD.DocumentObject]: + """Create a mesh object for each _TriangleSet.""" + objects: list[FreeCAD.DocumentObject] = [] + for triangle_set in triangle_sets: + mesh = Mesh.Mesh(triangle_set.triangles) + obj = doc.addObject("Mesh::Feature", name) + # Geometry name can be an ID, which often finishes with a number. + # In order to prevent FreeCAD from incrementing the number (because + # duplicate labels are not allowed by default) but adding + # its "001", "002", .. suffices, add "_". + suffix = "_" if name and (name[-1] in string.digits) else "" + obj.Label = name + suffix + # Label2 (i.e. Description) can be non-unique. + obj.Label2 = name + obj.Mesh = mesh + objects.append(obj) + if not triangle_set.material: + continue + if not FreeCAD.GuiUp: + # Unfortunately, material can only be treated in GUI mode. + continue + fc_mat = FreeCAD.Material() + # We do not import transparency because it is often set + # wrongly (transparency mistaken for opacity). + # TODO: Ask whether to import transparency. + field_map = { + "ambient": "AmbientColor", + "diffuse": "DiffuseColor", + "emission": "EmissiveColor", + "specular": "SpecularColor", + "shininess": "Shininess", + # "transparency": "Transparency", + } + for col_field, fc_field in field_map.items(): + try: + # Implementation note: using floats, so values must + # be within [0, 1]. OK. + setattr(fc_mat, fc_field, getattr(triangle_set.material.effect, col_field)) + except ValueError: + # The collada value is not compatible with FreeCAD. + pass + except TypeError: + # color is not a tuple but a texture. + pass + obj.ViewObject.ShapeAppearance = (fc_mat,) + return objects