From bccb3632aed8ff98fcbfe47fbf04e6198c67f983 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Fri, 23 Jan 2026 12:09:03 -0600 Subject: [PATCH] collada: incomplete collada file parser --- .gitignore | 2 + Makefile | 1 + collada/collada_types.py | 435 ++++++++++++++++++++++ collada/parse.py | 770 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 1208 insertions(+) create mode 100644 collada/collada_types.py create mode 100644 collada/parse.py diff --git a/.gitignore b/.gitignore index 3f60c72..68a0cdc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ *.obj *.res *.aps +__pycache__ +*.pyc \ No newline at end of file diff --git a/Makefile b/Makefile index 7e99faa..219b9fd 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ CFLAGS += -Wno-unknown-pragmas CFLAGS += -I./include CFLAGS += -municode LDFLAGS += -municode +LDFLAGS += -static-libgcc CXXFLAGS += -fno-exceptions diff --git a/collada/collada_types.py b/collada/collada_types.py new file mode 100644 index 0000000..59b54df --- /dev/null +++ b/collada/collada_types.py @@ -0,0 +1,435 @@ +from dataclasses import dataclass, field +from enum import Enum, auto +from typing import Optional, Union, List, Tuple + +URI = str +ID = str + +Float3 = Tuple[float, float, float] +Float4 = Tuple[float, float, float, float] +Float7 = Tuple[float, float, float, float, float, float, float] + +@dataclass +class Lookat: + sid: Optional[str] + eye: Float3 + at: Float3 + up: Float3 + +@dataclass +class Matrix: + # collada is the transpose of a directx matrix + # 1.0 0.0 0.0 0.0 + # 0.0 1.0 0.0 0.0 + # 0.0 0.0 1.0 0.0 + # 2.0 3.0 4.0 1.0 + sid: Optional[str] + values: Tuple[Float4, Float4, Float4, Float4] + +@dataclass +class Rotate: + # x y z w ; quaternion + sid: Optional[str] + rotate: Float4 + +@dataclass +class Scale: + sid: Optional[str] + scale: Float3 + +@dataclass +class Skew: + sid: Optional[str] + skew: Float7 + +@dataclass +class Translate: + sid: Optional[str] + translate: Float3 + +@dataclass +class Param: + name: Optional[str] + sid: Optional[str] + type: str + semantic: Optional[str] + +@dataclass +class Accessor: + count: int + offset: Optional[int] + source: URI + stride: Optional[int] + + params: List[Param] + + sid_lookup: dict = field(repr=False) + +@dataclass +class TechniqueCommon_SourceCore: + accessor: Accessor + +@dataclass +class Ambient: + color: Float3 + +@dataclass +class Directional: + color: Float3 + +@dataclass +class Point: + color: Float3 + +@dataclass +class Spot: + color: Float3 + +@dataclass +class BindVertexInput: + semantic: str + input_semantic: str + input_set: int + +@dataclass +class InstanceMaterial: + sid: Optional[str] + name: Optional[str] + target: URI + symbol: str + + bind_vertex_inputs: List[BindVertexInput] + +@dataclass +class TechniqueCommon_BindMaterial: + materials: List[InstanceMaterial] # one or more + + sid_lookup: dict = field(repr=False) + +@dataclass +class BindMaterial: + technique_common: TechniqueCommon_BindMaterial + +@dataclass +class InstanceGeometry: + sid: Optional[str] + name: Optional[str] + url: URI + + # child element + bind_material: Optional[BindMaterial] + +@dataclass +class InstanceLight: + sid: Optional[str] + name: Optional[str] + url: URI + +@dataclass +class InstanceVisualScene: + sid: Optional[str] + name: Optional[str] + url: URI + +@dataclass +class Scene: + instance_visual_scene: InstanceVisualScene + + sid_lookup: dict = field(repr=False) + +class FxSurfaceType(Enum): + UNTYPED = auto() + _1D = auto() + _2D = auto() + _3D = auto() + CUBE = auto() + DEPTH = auto() + RECT = auto() + +@dataclass +class InitFrom: + uri: str + +@dataclass +class Surface: + type: FxSurfaceType + + init_from: InitFrom + +@dataclass +class SourceFX: + sid: ID + +@dataclass +class Sampler2D: + source: SourceFX + +@dataclass +class Newparam: + sid: str # required + parameter_type: Union[Surface, Sampler2D] + +@dataclass +class Color: + value: Float4 + +@dataclass +class Float: + value: float + +@dataclass +class Texture: + texture: str + texcoord: str + +@dataclass +class Blinn: + emission: Optional[Union[Color, Texture]] + ambient: Optional[Union[Color, Texture]] + diffuse: Optional[Union[Color, Texture]] + specular: Optional[Union[Color, Texture]] + shininess: Optional[Float] + reflective: Optional[Union[Color, Texture]] + reflectivity: Optional[Float] + transparent: Optional[Union[Color, Texture]] + transparency: Optional[Float] + index_of_refraction: Optional[Float] + +@dataclass +class Lambert: + emission: Optional[Union[Color, Texture]] + ambient: Optional[Union[Color, Texture]] + diffuse: Optional[Union[Color, Texture]] + reflective: Optional[Union[Color, Texture]] + reflectivity: Optional[Float] + transparent: Optional[Union[Color, Texture]] + transparency: Optional[Float] + index_of_refraction: Optional[Float] + +@dataclass +class Phong: + emission: Optional[Union[Color, Texture]] + ambient: Optional[Union[Color, Texture]] + diffuse: Optional[Union[Color, Texture]] + specular: Optional[Union[Color, Texture]] + shininess: Optional[Float] + reflective: Optional[Union[Color, Texture]] + reflectivity: Optional[Float] + transparent: Optional[Union[Color, Texture]] + transparency: Optional[Float] + index_of_refraction: Optional[Float] + +@dataclass +class Constant: + emission: Optional[Color] + reflective: Optional[Union[Color, Texture]] + reflectivity: Optional[Float] + transparent: Optional[Union[Color, Texture]] + transparency: Optional[Float] + index_of_refraction: Optional[Float] + +@dataclass +class TechniqueFX: + id: Optional[ID] + sid: str # required + shader: Union[Blinn, Lambert, Phong, Constant] + +@dataclass +class ProfileCommon: + newparam: List[Newparam] + technique: TechniqueFX + + sid_lookup: dict = field(repr=False) + +@dataclass +class Effect: + id: str + name: Optional[str] + + profile_common: List[ProfileCommon] + +@dataclass +class LibraryEffects: + id: Optional[ID] + name: Optional[str] + + effects: List[Effect] + +@dataclass +class InstanceEffect: + sid: Optional[str] + name: Optional[str] + url: URI + +@dataclass +class Material: + id: Optional[ID] + name: Optional[str] + + instance_effect: InstanceEffect + + sid_lookup: dict = field(repr=False) + +@dataclass +class LibraryMaterials: + id: Optional[ID] + name: Optional[str] + + materials: List[Material] + +@dataclass +class FloatArray: + count: int + id: Optional[ID] + name: Optional[str] + digits: Optional[str] + magnitude: Optional[str] + + floats: List[float] + +@dataclass +class SourceCore: + id: ID + name: Optional[str] + + array_element: Union[FloatArray] + technique_common: TechniqueCommon_SourceCore + +@dataclass +class InputShared: + offset: int + semantic: str + source: URI + set: Optional[int] + +@dataclass +class Triangles: + name: Optional[str] + count: int + material: Optional[str] + inputs: List[InputShared] + p: List[int] + +@dataclass +class InputUnshared: + semantic: str + source: URI + +@dataclass +class Vertices: + id: ID + name: Optional[str] + + input: List[InputUnshared] + +@dataclass +class Mesh: + sources: List[SourceCore] + vertices: Vertices + primitive_elements: List[Union[Triangles]] + +@dataclass +class Geometry: + id: Optional[ID] + name: Optional[str] + + geometric_element: Union[Mesh] + +@dataclass +class LibraryGeometries: + id: Optional[ID] + name: Optional[str] + + geometries: List[Geometry] + +@dataclass +class TechniqueCommon_Light: + light: Union[Ambient, Directional, Point, Spot] + +@dataclass +class Light: + id: Optional[ID] + name: Optional[str] + + technique_common: TechniqueCommon_Light + +@dataclass +class LibraryLights: + id: Optional[ID] + name: Optional[str] + + lights: List[Light] + +@dataclass +class Image: + id: Optional[ID] + name: Optional[str] + format: Optional[str] + height: Optional[int] + width: Optional[int] + depth: Optional[int] + + image_source: Union[InitFrom] + +@dataclass +class LibraryImages: + id: Optional[ID] + name: Optional[str] + + images: List[Image] + +class NodeType(Enum): + JOINT = auto() + NODE = auto() + +TransformationElements = Union[Lookat, Matrix, Rotate, Scale, Skew, Translate] + +@dataclass +class Node: + id: Optional[ID] + name: Optional[str] + sid: Optional[str] + type: NodeType + layer: List[str] + + transformation_elements: List[TransformationElements] + instance_geometries: List[InstanceGeometry] + instance_lights: List[InstanceLight] + nodes: List['Node'] + + sid_lookup: dict = field(repr=False) + +@dataclass +class VisualScene: + id: Optional[ID] + name: Optional[str] + + nodes: List[Node] + + sid_lookup: dict = field(repr=False) + +@dataclass +class LibraryVisualScenes: + id: Optional[ID] + name: Optional[str] + + visual_scenes: List[VisualScene] + +@dataclass +class Collada: + library_effects: List[LibraryEffects] + library_materials: List[LibraryMaterials] + library_geometries: List[LibraryGeometries] + library_lights: List[LibraryLights] + library_images: List[LibraryImages] + library_visual_scenes: List[LibraryVisualScenes] + scenes: List[Scene] + + def __init__(self): + self.library_effects = [] + self.library_materials = [] + self.library_geometries = [] + self.library_lights = [] + self.library_images = [] + self.library_visual_scenes = [] + self.scenes = [] diff --git a/collada/parse.py b/collada/parse.py new file mode 100644 index 0000000..e5693a4 --- /dev/null +++ b/collada/parse.py @@ -0,0 +1,770 @@ +from lxml import etree +import collada_types as types +from functools import partial + +with open("cube_material.DAE") as f: + tree = etree.parse(f) + +xml_namespace = "http://www.collada.org/2005/11/COLLADASchema" + +def tag(s): + return f"{{{xml_namespace}}}{s}" + +def lookup_add(lookup, name, value): + if name is None: + return + assert name not in lookup + lookup[name] = value + +def parse_init_from(lookup, root): + return types.InitFrom(root.text.strip()) + +def parse_surface_type(s): + if s == "UNTYPED": + return types.FxSurfaceType.UNTYPED + if s == "1D": + return types.FxSurfaceType._1D + if s == "2D": + return types.FxSurfaceType._2D + if s == "3D": + return types.FxSurfaceType._3D + if s == "CUBE": + return types.FxSurfaceType.CUBE + if s == "DEPTH": + return types.FxSurfaceType.DEPTH + if s == "RECT": + return types.FxSurfaceType.RECT + assert False, s + +def parse_surface(lookup, root): + type = parse_surface_type(root.attrib["type"]) + + init_from = None + for child in root.getchildren(): + if child.tag == tag("init_from"): + assert init_from is None + init_from = parse_init_from(lookup, child) + + surface = types.Surface(type, init_from) + return surface + +def parse_source_fx(lookup, root): + return types.SourceFX(root.text.strip()) + +def parse_sampler2d(lookup, root): + source_fx = None + for child in root.getchildren(): + if child.tag == tag("source"): + assert source_fx is None + source_fx = parse_source_fx(lookup, child) + + assert source_fx is not None + sampler2d = types.Sampler2D(source_fx) + return sampler2d + +def parse_newparam(lookup, sid_lookup, root): + sid = root.attrib["sid"] + + parameter_type = None + for child in root.getchildren(): + if child.tag == tag("surface"): + assert parameter_type is None + parameter_type = parse_surface(lookup, child) + if child.tag == tag("sampler2D"): + assert parameter_type is None + parameter_type = parse_sampler2d(lookup, child) + + assert parameter_type is not None + newparam = types.Newparam(sid, parameter_type) + lookup_add(sid_lookup, sid, newparam) + return newparam + +def parse_fields(lookup, root, *, cls, fields): + field_kv = {} + for child in root.getchildren(): + for tag_name, parser in fields: + if child.tag == tag(tag_name): + assert tag_name not in field_kv + grandchildren = child.getchildren() + assert len(grandchildren) == 1 + field_kv[tag_name] = parser(lookup, grandchildren[0]) + for tag_name, _ in fields: + if tag_name not in field_kv: + field_kv[tag_name] = None + return cls(**field_kv) + +def parse_color_or_texture(lookup, root): + if root.tag == tag("color"): + return types.Color(tuple(map(float, root.text.strip().split()))) + if root.tag == tag("texture"): + return types.Texture(root.attrib["texture"], root.attrib["texcoord"]) + assert False, (root, root.tag) + +def parse_float(lookup, root): + assert root.tag == tag("float") + return types.Float(float(root.text.strip())) + +blinn_phong_fields = [ + ("emission", parse_color_or_texture), + ("ambient", parse_color_or_texture), + ("diffuse", parse_color_or_texture), + ("specular", parse_color_or_texture), + ("shininess", parse_float), + ("reflective", parse_color_or_texture), + ("reflectivity", parse_float), + ("transparent", parse_color_or_texture), + ("transparency", parse_float), + ("index_of_refraction", parse_float), +] + +lambert_fields = [ + ("emission", parse_color_or_texture), + ("ambient", parse_color_or_texture), + ("diffuse", parse_color_or_texture), + ("reflective", parse_color_or_texture), + ("reflectivity", parse_float), + ("transparent", parse_color_or_texture), + ("transparency", parse_float), + ("index_of_refraction", parse_float), +] + +constant_fields = [ + ("emission", parse_color_or_texture), + ("reflective", parse_color_or_texture), + ("reflectivity", parse_float), + ("transparent", parse_color_or_texture), + ("transparency", parse_float), + ("index_of_refraction", parse_float), +] + +parse_blinn = partial(parse_fields, cls=types.Blinn, fields=blinn_phong_fields) +parse_phong = partial(parse_fields, cls=types.Phong, fields=blinn_phong_fields) +parse_lambert = partial(parse_fields, cls=types.Lambert, fields=lambert_fields) +parse_constant = partial(parse_fields, cls=types.Constant, fields=constant_fields) + +def parse_technique_fx(lookup, sid_lookup, root): + id = root.attrib.get("id") + sid = root.attrib["sid"] + + shader = None + for child in root.getchildren(): + if child.tag == tag("blinn"): + assert shader is None + shader = parse_blinn(lookup, child) + if child.tag == tag("constant"): + assert shader is None + shader = parse_constant(lookup, child) + if child.tag == tag("lambert"): + assert shader is None + shader = parse_lambert(lookup, child) + if child.tag == tag("phong"): + assert shader is None + shader = parse_phong(lookup, child) + assert shader is not None + + technique = types.TechniqueFX(id, sid, shader) + lookup_add(lookup, id, technique) + lookup_add(sid_lookup, sid, technique) + return technique + +def parse_profile_common(lookup, root): + newparam = [] + technique_fx = None + sid_lookup = {} + + for child in root.getchildren(): + if child.tag == tag("newparam"): + newparam.append(parse_newparam(lookup, sid_lookup, child)) + if child.tag == tag("technique"): + assert technique_fx is None + technique_fx = parse_technique_fx(lookup, sid_lookup, child) + + assert technique_fx is not None + return types.ProfileCommon(newparam, technique_fx, sid_lookup) + +def parse_effect(lookup, root): + id = root.attrib["id"] + name = root.attrib.get("name") + + profile_common = [] + + for child in root.getchildren(): + if child.tag == tag("profile_COMMON"): + profile_common.append(parse_profile_common(lookup, child)) + + effect = types.Effect(id, name, profile_common) + lookup_add(lookup, id, effect) + return effect + +def parse_library_effects(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + effects = [] + + for child in root.getchildren(): + if child.tag == tag("effect"): + effects.append(parse_effect(lookup, child)) + + library_effects = types.LibraryEffects(id, name, effects) + lookup_add(lookup, id, library_effects) + return library_effects + +def parse_instance_effect(lookup, sid_lookup, root): + sid = root.attrib.get("sid") + name = root.attrib.get("name") + url = root.attrib["url"] + + instance_effect = types.InstanceEffect(sid, name, url) + lookup_add(sid_lookup, sid, instance_effect) + return instance_effect + +def parse_material(lookup, root): + sid_lookup = {} + + id = root.attrib.get("id") + name = root.attrib.get("name") + instance_effect = None + + for child in root.getchildren(): + if child.tag == tag("instance_effect"): + assert instance_effect is None + instance_effect = parse_instance_effect(lookup, sid_lookup, child) + assert instance_effect is not None + + material = types.Material(id, name, instance_effect, sid_lookup) + lookup_add(lookup, id, material) + return material + +def parse_library_materials(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + materials = [] + + for child in root.getchildren(): + if child.tag == tag("material"): + materials.append(parse_material(lookup, child)) + + library_materials = types.LibraryMaterials(id, name, materials) + lookup_add(lookup, id, library_materials) + return library_materials + +def parse_input_unshared(lookup, root): + semantic = root.attrib["semantic"] + source = root.attrib["source"] + return types.InputUnshared(semantic, source) + +def parse_vertices(lookup, root): + id = root.attrib["id"] + name = root.attrib.get("name") + input = [] + + for child in root.getchildren(): + if child.tag == tag("input"): + input.append(parse_input_unshared(lookup, child)) + + assert len(input) >= 1 + vertices = types.Vertices(id, name, input) + lookup_add(lookup, id, vertices) + return vertices + +def parse_float_array(lookup, root): + count = int(root.attrib["count"]) + id = root.attrib.get("id") + name = root.attrib.get("name") + digits = int(root.attrib.get("digits", 6)) + magnitude = int(root.attrib.get("digits", 38)) + + assert len(root.getchildren()) == 0 + floats = [float(s) for s in root.text.strip().split()] + + float_array = types.FloatArray(count, id, name, digits, magnitude, floats) + lookup_add(lookup, id, float_array) + return float_array + +def parse_param(lookup, sid_lookup, root): + name = root.attrib.get("name") + sid = root.attrib.get("sid") + type = root.attrib["type"] + semantic = root.attrib.get("semantic") + + param = types.Param(name, sid, type, semantic) + lookup_add(sid_lookup, sid, param) + return param + +def parse_accessor(lookup, root): + count = int(root.attrib["count"]) + offset = int(root.attrib.get("offset", 0)) + source = root.attrib["source"] + stride = int(root.attrib.get("stride", 1)) + + sid_lookup = {} + + params = [] + for child in root.getchildren(): + if child.tag == tag("param"): + params.append(parse_param(lookup, sid_lookup, child)) + + return types.Accessor(count, offset, source, stride, params, sid_lookup) + +def parse_technique_common_source_core(lookup, root): + accessor = None + + for child in root.getchildren(): + if child.tag == tag("accessor"): + assert accessor is None + accessor = parse_accessor(lookup, child) + + assert accessor is not None + return types.TechniqueCommon_SourceCore(accessor) + +def parse_source_core(lookup, root): + id = root.attrib["id"] + name = root.attrib.get("name") + array_element = None # 0 or 1 + technique_common = None # 0 or 1 + + for child in root.getchildren(): + if child.tag == tag("float_array"): + assert array_element is None + array_element = parse_float_array(lookup, child) + if child.tag == tag("technique_common"): + assert technique_common is None + technique_common = parse_technique_common_source_core(lookup, child) + + source_core = types.SourceCore(id, name, array_element, technique_common) + lookup_add(lookup, id, source_core) + return source_core + +def parse_input_shared(lookup, root): + offset = int(root.attrib["offset"]) + semantic = root.attrib["semantic"] + source = root.attrib["source"] + set = int(root.attrib["set"]) if "set" in root.attrib else None + return types.InputShared(offset, semantic, source, set) + +def parse_p(lookup, root): + assert len(root.getchildren()) == 0 + return [int(i) for i in root.text.strip().split()] + +def parse_triangles(lookup, root): + name = root.attrib.get("name") + count = int(root.attrib["count"]) + material = root.attrib.get("material") + + inputs = [] + p = None + + for child in root.getchildren(): + if child.tag == tag("input"): + inputs.append(parse_input_shared(lookup, child)) + if child.tag == tag("p"): + assert p is None + p = parse_p(lookup, child) + + return types.Triangles(name, count, material, inputs, p) + +def parse_mesh(lookup, root): + sources = [] + vertices = None + primitive_elements = [] + + for child in root.getchildren(): + if child.tag == tag("source"): + sources.append(parse_source_core(lookup, child)) + if child.tag == tag("vertices"): + assert vertices is None + vertices = parse_vertices(lookup, child) + if child.tag == tag("triangles"): + primitive_elements.append(parse_triangles(lookup, child)) + assert vertices is not None + assert len(sources) >= 1 + + mesh = types.Mesh(sources, vertices, primitive_elements) + return mesh + +def parse_geometry(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + + geometric_element = None + + for child in root.getchildren(): + if child.tag == tag("mesh"): + assert geometric_element is None + geometric_element = parse_mesh(lookup, child) + assert geometric_element is not None + + geometry = types.Geometry(id, name, geometric_element) + lookup_add(lookup, id, geometry) + return geometry + +def parse_library_geometries(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + geometries = [] + + for child in root.getchildren(): + if child.tag == tag("geometry"): + geometries.append(parse_geometry(lookup, child)) + + assert len(geometries) >= 1 + library_geometries = types.LibraryGeometries(id, name, geometries) + lookup_add(lookup, id, library_geometries) + return library_geometries + +def parse_light_color(lookup, root, *, cls): + color = None + for child in root.getchildren(): + if child.tag == tag("color"): + assert color is None + color = tuple([float(i) for i in child.text.strip().split()]) + assert color is not None + return cls(color) + +parse_ambient = partial(parse_light_color, cls=types.Ambient) +parse_directional = partial(parse_light_color, cls=types.Directional) +parse_point = partial(parse_light_color, cls=types.Point) +parse_spot = partial(parse_light_color, cls=types.Spot) + +def parse_technique_common_light(lookup, root): + light = None + + for child in root.getchildren(): + if child.tag == tag("ambient"): + assert light is None + light = parse_ambient(lookup, child) + if child.tag == tag("directional"): + assert light is None + light = parse_directional(lookup, child) + if child.tag == tag("point"): + assert light is None + light = parse_point(lookup, child) + if child.tag == tag("spot"): + assert light is None + light = parse_spot(lookup, child) + + assert light is not None + + return types.TechniqueCommon_Light(light) + +def parse_light(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + + technique_common = None + + for child in root.getchildren(): + if child.tag == tag("technique_common"): + assert technique_common is None + technique_common = parse_technique_common_light(lookup, child) + + assert technique_common is not None + + light = types.Light(id, name, technique_common) + lookup_add(lookup, id, light) + return light + +def parse_library_lights(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + lights = [] + + for child in root.getchildren(): + if child.tag == tag("light"): + lights.append(parse_light(lookup, child)) + + assert len(lights) >= 1 + library_lights = types.LibraryLights(id, name, lights) + lookup_add(lookup, id, library_lights) + return library_lights + +def parse_init_from(lookup, root): + assert len(root.getchildren()) == 0 + return types.InitFrom(root.text.strip()) + +def parse_image(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + format = root.attrib.get("format") + height = root.attrib.get("height") + width = root.attrib.get("width") + depth = root.attrib.get("depth") + + image_source = None + + for child in root.getchildren(): + if child.tag == tag("init_from"): + assert image_source is None + image_source = parse_init_from(lookup, child) + + assert image_source is not None + image = types.Image(id, name, format, height, width, depth, image_source) + lookup_add(lookup, id, image) + return image + +def parse_library_images(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + + images = [] + + for child in root.getchildren(): + if child.tag == tag("image"): + images.append(parse_image(lookup, child)) + + assert len(images) >= 1 + + library_images = types.LibraryImages(id, name, images) + lookup_add(lookup, id, library_images) + return library_images + +def parse_node_type(s): + if s == "JOINT": + return types.NodeType.JOINT + if s == "NODE": + return types.NodeType.NODE + assert False, s + +def parse_lookat(lookup, sid_lookup, root): + sid = root.attrib.get("sid") + + assert len(root.getchildren()) == 0 + values = [float(i) for i in root.text.strip().split()] + assert len(values) == 9 + + eye = tuple(values[0:3]) + at = tuple(values[3:6]) + up = tuple(values[6:9]) + + lookat = types.Lookat(sid, eye, at, up) + lookup_add(sid_lookup, sid, lookat) + return lookat + +def parse_matrix(lookup, sid_lookup, root): + sid = root.attrib.get("sid") + + assert len(root.getchildren()) == 0 + values = [float(i) for i in root.text.strip().split()] + assert len(values) == 16 + + r0 = tuple(values[0:4]) + r1 = tuple(values[4:8]) + r2 = tuple(values[8:12]) + r3 = tuple(values[12:16]) + + matrix = types.Matrix(sid, tuple([r0, r1, r2, r3])) + lookup_add(sid_lookup, sid, matrix) + return matrix + +def parse_simple_transform(lookup, sid_lookup, root, *, cls, count): + sid = root.attrib.get("sid") + + assert len(root.getchildren()) == 0 + values = [float(i) for i in root.text.strip().split()] + assert len(values) == count + + inst = cls(sid, values) + lookup_add(sid_lookup, sid, inst) + return inst + +parse_rotate = partial(parse_simple_transform, cls=types.Rotate, count=4) +parse_scale = partial(parse_simple_transform, cls=types.Scale, count=3) +parse_skew = partial(parse_simple_transform, cls=types.Skew, count=7) +parse_translate = partial(parse_simple_transform, cls=types.Translate, count=3) + +def parse_bind_vertex_input(lookup, root): + semantic = root.attrib["semantic"] + input_semantic = root.attrib["input_semantic"] + input_set = root.attrib.get("input_set") + + return types.BindVertexInput(semantic, input_semantic, input_set) + +def parse_instance_material(lookup, sid_lookup, root): + sid = root.attrib.get("sid") + name = root.attrib.get("name") + target = root.attrib["target"] + symbol = root.attrib["symbol"] + + bind_vertex_inputs = [] + for child in root.getchildren(): + if child.tag == tag("bind_vertex_input"): + bind_vertex_inputs.append(parse_bind_vertex_input(lookup, child)) + + inst = types.InstanceMaterial(sid, name, target, symbol, bind_vertex_inputs) + lookup_add(sid_lookup, sid, inst) + return inst + +def parse_technique_common_bind_material(lookup, root): + materials = [] + sid_lookup = {} + + for child in root.getchildren(): + if child.tag == tag("instance_material"): + materials.append(parse_instance_material(lookup, sid_lookup, child)) + + assert len(materials) >= 1 + return types.TechniqueCommon_BindMaterial(materials, sid_lookup) + +def parse_bind_material(lookup, root): + technique_common = None + for child in root.getchildren(): + if child.tag == tag("technique_common"): + assert technique_common is None + technique_common = parse_technique_common_bind_material(lookup, child) + + assert technique_common is not None + return types.BindMaterial(technique_common) + +def parse_instance_geometry(lookup, sid_lookup, root): + sid = root.attrib.get("sid") + name = root.attrib.get("name") + url = root.attrib["url"] + + bind_material = None # optional + + for child in root.getchildren(): + if child.tag == tag("bind_material"): + assert bind_material is None + bind_material = parse_bind_material(lookup, child) + + # bind_material may be none + instance_geometry = types.InstanceGeometry(sid, name, url, bind_material) + lookup_add(sid_lookup, sid, instance_geometry) + return instance_geometry + +def parse_instance_light(lookup, sid_lookup, root): + sid = root.attrib.get("sid") + name = root.attrib.get("name") + url = root.attrib["url"] + + instance_light = types.InstanceLight(sid, name, url) + lookup_add(sid_lookup, sid, instance_light) + return instance_light + +def parse_node(lookup, sid_lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + sid = root.attrib.get("sid") + type = root.attrib["type"] if "type" in root.attrib else types.NodeType.NODE + layer = root.attrib.get("layer", "").strip().split() + + transformation_elements = [] + instance_geometries = [] + instance_lights = [] + nodes = [] + + child_sid_lookup = {} + + for child in root.getchildren(): + if child.tag == tag("lookat"): + transformation_elements.append(parse_lookat(lookup, child_sid_lookup, child)) + if child.tag == tag("matrix"): + transformation_elements.append(parse_matrix(lookup, child_sid_lookup, child)) + if child.tag == tag("rotate"): + transformation_elements.append(parse_rotate(lookup, child_sid_lookup, child)) + if child.tag == tag("scale"): + transformation_elements.append(parse_scale(lookup, child_sid_lookup, child)) + if child.tag == tag("skew"): + transformation_elements.append(parse_skew(lookup, child_sid_lookup, child)) + if child.tag == tag("translate"): + transformation_elements.append(parse_translate(lookup, child_sid_lookup, child)) + if child.tag == tag("instance_geometry"): + instance_geometries.append(parse_instance_geometry(lookup, child_sid_lookup, child)) + if child.tag == tag("instance_light"): + instance_lights.append(parse_instance_light(lookup, child_sid_lookup, child)) + if child.tag == tag("node"): + nodes.append(parse_node(lookup, child_sid_lookup, child)) + + node = types.Node(id, name, sid, type, layer, + transformation_elements, + instance_geometries, + instance_lights, + nodes, + child_sid_lookup) + + lookup_add(lookup, id, node) + lookup_add(sid_lookup, sid, node) + return node + +def parse_visual_scene(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + + sid_lookup = {} + + nodes = [] + for child in root.getchildren(): + if child.tag == tag("node"): + nodes.append(parse_node(lookup, sid_lookup, child)) + + assert len(nodes) >= 1 + + visual_scenes = types.VisualScene(id, name, nodes, sid_lookup) + lookup_add(lookup, id, visual_scenes) + return visual_scenes + +def parse_library_visual_scenes(lookup, root): + id = root.attrib.get("id") + name = root.attrib.get("name") + + visual_scenes = [] + + for child in root.getchildren(): + if child.tag == tag("visual_scene"): + visual_scenes.append(parse_visual_scene(lookup, child)) + + assert len(visual_scenes) >= 1 + + library_visual_scenes = types.LibraryVisualScenes(id, name, visual_scenes) + lookup_add(lookup, id, library_visual_scenes) + return library_visual_scenes + +def parse_instance_visual_scene(lookup, sid_lookup, root): + sid = root.attrib.get("sid") + name = root.attrib.get("name") + url = root.attrib["url"] + + instance_visual_scene = types.InstanceVisualScene(sid, name, url) + lookup_add(sid_lookup, sid, instance_visual_scene) + return instance_visual_scene + +def parse_scene(lookup, root): + instance_visual_scene = None + sid_lookup = {} + + for child in root.getchildren(): + if child.tag == tag("instance_visual_scene"): + instance_visual_scene = parse_instance_visual_scene(lookup, sid_lookup, child) + + # instance_visual_scene may be none + return types.Scene(instance_visual_scene, sid_lookup) + +def parse_collada(tree): + root = tree.getroot() + assert root.tag == tag("COLLADA") + + collada = types.Collada() + lookup = {} + + for child in root.getchildren(): + if child.tag == tag("library_effects"): + collada.library_effects.append(parse_library_effects(lookup, child)) + if child.tag == tag("library_materials"): + collada.library_materials.append(parse_library_materials(lookup, child)) + if child.tag == tag("library_geometries"): + collada.library_geometries.append(parse_library_geometries(lookup, child)) + if child.tag == tag("library_lights"): + collada.library_lights.append(parse_library_lights(lookup, child)) + if child.tag == tag("library_images"): + collada.library_images.append(parse_library_images(lookup, child)) + if child.tag == tag("library_visual_scenes"): + collada.library_visual_scenes.append(parse_library_visual_scenes(lookup, child)) + if child.tag == tag("scene"): + collada.scenes.append(parse_scene(lookup, child)) + + return collada + +collada = parse_collada(tree) +from prettyprinter import pprint, install_extras +install_extras(include=["dataclasses"]) +pprint(collada, width=120)