From 7da60d20f11e26e4438a9981dc2c1d970161c504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20=C3=89corchard?= Date: Sat, 28 Sep 2024 21:36:34 +0200 Subject: [PATCH 1/5] BIM: Use labels in DAE export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaël Écorchard --- src/Mod/BIM/importers/importDAE.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Mod/BIM/importers/importDAE.py b/src/Mod/BIM/importers/importDAE.py index 6ab0e76da3..0e383b7d04 100644 --- a/src/Mod/BIM/importers/importDAE.py +++ b/src/Mod/BIM/importers/importDAE.py @@ -34,6 +34,7 @@ __url__ = "https://www.freecad.org" import os from typing import Optional +from xml.sax.saxutils import escape as sax_escape import numpy as np @@ -56,6 +57,17 @@ else: DEBUG = True +def xml_escape(text: str, entities: dict[str, str] = None) -> str: + """Escape text for XML. + + This is a wrapper around xml.sax.saxutils.escape that replaces also + `"` with `"` by default. + """ + if entities is None: + entities = {'"': """} + return sax_escape(text, entities=entities) + + def check_collada_import() -> bool: """Return True if the `collada` module is available. @@ -207,12 +219,13 @@ def export( author = FreeCAD.ActiveDocument.CreatedBy except UnicodeEncodeError: author = FreeCAD.ActiveDocument.CreatedBy.encode("utf8") - author = author.replace("<", "") - author = author.replace(">", "") + author = xml_escape(author) col_contributor.author = author ver = FreeCAD.Version() appli = f"FreeCAD v{ver[0]}.{ver[1]} build {ver[2]}" col_contributor.authoring_tool = appli + # Bug in collada from 0.4 to 0.9, contributors are not written to file. + # Set it anyway for future versions. col_mesh.assetInfo.contributors.append(col_contributor) col_mesh.assetInfo.unitname = "meter" col_mesh.assetInfo.unitmeter = 1.0 @@ -269,7 +282,7 @@ def export( geom = collada.geometry.Geometry( collada=col_mesh, id=f"geometry{obj_ind}", - name=obj.Name, + name=xml_escape(obj.Label), sourcebyid=[vert_src, normal_src], ) input_list = collada.source.InputList() From b13ac833c5ab5e0eb0f24d20e2252042172ecfc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20=C3=89corchard?= Date: Thu, 9 Jan 2025 10:39:39 +0100 Subject: [PATCH 2/5] BIM: improve style of importDAE.py --- src/Mod/BIM/importers/importDAE.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/Mod/BIM/importers/importDAE.py b/src/Mod/BIM/importers/importDAE.py index 0e383b7d04..fcb3324769 100644 --- a/src/Mod/BIM/importers/importDAE.py +++ b/src/Mod/BIM/importers/importDAE.py @@ -68,26 +68,23 @@ def xml_escape(text: str, entities: dict[str, str] = None) -> str: return sax_escape(text, entities=entities) -def check_collada_import() -> bool: +def import_collada() -> bool: """Return True if the `collada` module is available. Also imports the module. """ - global collada try: import collada except ImportError: FreeCAD.Console.PrintError(translate("BIM", "pycollada not found, collada support is disabled.") + "\n") return False - else: - return True + return True def triangulate(shape): """Triangulate the given shape.""" - mesher = params.get_param_arch("ColladaMesher") tessellation = params.get_param_arch("ColladaTessellation") grading = params.get_param_arch("ColladaGrading") @@ -114,8 +111,7 @@ def triangulate(shape): def open(filename): """Called when FreeCAD wants to open a file.""" - - if not check_collada_import(): + if not import_collada(): return docname = os.path.splitext(os.path.basename(filename))[0] doc = FreeCAD.newDocument(docname) @@ -127,8 +123,7 @@ def open(filename): def insert(filename, docname): """Called when FreeCAD wants to import a file.""" - - if not check_collada_import(): + if not import_collada(): return try: doc = FreeCAD.getDocument(docname) @@ -141,10 +136,9 @@ def insert(filename, docname): def read(filename): """Read a DAE file.""" - col = collada.Collada(filename, ignore=[collada.common.DaeUnsupportedError]) # Read the unitmeter info from DAE file and compute unit to convert to mm. - unitmeter = col.assetInfo.unitmeter or 1 + unitmeter = col.assetInfo.unitmeter or 1.0 unit = unitmeter / 0.001 # for geom in col.geometries: # for geom in col.scene.objects("geometry"): @@ -203,8 +197,7 @@ def export( mode if you want to be able to export colors. """ - - if not check_collada_import(): + if not import_collada(): return if colors is None: colors = {} @@ -340,7 +333,7 @@ def export( ) col_mesh.effects.append(effect) col_mesh.materials.append(mat) - mat_ref = "ref_" + obj.Name + mat_ref = f"ref_{obj.Name}" mat_node = collada.scene.MaterialNode( symbol=mat_ref, target=mat, From 6ea5891de6db4c46ca02636fc69f47f4cb5ffd9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20=C3=89corchard?= Date: Fri, 20 Jun 2025 13:28:52 +0200 Subject: [PATCH 3/5] BIM: remove unused variable `DEBUG` --- src/Mod/BIM/importers/importDAE.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Mod/BIM/importers/importDAE.py b/src/Mod/BIM/importers/importDAE.py index fcb3324769..9ef0c73e0f 100644 --- a/src/Mod/BIM/importers/importDAE.py +++ b/src/Mod/BIM/importers/importDAE.py @@ -54,9 +54,6 @@ else: return text # \endcond -DEBUG = True - - def xml_escape(text: str, entities: dict[str, str] = None) -> str: """Escape text for XML. From 1c13623a06482e9aca4f5745eacb8e47adc27d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20=C3=89corchard?= Date: Tue, 24 Jun 2025 11:02:08 +0200 Subject: [PATCH 4/5] BIM: fix geometry under node tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I took the file [link_3.dae](https://github.com/ros-industrial/kuka_experimental/blob/514790a553d556f7c58f0f34a4d29a71eca126e7/kuka_kr210_support/meshes/kr210l150/visual/link_3.dae) as test. Before this commit, no geometry is loaded because the childer of `node` are `Node` instances not geometry. To access all the geometries, `col.scene.objects("geometry")` is used, cf. https://pycollada.readthedocs.io/en/latest/structure.html. Signed-off-by: Gaël Écorchard --- src/Mod/BIM/importers/importDAE.py | 96 ++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/src/Mod/BIM/importers/importDAE.py b/src/Mod/BIM/importers/importDAE.py index 9ef0c73e0f..07a16b70ad 100644 --- a/src/Mod/BIM/importers/importDAE.py +++ b/src/Mod/BIM/importers/importDAE.py @@ -133,49 +133,83 @@ def insert(filename, docname): def read(filename): """Read a DAE file.""" + doc = FreeCAD.activeDocument() + if not doc: + return col = collada.Collada(filename, ignore=[collada.common.DaeUnsupportedError]) # Read the unitmeter info from DAE file and compute unit to convert to mm. - unitmeter = col.assetInfo.unitmeter or 1.0 - unit = unitmeter / 0.001 - # for geom in col.geometries: - # for geom in col.scene.objects("geometry"): - for node in col.scene.nodes: - for child in node.children: - if not isinstance(child, collada.scene.GeometryNode): + unit_meter = col.assetInfo.unitmeter or 1.0 + unit = unit_meter / 0.001 + bound_geom: collada.geometry.BoundGeometry + # Implementation note: there's also `col.geometries` but when using them, + # the materials are string and Gaël didn't find a way to get the material + # node from this string. + 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 - 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) + # 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: - 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 + 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: + pass + except TypeError: + # color is not a tuple but a texture. + pass + obj.ViewObject.ShapeAppearance = (fc_mat,) # Print the errors that occurred during reading. if col.errors: FreeCAD.Console.PrintWarning(translate("BIM", "File was read but some errors occurred:") + "\n") for e in col.errors: FreeCAD.Console.PrintWarning(str(e) + "\n") + if FreeCAD.GuiUp: + FreeCAD.Gui.SendMsgToActiveView("ViewFit") def export( From 681be5b49c3799448fddef1a13b9bd74497670fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20=C3=89corchard?= Date: Wed, 25 Jun 2025 07:40:09 +0200 Subject: [PATCH 5/5] BIM: satisfy github-advanced-security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gaël Écorchard --- src/Mod/BIM/importers/importDAE.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mod/BIM/importers/importDAE.py b/src/Mod/BIM/importers/importDAE.py index 07a16b70ad..8d787d2ed5 100644 --- a/src/Mod/BIM/importers/importDAE.py +++ b/src/Mod/BIM/importers/importDAE.py @@ -54,6 +54,7 @@ else: return text # \endcond + def xml_escape(text: str, entities: dict[str, str] = None) -> str: """Escape text for XML. @@ -197,6 +198,7 @@ def read(filename): # 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.