BIM: Colors in DAE import + code improvements (#18965)

* 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 <gael@km-robotics.cz>

* 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 <gael@km-robotics.cz>

---------

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-01-10 09:15:43 +01:00
committed by GitHub
parent 558eb19103
commit fe597c1933

View File

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