BIM: fix regression in DAE import, support for polylists (#24031)

* BIM: fix regression in DAE import, support for polylists

The support for non-triangular faces was remove in
commit 346c7581d4.
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 <gael@km-robotics.cz>

* BIM: Remove translation calls console

Use raw strings for console messages, as they are usually not
translated.

---------

Signed-off-by: Gaël Écorchard <gael@km-robotics.cz>
Co-authored-by: Gaël Écorchard <gael@km-robotics.cz>
This commit is contained in:
Gaël Écorchard
2025-09-25 14:28:05 +02:00
committed by GitHub
parent e4881dd92f
commit c32628c61c

View File

@@ -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