diff --git a/README.md b/README.md index 74af4b8..bd6356a 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,14 @@ gdtf_fixture.dmx_modes[0].dmx_channels.as_dict() 128, 'real_fade': 1.833, 'physical_to': 270.0, 'physical_from': -270.0, 'channel_sets': ['', 'Center', '']}, ... +# get geometry tree with expanded references +tree = gdtf_fixture.geometries.get_geometry_tree( + fixture_type=gdtf_fixture, + mode_name="Mode 1 - Standard 16 - bit", +) +tree.name +'Base' + # see the source code for more methods ``` diff --git a/pygdtf/__init__.py b/pygdtf/__init__.py index 7d9b43c..07af7ce 100644 --- a/pygdtf/__init__.py +++ b/pygdtf/__init__.py @@ -41,7 +41,7 @@ from .utils import * from .value import * # type: ignore -__version__ = "1.4.2" +__version__ = "1.4.3-dev1" # Standard predefined colour spaces: R, G, B, W-P COLOR_SPACE_SRGB = ColorSpaceDefinition( diff --git a/pygdtf/geometries.py b/pygdtf/geometries.py index 5f4594f..c55c216 100644 --- a/pygdtf/geometries.py +++ b/pygdtf/geometries.py @@ -22,7 +22,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from typing import List, Optional +import copy +from typing import List, Optional, TYPE_CHECKING from xml.etree import ElementTree from xml.etree.ElementTree import Element @@ -30,6 +31,9 @@ from .dmxbreak import * from .value import * # type: ignore +if TYPE_CHECKING: + from . import FixtureType + def _matrix_to_str(matrix: "Matrix") -> str: if hasattr(matrix, "raw") and matrix.raw: @@ -41,6 +45,25 @@ def _matrix_to_str(matrix: "Matrix") -> str: class Geometries(list): + def _resolve_root_geometry( + self, + fixture_type: "FixtureType", + mode_name: Optional[str] = None, + mode_index: Optional[int] = None, + ): + root_name = None + if mode_name is not None: + mode = fixture_type.dmx_modes.get_mode_by_name(mode_name) + if mode is not None: + root_name = mode.geometry + elif mode_index is not None and len(fixture_type.dmx_modes) > mode_index: + root_name = fixture_type.dmx_modes[mode_index].geometry + if root_name is None and len(self): + root_name = self[0].name + if root_name is None: + return None + return self.get_geometry_by_name(root_name) + def get_geometry_by_name(self, geometry_name): """Operates on the while kinematic chain of the device""" @@ -84,6 +107,103 @@ def iterate_geometries(collector): iterate_geometries(root_geometry) return matched + def get_geometry_tree( + self, + fixture_type: "FixtureType", + mode_name: Optional[str] = None, + mode_index: Optional[int] = None, + ): + root_geometry = self._resolve_root_geometry( + fixture_type, + mode_name=mode_name, + mode_index=mode_index, + ) + return self._expand_tree(root_geometry, fixture_type) + + def _expand_tree(self, geometry, fixture_type: "FixtureType"): + if geometry is None: + return None + geometry_copy = copy.deepcopy(geometry) + children = [] + for child in self._iter_children(geometry, fixture_type, True): + child_copy = self._expand_tree(child, fixture_type) + if child_copy is not None: + children.append(child_copy) + geometry_copy.geometries = Geometries(children) + return geometry_copy + + def _resolve_reference(self, geometry, fixture_type: "FixtureType"): + if isinstance(geometry, GeometryReference): + reference_name = geometry.geometry + if reference_name: + return fixture_type.geometries.get_geometry_by_name(reference_name) + return None + + def _iter_children(self, geometry, fixture_type: "FixtureType", expand_references): + if isinstance(geometry, GeometryReference): + reference_geometry = self._resolve_reference(geometry, fixture_type) + if expand_references and reference_geometry is not None: + return getattr(reference_geometry, "geometries", []) + return [] + return getattr(geometry, "geometries", []) + + def iter_tree(self, geometry, fixture_type: "FixtureType", expand_references=True): + if geometry is None: + return + yield geometry + for child in self._iter_children(geometry, fixture_type, expand_references): + yield from self.iter_tree(child, fixture_type, expand_references) + + def to_list(self, geometry, fixture_type: "FixtureType", expand_references=True): + return list(self.iter_tree(geometry, fixture_type, expand_references)) + + def as_dict( + self, + geometry=None, + fixture_type: Optional["FixtureType"] = None, + mode_name: Optional[str] = None, + mode_index: Optional[int] = None, + ): + if fixture_type is None: + raise ValueError("fixture_type is required to build geometry dicts.") + if geometry is None: + geometry = self._resolve_root_geometry( + fixture_type, + mode_name=mode_name, + mode_index=mode_index, + ) + if geometry is None: + return None + reference_geometry = self._resolve_reference(geometry, fixture_type) + model = geometry.model + if model is None and reference_geometry is not None: + model = reference_geometry.model + data = { + "name": geometry.name, + "type": type(geometry).__name__, + "model": model, + "matrix": geometry.position.matrix + if hasattr(geometry, "position") + else None, + "reference": geometry.geometry + if isinstance(geometry, GeometryReference) + else None, + } + children = [] + for child in self._iter_children( + geometry, + fixture_type, + True, + ): + child_data = self.as_dict( + child, + fixture_type, + ) + if child_data is not None: + children.append(child_data) + data["children"] = children + return data + class Geometry(BaseNode): def __init__( diff --git a/tests/test_geometries.py b/tests/test_geometries.py new file mode 100644 index 0000000..f38d0b4 --- /dev/null +++ b/tests/test_geometries.py @@ -0,0 +1,99 @@ +# MIT License +# +# Copyright (C) 2026 vanous +# +# This file is part of pygdtf. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from pathlib import Path + + +def _find_geometry(geometry, name): + if geometry is None: + return None + if geometry.name == name: + return geometry + for child in getattr(geometry, "geometries", []): + found = _find_geometry(child, name) + if found is not None: + return found + return None + + +def test_get_geometry_tree_expands_references(pygdtf_module): + root = pygdtf_module.Geometry(name="Root") + referenced_child = pygdtf_module.Geometry(name="RefChild") + referenced = pygdtf_module.Geometry( + name="Referenced", + geometries=pygdtf_module.Geometries([referenced_child]), + ) + root.geometries = pygdtf_module.Geometries( + [pygdtf_module.GeometryReference(name="Ref", geometry="Referenced")] + ) + + geometries = pygdtf_module.Geometries([root, referenced]) + fixture_type = type("Fixture", (), {})() + fixture_type.geometries = geometries + fixture_type.dmx_modes = pygdtf_module.DmxModes( + [pygdtf_module.DmxMode(name="Default", geometry="Root")] + ) + + tree = geometries.get_geometry_tree(fixture_type) + + assert tree.name == "Root" + assert [child.name for child in tree.geometries] == ["Ref"] + assert [child.name for child in tree.geometries[0].geometries] == ["RefChild"] + + +def test_get_geometry_tree_expands_references_from_files(pygdtf_module): + test_fixture_test_file = Path(Path(__file__).parents[0], "test2.xml").as_posix() + fixture = pygdtf_module.FixtureType(dsc_file=test_fixture_test_file) + tree = fixture.geometries.get_geometry_tree(fixture, mode_name="Mode 1 - Wash") + + pixel = _find_geometry(tree, "Pixel 1") + + assert pixel is not None + assert isinstance(pixel, pygdtf_module.GeometryReference) + assert [child.name for child in pixel.geometries] == ["Patt cross 1"] + + +def test_as_dict_uses_mode_name_for_root(pygdtf_module): + root = pygdtf_module.Geometry(name="Root") + referenced_child = pygdtf_module.Geometry(name="RefChild") + referenced = pygdtf_module.Geometry( + name="Referenced", + geometries=pygdtf_module.Geometries([referenced_child]), + ) + root.geometries = pygdtf_module.Geometries( + [pygdtf_module.GeometryReference(name="Ref", geometry="Referenced")] + ) + + geometries = pygdtf_module.Geometries([root, referenced]) + fixture_type = type("Fixture", (), {})() + fixture_type.geometries = geometries + fixture_type.dmx_modes = pygdtf_module.DmxModes( + [pygdtf_module.DmxMode(name="Default", geometry="Root")] + ) + + tree = geometries.as_dict(None, fixture_type, mode_name="Default") + + assert tree["name"] == "Root" + assert [child["name"] for child in tree["children"]] == ["Ref"] + assert [child["name"] for child in tree["children"][0]["children"]] == ["RefChild"]