Skip to content

Commit 5fe6e91

Browse files
committed
Add geometry helper
1 parent b697c13 commit 5fe6e91

File tree

4 files changed

+229
-2
lines changed

4 files changed

+229
-2
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ gdtf_fixture.dmx_modes[0].dmx_channels.as_dict()
9898
128, 'real_fade': 1.833, 'physical_to': 270.0, 'physical_from': -270.0,
9999
'channel_sets': ['', 'Center', '']}, ...
100100

101+
# get geometry tree with expanded references
102+
tree = gdtf_fixture.geometries.get_geometry_tree(
103+
fixture_type=gdtf_fixture,
104+
mode_name="Mode 1 - Standard 16 - bit",
105+
)
106+
tree.name
107+
'Base'
108+
101109
# see the source code for more methods
102110
```
103111

pygdtf/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from .utils import *
4242
from .value import * # type: ignore
4343

44-
__version__ = "1.4.2"
44+
__version__ = "1.4.3-dev1"
4545

4646
# Standard predefined colour spaces: R, G, B, W-P
4747
COLOR_SPACE_SRGB = ColorSpaceDefinition(

pygdtf/geometries.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@
2222
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2323
# SOFTWARE.
2424

25-
from typing import List, Optional
25+
import copy
26+
from typing import List, Optional, TYPE_CHECKING
2627
from xml.etree import ElementTree
2728
from xml.etree.ElementTree import Element
2829

2930
from .base_node import BaseNode
3031
from .dmxbreak import *
3132
from .value import * # type: ignore
3233

34+
if TYPE_CHECKING:
35+
from . import FixtureType
36+
3337

3438
def _matrix_to_str(matrix: "Matrix") -> str:
3539
if hasattr(matrix, "raw") and matrix.raw:
@@ -41,6 +45,25 @@ def _matrix_to_str(matrix: "Matrix") -> str:
4145

4246

4347
class Geometries(list):
48+
def _resolve_root_geometry(
49+
self,
50+
fixture_type: "FixtureType",
51+
mode_name: Optional[str] = None,
52+
mode_index: Optional[int] = None,
53+
):
54+
root_name = None
55+
if mode_name is not None:
56+
mode = fixture_type.dmx_modes.get_mode_by_name(mode_name)
57+
if mode is not None:
58+
root_name = mode.geometry
59+
elif mode_index is not None and len(fixture_type.dmx_modes) > mode_index:
60+
root_name = fixture_type.dmx_modes[mode_index].geometry
61+
if root_name is None and len(self):
62+
root_name = self[0].name
63+
if root_name is None:
64+
return None
65+
return self.get_geometry_by_name(root_name)
66+
4467
def get_geometry_by_name(self, geometry_name):
4568
"""Operates on the while kinematic chain of the device"""
4669

@@ -84,6 +107,103 @@ def iterate_geometries(collector):
84107
iterate_geometries(root_geometry)
85108
return matched
86109

110+
def get_geometry_tree(
111+
self,
112+
fixture_type: "FixtureType",
113+
mode_name: Optional[str] = None,
114+
mode_index: Optional[int] = None,
115+
):
116+
root_geometry = self._resolve_root_geometry(
117+
fixture_type,
118+
mode_name=mode_name,
119+
mode_index=mode_index,
120+
)
121+
return self._expand_tree(root_geometry, fixture_type)
122+
123+
def _expand_tree(self, geometry, fixture_type: "FixtureType"):
124+
if geometry is None:
125+
return None
126+
geometry_copy = copy.deepcopy(geometry)
127+
children = []
128+
for child in self._iter_children(geometry, fixture_type, True):
129+
child_copy = self._expand_tree(child, fixture_type)
130+
if child_copy is not None:
131+
children.append(child_copy)
132+
geometry_copy.geometries = Geometries(children)
133+
return geometry_copy
134+
135+
def _resolve_reference(self, geometry, fixture_type: "FixtureType"):
136+
if isinstance(geometry, GeometryReference):
137+
reference_name = geometry.geometry
138+
if reference_name:
139+
return fixture_type.geometries.get_geometry_by_name(reference_name)
140+
return None
141+
142+
def _iter_children(self, geometry, fixture_type: "FixtureType", expand_references):
143+
if isinstance(geometry, GeometryReference):
144+
reference_geometry = self._resolve_reference(geometry, fixture_type)
145+
if expand_references and reference_geometry is not None:
146+
return getattr(reference_geometry, "geometries", [])
147+
return []
148+
return getattr(geometry, "geometries", [])
149+
150+
def iter_tree(self, geometry, fixture_type: "FixtureType", expand_references=True):
151+
if geometry is None:
152+
return
153+
yield geometry
154+
for child in self._iter_children(geometry, fixture_type, expand_references):
155+
yield from self.iter_tree(child, fixture_type, expand_references)
156+
157+
def to_list(self, geometry, fixture_type: "FixtureType", expand_references=True):
158+
return list(self.iter_tree(geometry, fixture_type, expand_references))
159+
160+
def as_dict(
161+
self,
162+
geometry=None,
163+
fixture_type: Optional["FixtureType"] = None,
164+
mode_name: Optional[str] = None,
165+
mode_index: Optional[int] = None,
166+
):
167+
if fixture_type is None:
168+
raise ValueError("fixture_type is required to build geometry dicts.")
169+
if geometry is None:
170+
geometry = self._resolve_root_geometry(
171+
fixture_type,
172+
mode_name=mode_name,
173+
mode_index=mode_index,
174+
)
175+
if geometry is None:
176+
return None
177+
reference_geometry = self._resolve_reference(geometry, fixture_type)
178+
model = geometry.model
179+
if model is None and reference_geometry is not None:
180+
model = reference_geometry.model
181+
data = {
182+
"name": geometry.name,
183+
"type": type(geometry).__name__,
184+
"model": model,
185+
"matrix": geometry.position.matrix
186+
if hasattr(geometry, "position")
187+
else None,
188+
"reference": geometry.geometry
189+
if isinstance(geometry, GeometryReference)
190+
else None,
191+
}
192+
children = []
193+
for child in self._iter_children(
194+
geometry,
195+
fixture_type,
196+
True,
197+
):
198+
child_data = self.as_dict(
199+
child,
200+
fixture_type,
201+
)
202+
if child_data is not None:
203+
children.append(child_data)
204+
data["children"] = children
205+
return data
206+
87207

88208
class Geometry(BaseNode):
89209
def __init__(

tests/test_geometries.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# MIT License
2+
#
3+
# Copyright (C) 2026 vanous
4+
#
5+
# This file is part of pygdtf.
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
25+
from pathlib import Path
26+
27+
28+
def _find_geometry(geometry, name):
29+
if geometry is None:
30+
return None
31+
if geometry.name == name:
32+
return geometry
33+
for child in getattr(geometry, "geometries", []):
34+
found = _find_geometry(child, name)
35+
if found is not None:
36+
return found
37+
return None
38+
39+
40+
def test_get_geometry_tree_expands_references(pygdtf_module):
41+
root = pygdtf_module.Geometry(name="Root")
42+
referenced_child = pygdtf_module.Geometry(name="RefChild")
43+
referenced = pygdtf_module.Geometry(
44+
name="Referenced",
45+
geometries=pygdtf_module.Geometries([referenced_child]),
46+
)
47+
root.geometries = pygdtf_module.Geometries(
48+
[pygdtf_module.GeometryReference(name="Ref", geometry="Referenced")]
49+
)
50+
51+
geometries = pygdtf_module.Geometries([root, referenced])
52+
fixture_type = type("Fixture", (), {})()
53+
fixture_type.geometries = geometries
54+
fixture_type.dmx_modes = pygdtf_module.DmxModes(
55+
[pygdtf_module.DmxMode(name="Default", geometry="Root")]
56+
)
57+
58+
tree = geometries.get_geometry_tree(fixture_type)
59+
60+
assert tree.name == "Root"
61+
assert [child.name for child in tree.geometries] == ["Ref"]
62+
assert [child.name for child in tree.geometries[0].geometries] == ["RefChild"]
63+
64+
65+
def test_get_geometry_tree_expands_references_from_files(pygdtf_module):
66+
test_fixture_test_file = Path(Path(__file__).parents[0], "test2.xml").as_posix()
67+
fixture = pygdtf_module.FixtureType(dsc_file=test_fixture_test_file)
68+
tree = fixture.geometries.get_geometry_tree(fixture, mode_name="Mode 1 - Wash")
69+
70+
pixel = _find_geometry(tree, "Pixel 1")
71+
72+
assert pixel is not None
73+
assert isinstance(pixel, pygdtf_module.GeometryReference)
74+
assert [child.name for child in pixel.geometries] == ["Patt cross 1"]
75+
76+
77+
def test_as_dict_uses_mode_name_for_root(pygdtf_module):
78+
root = pygdtf_module.Geometry(name="Root")
79+
referenced_child = pygdtf_module.Geometry(name="RefChild")
80+
referenced = pygdtf_module.Geometry(
81+
name="Referenced",
82+
geometries=pygdtf_module.Geometries([referenced_child]),
83+
)
84+
root.geometries = pygdtf_module.Geometries(
85+
[pygdtf_module.GeometryReference(name="Ref", geometry="Referenced")]
86+
)
87+
88+
geometries = pygdtf_module.Geometries([root, referenced])
89+
fixture_type = type("Fixture", (), {})()
90+
fixture_type.geometries = geometries
91+
fixture_type.dmx_modes = pygdtf_module.DmxModes(
92+
[pygdtf_module.DmxMode(name="Default", geometry="Root")]
93+
)
94+
95+
tree = geometries.as_dict(None, fixture_type, mode_name="Default")
96+
97+
assert tree["name"] == "Root"
98+
assert [child["name"] for child in tree["children"]] == ["Ref"]
99+
assert [child["name"] for child in tree["children"][0]["children"]] == ["RefChild"]

0 commit comments

Comments
 (0)