From 03dfbac194a7eaf10bc4ae44e668ec91391e8815 Mon Sep 17 00:00:00 2001 From: vanous Date: Sun, 20 Jul 2025 13:45:42 +0200 Subject: [PATCH 01/16] Implement to_xml for AUXData and its children --- pymvr/__init__.py | 41 ++++++++++++++++++++++++++-- pymvr/value.py | 3 +- tests/test_example.py | 1 - tests/test_mvr_01_write_ours_json.py | 1 - 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index 7b8952d..04f48f5 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -1,4 +1,4 @@ -from typing import List, Union, Optional +from typing import List, Union from xml.etree import ElementTree from xml.etree.ElementTree import Element import zipfile @@ -81,7 +81,7 @@ def write_mvr(self, path=None): for file_path, file_name in self.files_list: try: z.write(file_path, arcname=file_name) - except Exception as e: + except Exception: print(f"File does not exist {file_path}") @@ -283,6 +283,12 @@ def to_xml(self, parent: Element): element = ElementTree.SubElement(parent, type(self).__name__) for _class in self.classes: element.append(_class.to_xml()) + for symdef in self.symdefs: + element.append(symdef.to_xml()) + for position in self.positions: + element.append(position.to_xml()) + for mapping_definition in self.mapping_definitions: + element.append(mapping_definition.to_xml()) return element @@ -313,6 +319,13 @@ def _read_xml(self, xml_node: "Element"): self.source = xml_node.find("Source") # TODO self.scale_handling = xml_node.find("ScaleHandeling").text # TODO ENUM + def to_xml(self): + element = ElementTree.Element(type(self).__name__, name=self.name, uuid=self.uuid) + ElementTree.SubElement(element, "SizeX").text = str(self.size_x) + ElementTree.SubElement(element, "SizeY").text = str(self.size_y) + # TODO source and scale_handling + return element + class Fixture(BaseChildNode): def __init__( @@ -640,6 +653,10 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.name}" + def to_xml(self): + element = ElementTree.Element(type(self).__name__, name=self.name, uuid=self.uuid) + return element + class Symdef(BaseNode): def __init__( @@ -674,6 +691,14 @@ def _read_xml(self, xml_node: "Element"): # sometimes the list of geometry3d is full of duplicates, eliminate them here self.geometry3d = list(set(_geometry3d)) + def to_xml(self): + element = ElementTree.Element(type(self).__name__, name=self.name, uuid=self.uuid) + for geo in self.geometry3d: + element.append(geo.to_xml()) + for sym in self.symbol: + element.append(sym.to_xml()) + return element + class Geometry3D(BaseNode): def __init__( @@ -707,6 +732,11 @@ def __ne__(self, other): def __hash__(self): return hash((self.file_name, str(self.matrix))) + def to_xml(self): + element = ElementTree.Element(type(self).__name__, fileName=self.file_name) + Matrix(self.matrix.matrix).to_xml(parent=element) + return element + class Symbol(BaseNode): def __init__( @@ -731,6 +761,11 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.uuid}" + def to_xml(self): + element = ElementTree.Element(type(self).__name__, uuid=self.uuid, symdef=self.symdef) + Matrix(self.matrix.matrix).to_xml(parent=element) + return element + class Geometries(BaseNode): def __init__( @@ -1039,4 +1074,4 @@ def _read_xml(self, xml_node: "Element"): self.type_ = xml_node.attrib.get("type") def __str__(self): - return f"{self.linked_geometry} {self.type_}" + return f"{self.linked_geometry} {self.type_}" \ No newline at end of file diff --git a/pymvr/value.py b/pymvr/value.py index 702c3ec..cc59e68 100644 --- a/pymvr/value.py +++ b/pymvr/value.py @@ -1,6 +1,5 @@ from typing import List, Union from xml.etree import ElementTree -from xml.etree.ElementTree import Element # Data type that only allows a specific set of values, if given a value @@ -39,7 +38,7 @@ def __init__( self.x = float(str_repr.split(",")[0]) self.y = float(str_repr.split(",")[1]) self.Y = float(str_repr.split(",")[2]) - except: + except Exception: # Fail gracefully with default color (White) self.x = 0.3127 self.y = 0.3290 diff --git a/tests/test_example.py b/tests/test_example.py index 7efb073..9359c85 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -1,4 +1,3 @@ -from pathlib import Path import pymvr diff --git a/tests/test_mvr_01_write_ours_json.py b/tests/test_mvr_01_write_ours_json.py index 0af6e5b..b0e26fc 100644 --- a/tests/test_mvr_01_write_ours_json.py +++ b/tests/test_mvr_01_write_ours_json.py @@ -1,4 +1,3 @@ -import pytest from pathlib import Path import pymvr From 358a7ef96f99a2fd37deff67a4776516520f7ea4 Mon Sep 17 00:00:00 2001 From: vanous Date: Sun, 20 Jul 2025 14:09:39 +0200 Subject: [PATCH 02/16] Add missing features to Fixture class --- pymvr/__init__.py | 51 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index 04f48f5..7725b49 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -190,7 +190,8 @@ def _read_xml(self, xml_node: "Element"): self.custom_id_type = int(xml_node.find("CustomIdType").text or 0) if xml_node.find("CastShadow") is not None: - self.cast_shadow = bool(xml_node.find("CastShadow").text) + text_value = (xml_node.find("CastShadow").text or "false").lower() + self.cast_shadow = text_value in ("true", "1") if xml_node.find("Addresses") is not None: self.addresses = [Address(xml_node=i) for i in xml_node.find("Addresses").findall("Address")] @@ -370,10 +371,12 @@ def _read_xml(self, xml_node: "Element"): self.color = Color(str_repr=xml_node.find("Color").text) if xml_node.find("DMXInvertPan") is not None: - self.dmx_invert_pan = bool(xml_node.find("DMXInvertPan").text) + text_value = (xml_node.find("DMXInvertPan").text or "false").lower() + self.dmx_invert_pan = text_value in ("true", "1") if xml_node.find("DMXInvertTilt") is not None: - self.dmx_invert_tilt = bool(xml_node.find("DMXInvertTilt").text) + text_value = (xml_node.find("DMXInvertTilt").text or "false").lower() + self.dmx_invert_tilt = text_value in ("true", "1") if xml_node.find("Position") is not None: self.position = xml_node.find("Position").text @@ -389,7 +392,7 @@ def _read_xml(self, xml_node: "Element"): if xml_node.find("Mappings") is not None: self.mappings = [Mapping(xml_node=i) for i in xml_node.find("Mappings").findall("Mapping")] if xml_node.find("Gobo") is not None: - self.gobo = Gobo(xml_node.attrib.get("Gobo")) + self.gobo = Gobo(xml_node=xml_node.find("Gobo")) def to_xml(self): fixture_element = ElementTree.Element(type(self).__name__, name=self.name, uuid=self.uuid) @@ -400,6 +403,12 @@ def to_xml(self): if self.focus is not None: ElementTree.SubElement(fixture_element, "Focus").text = self.focus + if self.cast_shadow: + ElementTree.SubElement(fixture_element, "CastShadow").text = "true" + + if self.position is not None: + ElementTree.SubElement(fixture_element, "Position").text = self.position + ElementTree.SubElement(fixture_element, "FixtureID").text = str(self.fixture_id) or "0" ElementTree.SubElement(fixture_element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) ElementTree.SubElement(fixture_element, "UnitNumber").text = str(self.unit_number) @@ -417,6 +426,15 @@ def to_xml(self): addresses = ElementTree.SubElement(fixture_element, "Addresses") for address in self.addresses: Address(address.dmx_break, address.universe, address.address).to_xml(addresses) + + if self.mappings: + mappings_element = ElementTree.SubElement(fixture_element, "Mappings") + for mapping in self.mappings: + mappings_element.append(mapping.to_xml()) + + if self.gobo: + fixture_element.append(self.gobo.to_xml()) + return fixture_element def __str__(self): @@ -990,7 +1008,7 @@ def __init__( uy: Union[int, None] = None, ox: Union[int, None] = None, oy: Union[int, None] = None, - rz: Union[int, None] = None, + rz: Union[float, None] = None, *args, **kwargs, ): @@ -1008,11 +1026,25 @@ def _read_xml(self, xml_node: "Element"): self.uy = int(xml_node.find("uy").text) self.ox = int(xml_node.find("ox").text) self.oy = int(xml_node.find("oy").text) - self.rz = int(xml_node.find("rz").text) + self.rz = float(xml_node.find("rz").text) def __str__(self): return f"{self.link_def}" + def to_xml(self): + element = ElementTree.Element(type(self).__name__, linkedDef=self.link_def) + if self.ux is not None: + ElementTree.SubElement(element, "ux").text = str(self.ux) + if self.uy is not None: + ElementTree.SubElement(element, "uy").text = str(self.uy) + if self.ox is not None: + ElementTree.SubElement(element, "ox").text = str(self.ox) + if self.oy is not None: + ElementTree.SubElement(element, "oy").text = str(self.oy) + if self.rz is not None: + ElementTree.SubElement(element, "rz").text = str(self.rz) + return element + class Gobo(BaseNode): def __init__( @@ -1033,6 +1065,11 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.filename} {self.rotation}" + def to_xml(self): + element = ElementTree.Element(type(self).__name__, rotation=str(self.rotation)) + element.text = self.filename + return element + class CustomCommand(BaseNode): # TODO: split more: Body_Pan,f 50 @@ -1074,4 +1111,4 @@ def _read_xml(self, xml_node: "Element"): self.type_ = xml_node.attrib.get("type") def __str__(self): - return f"{self.linked_geometry} {self.type_}" \ No newline at end of file + return f"{self.linked_geometry} {self.type_}" From 795804d1d7033f1fe8f7bf932f7d230eef9a4ae5 Mon Sep 17 00:00:00 2001 From: vanous Date: Sun, 20 Jul 2025 15:04:31 +0200 Subject: [PATCH 03/16] Align MVR classes with specification v1.6 --- pymvr/__init__.py | 98 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 27 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index 7725b49..e4a5f94 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -121,7 +121,6 @@ def __init__( fixture_id: Union[str, None] = None, fixture_id_numeric: int = 0, unit_number: int = 0, - fixture_type_id: int = 0, custom_id: int = 0, custom_id_type: int = 0, cast_shadow: bool = False, @@ -145,7 +144,6 @@ def __init__( self.fixture_id = fixture_id self.fixture_id_numeric = fixture_id_numeric self.unit_number = unit_number - self.fixture_type_id = fixture_type_id self.custom_id = custom_id self.custom_id_type = custom_id_type self.cast_shadow = cast_shadow @@ -180,9 +178,6 @@ def _read_xml(self, xml_node: "Element"): if xml_node.find("UnitNumber") is not None: self.unit_number = int(xml_node.find("UnitNumber").text) - if xml_node.find("FixtureTypeId") is not None: - self.fixture_type_id = int(xml_node.find("FixtureTypeId").text or 0) - if xml_node.find("CustomId") is not None: self.custom_id = int(xml_node.find("CustomId").text or 0) @@ -382,7 +377,7 @@ def _read_xml(self, xml_node: "Element"): self.position = xml_node.find("Position").text if xml_node.find("Function") is not None: - self.function_ = xml_node.find("Position").text + self.function_ = xml_node.find("Function").text if xml_node.find("ChildPosition") is not None: self.child_position = xml_node.find("ChildPosition").text @@ -395,45 +390,80 @@ def _read_xml(self, xml_node: "Element"): self.gobo = Gobo(xml_node=xml_node.find("Gobo")) def to_xml(self): - fixture_element = ElementTree.Element(type(self).__name__, name=self.name, uuid=self.uuid) + attributes = {"name": self.name, "uuid": self.uuid} + if self.multipatch: + attributes["multipatch"] = self.multipatch + fixture_element = ElementTree.Element(type(self).__name__, **attributes) Matrix(self.matrix.matrix).to_xml(fixture_element) - ElementTree.SubElement(fixture_element, "GDTFSpec").text = self.gdtf_spec - ElementTree.SubElement(fixture_element, "GDTFMode").text = self.gdtf_mode - if self.focus is not None: + if self.classing: + ElementTree.SubElement(fixture_element, "Classing").text = self.classing + if self.gdtf_spec: + ElementTree.SubElement(fixture_element, "GDTFSpec").text = self.gdtf_spec + if self.gdtf_mode: + ElementTree.SubElement(fixture_element, "GDTFMode").text = self.gdtf_mode + if self.focus: ElementTree.SubElement(fixture_element, "Focus").text = self.focus - if self.cast_shadow: ElementTree.SubElement(fixture_element, "CastShadow").text = "true" - - if self.position is not None: + if self.dmx_invert_pan: + ElementTree.SubElement(fixture_element, "DMXInvertPan").text = "true" + if self.dmx_invert_tilt: + ElementTree.SubElement(fixture_element, "DMXInvertTilt").text = "true" + if self.position: ElementTree.SubElement(fixture_element, "Position").text = self.position + if self.function_: + ElementTree.SubElement(fixture_element, "Function").text = self.function_ + if self.child_position: + ElementTree.SubElement(fixture_element, "ChildPosition").text = self.child_position ElementTree.SubElement(fixture_element, "FixtureID").text = str(self.fixture_id) or "0" ElementTree.SubElement(fixture_element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) ElementTree.SubElement(fixture_element, "UnitNumber").text = str(self.unit_number) - if self.classing is not None: - ElementTree.SubElement(fixture_element, "Classing").text = str(self.classing) - if self.custom_id: - ElementTree.SubElement(fixture_element, "CustomId").text = str(self.custom_id) - if self.custom_id_type: - ElementTree.SubElement(fixture_element, "CustomIdType").text = str(self.custom_id_type) + + if self.addresses: + addresses_element = ElementTree.SubElement(fixture_element, "Addresses") + for address in self.addresses: + address.to_xml(addresses_element) + if self.protocols: + protocols_element = ElementTree.SubElement(fixture_element, "Protocols") + for protocol in self.protocols: + protocols_element.append(protocol.to_xml()) + if self.alignments: + alignments_element = ElementTree.SubElement(fixture_element, "Alignments") + for alignment in self.alignments: + alignments_element.append(alignment.to_xml()) + if self.custom_commands: + commands_element = ElementTree.SubElement(fixture_element, "CustomCommands") + for command in self.custom_commands: + commands_element.append(command.to_xml()) + if self.overwrites: + overwrites_element = ElementTree.SubElement(fixture_element, "Overwrites") + for overwrite in self.overwrites: + overwrites_element.append(overwrite.to_xml()) + if self.connections: + connections_element = ElementTree.SubElement(fixture_element, "Connections") + for connection in self.connections: + connections_element.append(connection.to_xml()) + if isinstance(self.color, Color): self.color.to_xml(fixture_element) - else: + elif self.color: Color(str_repr=self.color).to_xml(fixture_element) - addresses = ElementTree.SubElement(fixture_element, "Addresses") - for address in self.addresses: - Address(address.dmx_break, address.universe, address.address).to_xml(addresses) + if self.custom_id_type: + ElementTree.SubElement(fixture_element, "CustomIdType").text = str(self.custom_id_type) + if self.custom_id: + ElementTree.SubElement(fixture_element, "CustomId").text = str(self.custom_id) if self.mappings: mappings_element = ElementTree.SubElement(fixture_element, "Mappings") for mapping in self.mappings: mappings_element.append(mapping.to_xml()) - if self.gobo: fixture_element.append(self.gobo.to_xml()) + if self.child_list: + self.child_list.to_xml(fixture_element) return fixture_element @@ -810,6 +840,10 @@ def _read_xml(self, xml_node: "Element"): def to_xml(self, parent: Element): element = ElementTree.SubElement(parent, type(self).__name__) + for geo in self.geometry3d: + element.append(geo.to_xml()) + for sym in self.symbol: + element.append(sym.to_xml()) return element @@ -828,6 +862,8 @@ def __init__( self.uuid = uuid self.matrix = matrix self.classing = classing + if geometries is None: + geometries = Geometries() self.geometries = geometries super().__init__(*args, **kwargs) @@ -848,7 +884,9 @@ def __str__(self): def to_xml(self): element = ElementTree.Element(type(self).__name__, name=self.name, uuid=self.uuid) Matrix(self.matrix.matrix).to_xml(parent=element) - Geometries().to_xml(parent=element) + if self.classing: + ElementTree.SubElement(element, "Classing").text = self.classing + self.geometries.to_xml(parent=element) return element @@ -1051,12 +1089,13 @@ def __init__( self, rotation: Union[str, float, None] = None, filename: Union[str, None] = None, + xml_node: "Element" = None, *args, **kwargs, ): self.rotation = rotation self.filename = filename - super().__init__(*args, **kwargs) + super().__init__(xml_node, *args, **kwargs) def _read_xml(self, xml_node: "Element"): self.rotation = float(xml_node.attrib.get("rotation", 0)) @@ -1088,6 +1127,11 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.custom_command}" + def to_xml(self): + element = ElementTree.Element(type(self).__name__) + element.text = self.custom_command + return element + class Projections(BaseNode): ... @@ -1111,4 +1155,4 @@ def _read_xml(self, xml_node: "Element"): self.type_ = xml_node.attrib.get("type") def __str__(self): - return f"{self.linked_geometry} {self.type_}" + return f"{self.linked_geometry} {self.type_}" \ No newline at end of file From f8ff37c5d44ba5e36c71d3ebf760aae105f48e67 Mon Sep 17 00:00:00 2001 From: vanous Date: Sun, 20 Jul 2025 15:09:08 +0200 Subject: [PATCH 04/16] Improve Sources and VideoScreen class structure --- pymvr/__init__.py | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index e4a5f94..f789217 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -924,7 +924,8 @@ def __init__( super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): - if xml_node.find("Sources") is None: + super()._read_xml(xml_node) + if xml_node.find("Sources") is not None: self.sources = Sources(xml_node=xml_node.find("Sources")) @@ -1138,21 +1139,56 @@ class Projections(BaseNode): # todo -class Sources(BaseNode): +class Source(BaseNode): def __init__( self, linked_geometry: Union[str, None] = None, type_: Union[str, None] = None, + value: Union[str, None] = None, + xml_node: "Element" = None, *args, **kwargs, ): self.linked_geometry = linked_geometry self.type_ = type_ - super().__init__(*args, **kwargs) + self.value = value + super().__init__(xml_node, *args, **kwargs) def _read_xml(self, xml_node: "Element"): self.linked_geometry = xml_node.attrib.get("linkedGeometry") self.type_ = xml_node.attrib.get("type") + self.value = xml_node.text def __str__(self): - return f"{self.linked_geometry} {self.type_}" \ No newline at end of file + return f"{self.linked_geometry} {self.type_}" + + def to_xml(self): + attributes = {} + if self.linked_geometry: + attributes["linkedGeometry"] = self.linked_geometry + if self.type_: + attributes["type"] = self.type_ + element = ElementTree.Element(type(self).__name__, **attributes) + element.text = self.value + return element + + +class Sources(BaseNode): + def __init__( + self, + sources: List["Source"] = [], + xml_node: "Element" = None, + *args, + **kwargs, + ): + self.sources = sources + super().__init__(xml_node, *args, **kwargs) + + def _read_xml(self, xml_node: "Element"): + self.sources = [Source(xml_node=i) for i in xml_node.findall("Source")] + + def to_xml(self, parent: Element): + element = ElementTree.SubElement(parent, type(self).__name__) + for source in self.sources: + element.append(source.to_xml()) + return element From 1d069bfd64d8d5332939620bf8e93ce8902d03ec Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 00:11:46 +0200 Subject: [PATCH 05/16] Reorganize structure to follow the original XML, this is breaking change --- .gitignore | 2 + README.md | 27 +- conftest.py | 2 +- pymvr/__init__.py | 171 +- spec/mvr-spec.md | 1793 +++++++++++++++++ tests/test_example.py | 20 - tests/test_fixture_1_5.py | 2 +- tests/test_fixture_scene_object_1_5.py | 4 +- tests/test_group_objects_1_4.py | 6 +- tests/test_mvr_01_write_ours.py | 23 +- tests/test_mvr_02_read_ours.py | 3 +- ...json.py => test_mvr_03_write_ours_json.py} | 33 +- ..._json.py => test_mvr_04_read_ours_json.py} | 4 +- tests/test_mvr_05_example.py | 26 + 14 files changed, 2013 insertions(+), 103 deletions(-) create mode 100644 spec/mvr-spec.md delete mode 100644 tests/test_example.py rename tests/{test_mvr_01_write_ours_json.py => test_mvr_03_write_ours_json.py} (65%) rename tests/{test_mvr_02_read_ours_json.py => test_mvr_04_read_ours_json.py} (90%) create mode 100644 tests/test_mvr_05_example.py diff --git a/.gitignore b/.gitignore index d88893d..bc206cc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ __pycache__ pymvr.egg-info/ devel/ *whl +*mvr +uv.lock diff --git a/README.md b/README.md index 1247961..b1faad2 100644 --- a/README.md +++ b/README.md @@ -34,31 +34,38 @@ python -m pip install https://codeload.github.com/open-stage/python-mvr/zip/refs ```python import pymvr -mvr_scene = pymvr.GeneralSceneDescription("mvr_file.mvr") +mvr_file = pymvr.GeneralSceneDescription("mvr_file.mvr") -for layer_index, layer in enumerate(mvr_scene.layers): +for layer_index, layer in enumerate(mvr_file.scene.layers): ... #process data ``` ### Writing ```python -fixtures_list = [] + mvr = pymvr.GeneralSceneDescriptionWriter() pymvr.UserData().to_xml(parent=mvr.xml_root) -scene = pymvr.SceneElement().to_xml(parent=mvr.xml_root) -layers = pymvr.LayersElement().to_xml(parent=scene) -layer = pymvr.Layer(name="Test layer").to_xml(parent=layers) -child_list = pymvr.ChildList().to_xml(parent=layer) +scene = pymvr.Scene().to_xml(parent=mvr.xml_root) +pymvr.AUXData().to_xml(parent=scene) +fixtures_list = [] + +layers = pymvr.Layers() +layer = pymvr.Layer(name="Test layer") +layers.layers.append(layer) + +child_list = pymvr.ChildList() +layer.child_list = child_list fixture = pymvr.Fixture(name="Test Fixture") # not really a valid fixture -child_list.append(fixture.to_xml()) +child_list.fixtures.append(fixture) fixtures_list.append((fixture.gdtf_spec, fixture.gdtf_spec)) -pymvr.AUXData().to_xml(parent=scene) +layers.to_xml(parent=scene) mvr.files_list = list(set(fixtures_list)) -mvr.write_mvr("example.mvr") +test_file_path = Path(Path(__file__).parent, "example.mvr") +mvr.write_mvr(test_file_path) ``` See [BlenderDMX](https://github.com/open-stage/blender-dmx) and diff --git a/conftest.py b/conftest.py index f89dab7..3b47c0a 100644 --- a/conftest.py +++ b/conftest.py @@ -15,7 +15,7 @@ def mvr_scene(request): yield mvr_scene -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") def pymvr_module(): yield pymvr diff --git a/pymvr/__init__.py b/pymvr/__init__.py index f789217..0e6fcde 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -26,8 +26,6 @@ def __init__(self, path=None): self._package = zipfile.ZipFile(path, "r") if self._package is not None: self._root = _find_root(self._package) - self._user_data = self._root.find("UserData") - self._scene = self._root.find("Scene") if self._root is not None: self._read_xml() @@ -37,21 +35,14 @@ def _read_xml(self): self.provider: str = self._root.get("provider", "") self.providerVersion: str = self._root.get("providerVersion", "") - layers_collect = self._scene.find("Layers") - if layers_collect is not None: - self.layers: List["Layer"] = [Layer(xml_node=i) for i in layers_collect.findall("Layer")] - else: - self.layers = [] - - aux_data_collect = self._scene.find("AUXData") + scene = self._root.find("Scene") + if scene is not None: + self.scene = Scene(xml_node=scene) - if aux_data_collect is not None: - self.aux_data = AUXData(xml_node=aux_data_collect) - else: - self.aux_data = None + user_data = self._root.find("UserData") - if self._user_data is not None: - self.user_data: List["Data"] = [Data(xml_node=i) for i in self._user_data.findall("Data")] + if user_data is not None: + self.user_data = UserData(xml_node=user_data) class GeneralSceneDescriptionWriter: @@ -85,28 +76,114 @@ def write_mvr(self, path=None): print(f"File does not exist {file_path}") -class SceneElement: - def to_xml(self, parent: Element): - return ElementTree.SubElement(parent, "Scene") +class BaseNode: + def __init__(self, xml_node: "Element" = None): + if xml_node is not None: + self._read_xml(xml_node) + def _read_xml(self, xml_node: "Element"): + pass + + +class Scene(BaseNode): + def __init__( + self, + layers: "Layers" = None, + aux_data: "AUXData" = None, + xml_node: "Element" = None, + *args, + **kwargs, + ): + self.layers = layers + self.aux_data = aux_data + super().__init__(xml_node, *args, **kwargs) + + def _read_xml(self, xml_node: "Element"): + self.layers = Layers(xml_node=xml_node.find("Layers")) + + aux_data_collect = xml_node.find("AUXData") + + if aux_data_collect is not None: + self.aux_data = AUXData(xml_node=aux_data_collect) + else: + self.aux_data = None -class LayersElement: def to_xml(self, parent: Element): - return ElementTree.SubElement(parent, "Layers") + element = ElementTree.SubElement(parent, "Scene") + if self.layers: + self.layers.to_xml(element) + if self.aux_data: + self.aux_data.to_xml(element) + return element -class UserData: +class Layers(BaseNode): + def __init__( + self, + layers: List["Layer"] = [], + xml_node: "Element" = None, + *args, + **kwargs, + ): + self.layers = layers + super().__init__(xml_node, *args, **kwargs) + + def _read_xml(self, xml_node: "Element"): + self.layers = [Layer(xml_node=i) for i in xml_node.findall("Layer")] + def to_xml(self, parent: Element): - return ElementTree.SubElement(parent, "UserData") + element = ElementTree.SubElement(parent, "Layers") + for layer in self.layers: + element.append(layer.to_xml()) + return element + def __iter__(self): + return iter(self.layers) + + def __getitem__(self, item): + return self.layers[item] + + def append(self, layer: "Layer"): + self.layers.append(layer) + + def extend(self, layers: List["Layer"]): + self.layers.extend(layers) + + def insert(self, index: int, layer: "Layer"): + self.layers.insert(index, layer) + + def remove(self, layer: "Layer"): + self.layers.remove(layer) + + def pop(self, index: int = -1): + return self.layers.pop(index) + + def clear(self): + self.layers.clear() + + def __len__(self): + return len(self.layers) -class BaseNode: - def __init__(self, xml_node: "Element" = None): - if xml_node is not None: - self._read_xml(xml_node) + +class UserData(BaseNode): + def __init__( + self, + data: List["Data"] = [], + xml_node: "Element" = None, + *args, + **kwargs, + ): + self.data = data + super().__init__(xml_node, *args, **kwargs) def _read_xml(self, xml_node: "Element"): - pass + self.data = [Data(xml_node=i) for i in xml_node.findall("Data")] + + def to_xml(self, parent: Element): + element = ElementTree.SubElement(parent, type(self).__name__) + for _data in self.data: + element.append(_data.to_xml()) + return element class BaseChildNode(BaseNode): @@ -252,6 +329,10 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.provider} {self.ver}" + def to_xml(self): + attributes = {"name": self.name, "uuid": self.uuid} + return ElementTree.Element(type(self).__name__, provider=self.provider, ver=self.ver) + class AUXData(BaseNode): def __init__( @@ -502,6 +583,15 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.name}" + def to_xml(self): + element = ElementTree.Element(type(self).__name__, name=self.name, uuid=self.uuid) + Matrix(self.matrix.matrix).to_xml(parent=element) + if self.classing: + ElementTree.SubElement(element, "Classing").text = self.classing + if self.child_list: + self.child_list.to_xml(parent=element) + return element + class ChildList(BaseNode): def __init__( @@ -576,7 +666,24 @@ def _read_xml(self, xml_node: "Element"): self.projectors = [Projector(xml_node=i) for i in xml_node.findall("Projector")] def to_xml(self, parent: Element): - return ElementTree.SubElement(parent, type(self).__name__) + element = ElementTree.SubElement(parent, type(self).__name__) + for fixture in self.fixtures: + element.append(fixture.to_xml()) + for focus_point in self.focus_points: + element.append(focus_point.to_xml()) + for group_object in self.group_objects: + element.append(group_object.to_xml()) + for scene_object in self.scene_objects: + element.append(scene_object.to_xml()) + for support in self.supports: + element.append(support.to_xml()) + for truss in self.trusses: + element.append(truss.to_xml()) + for video_screen in self.video_screens: + element.append(video_screen.to_xml()) + for projector in self.projectors: + element.append(projector.to_xml()) + return element class Layer(BaseNode): @@ -606,8 +713,12 @@ def _read_xml(self, xml_node: "Element"): if xml_node.find("Matrix") is not None: self.matrix = Matrix(str_repr=xml_node.find("Matrix").text) - def to_xml(self, parent: Element): - return ElementTree.SubElement(parent, type(self).__name__, name=self.name, uuid=self.uuid) + def to_xml(self): + element = ElementTree.Element(type(self).__name__, name=self.name, uuid=self.uuid) + Matrix(self.matrix.matrix).to_xml(parent=element) + if self.child_list: + self.child_list.to_xml(parent=element) + return element def __str__(self): return f"{self.name}" diff --git a/spec/mvr-spec.md b/spec/mvr-spec.md new file mode 100644 index 0000000..c21644e --- /dev/null +++ b/spec/mvr-spec.md @@ -0,0 +1,1793 @@ +MVR Version 1.6 - DIN SPEC 15801 + +# Introduction + +MVR - My Virtual Rig - specified in this DIN SPEC will unify the information exchange between different applications within the entertainment industry. Based on GDTF, as specified in DIN SPEC 15800, MVR allows the exchange of scenic and environmental information and complete show setups as planning status. Furthermore the MVR file format allows programs to share data and geometry for a scene. A scene is a set of parametric objects such as fixtures, trusses, video screens, and other objects that are used secifically in the entertainment industry. + +Typical workflow +1. Program A saves an MVR file containing a scene; +2. Program B imports this file; +3. Program B changes some parametric data in the scene; +4. Program B saves an MVR containing the scene; +5. Program A imports this file and applies the changes to the existing objects. + + +# Scope + +This document specifies "My Virtual Rig" (MVR), which is designed to provide a unified way of listing and describing the hierarchical and logical structure based on DIN SPEC 15800 "General Device Type Format" (GDTF) - and further environmental information of a show setup in the entertainment business. It will be used as a foundation for the exchange of extended device and environmental data between lighting consoles, CAD and 3D-pre-visualization applications. The purpose of an MVR-file is to reflect the real-world physical components of a show setup and the logical patch information of the devices while maintaining the kinematic chain. + + +# Normative references + +The following documents are referred to in the text in such a way that some or all of their content constitutes +requirements of this document. For dated references, only the edition cited applies. For undated references, +the latest edition of the referenced document (including any amendments) applies. + +- [DIN SPEC 15800:2022-02, Entertainment Technology— General Device Type Format (GDTF)](https://www.beuth.de/en/technical-rule/din-spec-15800/349717520) +- [ISO/IEC 21778:2017, Information technology— The JSON data interchange syntax](https://standards.iso.org/ittf/PubliclyAvailableStandards/c071616_ISO_IEC_21778_2017.zip) +- [Extensible Markup Language (XML) 1.0](https://www.w3.org/TR/2008/REC-xml-20081126/) +- [PKWARE 6.3.3](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) +- [Domain Names— Implementation and Specification](https://www.ietf.org/rfc/rfc1035.txt) +- [RFC3629, UTF-8, a transformation format of ISO 10646](https://datatracker.ietf.org/doc/html/rfc3629) +- [RFC4122, A Universally Unique IDentifier (UUID) URN Namespace](https://www.rfc-editor.org/rfc/rfc4122) +- [RFC6455, The WebSocket Protocol](https://www.ietf.org/rfc/rfc6455.txt) +- [RFC6762, Multicast DNS](https://www.ietf.org/rfc/rfc6762.txt) +- [RFC6763, DNS-Based Service Discovery](https://www.ietf.org/rfc/rfc6763.txt) + + +# Terms and definitions + +For the purposes of this document, the following terms and definitions apply. +DIN and DKE maintain terminological databases for use in standardization at the following addresses: + + - DIN-TERMinologieportal: available at + + - DKE-IEV: available at + + +### My Virtual Rig, MVR +descriptive name of the specification + +### MVR-xchange +protocol to share MVR files over the network + +### MVR-xchange client +application that participates in the MVR-xchange + +### MVR-xchange group +group of MVR-xchange clients that work on the same project and communicate together + +### TCP Mode +MVR-xchange communication via TCP packages and discovery via mDNS + +### WebSocket Mode +MVR-xchange communication via WebSockets and discovery via DNS + + + +# MVR Definitions + +## General + +MVR consists of two parts to enable any application to exchange GDTF but also general information in the same +common format. Firstly the MVR file format as described in the following section. Secondly a MVR communication format to simplify exchange between applications. + + +## File Format Definition + +To describe all information within one file, a zip file according to PKWARE 6.3.3 with the extension `*.mvr` is used. The archive shall contain one root file named `GeneralSceneDescription.xml`, along with all other resource files referenced via this Root File. The root file `GeneralSceneDescription.xml` is mandatory inside the archive to be a valid MVR file. + +UTF-8 according to RFC3629 has to be used to encode the XML file. Each XML file internally consists of XML nodes. Each XML node could have XML attributes and XML node children. Each XML attribute has a value. If a XML attribute is not specified, the default value of this XML attribute will be used. If the XML attribute value is +specified as a string, the format of the string will depend on the XML attribute type. + +- The archive shall not use encryption or password protection. +- All files referenced by the Root File shall be placed at the root level. They shall not be placed in folders. +- Files shall be placed using either STORE (uncompressed) or DEFLATE compression. No other compression algorithms are supported. +- Files may be placed into the archive in any order. +- A `Universal.gdtt` GDTF template file with a `.gdtt` extension can be added to define Gobos, Emitters, Filter and such for referencing. +- Filenames within the archive must not differ only by case. Eg it is prohibited to have the files `GEO1.glb` and `geo1.glb` within the same archive. +- The file name of the ZIP archive can be chosen freely. + +All objects used have a persistent unique ID to track changes between the different applications. If there are no changes to the original GDTF file it is mandatory to keep it in the MVR during export. If there are changes to the GDTF file it is mandatory to add a revision to the GDTF file in order to reflect it. + +Only user-intended modifications of any part of the MVR file shall be processed. This is particular important if applications in the workflow do not need or accept all data of the MVR file. Such behaviour guarantees that all later steps in the workflow have access to the original intended data. + +- EXAMPLE An example of a typical MVR archive is shown below: + +``` +GeneralSceneDescription.xml +Custom@Fixture1.gdtf +Custom@Fixture2.gdtf +geo1.3ds +geo1.glb +Textr12.png +Universal.gdtt +``` + + +## Generic Value Types + +Table 1 contains a list of the available types for node or attribute values: + +##### Table 1 — *XML Generic Value Types* + +| Value Type Name | Description | +| ------------------ |--------------------------------------------------------------------------- | +| Integer | A signed or unsigned integer value represented in base 10. Uses a dash ‘-’ (U+002D) +as a prefix to denote negative numbers. E.g. `15` or `-6`| +| Float | A floating point numeric value represented in #attrType-Bool base 10 decimal or scientific format.
Uses full stop '.' (U+002E) to delimit the whole and decimal part and 'e' or 'E' to delimit mantissa and exponent.
Implementations shall write sufficient decimal places to precisely round-trip their internal level of precision.
Infinities and not-a-number (NaN) are not permitted.
Eg `1.5`, `3.9265e+2`| +| Bool | A boolean value. When representing `true` inidcate with true, when `false` indicate with false. | +| String | Any sequence of Unicode codepoints, encoded as necessary for XML.
Eg The following XML encodings (with their meaning in brackets):
`<` (\<), `&` (&), `>` (\>), `"` ("), and `'` (') | +| Enum | Possible values are predefined | +| UUID | A UUID to RFC4122 in text representation.
The nil UUID (all zeros) is not permitted.
Formatted as `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`.
Used to link objects. | +| Vector | Three Float values separated by ',' defining a 3D vector's X, Y, and Z components.
Eg `1.0,2.0,3.0`| +| FileName | The case-sensitive name of a file within the archive including the extension.
The filename must not contain any FAT32 or NTFS reserved characters.
The extension is delimited from the base name by full stop '.' and the base name shall not be empty.
It is recommended to limit filenames to the POSIX "Fully Portable Filenames" character set: [A-Z], [a-z], [0-9], the symbols '\_' (U+005F), '-' (U+002D) and a maximum of one '.' (U+002E)
Eg `My-Fixture_5.gdtf`| +| CIE Color | CIE 1931 xyY absolute color point.
Formatted as three Floats `x,y,Y`
Eg `0.314303,0.328065,87.699166`| +| IPv4 Address | Common IPv4 Address in the format of dotted decimal notation.
Eg `192.168.1.10` | +| IPv6 Address | Common IPv6 Address in the format of hexadecimal notation.
Eg `2001:0db8:85a3:0000:0000:8a2e:0370:7344` | + + +## Node Definiftions + +The first XML node is always the XML description node: `` + +## GeneralSceneDescription Node + +The second XML node is the mandatory GeneralSceneDescription node. The attributes of this node are listed in Table 2, the children of this node are given in Table 3. + +##### Table 2 — *GeneralSceneDescription Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------------- | --------------------------- | ------------------------------------------------------------------- | +| verMajor | [Integer](#user-content-attrtype-integer) | Not Optional | Denotes the major version of the format used when saving this file. | +| verMinor | [Integer](#user-content-attrtype-integer) | Not Optional | Denotes the minor version of the format used when saving this file. | +| provider | [String](#user-content-attrtype-string) | Not Optional | The name of the application that is generating the MVR export. This should stay the same between multiple version. | +| providerVersion| [String](#user-content-attrtype-string) | Not Optional | The version of the software that is generating the MVR export. This should be different for each version that is available. | + +The current version of MVR reflected by this document is 1.6. + + +##### Table 3 — *GeneralSceneDescription Node Children* + +| Child Node | Allowed Count | Description | +| ---------- | ------------- | ---------------------------------------------- | +| UserData | 0 or 1 | Specifies user data associated with this file. | +| Scene | 1 | Defines the scene described in this file. | + +## Node Definition: UserData + +### General + +This node contains a collection of user data nodes defined and used by provider applications if required. User data should not be expected to be preserved in the workflow of multiple applications importing and exporting the data. The defined UserData Node Children are specified in Table 4. + +Node name: `UserData` + +##### Table 4 — *UserData Node Children* + +| Child Node | Allowed Count | Description | +| ----------------------------- | ------------- | ----------------------------- | +| [Data](#node-definition-data) | 0 or many | Defines a block of user data. | + + +### Node Definition: Data + +This node contains a collection of data specified by the provider application. The defined Data Node Attributes are specified in Table 5. + +Node name: `Data` + +##### Table 5 — *Data Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------------------------------------------- | +| provider | [String](#user-content-attrtype-string) | Not Optional | Specifies the name of the provider application that created this data. | +| ver | [String](#user-content-attrtype-string) | 1 | Version information of the data as specified by the provider application. | + + +## Node Definition: Scene + +This node contains information about the scene. The defined Scene Node Children are specified in Table 6. + +Node name: `Scene` + +##### Table 6 — *Scene Node Children* + +| Child Node | Allowed Count | Description | +| ----------------------------------- | ------------- | ------------------------------------- | +| [AUXData](#node-definition-auxdata) | 0 or 1 | Defines auxiliary data for the scene. | +| [Layers](#node-definition-layers) | 1 | A list of layers in the scene. | + + +## Node Definition: AUXData + +### General + +This node contains auxiliary data for the scene node. The defined AUXData Node Children are specified in Table 7. + +Node name: `AUXData` + +##### Table 7 — *AUXData Node Children* + +| Child Node | Allowed Count | Description | +| ------------------------------------------------------- | ------------- | ---------------------------------------------------------------| +| [Symdef](#node-definition-symdef) | 0 or any | Graphical representation that will be instanced in the scene. | +| [Position](#node-definition-position) | 0 or any | Defines a logical group of lighting devices. | +| [MappingDefinition](#node-definition-mappingdefinition) | 0 or any | Defines a input source for fixture color mapping applications. | +| [Class](#node-definition-class) | 0 or any | Defines a Class for object visiblity filtering. | + + +### Node Definition: Symdef + +This node contains the graphics so the scene can refer to this, thus optimizing repetition of the geometry. The child objects are located within a local coordinate system. The defined Symdef Node Attributes are specified in Table 8. + +Node name: `Symdef` + +##### Table 8 — *Symdef Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object | + + +The Symdef node (Table 9) contains the following children. + +##### Table 9 — *Symdef Node Children* + +| Child Node | Allowed Count | Value Type | Description | +| ------------------------------------------------- | ------------- | ----------------------------------------- | --------------------------------------------------- | +| ChildList | 1 | [Integer](#user-content-attrtype-integer) | The size in x direction in pixels of the source. | + +The child list (Table 10) contains a list of the following nodes: + +##### Table 10 — *Symdef Childlist Node Children* + +| Child Node | Description | +| ----------------------------------------- | -------------------------------------------------------------------- | +| [Geometry3D](#node-definition-geometry3d) | The geometry of this definition that will be instanced in the scene. | +| [Symbol](#node-definition-symbol) | The symbol instance that will provide a geometry of this definition. | + + +### Node Definition: Position + +This node defines a logical grouping of lighting devices and trusses. The defined Position Node Attributes are specified in Table 11. + +Node name: `Position` + +##### Table 11 — *Position Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object | + + +### Node Definition: MappingDefinition + +This node specifies an input source for fixture color mapping applications. The defined MappingDefinition Node Attributes are specified in Table 12. + +Node name: `MappingDefinition` + +##### Table 12 — *MappingDefinition Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | --------------------------------------- | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | | The name of the source for the mapping. | + +The child list (Table 13) contains a list of the following nodes: + +##### Table 13 — *MappingDefinition Node Children* + +| Child Node | Allowed Count | Value Type | Description | +| ------------------------------------------------- | ------------- | ----------------------------------------- | --------------------------------------------------- | +| SizeX | 1 | [Integer](#user-content-attrtype-integer) | The size in x direction in pixels of the source. | +| SizeY | 1 | [Integer](#user-content-attrtype-integer) | The size in y direction in pixels of the source. | +| [Source](#node-definition-source) | 1 | | The video source that will be used for the Mapping. | +| [ScaleHandeling](#node-definition-scalehandeling) | 0 or 1 | | How the source will be scaled to the mapping. | + +```xml + + 1920 + 1080 + movie.mov + + UpScale + +``` + +### Node Definition: Class + +This node defines a logical grouping across different layers. Primarily used for controlling object visibility of objects across multiple Layers. The defined Class Node Attributes are specified in Table 14. + +Node name: `Class` + +##### Table 14 — *Class Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------- | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the class. | +| name | [String](#user-content-attrtype-string) | | The name of the Class. | + + +### Node Definition: Layers + +This node defines a list of layers inside the scene. The layer is a container of graphical objects defining a local coordinate system. + +Node name: `Layers` + +The child list (Table 15) contains a list of layer nodes: + +##### Table 15 — *Layers Node Childs* + +| Child Node | Description | +| ------------------------------- | ----------------------- | +| [Layer](#node-definition-layer) | A layer representation. | + + +#### Node Definition: Layer + +This node defines a layer. The layer is a spatial representation of a geometric container. The child objects are located inside a local coordinate system. The defined Layer Node Attributes are specified in Table 16. + +Node name: `Layer` + +##### Table 16 — *Layer Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object | + +The child list (Table 17) contains a list of layer nodes: + +##### Table 17 — *Layer Node Childs* + +| Child Node | Allowed Count | Description | +| --------------------------------------- | ------------- | -------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | The transformation matrix that defines the location and orientation of this the layer inside its global coordinate space. This effectively defines local coordinate space for the objects inside. The Matrix of the Layer is only allowed to have a vertical Transform (elevation). Rotation and scale must be identity. Rotation and scale must be identity, means no rotation and no scale. | +| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | + + +## Node Definition: ChildList + +This node defines a list of graphical objects. + +Node name: `ChildList` + +The child list (Table 18) contains a list of the following nodes: + +##### Table 18 — *ChildList Node Childs* + +| Child Node | Description | +| ------------------------------------------- | ---------------------------------------------------------------------------- | +| [SceneObject](#node-definition-sceneobject) | A generic graphical object from the scene. | +| [GroupObject](#node-definition-groupobject) | A grouping object of other graphical objects inside local coordinate system. | +| [FocusPoint](#node-definition-focuspoint) | A definition of a focus point. | +| [Fixture](#node-definition-fixture) | A definition of a fixture. | +| [Support](#node-definition-support) | A definition of a support. | +| [Truss](#node-definition-truss) | A definition of a truss. | +| [VideoScreen](#node-definition-videoscreen) | A definition of a video screen. | +| [Projector](#node-definition-projector) | A definition of a projector. | + + +## Node Definition for Parametric Objects + +### Node Definition: SceneObject + +This node defines a generic graphical object. The defined SceneObject Node Attributes are specified in Table 19. + +Node name: `SceneObject` + +##### Table 19 — *SceneObject Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object | +| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | + +The child list (Table 20) contains a list of one of the following nodes: + +##### Table 20 — *SceneObject Node Childs* + +| Child Node | Allowed Count | Value Type | Description | +| ------------------------------------------------- | ------------- | ------------------------------------------- | ----------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | | The location and orientation of the object inside the parent coordinate system. | +| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | +| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are part of the object. | +| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | +| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | +| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if an object cast shadows. | +| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object.| +| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | +| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object.| +| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | +| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | +| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | +| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The Custom ID is a value that can be used as a short name of the Fixture Instance. This does not have to be unique. The default value is 0. | +| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this object belongs to. | +| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | + + + +### Node Definition: GroupObject + +This node defines logical group of objects. The child objects are located inside a local coordinate system. The defined GroupObject Node Attributes are specified in Table 21. + +Node name: `GroupObject` + +##### Table 21 — *GroupObject Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object | + +The child list (Table 22) contains a list of one of the following nodes: + +##### Table 22 — *GroupObject Node Childs* + +| Child Node | Allowed Count | Value Type | Description | +| --------------------------------------- | ------------- | ----------------------------------- | ------------------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | | The location and orientation of the object inside the parent coordinate system. | +| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | +| [ChildList](#node-definition-childlist) | 0 or 1 | | A list of graphic objects that are part of the group. | + + +### Node Definition: FocusPoint + +This node defines a focus point object. The defined FocusPoint Node Attributes are specified in Table 23. + +Node name: `FocusPoint` + +##### Table 23 — *FocusPoint Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object | + +The child list (Table 24) contains a list of one of the following nodes: + +##### Table 24 — *FocusPoint Node Childs* + +| Child Node | Allowed Count | Value Type | Description | +| ----------------------------------------- | ------------- | ----------------------------------- | ------------------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | | The location and orientation of the object inside the parent coordinate system. | +| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | +| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are part of the object. | + + +### Node Definition: Fixture + +This node defines an entertainment fixture object. The defined FixtureNode Attributes are specified in Table 25. + +Node name: `Fixture` + +##### Table 25 — *Fixture Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The Name is the value that represents the fixture object. Is is not unique, and normally pairs with FID in Display | +| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | + +The child list (Table 26) contains a list of one of the following nodes: + +##### Table 26 — *Fixture Node Childs* + +| Child Node | Allowed Count | Value Type | Description | +| --------------------------------------- | ------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | +| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | +| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | +| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | +| Focus | 0 or 1 | [UUID](#user-content-attrtype-uuid) | A focus point reference that this lighting fixture aims at if this reference exists. | +| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | +| DMXInvertPan | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines of all Pan Channels of the fixture should be DMX Inverted. | +| DMXInvertTilt | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines of all Tilt Channels of the fixture should be DMX Inverted. | +| Position | 0 or 1 | [UUID](#user-content-attrtype-uuid) | A position reference that this lighting fixture belongs to if this reference exists. | +| Function | 0 or 1 | [String](#user-content-attrtype-string) | The name of the purpose this Fixture has. | +| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| UnitNumber | 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | +| ChildPosition | 0 or 1 | [String](#user-content-attrtype-string) | Node link to the geometry. Starting point is the Geometry Collect of the linked parent GDTF of this object. | +| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this fixture. | +| [Protocols](#node-definition-protocols) | 0 or 1 | | The container for protocols assignments. | +| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this fixture. | +| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this fixture. | +| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this fixture. | +| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this fixture. | +| Color | 0 or 1 | [CIE Color](#user-content-attrtype-ciecolor) | A color assigned to a fixture. If it is not defined, there is no color for the fixture. | +| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | +| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | +| [Mappings](#node-definition-mappings) | 0 or 1 | | The container for Mappings for this fixture. | +| [Gobo](#node-definition-gobo) | 0 or 1 | | The Gobo used for the fixture. The image resource must conform to the GDTF standard. | +| [ChildList](#node-definition-childlist) | 0 or 1 | | A list of graphic objects that are part of the layer. | + +Note: _The fixture has no `Geometries` node as geometry is defined in a GDTF file._ + +CustomID Types +- 0 Undefined +- 1 Fixture +- 2 Channel +- 4 Houselights +- 5 NonDim +- 6 Media +- 7 Fog +- 8 Effect +- 9 Pyro +- 10 Marker + +For further information about the difference between FixtureID and CustomID refer to Annex A. + +EXAMPLE An example of a node definition is shown below: + + +```xml + + {0.158127,-0.987419,0.000000}{0.987419,0.158127,0.000000}{0.000000,0.000000,1.000000}{6020.939200,2838.588955,4978.134459} + Custom@Robe Robin MMX WashBeam + DMX Mode + 4A B1 94 62 A6 E3 4C 3B B2 5A D8 09 9F 78 17 0C + 77 BC DE 16 95 A6 47 25 9D 04 16 A0 BD 67 CD 1A + +
45
+ + + +
+ + + + + + + + + Body_Pan,f 50 + Yoke_Tilt,f 50 + + + + + + + + + + 10 + 10 + 5 + 5 + 45 + + + + + + + + + 0 + Speaker 1 + 0 + 0 + true + true + 2.533316,-5.175210,3.699302 + image_file_forgobo + + + Fancy@Attachment + DMX Mode + The parent GDTF here is the one from the Robe Robin MMX WashBeam + Base.Yoke.Head + The position is now defined based on the ECS from the geometry of parents GDTF including all applied Rotation via DMX or other protocols + {0.158127,-0.987419,0.000000}{0.987419,0.158127,0.000000}{0.000000,0.000000,1.000000}{6020.939200,2838.588955,4978.134459} + + +
+``` + + +## Node Definition: Truss + +This node defines a truss object. The defined Truss Node Attributes are specified in Table 27. + +Node name: `Truss` + +##### Table 27 — *Truss Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object | +| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | + +The child list (Table 28) contains a list of one of the following nodes: + +##### Table 28 — *Truss Node Childs* + +| Child Node | Allowed Count | Value Type | Description | +| ------------------------------------------------- | ------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | +| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | +| [Position](#node-definition-position) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | A position reference that this truss belongs to if this reference exists. | +| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are a part of the object. | +| Function | 0 or 1 | [String](#user-content-attrtype-string) | The name of the function this Truss is used for. | +| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | +| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | +| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | +| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object. | +| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | +| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object. | +| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | +| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | +| ChildPosition | 0 or 1 | [String](#user-content-attrtype-string) | Node Link to the Geometry. Starting point is the Geometry Collect of the linked parent GDTF of this object. | +| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | +| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | +| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | +| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | + + + +## Node Definition: Support + +This node defines a support object. The defined Support Node Attributes are specified in Table 29. + +Node name: `Support` + +##### Table 29 — *Support Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object | +| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | + +The child list (Table 30) contains a list of one of the following nodes: + +##### Table 30 — *Support Node Childs* + +| Child Node | Allowed Count | Value Type | Description | +| ------------------------------------------------- | ------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | +| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | +| [Position](#node-definition-position) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | A position reference that this support belongs to if this reference exists. | +| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are a part of the object. | +| Function | 0 or 1 | [String](#user-content-attrtype-string) | The name of the function this support is used for. | +| ChainLength | 1 | [Float](#user-content-attrtype-float) | The chain length that will be applied to the GDTF . | +| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | +| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | +| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | +| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object. | +| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | +| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object. | +| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | +| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | +| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | +| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | +| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | +| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | + + +## Node Definition: VideoScreen + +This node defines a video screen object. The defined VideoScreen Node Attributes are specified in Table 31. + +Node name: `VideoScreen` + +##### Table 31 — *VideoScreen Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object. | +| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | + +The child list (Table 32) contains a list of one of the following nodes: + +##### Table 32 — *VideoScreen Node Childs* + +| Child Node | Allowed Count | Value Type | Description | +| ------------------------------------------------- | ------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | +| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | +| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are a part of the object. | +| [Sources](#node-definition-sources) | 0 or 1 | | A list of video input sources.. | +| Function | 0 or 1 | [String](#user-content-attrtype-string) | The name of the function this VideoScreen is used for. | +| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | +| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | +| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | +| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object. | +| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | +| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object. | +| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | +| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | +| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | +| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | +| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | +| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | + + +EXAMPLE An example of a node definition is shown below: + +```xml + + {0.158127,-0.987419,0.000000}{0.987419,0.158127,0.000000}{0.000000,0.000000,1.000000}{6020.939200,2838.588955,4978.134459} + Generic@TV + DisplayModeWideScreen + +
45
+
+ 25 + 0 + 0 + + movie.mov + + +``` + + +## Node Definition: Projector + +This node defines a video projector object. The defined Projector Node Attributes are specified in Table 33. + +Node name: `Projector` + +##### Table 33 — *Projector Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| name | [String](#user-content-attrtype-string) | Empty | The name of the object. | +| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | + +The child list (Table 34) contains a list of one of the following nodes: + +##### Table 34 — *Projector Node Childs* + +| Child Node | Allowed Count | Value Type | Description | +| ------------------------------------------------- | ------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | +| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | +| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are a part of the object. | +| [Projections](#node-definition-projections) | 1 | | A list of video source for Beam Geometries in the GDTF file. | +| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | +| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | +| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | +| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object. | +| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | +| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object. | +| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | +| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | +| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | +| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | +| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | +| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | +| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | + + +EXAMPLE An example of a node definition is shown below: + + +```xml + + {0.158127,-0.987419,0.000000}{0.987419,0.158127,0.000000}{0.000000,0.000000,1.000000}{6020.939200,2838.588955,4978.134459} + Generic@Projector + Projector@ThrowRatio1_7_to_2_2 + +
45
+
+ 25 + 0 + 0 + + movie.mov + + UpScale + + +
+``` + + +## Other Node Definition + +### Node Definition: Matrix + +This node contains a definition of a transformation matrix: + +- Right-handed +- Z-Up +- 1 Distance Unit equals 1 mm + +Node name: `Matrix` + +The defined Matrix Node Value Types are specified in Table 35. + +##### Table 35 — *Matrix Node Value Types* + +| Value Type | Default Value When Missing | Description | +| ---------------------- | ---------------------------- | --------------------------------------------- | +| {[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float)} {[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float)} {[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float)} {[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float)} | {1,0,0}
{0,1,0}
{0,0,1}
{0,0,0} | This node contains the array for a 4x3 transform matrix.
The order is:
`u1,u2,u3`
`v1,v2,v3`
`w1,w2,w3`
`o1,o2,o3` | + +### Node Definition: Gobo + +This node defines a Gobo. The defined Gobo Node Attributes are specified in Table 36. + +Node name: `Gobo` + +##### Table 36 — *Gobo Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ------------------------------------- | --------------------------- | ---------------------------------- | +| rotation | [Float](#user-content-attrtype-float) | 0 | The roation of the Gobo in degree. | + +The node value is the Gobo used for the fixture. The image resource shall apply to the GDTF standard. Use a FileName to specify. + + +### Node Definition: Sources + +This node defines a group of sources for VideoScreen. + +Node name: `Sources` + +The child list (Table 37) contains a list of the following nodes: + +##### Table 37 — *Sources Node Children* + +The child list contains a list of the following nodes: + +| Child Node | Description | +| --------------------------------- | --------------------------- | +| [Source](#node-definition-source) | One Source for the fixture. | + + +#### Node Definition: Source + +This node defines a Source. The defined Source Node Attributes are specified in Table 38. The defined Source Node Value Types are specified in Table 39. + +Node name: `Source` + +##### Table 38 — *Source Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | -------------------------------------- | --------------------------- | -------------------------------------------------------------- | +| linkedGeometry | [String](#user-content-attrtype-string)| Not Optional | For a Display: The GDTF Geometry Type Display whose linked texture will get replaced by the source value.

`For a Beam: Defines the source for the GDTF Geometry Type Beam. Only applicable when BeamType is "Rectangle".` | +| type | [Enum](#user-content-attrtype-enum) | Not Optional | Defines the type of source of the media resource that will be used. The currently defined types are: NDI, File, CITP, CaptureDevice | + +##### Table 39 — *Source Node Value Types* + +| Value Type | Default Value When Missing | Description | +| --------------------------------------- | -------------------------- | ------------------------------------------------------------------------------ | +| [String](#user-content-attrtype-string) | Not Optional | Based on the Attribute name `type`:
- If type is NDI or CITP use the Stream Name
- If type is File use the filename in MVR file
- If type is CaptureDevice use the CaptureDevice Name | + +### Node Definition: ScaleHandeling + +This node defines how the MappingDefinition will react if the video source has not the same resolution. The defined ScaleHandeling Node Attributes are specified in Table 40. + +Node name: `ScaleHandeling` + +##### Table 40 — *ScaleHandeling Node Attributes* + +| Value Type | Default Value When Missing | Description | +| --------------------------------------- | -------------------------- | ------------------------------------------------------------------------------- | +| [Enum](#user-content-attrtype-enum) | ScaleKeepRatio | The available value are `ScaleKeepRatio`, `ScaleIgnoreRatio`, `KeepSizeCenter`. | + +Figure 1 shows how the scaling should look like. + +##### Figure 1 — *ScaleHandeling Node Attributes* + +| a) ScaleKeepRatio | b) ScaleIgnoreRatio | c) KeepSizeCenter | +|------------------------------|------------------------------------------|-------------------------------------------------------| +| ![media/ScaleKeepRatio.png](media/ScaleKeepRatio.png) | ![media/ScaleIgnoreRatio.png](media/ScaleIgnoreRatio.png) | ![media/KeepSizeCenter.png](media/KeepSizeCenter.png) | + + + +## Node Definition: Geometries + +This node defines a group of graphical objects. + +Node name: `Geometries` + +The child list (Table 41) contains a list of the following nodes: + +##### Table 41 — *Geometries Node Childs* + +| Child Node | Description | +| ----------------------------------------- | -------------------------------------------------------------------- | +| [Geometry3D](#node-definition-geometry3d) | The geometry of this definition that will be instanced in the scene. | +| [Symbol](#node-definition-symbol) | The symbol instance that will provide a geometry of this definition. | + + +## Node Definition: Symbol + +This node specified a symbol instance (geometry insert) of the definition geometry defined by a [Symdef](#node-definition-symdef) node. The defined Symbol Node Attributes are specified in Table 42. + +Node name: `Symbol` + +##### Table 42 — *Symbol Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | +| symdef | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the Symdef node that will be the source of geometry. | + +The child list (Table 43) contains a list of the following nodes: + +##### Table 43 — *Symbol Node Childs* + +| Child Node | Allowed Count | Description | +| -----------------------| ------------- | -------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | The transformation matrix that defines the location. orientation and scale of the geometry inside the local coordinate space of the container. Considered identity when missing. | + + +## Node Definition: Geometry3D + +This node provides geometry from another file within the archive. The defined Geometry3D Node Attributes +are specified in Table 44. + +Node name: `Geometry3D` + +##### Table 44 — *Geometry3D Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------- | --------------------------- | ------------------------------------------------------------------------- | +| fileName | [FileName](#user-content-attrtype-filename) | Not Optional | The file name, including extension, of the external file in the archive. If there is no extension, it will assume that the extension is 3ds. | + +The child list (Table 45) contains a list of the following nodes: + +##### Table 45 — *Geometry3D Node Childs* + +| Child Node | Allowed Count | Description | +| ----------------- | ------------- | ----------------------------------------------------------------------------------- | +| [Matrix](#node-definition-matrix) | 0 or 1 | The transformation matrix that defines the location, orientation and scale of the geometry inside the local coordinate space of the container. Considered identity when missing. | + + +### Supported 3D file formats + +The supported 3D file formats are specified in Table 46. + +##### Table 46 — *Supported 3D file formats* + +| Format Name | File Extensions | Requirements | Notes | +| ----------- | --------------- | ----------------------------------- | --------------------------------------------------------- | +| 3DS | 3ds | 1 unit = 1 mm | [Deprecated Discreet 3DS](https://en.wikipedia.org/wiki/.3ds) | +| gltf 2.0 | gltf, glb | `extensionsRequired` shall be empty | GLB packaging is recommended [ISO/IEC 12113 Khronos glTF 2.0](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html) | + +All referenced files (e.g. texture images, binary blobs) shall be present in the archive. + +All file references (URIs etc) shall be relative to the root of the archive. Absolute URIs and file paths are not permitted. + + +### Node Definition: Projections + +This node defines a group of Projections. + +Node name: `Projections` + +The child list (Table 47) contains a list of the following nodes: + +##### Table 47 — *Projections Node Children* + +| Child Node | Description | +| ----------------------------------------- | ----------------------- | +| [Projection](#node-definition-projection) | Defines the Projection. | + + +#### Node Definition: Projection + +This node defines a Projection. + +Node name: `Projection` + +The child list (Table 48) contains a list of the following nodes: + +##### Table 48 — *Projection Node Childs* + +| Child Node | Description | +| ------------------------------------------------- | ------------------------------------------------ | +| [Source](#node-definition-source) | Defines the source for the projection. | +| [ScaleHandeling](#node-definition-scalehandeling) | How the source will be scaled to the projection. | + + +### Node Definition: Addresses + +This node defines a group of Addresses. + +Node name: `Addresses` + +The child list (Table 49) contains a list of the following nodes: + +##### Table 49 — *Adresses Node Childs* + +| Child Node | Description | +| ----------------------------------- | ----------------------- | +| [Address](#node-definition-address) | DMX address of the fixture. | +| [Network](#node-definition-network) | Network address of the fixture. | + + +#### Node Definition: Address + +This node defines a DMX address. The defined Address Node Attributes are specified in Table 50. + +Node name: `Address` + +##### Table 50 — *Address Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | --------------------- | --------------------------- | ----------------- | +| break | [Integer](#user-content-attrtype-integer) | 0 | This is the break ident for this address. This value has to be unique for one fixture. | + +The child list (Table 51) contains a list of the following nodes: + +##### Table 51 — *Address Node Children* + +| Value Type | Default Value When Missing | Description | +| ----------- | -------------------------- | ---------------- | +| [Integer](#user-content-attrtype-integer) or [String](#user-content-attrtype-string)| Not Optional | This is the DMX address.
`Integer Format:` `Absolute DMX address;`
`String format:` `Universe - integer universe number, starting with 1; Address - address within universe from 1 to 512. `*`Universe.Address`* | + +#### Node Definition: Network + +This node defines a network IP-address according to the physical interface. The defined Network Node Attributes are specified in Table 52. + +Node name: `Network` + +##### Table 52 — *Network Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +|----------------|-------------------------------------------|-----------------------------|--------------------------------------------------------------| +| geometry | [String](#user-content-attrtype-string) | Not Optional | This is the name of the wire geometry of the linked GDTF that this information is of.. Typically used "ethernet_x", "wireless_x", "loopback_x" (x starting at 1 and incrementing) | +| ipv4 | [IPv4](#user-content-attrtype-ipv4) | Optional | This is the IPv4-address. | +| subnetmask | [IPv4](#user-content-attrtype-ipv4) | optional | This is the SubnetMask-address. Only needed for IPv4. | +| ipv6 | [IPv6](#user-content-attrtype-ipv6) | optional | This is the IPv6-address. | +| dhcp | [Bool](#user-content-attrtype-bool) | false | This is the automated-address. DHCP is considered off. If present it should be set "on" (true). | +| hostname | [hostname](#user-content-attrtype-string) | optional | This is the hostname for the device with an automated address. | + +### Node Definition: Protocols + +This node defines the supported protocols and the used interface. + +Node name: `Protocols` + +The child list (Table 53) contains a list of the following nodes: + +##### Table 53 — *Protocols Node Childs* + +| Child Node | Description | +| --------------------------------- | --------------------------- | +| [Protocol](#node-definition-protocol) | The protocol used by this instance of object. | + +### Node Definition: Protocol + +This node defines the protocol used by the instance of this object. The defined Protocol Node Attributes are specified in Table 54. + +Node name: `Protocol` + +##### Table 54 — *Protocol Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | -------------------- | --------------------------- | ------------ | +| geometry | [String](#user-content-attrtype-string) | NetworkInOut_1 | This is the interface name. | +| name | [String](#user-content-attrtype-string) | empty | Custom Name of the protocol to identify the protocol. Needs to be unique for this instance of object. | +| type | [String](#user-content-attrtype-string) | empty | Name of the protocol. | +| version | [String](#user-content-attrtype-string) | empty | This is the protocol version if available.| +| transmission | [Enum](#user-content-attrtype-enum) | undefined | Unicast, Multicast, Broadcast, Anycast | + +The following names for the `type` are predefined: +- RDMNet +- Art-Net +- sACN +- PosiStageNet +- OpenSoundControl +- CITP +- NDI + +Any other protocol can be freely defined. + + +### Node Definition: Alignments + +This node defines a group of Alignment. + +Node name: `Alignments` + +The child list (Table 55) contains a list of the following nodes: + +##### Table 55 — *Alignments Node Childs* + +| Child Node | Description | +| ------------------------------------- | ------------------------------------------------------------- | +| [Alignment](#node-definition-alignment) | Defines a custom alignment for a beam inside the linked GDTF. | + + +#### Node Definition: Alignment + +This node defines an alignment for a Beam Geometry inside the linked GDTF. The defined Alignment Node Attributes are specified in Table 56. + +Node name: `Alignment` + +##### Table 56 — *Alignment Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value | Description | +| -------------- | -------------------------------------- | --------------------------- | -------------------------------------------- | +| geometry | [String](#user-content-attrtype-string) | Beam Geometry of the first Beam in the kinematic chain of the GDTF. | Defines the Beam Geometry that gets aligned. | +| up | [String](#user-content-attrtype-string)| 0,0,1 | Defines the up vector of the direction. | +| direction | [String](#user-content-attrtype-string)| 0,0,-1 | Defines the direction vector of the lamp. | + + +### Node Definition: CustomCommands + +This node defines a group of CustomCommands. + +Node name: `CustomCommands` + +The child list (Table 57) contains a list of the following nodes: + +##### Table 57 — *CustomCommands Node Childs* + +| Child Node | Description | +| ----------------------------------------------- | ---------------------------------------------------------------------------- | +| [CustomCommand](#node-definition-customcommand) | Contains a list with custom commands that should be executed on the fixture | + + +#### Node Definition: CustomCommand + +This node defines a custom command for the linked GDTF. + +Node name: `CustomCommand` + +The Custom command contains the command that will be executed on the fixture. The definition from the syntax for the command +aligns with the DIN SPEC 15800:2022-02, 11.2.1.2.3, Channel Functions, [for command based control systems](https://github.com/mvrdevelopment/spec/blob/main/gdtf-spec.md#channel-function). + +With this feature you can also control static properties for fixture that cannot be controlled via DMX. + + +### Node Definition: Overwrites + +This node defines a group of Overwrite. + +Node name: `Overwrites` + +The child list (Table 58) contains a list of the following nodes: + +##### Table 58 — *Overwrites Node Childs* + +| Child Node | Description | +| --------------------------------------- | ----------------------------------------------------------------- | +| [Overwrite](#node-definition-overwrite) | Contains a list with overwrites for gobos, filters and emitters. | + + +#### Node Definition: Overwrite + +This node defines an overwrite with the `Universal.gdtt` GDTF template inside the MVR to overwrite Wheel Slots, Emitters and Filters for the fixture. The defined Overwrite Node Attributes are specified in Table 59. + +Node name: `Overwrite` + +##### Table 59 — *Overwrtie Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value | Description | +| -------------- | ----------------------------------------- | ------------- | -------------------------------------------------------------- | +| universal | [String](#user-content-attrtype-string) | Mandatory | Node Link to the Wheel, Emitter or Filter. Starting point is the the collect of the Universal GDTF. | +| target | [String](#user-content-attrtype-string) | Empty String | Node Link to the Wheel, Emitter or Filter. Starting point is the the collect of the linked GDTF of the fixture. When no target is given, it will be like a static gobo or filter that you attach in front of all beams. | + + +### Node Definition: Connections + +This node defines a group of Connection. + +Node name: `Connections` + +The child list (Table 60) contains a list of the following nodes: + +##### Table 61 — *Connections Node Childs* + +| Child Node | Description | +| ---------------------------------------- | --------------------------------------------------------- | +| [Connection](#node-definition-connection) | Contains an definition of an object to object connection. | + + +#### Node Definition: Connection + +This nodes defines an connection of two scene object. The connection can be an electrical or data connection. The defined Connection Node Attributes are specified in Table 61. + +Node name: `Connection` + +##### Table 61 — *Connection Node Attributes* + +| Attribute Name | Attribute Value Type | Default Value | Description | +| -------------- | ----------------------------------------- | --------------------------- | ----------------------------------------------------- | +| own | [String](#user-content-attrtype-string) | Mandatory | Node Link to the Geometry with DIN SPEC 15800 Type [Wiring Object](https://github.com/mvrdevelopment/spec/blob/main/gdtf-spec.md#geometry-type-wiring-object) . Starting point is the Geometry Collect of the linked GDTF. | +| other | [String](#user-content-attrtype-string) | Mandatory | Node Link to the Geometry with DIN SPEC 15800 Type [Wiring Object](https://github.com/mvrdevelopment/spec/blob/main/gdtf-spec.md#geometry-type-wiring-object) . Starting point is the Geometry Collect of the linked GDTF of the object defined in `toObject`. | +| toObject | [UUID](#user-content-attrtype-uuid) | Mandatory | UUID of an other object in the scene. | + +### Node Definition: Mappings + +This node defines a group of Mappings. + +Node name: `Mappings` + +The child list (Table 62) contains a list of the following nodes: + +##### Table 62 — *Mappings Node Childs* + +| Child Node | Allowed Count | Description | +| ------------------------------------- | ------------- | ---------------------------- | +| [Mapping](#node-definition-mapping) | 0 or any | One Mapping for the fixture. | + +It is only allowed to have one Mapping linked to the same Mapping Definition once per Fixture + + +#### Node Definition: Mapping + +This node defines a Mapping. The defined Mapping Node Attributes are specified in Table 63. + +Node name: `Mapping` + +##### Table 63 — *Mapping Node Attributes* + +| Attribute Name | Attribute Value Type | Description | +| -------------- | ----------------------------------- | ------------------------------------------------------------------------------------------- | +| linkedDef | [UUID](#user-content-attrtype-uuid) | The unique identifier of the MappingDefinition node that will be the source of the mapping. | + +The child list (Table 64) contains a list of the following nodes: + +##### Table 64 — *Mapping Node Childs* + +| Child Node | Allowed Count | Value Type | Description | +| ---------- | ------------- | ---------- | --------------------------------------------------------------------------------------------- | +| ux | 0 or 1 | [Integer](#user-content-attrtype-integer) | The offset in pixels in x direction from top left corner of the source that will be used for the mapped object. | +| uy | 0 or 1 | [Integer](#user-content-attrtype-integer) | The offset in pixels in y direction from top left corner of the source that will be used for the mapped object. | +| ox | 0 or 1 | [Integer](#user-content-attrtype-integer) | The size in pixels in x direction from top left of the starting point. | +| oy | 0 or 1 | [Integer](#user-content-attrtype-integer) | The size in pixels in y direction from top left of the starting point. | +| rz | 0 or 1 | [Float](#user-content-attrtype-float) | The rotation around the middle point of the defined rectangle in degree. Positive direction is counter cock wise. | + +NOTE The transformation will be applied in the following order: – Translation – Rotation + + +# Communication Format Definition + + +## General +The MVR communication format - MVR-xchange - shall support the exchange of MVR files over network without the need of an external transport device like a USB-stick. The exchange allows multiple clients within the same network to share MVR files. + +MVR-xchange defines two modes of operation (see Figure 2): +- TCP Mode, which works without configuration but does not support routing. +- WebSocket Mode, which need minimal configuration but allows for routing. + +##### Figure 2 — *MVR-xchange mode of operation* + +| a) TCP Mode of protocol | b) WebSocket Mode of protocol | +|---|---| +| ![media/MVR_LocalNetwork.png](media/MVR_LocalNetwork.png) | ![media/MVR_Websockets.png](media/MVR_Websockets.png) | + + +## TCP Mode of protocol + +The TCP Mode allows users to directly use the MVR-xchange without the need for configuration or special hardware. Discovery of available MVR-xchange clients shall be performed by mDNS (RFC6762 Multicast DNS). Every application that wants to join a MVR-xchange group, need to register a mDNS service. + +The service name shall be `_mvrxchange._tcp.local.`. The sub service name shall be `xxxx._mvrxchange._tcp.local.` where *xxxx* is the name of the group. +. Each client shall negotiate a unique hostname via the methods described in the mDNS standards. Each client shall have a PTR, SRV, TXT and A and/or AAAA +record. + +The TXT record should contain the information given in Table 65: + + +##### Table 65 — *TXT Record Attributes* + +| Attribute Name | Attribute Value Type | Description | +| -------------- | ----------------------------------- | ----------------------------------------------------------------------------- | +| StationName | [String](#user-content-attrtype-string) | The Name of the sending station to be shown on the clients UI. | +| StationUUID | [UUID](#user-content-attrtype-uuid) | UUID of sending station inside the network. This UUID should be persistent across multiple start-ups of the same software on the same computer | + +The format of the TXT record matches RFC1035. + +When a MVR-xchange client wants to join a MVR-xchange group, he needs to register the service and sub service, and send a `MVR_JOIN` message to the other stations that register this sub service name. When a MVRxchange client wants to create a MVR-xchange group, he needs to register a service name which is currently not in use and wait for other MVR-xchange clients to join. + +You can upgrade a TCP Mode MVR-xchange group to use the WebSocket Mode with sending a `MVR_NEW_SESSION_HOST` message providing the URL of the new service. + +## WebSocket Mode of protocol + +The WebSocket Mode allows users to create a routable service for the MVR-xchange. Discovery works with the normal DNS according to RFC6763. The service name needs to be a valid URL that can be resolved by the DNS server. + +The DNS entry should point to the IP of the service running the websocket server. MVR-xchange clients that want to join this MVR-xchange Group need to connect with a web socket client (RFC6455— The WebSocket Protocol). + + +## Packet & Message definition + +### General + +Packages define how the message will be send to the MVR-xchange client, while the message describes the content. All the messages are defined, unless otherwise stated, as JSON documents (ISO/IEC 21778:2017). Packages are defined based on the mode of communication. They are defined for TCP Mode and WebSocket mode differently. + +### TCP Mode + +When in TCP Mode, all messages are send via TCP directly to the client. The packet is encoded as specified in Table 66: + +##### Table 66 — *Packet & Message Definitions* + +| Type | Symbol | +|---|---| +| `MVR_PACKAGE_HEADER` | Number that defines the package. Use 778682. | +| `MVR_PACKAGE_VERSION` | Number that defines the version of the package format. Use 1. | +| `MVR_PACKAGE_COUNT` | Number that defines how many packages the current message consists of. | +| `MVR_PACKAGE_NUMBER` | Number that defines what number this package in the complete message has. Zero based. | +| `MVR_PACKAGE_TYPE` | Number that defines the package type. Use 0 for JSON UTF-8 Payload, use 1 for MVR FILES. | +| `MVR_PAYLOAD_LENGTH` | Number showing the byte-length of transferred buffer. | +| `MVR_PAYLOAD_BUFFER` | Buffer data that stores the payload encoded. | + + +The order and size is defined as follows: +``` +uint32 MVR_PACKAGE_HEADER +uint32 MVR_PACKAGE_VERSION +uint32 MVR_PACKAGE_NUMBER +uint32 MVR_PACKAGE_COUNT +uint32 MVR_PACKAGE_TYPE +uint64 MVR_PAYLOAD_LENGTH +char[] MVR_PAYLOAD_BUFFER +``` + +Where the following applies (Table 67): + +##### Table 67 — *Data Type MVR-xchange package* + +| Type | Symbol | +|---|---| +| uint32 | 32-bit unsigned integer | +| uint64 | 64-bit unsigned integer | +| char[] | 8-bit character array | + +NOTE All multi-byte fields defined shall be transmitted in network byte (big-endian) order + +### WebSocket Mode + +When in WebSocket Mode, all messages should be send as data frame Text *[RFC6455 5.6 Text 0x1](https://datatracker.ietf.org/doc/html/rfc6455#section-5.6)* unless otherwise defined. + +## `MVR_JOIN` message + +### General + +When a MVR-xchange client connects with another MVR-xchange client, the first MVR-xchange client needs to send a `MVR_JOIN` message. + +NOTE A MVR-xchange client can send multiple `MVR_JOIN` messages to the same server during the same connection to update its name or get the latest MVR file list. + +### TCP Mode + +Figure 3 shows the TCP mode for a MVR-xchange client joining MVR-xchange group. + +##### Figure 3 — *TCP mode: MVR-xchange client joining MVR-xchange group* + +| a) MVR-xchange client 2 joins the MVR-xchange Group | b) and sends to all mDNS Service a `MVR_JOIN` message | +|---|---| +| ![media/MVR_Join_mDNS_1.png](media/MVR_Join_mDNS_1.png) | ![media/MVR_Join_mDNS_2.png](media/MVR_Join_mDNS_2.png) | + + +### WebSocket Mode + +Figure 4 shows the Websocket mode for a MVR-xchange client joining MVR-xchange group. + +##### Figure 4 — *Websocket mode: MVR-xchange client joining MVR-xchange group* + +| a) 1 Is a Websocket Server and has a URL | b) MVR-xchange client 2 connects to the websocket sever and send a `MVR_JOIN` message | +|---|---| +| ![media/MVR_Join_1.png](media/MVR_Join_1.png) | ![media/MVR_Join_2.png](media/MVR_Join_2.png) | + +| c) MVR-xchange client 3 connects to the websocket sever and send a `MVR_JOIN` message | d) MVR-xchange client 3 connects to the websocket sever and send a `MVR_JOIN` message | +|---|---| +| ![media/MVR_Join_3.png](media/MVR_Join_3.png) | ![media/MVR_Join_4.png](media/MVR_Join_4.png) | + +The defined MVR_JOIN message Attributes are specified in Table 68. + +##### Table 68 — *MVR_JOIN message Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_JOIN | +| Provider | [String](#user-content-attrtype-string) | Not Optional | The application name providing MVR Import & Export | +| StationName | [String](#user-content-attrtype-string) | Not Optional | The Name of the sending station to be shown on the clients UI. | +| verMajor | [Integer](#user-content-attrtype-integer) | 0 | It is mandatory to transmit the version of the MVR file that the sender station supports. | +| verMinor | [Integer](#user-content-attrtype-integer) | 0 | It is mandatory to transmit the version of the MVR file that the sender station supports. | +| StationUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | UUID of sending station inside the network. This UUID should be persistent across multiple start-ups of the same software on the same computer | +| Commits | [Array of `MVR_COMMIT`](#user-content-attrtype-string) | Empty Array | List all available MVR files that are on sender station in the format of the `MVR_COMMIT` packet. | | + +The defined MVR_JOIN response Attributes are specified in Table 69. + +##### Table 69 — *MVR_JOIN response Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_JOIN_RET | +| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | +| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message if there is an error. | | +| Provider | [String](#user-content-attrtype-string) | Not Optional | The application name providing MVR Import & Export | +| StationName | [String](#user-content-attrtype-string) | Not Optional | The Name of the receiving station to be shown on the UI. | +| verMajor | [Integer](#user-content-attrtype-integer) | 0 | It is mandatory to transmit the version of the MVR file that the receiver station supports. | +| verMinor | [Integer](#user-content-attrtype-integer) | 0 | It is mandatory to transmit the version of the MVR file that the receiver station supports. | +| StationUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | UUID for receiving station inside the network. This UUID should be persistent across multiple start-ups of the same software on the same computer | +| Commits | [Array of `MVR_COMMIT`](#user-content-attrtype-string) | Empty Array | List all available MVR files that are on receiver station in the format of the `MVR_COMMIT` packet. | | + +EXAMPLE + +>ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. + +Request: +``` +{ + "Type": "MVR_JOIN", + "Provider":"MVRApplication", + "verMajor":1, + "verMinor":6, + "StationUUID":"4aa291a1-1a62-45fe-aabc-e90e5e2399a8", + "StationName":"MVR Application from user A at location B", + "Commits": [ + { + ...MVR_COMMIT_MESSAGE_ARGS + }, + { + ...MVR_COMMIT_MESSAGE_ARGS + }, + { + ...MVR_COMMIT_MESSAGE_ARGS + } + ] + +} +``` + +Response: +``` +{ + "Type": "MVR_JOIN_RET", + "OK": true, + "Message": "", + "verMajor":1, + "verMinor":6, + "StationUUID":"a7669ff9-bd61-4486-aea6-c190f8ba6b8c", + "StationName":"MVR Application from user A at location B", + "Commits": [ + { + ...MVR_COMMIT_MESSAGE_ARGS + }, + { + ...MVR_COMMIT_MESSAGE_ARGS + }, + { + ...MVR_COMMIT_MESSAGE_ARGS + } + ] +} +``` + + +## `MVR_LEAVE` message + +A client sends a `MVR_LEAVE` when it wants to quit an MVR-xchange Group and does not want to get updates about new MVR files anymore. + +For the WebSocket mode [Figure 5 a)]: it is not required to terminate the Websockets connection, but it can be done. For the TCP mode [Figure 5 b)]: it is not required to turn down the mDNS service, but it can be done. + +In order to join again, the client needs to send a `MVR_JOIN` message again. + +##### Figure 5 — *MVR_LEAVE message to quit MVR-xchange group* + +|a) In Webssocket mode: MVR-xchange client 4 send a `MVR_LEAVE` message to the websocket server. | b) In TCP Mode: MVR-xchange client 2 send a `MVR_LEAVE` message to all stations | +|---|---| +| ![media/MVR_Leave_1.png](media/MVR_Leave_2.png) | ![media/MVR_Leave_2.png](media/MVR_Leave_1.png) | + +The defined MVR_LEAVE message Attributes are specified in Table 70. + +##### Table 70 — *MVR_LEAVE message Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_LEAVE | +| FromStationUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | The UUID of the station. | + +The defined MVR_LEAVE response Attributes are specified in Table 71. + +##### Table 71 — *MVR_LEAVE response Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_LEAVE_RET. | +| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | +| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message when there is an error. | | + + +EXAMPLE + +>ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. + +Request: +``` +{ + "Type": "MVR_LEAVE", + "StationUUID":"", +} +``` +Response: +``` +{ + "Type": "MVR_LEAVE_RET", + "OK": true, + "Message": "" +} +``` + +## `MVR_COMMIT` message + +### General + +The MVR commit message informs all connected stations that there is a new MVR commit. This message only informs the stations about the existence of the new file. Stations needs to request the MVR file with a `MVR_REQUEST` message. + +Each MVR commit represents one revision of the project. Therefore an array of MVR commits, as found in the `MVR_JOIN` message, represents the working history of the project. It is up to the client how many commits are kept in store at any time. + +The following chart displays the process when one client sends a `MVR_COMMIT` message to the server, and the server distributes this in the session. + +### TCP Mode + +The MVR-xchange client informs all other MVR-xchange clients about the new commit (see Figure 6). Note that the client needs to respect any previous `MVR_LEAVE` messages themselves. + +##### Figure 6 — *TCP mode: MVR-xchange client commits to MVR-xchange group.* + +| MVR-xchange client sends the `MVR_COMMIT` message to the connected stations. | +|---| +| ![media/MVR_Commit_4.png](media/MVR_Commit_4.png) | + + +### WebSocket Mode + +Figure 7 shows the WebSocket mode for a MVR-xchange client that commits to MVR-xchange group. + +##### Figure 7 — *Websocket mode: MVR-xchange client commits to MVR-xchange group.* + +| a) MVR-xchange client sends message to server | b) Server sends messages to all connected MVR-xchange clients but the sender | +|---|---| +| ![media/MVR_Commit_1.png](media/MVR_Commit_1.png) | ![media/MVR_Commit_2.png](media/MVR_Commit_2.png) | + +Figure 8 displays the process when the server is the station who is providing a new MVR file. In this case the MVR info is directly transmitted to the connected stations. + +##### Figure 8 — *Server makes the MVR_COMMIT itself, and only sends it to connected MVR-xchange clients* + +| Server makes the `MVR_COMMIT` itself, and only sends it to connected MVR-xchange clients | +|---| +| ![media/MVR_Commit_3.png](media/MVR_Commit_3.png) | + +The defined MVR_COMMIT message Attributes are specified in Table 72. + +##### Table 72 — *MVR_COMMIT message Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_COMMIT. | +| verMajor | [Integer](#user-content-attrtype-integer) | Not Optional | It is mandatory to transmit the current version of the MVR file as specified in Root File. If joining as new member send "0". | +| verMinor | [Integer](#user-content-attrtype-integer) | Not Optional | It is mandatory to transmit the current version of the MVR file as specified in Root File. If joining as new member send "0". | +| FileSize | [Integer](#user-content-attrtype-integer) | Not Optional | | +| FileUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | The UUID of the MVR file. Generate a UUID using | +| StationUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | UUID for the station inside the network. This UUID should be persistent across multiple start-ups of the same software on the same computer | +| ForStationsUUID | Array of [UUID](#user-content-attrtype-uuid) | [] | Array with the station UUID that this MVR should be send to. When it is an empty array, the MVR will be send to all connected *MVR-xchange clients* | +| Comment | [String](#user-content-attrtype-string) | | Describes the changes made in this version of the MVR file. | +| FileName | [String](#user-content-attrtype-string) | | Describes the file name that can be used to store the file on disk to preserve it across multiple MVR-xchange clients. The usage of this attribute is optional, when not defined, the receiving MVR-xchange client can decide which file name it uses to store it on disk. | + +The defined MVR_COMMIT response Attributes are specified in Table 73. + +##### Table 73 — *MVR_COMMIT response Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_COMMIT_RET. | +| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | +| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message when there is an error. | | + + +>ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. + +``` +Request: +{ + "Type": "MVR_COMMIT", + "verMajor":1, + "verMinor":6, + "FileUUID":"", + "ForStationsUUID":[], + "FileSize":256, + "Comment":"My complete description of what I have changed", +} +Response: +{ + "Type": "MVR_COMMIT_RET", + "OK": true, + "Message": "" +} +``` + +## `MVR_REQUEST` message + +This packet requests a MVR file from a station. You either can request a specific MVR via its UUID or get the last available MVR File by leaving the field empty. The underlying software will then generate a file based on the current state. This also triggers a `MVR_COMMIT` message to other connected stations. + +The available MVR UUIDs can be retrieved using the `MVR_COMMIT` message. + +If the station does not have the specified MVR file, it returns a MVR_REQUEST Json Response, otherwise it sends the buffer of the MVR file. + +NOTE 1 When in WebSocket Mode, the binary frame flag will be used to tell the receiver if a Buffer or JSON is sent. +NOTE 2 When in TCP Mode, the `MVR_PACKAGE_TYPE` flag will be used to tell the receiver if a Buffer or JSON was sent. + +Figure 9 shows the Websocket mode for a MVR-xchange client that is requesting a file. + +##### Figure 9 — *Websocket mode: MVR-xchange client requesting file* + +| a) Station requests a MVR from another station | b) Server sends the request to the right station | +|---|---| +| ![media/MVR_Request_1.png](media/MVR_Request_1.png) | ![media/MVR_Request_2.png](media/MVR_Request_2.png) | + +| c) Station sends the MVR file as binary data to the server | d) Server sends the MVR the MVR file as binary data to the station | +|---|---| +| ![media/MVR_Commit_3.png](media/MVR_Request_3.png) | ![media/MVR_Request_4.png](media/MVR_Request_4.png) | + +Figure 10 shows the TCP mode for a MVR-xchange client that is requesting a file. + +##### Figure 10 — *TCP mode: MVR-xchange client requesting file* + +| a) MVR-xchange client requests a MVR from another station | b) First requested station does not have the MVR and sends back a failure message, | +|---|---| +| ![media/MVR_Request_mDNS1.png](media/MVR_Request_mDNS1.png) | ![media/MVR_Request_mDNS2.png](media/MVR_Request_mDNS2.png) | + +| c) MVR-xchange client requests a MVR from another station | d) Second requested station does have the MVR and sends back the MVR file | +|---|---| +| ![media/MVR_Request_mDNS3.png](media/MVR_Request_mDNS3.png) | ![media/MVR_Request_mDNS4.png](media/MVR_Request_mDNS4.png) | + +The defined MVR_REQUEST message Attributes are specified in Table 74. + +##### Table 74 — *MVR_REQUEST message Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_REQUEST- | +| FileUUID | [UUID](#user-content-attrtype-uuid) | Last MVR File from station | The UUID of the requested MVR file. If not set, the last available file is sent. | +| FromStationUUID | Array of [UUID](#user-content-attrtype-uuid) | | The UUID of the station that you want to retrieve the MVR from. | + +The defined MVR_REQUEST error response Attributes are specified in Table 75. + +##### Table 75 — *MVR_REQUEST error response Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_REQUEST_RET | +| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | +| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message when there is an error. | | + + +>ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. + +Request: +``` +{ + "Type": "MVR_REQUEST", + "FromStationUUID":"", + "FileUUID":"", +} +``` +Response: + +binary frame + +OR +``` + +{ + "Type": "MVR_REQUEST_RET", + "OK": false, + "Message": "The MVR is not available on this client" +} +``` + +## `MVR_NEW_SESSION_HOST` message + +This message is used to inform other MVR-xchange clients of impending network configuration changes. This message is sent to all nodes in the network. + +This message type is meantfor two use cases: +- Change the Service URL (WebSocket Mode) or the ServiceName (TCP Mode) of a network +- Switch the Mode of a network + +This requires that only either `ServiceName` or `SerivceURL` are set. Setting both will return OK: false. + +### Change Service URL/Name + +This requires, that the current Network mode and the supplied message data are matching: +- If in WebSocket Mode, the **ServiceURL** shall be set +- If in TCP Mode, the **ServiceName** shall be set + +When the receiving nodes are in TCP Mode: + +Each receiver will try to connect to the mDNS service given in `ServiceName` and send a `MVR_JOIN` message. If this is successful, the nodes save the new Service Name and modify their own mDNS service. OK: true is returned. If no connection could be established, OK: false is returned. + +When the receiving nodes are in WebSocket Mode: + +Each receiver will try to connect to the URL given in `ServiceURL` and send a `MVR_JOIN` Message. If this is successful, the nodes save the URL and return OK: true. Otherwise OK: false is returned. + +## Switch Mode of a Network + +This requires, that the current Network mode and the supplied message data are **not** matching: +- If in WebSocket Mode, the **ServiceName** shall be set +- If in TCP Mode, the **ServiceURL** shall be set. + +When the receiving nodes are in TCP Mode: + +Each receiver will try to switch into WebSocket Mode by connecting to the URL given in `ServiceURL` and send a `MVR_JOIN` Message. If this is successful, then OK: true is returned and the mode is switched. If the URL is not reachable, then OK: false is returned. + +When the receiving nodes are in WebSocket Mode: + +Each receiver will try to switch into TCP Mode by connecting to the mDNS service given in `ServiceName` and send a `MVR_JOIN` Message. If this is successful, the nodes switch to TCP Mode and establish their own mDNS client as described above. OK: true is returned in this case. If the new mDNS service is not reachable OK: false is returned. + +The defined MVR_NEW_SESSION_HOST message Attributes are specified in Table 76. + +##### Table 76 — *MVR_NEW_SESSION_HOST message Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional |Defines the type of the message. Should be MVR_NEW_SESSION_HOST | +| ServiceName | [String](#user-content-attrtype-string) | Empty | New mDNS Service Name to connect to. If Empty, ignore. Cannot be set together with ServiceURL | +| ServiceURL | [String](#user-content-attrtype-string) | Empty. | New WebSocket Service URL to connect to. If Empty, ignore. Cannot be set together with ServiceURL + +The defined MVR_NEW_SESSION_HOST error response Attributes Attributes are specified in Table 77. + +##### Table 77 — *MVR_NEW_SESSION_HOST error response Attributes* + +| Attribute Name | Attribute Value Type | Default Value when Optional | Description | +| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_NEW_SESSION_HOST_RET | +| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | +| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message when there is an error. | | + + +>ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. + +Request: +``` +{ + "Type": "MVR_NEW_SESSION_HOST", + "ServiceName":"fancyProjectGroup._mvrxchange._tcp.local.", + "ServiceURL":"", +} +``` + +Response: +``` +{ + "Type": "MVR_NEW_SESSION_HOST_RET", + "OK": true, + "Message": "" + +} +``` +--- + + + +# Annex A. Object ID for Selection purposes (informative) + +In order to control or reference, all objects in the MVR Spec have human readable object IDs. For this both the FixtureIDNumeric and the CustomId is used. +The FixtureIDNumeric is a generic name pool that applies to all objects. All FixtureIDNumerics should be unique in one scene, so that objects can be selected without collisions. +The CustomId has a similar approach, but allows you define the pool type for the numbers as well. An object can so report that it is a Pyro device, and in the Pyro ID Pool it has the number 100. +Normally FixtureIDs are numeric to allow range selection. For Descriptive display on plots, some tools also append a letter like # or A before the FixtureID. A lot of tools have a concept of selecting objects with a range. Like 100 thru 200. So the Numeric portion of the FixtureID should be placed into the FixtureIDNumeric field. +A similar concept is the multipatch. Sometimes you want to group multiple objects behind the same FixtureIDNumeric or CustomId. This can be objects of the same GDTF Type, but not forced to be. When you select the FixtureIDNumeric or CustomId from the multipatch parent, all objects that reference this object in multipatch +parent should also be selected. + + +# Annex B. UUID purposes (informative) + +UUIDs are randomly generated numbers which are, practically speaking, unique and unable to conflict. The way UUIDs are designed is what allows them to uniquely identify an object with certainty. They are so unique that if you generate one today, you can be reasonably certain that this UUID has never been generated before and will never be generated by someone else in the future. This means that UUIDs in MVR will not conflict even across many files. Because it is easier to disregard data than try to derive it, MVR requires UUIDs for many things. This design and its incorporation into MVR is advantageous for many reasons, a few of which we will discuss below. +One of the most important aspects of UUIDs in MVR is that they are persistent. A UUID should identify an item throughout its entire life cycle. This means that if a document is exported, then objects should have the same UUID every time an export is performed. +One use case for UUIDs is importing or merging MVRs into an existing document. This is one reason that persistent UUIDs are valuable. If you export an MVR from one program, open it in another, and make modifications, then you may want to incorporate those changes into the original document. By cross referencing UUIDs, you can avoid creating duplicate objects and instead update existing ones. +UUIDs are also used inside of the MVR file format as a form of reference. For example, a symbol instance shall refer to a symbol definition. Because the symbol definition is given a UUID, the symbol instance can reference its symbol through the use of this UUID. + diff --git a/tests/test_example.py b/tests/test_example.py deleted file mode 100644 index 9359c85..0000000 --- a/tests/test_example.py +++ /dev/null @@ -1,20 +0,0 @@ -import pymvr - - -def test_write_example_mvr_file(): - fixtures_list = [] - mvr = pymvr.GeneralSceneDescriptionWriter() - pymvr.UserData().to_xml(parent=mvr.xml_root) - scene = pymvr.SceneElement().to_xml(parent=mvr.xml_root) - layers = pymvr.LayersElement().to_xml(parent=scene) - layer = pymvr.Layer(name="Test layer").to_xml(parent=layers) - child_list = pymvr.ChildList().to_xml(parent=layer) - - fixture = pymvr.Fixture(name="Test Fixture") # not really a valid fixture - child_list.append(fixture.to_xml()) - fixtures_list.append((fixture.gdtf_spec, fixture.gdtf_spec)) - - pymvr.AUXData().to_xml(parent=scene) - - mvr.files_list = list(set(fixtures_list)) - mvr.write_mvr("example.mvr") diff --git a/tests/test_fixture_1_5.py b/tests/test_fixture_1_5.py index 974cdbd..8b85116 100644 --- a/tests/test_fixture_1_5.py +++ b/tests/test_fixture_1_5.py @@ -30,5 +30,5 @@ def process_mvr_fixture(fixture): @pytest.mark.parametrize("mvr_scene", [("basic_fixture.mvr",)], indirect=True) def test_fixture(mvr_scene): - for layer in mvr_scene.layers: + for layer in mvr_scene.scene.layers: process_mvr_child_list(layer.child_list, mvr_scene) diff --git a/tests/test_fixture_scene_object_1_5.py b/tests/test_fixture_scene_object_1_5.py index 3abfaac..6f50b0b 100644 --- a/tests/test_fixture_scene_object_1_5.py +++ b/tests/test_fixture_scene_object_1_5.py @@ -58,7 +58,7 @@ def process_classes(mvr_scene): @pytest.mark.parametrize("mvr_scene", [("scene_objects.mvr",)], indirect=True) def test_fixture(mvr_scene): - for layer in mvr_scene.layers: + for layer in mvr_scene.scene.layers: process_mvr_child_list(layer.child_list, mvr_scene) - process_classes(mvr_scene) + process_classes(mvr_scene.scene) diff --git a/tests/test_group_objects_1_4.py b/tests/test_group_objects_1_4.py index 72c94c3..0e9ce00 100644 --- a/tests/test_group_objects_1_4.py +++ b/tests/test_group_objects_1_4.py @@ -11,10 +11,10 @@ def test_version(mvr_scene): @pytest.mark.parametrize("mvr_scene", [("capture_demo_show.mvr",)], indirect=True) def test_auxdata(mvr_scene): """Check symdefs""" - assert mvr_scene.aux_data.symdefs[0].uuid == "12fcdd5e-4194-56a0-96de-1c3c4edf1cd3" + assert mvr_scene.scene.aux_data.symdefs[0].uuid == "12fcdd5e-4194-56a0-96de-1c3c4edf1cd3" @pytest.mark.parametrize("mvr_scene", [("capture_demo_show.mvr",)], indirect=True) def test_child_list(mvr_scene): - assert mvr_scene.layers[1].child_list.fixtures[0].uuid == "2e149740-6a41-bc43-bd59-8968781b11b9" - assert mvr_scene.layers[2].child_list.scene_objects[1].uuid == "d1d76649-35bd-9d49-baa4-abcb481fb2c8" + assert mvr_scene.scene.layers[1].child_list.fixtures[0].uuid == "2e149740-6a41-bc43-bd59-8968781b11b9" + assert mvr_scene.scene.layers[2].child_list.scene_objects[1].uuid == "d1d76649-35bd-9d49-baa4-abcb481fb2c8" diff --git a/tests/test_mvr_01_write_ours.py b/tests/test_mvr_01_write_ours.py index 3fc0731..4e55596 100644 --- a/tests/test_mvr_01_write_ours.py +++ b/tests/test_mvr_01_write_ours.py @@ -12,34 +12,21 @@ def process_mvr_child_list(child_list, mvr_scene): for group in child_list.group_objects: if group.child_list is not None: - new_fixtures = process_mvr_child_list( + new_fixtures, new_focus_points = process_mvr_child_list( group.child_list, mvr_scene, ) all_fixtures += new_fixtures - all_focus_points += all_focus_points + all_focus_points += new_focus_points return (all_fixtures, all_focus_points) @pytest.mark.parametrize("mvr_scene", [("basic_fixture.mvr",)], indirect=True) def test_write_mvr_file(mvr_scene): - fixtures_list = [] mvr = pymvr.GeneralSceneDescriptionWriter() - pymvr.UserData().to_xml(parent=mvr.xml_root) - scene = pymvr.SceneElement().to_xml(parent=mvr.xml_root) - layers = pymvr.LayersElement().to_xml(parent=scene) - layer = pymvr.Layer(name="My layer").to_xml(parent=layers) - child_list = pymvr.ChildList().to_xml(parent=layer) - for layer in mvr_scene.layers: - fixtures, focus_points = process_mvr_child_list(layer.child_list, mvr_scene) - for fixture in fixtures: - child_list.append(fixture.to_xml()) - fixtures_list.append((fixture.gdtf_spec, fixture.gdtf_spec)) - for point in focus_points: - child_list.append(point.to_xml()) + mvr_scene.scene.to_xml(mvr.xml_root) + mvr_scene.user_data.to_xml(mvr.xml_root) + # TODO: add back file iteration to include gdtf files in the zip file - mvr_scene.aux_data.to_xml(parent=scene) - - mvr.files_list = list(set(fixtures_list)) test_file_path = Path(Path(__file__).parent, "test.mvr") mvr.write_mvr(test_file_path) diff --git a/tests/test_mvr_02_read_ours.py b/tests/test_mvr_02_read_ours.py index c7ff4d4..a5edab2 100644 --- a/tests/test_mvr_02_read_ours.py +++ b/tests/test_mvr_02_read_ours.py @@ -30,5 +30,6 @@ def process_mvr_fixture(fixture): @pytest.mark.parametrize("mvr_scene", [("test.mvr",)], indirect=True) def test_fixture(mvr_scene): - for layer in mvr_scene.layers: + assert len(mvr_scene.scene.layers) > 0 + for layer in mvr_scene.scene.layers: process_mvr_child_list(layer.child_list, mvr_scene) diff --git a/tests/test_mvr_01_write_ours_json.py b/tests/test_mvr_03_write_ours_json.py similarity index 65% rename from tests/test_mvr_01_write_ours_json.py rename to tests/test_mvr_03_write_ours_json.py index b0e26fc..e3752a2 100644 --- a/tests/test_mvr_01_write_ours_json.py +++ b/tests/test_mvr_03_write_ours_json.py @@ -28,29 +28,30 @@ def test_write_from_json(): ] fixtures_list = [] mvr = pymvr.GeneralSceneDescriptionWriter() - pymvr.UserData().to_xml(parent=mvr.xml_root) - scene = pymvr.SceneElement().to_xml(parent=mvr.xml_root) - layers = pymvr.LayersElement().to_xml(parent=scene) - for layer in data: - new_layer = pymvr.Layer(name=layer["name"], uuid=layer["uuid"]).to_xml(parent=layers) - child_list = pymvr.ChildList().to_xml(parent=new_layer) - for fixture in layer["fixtures"]: + layers = pymvr.Layers() + for layer_data in data: + layer = pymvr.Layer(name=layer_data["name"], uuid=layer_data["uuid"]) + layers.append(layer) + child_list = pymvr.ChildList() + layer.child_list = child_list + + for fixture_data in layer_data["fixtures"]: new_addresses = [ - pymvr.Address(dmx_break=address["dmx_break"], address=address["address"], universe=address["universe"]) for address in fixture["addresses"] + pymvr.Address(dmx_break=address["dmx_break"], address=address["address"], universe=address["universe"]) for address in fixture_data["addresses"] ] new_fixture = pymvr.Fixture( - name=fixture["gdtf_spec"], - uuid=fixture["uuid"], - gdtf_spec=fixture["gdtf_spec"], - gdtf_mode=fixture["gdtf_mode"], - fixture_id=fixture["fixture_id"], + name=fixture_data["gdtf_spec"], + uuid=fixture_data["uuid"], + gdtf_spec=fixture_data["gdtf_spec"], + gdtf_mode=fixture_data["gdtf_mode"], + fixture_id=fixture_data["fixture_id"], addresses=new_addresses, ) - child_list.append(new_fixture.to_xml()) + child_list.fixtures.append(new_fixture) fixtures_list.append((new_fixture.gdtf_spec, new_fixture.gdtf_spec)) - pymvr.AUXData().to_xml(parent=scene) - mvr.files_list = list(set(fixtures_list)) + scene = pymvr.Scene(layers=layers) + scene.to_xml(mvr.xml_root) test_file_path = Path(Path(__file__).parent, "test_json.mvr") mvr.write_mvr(test_file_path) diff --git a/tests/test_mvr_02_read_ours_json.py b/tests/test_mvr_04_read_ours_json.py similarity index 90% rename from tests/test_mvr_02_read_ours_json.py rename to tests/test_mvr_04_read_ours_json.py index f35b334..ac1360a 100644 --- a/tests/test_mvr_02_read_ours_json.py +++ b/tests/test_mvr_04_read_ours_json.py @@ -10,6 +10,7 @@ def test_version(mvr_scene): def process_mvr_child_list(child_list, mvr_scene): + assert len(child_list.fixtures) == 2 for fixture in child_list.fixtures: process_mvr_fixture(fixture) for group in child_list.group_objects: @@ -30,7 +31,8 @@ def process_mvr_fixture(fixture): @pytest.mark.parametrize("mvr_scene", [("test_json.mvr",)], indirect=True) def test_fixture(mvr_scene): - for layer in mvr_scene.layers: + assert len(mvr_scene.scene.layers) > 0 + for layer in mvr_scene.scene.layers: assert layer.name == "Layer 1" assert layer.uuid == "1e4954b5-992c-4146-b71f-5b497834087f" process_mvr_child_list(layer.child_list, mvr_scene) diff --git a/tests/test_mvr_05_example.py b/tests/test_mvr_05_example.py new file mode 100644 index 0000000..0e9b432 --- /dev/null +++ b/tests/test_mvr_05_example.py @@ -0,0 +1,26 @@ +from pathlib import Path +import pymvr + + +def test_write_example_mvr_file(): + fixtures_list = [] + mvr = pymvr.GeneralSceneDescriptionWriter() + + layers = pymvr.Layers() + layer = pymvr.Layer(name="Test layer") + layers.layers.append(layer) + + child_list = pymvr.ChildList() + layer.child_list = child_list + + fixture = pymvr.Fixture(name="Test Fixture") # not really a valid fixture + child_list.fixtures.append(fixture) + fixtures_list.append((fixture.gdtf_spec, fixture.gdtf_spec)) + + scene = pymvr.Scene(layers=layers) + + scene.to_xml(parent=mvr.xml_root) + + mvr.files_list = list(set(fixtures_list)) + test_file_path = Path(Path(__file__).parent, "example.mvr") + mvr.write_mvr(test_file_path) From 2fd1ec194f3c7a5257196e2c7370540b36d93042 Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 09:12:37 +0200 Subject: [PATCH 06/16] Add xml export to Connections --- pymvr/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index 0e6fcde..ad0337a 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -1149,6 +1149,15 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.own} {self.other}" + def to_xml(self): + element = ElementTree.Element( + type(self).__name__, + own=self.own, + other=self.other, + toObject=self.to_object, + ) + return element + class Mapping(BaseNode): def __init__( From 828da21a91e15949aeeacd955fc62efc2a9cee7a Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 09:58:39 +0200 Subject: [PATCH 07/16] Fixing implementation --- .gitignore | 2 +- pymvr/__init__.py | 313 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 308 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index bc206cc..a8263a8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,5 @@ __pycache__ pymvr.egg-info/ devel/ *whl -*mvr +*.mvr uv.lock diff --git a/pymvr/__init__.py b/pymvr/__init__.py index ad0337a..aa05572 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -1013,46 +1013,271 @@ class Support(BaseChildNodeExtended): def __init__( self, chain_length: float = 0, + multipatch: Union[str, None] = None, + position: Union[str, None] = None, + function_: Union[str, None] = None, *args, **kwargs, ): self.chain_length = chain_length + self.multipatch = multipatch + self.position = position + self.function_ = function_ super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): - if xml_node.find("ChainLength") is None: - self.chain_length = float(xml_node.find("ChainLength").text or 0) + super()._read_xml(xml_node) + chain_length_node = xml_node.find("ChainLength") + if chain_length_node is not None: + self.chain_length = float(chain_length_node.text or 0) + + if xml_node.attrib.get("multipatch") is not None: + self.multipatch = xml_node.attrib.get("multipatch") + + position_node = xml_node.find("Position") + if position_node is not None: + self.position = position_node.text + + function_node = xml_node.find("Function") + if function_node is not None: + self.function_ = function_node.text + + def to_xml(self): + attributes = {"name": self.name, "uuid": self.uuid} + if self.multipatch: + attributes["multipatch"] = self.multipatch + element = ElementTree.Element(type(self).__name__, **attributes) + + Matrix(self.matrix.matrix).to_xml(element) + if self.classing: + ElementTree.SubElement(element, "Classing").text = self.classing + + if self.position: + ElementTree.SubElement(element, "Position").text = self.position + + if self.geometries: + self.geometries.to_xml(element) + + if self.function_: + ElementTree.SubElement(element, "Function").text = self.function_ + + ElementTree.SubElement(element, "ChainLength").text = str(self.chain_length) + + if self.gdtf_spec: + ElementTree.SubElement(element, "GDTFSpec").text = self.gdtf_spec + if self.gdtf_mode: + ElementTree.SubElement(element, "GDTFMode").text = self.gdtf_mode + if self.cast_shadow: + ElementTree.SubElement(element, "CastShadow").text = "true" + + if self.addresses: + addresses_element = ElementTree.SubElement(element, "Addresses") + for address in self.addresses: + address.to_xml(addresses_element) + + if self.alignments: + alignments_element = ElementTree.SubElement(element, "Alignments") + for alignment in self.alignments: + alignments_element.append(alignment.to_xml()) + + if self.custom_commands: + commands_element = ElementTree.SubElement(element, "CustomCommands") + for command in self.custom_commands: + commands_element.append(command.to_xml()) + + if self.overwrites: + overwrites_element = ElementTree.SubElement(element, "Overwrites") + for overwrite in self.overwrites: + overwrites_element.append(overwrite.to_xml()) + + if self.connections: + connections_element = ElementTree.SubElement(element, "Connections") + for connection in self.connections: + connections_element.append(connection.to_xml()) + + ElementTree.SubElement(element, "FixtureID").text = str(self.fixture_id) or "0" + ElementTree.SubElement(element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) + if self.unit_number is not None: + ElementTree.SubElement(element, "UnitNumber").text = str(self.unit_number) + if self.custom_id_type is not None: + ElementTree.SubElement(element, "CustomIdType").text = str(self.custom_id_type) + if self.custom_id is not None: + ElementTree.SubElement(element, "CustomId").text = str(self.custom_id) + + if self.child_list: + self.child_list.to_xml(element) + + return element class VideoScreen(BaseChildNodeExtended): def __init__( self, sources: "Sources" = None, + multipatch: Union[str, None] = None, + function_: Union[str, None] = None, *args, **kwargs, ): self.sources = sources + self.multipatch = multipatch + self.function_ = function_ super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): super()._read_xml(xml_node) + if xml_node.attrib.get("multipatch") is not None: + self.multipatch = xml_node.attrib.get("multipatch") if xml_node.find("Sources") is not None: self.sources = Sources(xml_node=xml_node.find("Sources")) + if xml_node.find("Function") is not None: + self.function_ = xml_node.find("Function").text + + def to_xml(self): + attributes = {"name": self.name, "uuid": self.uuid} + if self.multipatch: + attributes["multipatch"] = self.multipatch + element = ElementTree.Element(type(self).__name__, **attributes) + + Matrix(self.matrix.matrix).to_xml(element) + if self.classing: + ElementTree.SubElement(element, "Classing").text = self.classing + + if self.geometries: + self.geometries.to_xml(element) + + if self.sources: + self.sources.to_xml(element) + if self.function_: + ElementTree.SubElement(element, "Function").text = self.function_ + + if self.gdtf_spec: + ElementTree.SubElement(element, "GDTFSpec").text = self.gdtf_spec + if self.gdtf_mode: + ElementTree.SubElement(element, "GDTFMode").text = self.gdtf_mode + if self.cast_shadow: + ElementTree.SubElement(element, "CastShadow").text = "true" + + if self.addresses: + addresses_element = ElementTree.SubElement(element, "Addresses") + for address in self.addresses: + address.to_xml(addresses_element) + + if self.alignments: + alignments_element = ElementTree.SubElement(element, "Alignments") + for alignment in self.alignments: + alignments_element.append(alignment.to_xml()) + + if self.custom_commands: + commands_element = ElementTree.SubElement(element, "CustomCommands") + for command in self.custom_commands: + commands_element.append(command.to_xml()) + + if self.overwrites: + overwrites_element = ElementTree.SubElement(element, "Overwrites") + for overwrite in self.overwrites: + overwrites_element.append(overwrite.to_xml()) + + if self.connections: + connections_element = ElementTree.SubElement(element, "Connections") + for connection in self.connections: + connections_element.append(connection.to_xml()) + + if self.child_list: + self.child_list.to_xml(element) + + ElementTree.SubElement(element, "FixtureID").text = str(self.fixture_id) or "0" + ElementTree.SubElement(element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) + if self.unit_number is not None: + ElementTree.SubElement(element, "UnitNumber").text = str(self.unit_number) + if self.custom_id_type is not None: + ElementTree.SubElement(element, "CustomIdType").text = str(self.custom_id_type) + if self.custom_id is not None: + ElementTree.SubElement(element, "CustomId").text = str(self.custom_id) + + return element class Projector(BaseChildNodeExtended): def __init__( self, projections: "Projections" = None, + multipatch: Union[str, None] = None, *args, **kwargs, ): self.projections = projections + self.multipatch = multipatch super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): - if xml_node.find("Projections") is None: - self.projections = Projections(xml_node.find("Projections")) + super()._read_xml(xml_node) + if xml_node.attrib.get("multipatch") is not None: + self.multipatch = xml_node.attrib.get("multipatch") + if xml_node.find("Projections") is not None: + self.projections = Projections(xml_node=xml_node.find("Projections")) + + def to_xml(self): + attributes = {"name": self.name, "uuid": self.uuid} + if self.multipatch: + attributes["multipatch"] = self.multipatch + element = ElementTree.Element(type(self).__name__, **attributes) + + Matrix(self.matrix.matrix).to_xml(element) + if self.classing: + ElementTree.SubElement(element, "Classing").text = self.classing + + if self.geometries: + self.geometries.to_xml(element) + + if self.projections: + self.projections.to_xml(element) + + if self.gdtf_spec: + ElementTree.SubElement(element, "GDTFSpec").text = self.gdtf_spec + if self.gdtf_mode: + ElementTree.SubElement(element, "GDTFMode").text = self.gdtf_mode + if self.cast_shadow: + ElementTree.SubElement(element, "CastShadow").text = "true" + + if self.addresses: + addresses_element = ElementTree.SubElement(element, "Addresses") + for address in self.addresses: + address.to_xml(addresses_element) + + if self.alignments: + alignments_element = ElementTree.SubElement(element, "Alignments") + for alignment in self.alignments: + alignments_element.append(alignment.to_xml()) + + if self.custom_commands: + commands_element = ElementTree.SubElement(element, "CustomCommands") + for command in self.custom_commands: + commands_element.append(command.to_xml()) + + if self.overwrites: + overwrites_element = ElementTree.SubElement(element, "Overwrites") + for overwrite in self.overwrites: + overwrites_element.append(overwrite.to_xml()) + + if self.connections: + connections_element = ElementTree.SubElement(element, "Connections") + for connection in self.connections: + connections_element.append(connection.to_xml()) + + if self.child_list: + self.child_list.to_xml(element) + + ElementTree.SubElement(element, "FixtureID").text = str(self.fixture_id) or "0" + ElementTree.SubElement(element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) + if self.unit_number is not None: + ElementTree.SubElement(element, "UnitNumber").text = str(self.unit_number) + if self.custom_id_type is not None: + ElementTree.SubElement(element, "CustomIdType").text = str(self.custom_id_type) + if self.custom_id is not None: + ElementTree.SubElement(element, "CustomId").text = str(self.custom_id) + + return element class Protocol(BaseNode): @@ -1083,6 +1308,21 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.name}" + def to_xml(self): + attributes = {} + if self.geometry: + attributes["geometry"] = self.geometry + if self.name: + attributes["name"] = self.name + if self.type: + attributes["type"] = self.type + if self.version: + attributes["version"] = self.version + if self.transmission: + attributes["transmission"] = self.transmission + element = ElementTree.Element(type(self).__name__, **attributes) + return element + class Alignment(BaseNode): def __init__( @@ -1106,6 +1346,17 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.geometry}" + def to_xml(self): + attributes = {} + if self.geometry: + attributes["geometry"] = self.geometry + if self.up: + attributes["up"] = self.up + if self.direction: + attributes["direction"] = self.direction + element = ElementTree.Element(type(self).__name__, **attributes) + return element + class Overwrite(BaseNode): def __init__( @@ -1126,6 +1377,13 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.universal} {self.target}" + def to_xml(self): + attributes = {"universal": self.universal} + if self.target: + attributes["target"] = self.target + element = ElementTree.Element(type(self).__name__, **attributes) + return element + class Connection(BaseNode): def __init__( @@ -1254,9 +1512,52 @@ def to_xml(self): return element +class Projection(BaseNode): + def __init__( + self, + source: "Source" = None, + scale_handling: Union[str, None] = None, + *args, + **kwargs, + ): + self.source = source + self.scale_handling = scale_handling + super().__init__(*args, **kwargs) + + def _read_xml(self, xml_node: "Element"): + if xml_node.find("Source") is not None: + self.source = Source(xml_node=xml_node.find("Source")) + if xml_node.find("ScaleHandeling") is not None: + self.scale_handling = xml_node.find("ScaleHandeling").text + + def to_xml(self): + element = ElementTree.Element(type(self).__name__) + if self.source: + element.append(self.source.to_xml()) + if self.scale_handling: + ElementTree.SubElement(element, "ScaleHandeling").text = self.scale_handling + return element + + class Projections(BaseNode): - ... - # todo + def __init__( + self, + projections: List["Projection"] = [], + xml_node: "Element" = None, + *args, + **kwargs, + ): + self.projections = projections + super().__init__(xml_node, *args, **kwargs) + + def _read_xml(self, xml_node: "Element"): + self.projections = [Projection(xml_node=i) for i in xml_node.findall("Projection")] + + def to_xml(self, parent: Element): + element = ElementTree.SubElement(parent, type(self).__name__) + for projection in self.projections: + element.append(projection.to_xml()) + return element class Source(BaseNode): From c73c50626e5690a9359530d5e2c159edc988d473 Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 10:07:58 +0200 Subject: [PATCH 08/16] Streamline to_xml --- pymvr/__init__.py | 298 ++++++++++++---------------------------------- 1 file changed, 74 insertions(+), 224 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index aa05572..6f0a252 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -207,6 +207,7 @@ def __init__( overwrites: List["Overwrite"] = [], connections: List["Connection"] = [], child_list: Union["ChildList", None] = None, + multipatch: Union[str, None] = None, *args, **kwargs, ): @@ -230,11 +231,13 @@ def __init__( self.overwrites = overwrites self.connections = connections self.child_list = child_list + self.multipatch = multipatch super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): self.name = xml_node.attrib.get("name") self.uuid = xml_node.attrib.get("uuid") + self.multipatch = xml_node.attrib.get("multipatch") _gdtf_spec = xml_node.find("GDTFSpec") if _gdtf_spec is not None: self.gdtf_spec = _gdtf_spec.text @@ -286,6 +289,55 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.name}" + def to_xml(self, element: Element): + Matrix(self.matrix.matrix).to_xml(element) + if self.classing: + ElementTree.SubElement(element, "Classing").text = self.classing + if self.gdtf_spec: + ElementTree.SubElement(element, "GDTFSpec").text = self.gdtf_spec + if self.gdtf_mode: + ElementTree.SubElement(element, "GDTFMode").text = self.gdtf_mode + if self.cast_shadow: + ElementTree.SubElement(element, "CastShadow").text = "true" + + if self.addresses: + addresses_element = ElementTree.SubElement(element, "Addresses") + for address in self.addresses: + address.to_xml(addresses_element) + + if self.alignments: + alignments_element = ElementTree.SubElement(element, "Alignments") + for alignment in self.alignments: + alignments_element.append(alignment.to_xml()) + + if self.custom_commands: + commands_element = ElementTree.SubElement(element, "CustomCommands") + for command in self.custom_commands: + commands_element.append(command.to_xml()) + + if self.overwrites: + overwrites_element = ElementTree.SubElement(element, "Overwrites") + for overwrite in self.overwrites: + overwrites_element.append(overwrite.to_xml()) + + if self.connections: + connections_element = ElementTree.SubElement(element, "Connections") + for connection in self.connections: + connections_element.append(connection.to_xml()) + + ElementTree.SubElement(element, "FixtureID").text = str(self.fixture_id) or "0" + ElementTree.SubElement(element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) + if self.unit_number is not None: + ElementTree.SubElement(element, "UnitNumber").text = str(self.unit_number) + if self.custom_id_type is not None: + ElementTree.SubElement(element, "CustomIdType").text = str(self.custom_id_type) + if self.custom_id is not None: + ElementTree.SubElement(element, "CustomId").text = str(self.custom_id) + + if self.child_list: + self.child_list.to_xml(element) + + class BaseChildNodeExtended(BaseChildNode): def __init__( @@ -309,6 +361,11 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.name}" + def to_xml(self, element: Element): + super().to_xml(element) + if self.geometries: + self.geometries.to_xml(element) + class Data(BaseNode): def __init__( @@ -407,7 +464,6 @@ def to_xml(self): class Fixture(BaseChildNode): def __init__( self, - multipatch: Union[str, None] = None, focus: Union[str, None] = None, color: Union["Color", str, None] = Color(), dmx_invert_pan: bool = False, @@ -421,7 +477,6 @@ def __init__( *args, **kwargs, ): - self.multipatch = multipatch self.focus = focus self.color = color self.dmx_invert_pan = dmx_invert_pan @@ -437,9 +492,6 @@ def __init__( def _read_xml(self, xml_node: "Element"): super()._read_xml(xml_node) - if xml_node.attrib.get("multipatch") is not None: - self.multipatch = xml_node.attrib.get("multipatch") - if xml_node.find("Focus") is not None: self.focus = xml_node.find("Focus").text @@ -474,79 +526,40 @@ def to_xml(self): attributes = {"name": self.name, "uuid": self.uuid} if self.multipatch: attributes["multipatch"] = self.multipatch - fixture_element = ElementTree.Element(type(self).__name__, **attributes) + element = ElementTree.Element(type(self).__name__, **attributes) + super().to_xml(element) - Matrix(self.matrix.matrix).to_xml(fixture_element) - if self.classing: - ElementTree.SubElement(fixture_element, "Classing").text = self.classing - if self.gdtf_spec: - ElementTree.SubElement(fixture_element, "GDTFSpec").text = self.gdtf_spec - if self.gdtf_mode: - ElementTree.SubElement(fixture_element, "GDTFMode").text = self.gdtf_mode if self.focus: - ElementTree.SubElement(fixture_element, "Focus").text = self.focus - if self.cast_shadow: - ElementTree.SubElement(fixture_element, "CastShadow").text = "true" + ElementTree.SubElement(element, "Focus").text = self.focus if self.dmx_invert_pan: - ElementTree.SubElement(fixture_element, "DMXInvertPan").text = "true" + ElementTree.SubElement(element, "DMXInvertPan").text = "true" if self.dmx_invert_tilt: - ElementTree.SubElement(fixture_element, "DMXInvertTilt").text = "true" + ElementTree.SubElement(element, "DMXInvertTilt").text = "true" if self.position: - ElementTree.SubElement(fixture_element, "Position").text = self.position + ElementTree.SubElement(element, "Position").text = self.position if self.function_: - ElementTree.SubElement(fixture_element, "Function").text = self.function_ + ElementTree.SubElement(element, "Function").text = self.function_ if self.child_position: - ElementTree.SubElement(fixture_element, "ChildPosition").text = self.child_position + ElementTree.SubElement(element, "ChildPosition").text = self.child_position - ElementTree.SubElement(fixture_element, "FixtureID").text = str(self.fixture_id) or "0" - ElementTree.SubElement(fixture_element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) - ElementTree.SubElement(fixture_element, "UnitNumber").text = str(self.unit_number) - - if self.addresses: - addresses_element = ElementTree.SubElement(fixture_element, "Addresses") - for address in self.addresses: - address.to_xml(addresses_element) if self.protocols: - protocols_element = ElementTree.SubElement(fixture_element, "Protocols") + protocols_element = ElementTree.SubElement(element, "Protocols") for protocol in self.protocols: protocols_element.append(protocol.to_xml()) - if self.alignments: - alignments_element = ElementTree.SubElement(fixture_element, "Alignments") - for alignment in self.alignments: - alignments_element.append(alignment.to_xml()) - if self.custom_commands: - commands_element = ElementTree.SubElement(fixture_element, "CustomCommands") - for command in self.custom_commands: - commands_element.append(command.to_xml()) - if self.overwrites: - overwrites_element = ElementTree.SubElement(fixture_element, "Overwrites") - for overwrite in self.overwrites: - overwrites_element.append(overwrite.to_xml()) - if self.connections: - connections_element = ElementTree.SubElement(fixture_element, "Connections") - for connection in self.connections: - connections_element.append(connection.to_xml()) if isinstance(self.color, Color): - self.color.to_xml(fixture_element) + self.color.to_xml(element) elif self.color: - Color(str_repr=self.color).to_xml(fixture_element) - - if self.custom_id_type: - ElementTree.SubElement(fixture_element, "CustomIdType").text = str(self.custom_id_type) - if self.custom_id: - ElementTree.SubElement(fixture_element, "CustomId").text = str(self.custom_id) + Color(str_repr=self.color).to_xml(element) if self.mappings: - mappings_element = ElementTree.SubElement(fixture_element, "Mappings") + mappings_element = ElementTree.SubElement(element, "Mappings") for mapping in self.mappings: mappings_element.append(mapping.to_xml()) if self.gobo: - fixture_element.append(self.gobo.to_xml()) - if self.child_list: - self.child_list.to_xml(fixture_element) + element.append(self.gobo.to_xml()) - return fixture_element + return element def __str__(self): return f"{self.name}" @@ -1013,14 +1026,12 @@ class Support(BaseChildNodeExtended): def __init__( self, chain_length: float = 0, - multipatch: Union[str, None] = None, position: Union[str, None] = None, function_: Union[str, None] = None, *args, **kwargs, ): self.chain_length = chain_length - self.multipatch = multipatch self.position = position self.function_ = function_ super().__init__(*args, **kwargs) @@ -1031,9 +1042,6 @@ def _read_xml(self, xml_node: "Element"): if chain_length_node is not None: self.chain_length = float(chain_length_node.text or 0) - if xml_node.attrib.get("multipatch") is not None: - self.multipatch = xml_node.attrib.get("multipatch") - position_node = xml_node.find("Position") if position_node is not None: self.position = position_node.text @@ -1047,66 +1055,16 @@ def to_xml(self): if self.multipatch: attributes["multipatch"] = self.multipatch element = ElementTree.Element(type(self).__name__, **attributes) - - Matrix(self.matrix.matrix).to_xml(element) - if self.classing: - ElementTree.SubElement(element, "Classing").text = self.classing + super().to_xml(element) if self.position: ElementTree.SubElement(element, "Position").text = self.position - if self.geometries: - self.geometries.to_xml(element) - if self.function_: ElementTree.SubElement(element, "Function").text = self.function_ ElementTree.SubElement(element, "ChainLength").text = str(self.chain_length) - if self.gdtf_spec: - ElementTree.SubElement(element, "GDTFSpec").text = self.gdtf_spec - if self.gdtf_mode: - ElementTree.SubElement(element, "GDTFMode").text = self.gdtf_mode - if self.cast_shadow: - ElementTree.SubElement(element, "CastShadow").text = "true" - - if self.addresses: - addresses_element = ElementTree.SubElement(element, "Addresses") - for address in self.addresses: - address.to_xml(addresses_element) - - if self.alignments: - alignments_element = ElementTree.SubElement(element, "Alignments") - for alignment in self.alignments: - alignments_element.append(alignment.to_xml()) - - if self.custom_commands: - commands_element = ElementTree.SubElement(element, "CustomCommands") - for command in self.custom_commands: - commands_element.append(command.to_xml()) - - if self.overwrites: - overwrites_element = ElementTree.SubElement(element, "Overwrites") - for overwrite in self.overwrites: - overwrites_element.append(overwrite.to_xml()) - - if self.connections: - connections_element = ElementTree.SubElement(element, "Connections") - for connection in self.connections: - connections_element.append(connection.to_xml()) - - ElementTree.SubElement(element, "FixtureID").text = str(self.fixture_id) or "0" - ElementTree.SubElement(element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) - if self.unit_number is not None: - ElementTree.SubElement(element, "UnitNumber").text = str(self.unit_number) - if self.custom_id_type is not None: - ElementTree.SubElement(element, "CustomIdType").text = str(self.custom_id_type) - if self.custom_id is not None: - ElementTree.SubElement(element, "CustomId").text = str(self.custom_id) - - if self.child_list: - self.child_list.to_xml(element) - return element @@ -1114,20 +1072,16 @@ class VideoScreen(BaseChildNodeExtended): def __init__( self, sources: "Sources" = None, - multipatch: Union[str, None] = None, function_: Union[str, None] = None, *args, **kwargs, ): self.sources = sources - self.multipatch = multipatch self.function_ = function_ super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): super()._read_xml(xml_node) - if xml_node.attrib.get("multipatch") is not None: - self.multipatch = xml_node.attrib.get("multipatch") if xml_node.find("Sources") is not None: self.sources = Sources(xml_node=xml_node.find("Sources")) if xml_node.find("Function") is not None: @@ -1138,63 +1092,13 @@ def to_xml(self): if self.multipatch: attributes["multipatch"] = self.multipatch element = ElementTree.Element(type(self).__name__, **attributes) - - Matrix(self.matrix.matrix).to_xml(element) - if self.classing: - ElementTree.SubElement(element, "Classing").text = self.classing - - if self.geometries: - self.geometries.to_xml(element) + super().to_xml(element) if self.sources: self.sources.to_xml(element) if self.function_: ElementTree.SubElement(element, "Function").text = self.function_ - if self.gdtf_spec: - ElementTree.SubElement(element, "GDTFSpec").text = self.gdtf_spec - if self.gdtf_mode: - ElementTree.SubElement(element, "GDTFMode").text = self.gdtf_mode - if self.cast_shadow: - ElementTree.SubElement(element, "CastShadow").text = "true" - - if self.addresses: - addresses_element = ElementTree.SubElement(element, "Addresses") - for address in self.addresses: - address.to_xml(addresses_element) - - if self.alignments: - alignments_element = ElementTree.SubElement(element, "Alignments") - for alignment in self.alignments: - alignments_element.append(alignment.to_xml()) - - if self.custom_commands: - commands_element = ElementTree.SubElement(element, "CustomCommands") - for command in self.custom_commands: - commands_element.append(command.to_xml()) - - if self.overwrites: - overwrites_element = ElementTree.SubElement(element, "Overwrites") - for overwrite in self.overwrites: - overwrites_element.append(overwrite.to_xml()) - - if self.connections: - connections_element = ElementTree.SubElement(element, "Connections") - for connection in self.connections: - connections_element.append(connection.to_xml()) - - if self.child_list: - self.child_list.to_xml(element) - - ElementTree.SubElement(element, "FixtureID").text = str(self.fixture_id) or "0" - ElementTree.SubElement(element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) - if self.unit_number is not None: - ElementTree.SubElement(element, "UnitNumber").text = str(self.unit_number) - if self.custom_id_type is not None: - ElementTree.SubElement(element, "CustomIdType").text = str(self.custom_id_type) - if self.custom_id is not None: - ElementTree.SubElement(element, "CustomId").text = str(self.custom_id) - return element @@ -1202,18 +1106,14 @@ class Projector(BaseChildNodeExtended): def __init__( self, projections: "Projections" = None, - multipatch: Union[str, None] = None, *args, **kwargs, ): self.projections = projections - self.multipatch = multipatch super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): super()._read_xml(xml_node) - if xml_node.attrib.get("multipatch") is not None: - self.multipatch = xml_node.attrib.get("multipatch") if xml_node.find("Projections") is not None: self.projections = Projections(xml_node=xml_node.find("Projections")) @@ -1222,61 +1122,11 @@ def to_xml(self): if self.multipatch: attributes["multipatch"] = self.multipatch element = ElementTree.Element(type(self).__name__, **attributes) - - Matrix(self.matrix.matrix).to_xml(element) - if self.classing: - ElementTree.SubElement(element, "Classing").text = self.classing - - if self.geometries: - self.geometries.to_xml(element) + super().to_xml(element) if self.projections: self.projections.to_xml(element) - if self.gdtf_spec: - ElementTree.SubElement(element, "GDTFSpec").text = self.gdtf_spec - if self.gdtf_mode: - ElementTree.SubElement(element, "GDTFMode").text = self.gdtf_mode - if self.cast_shadow: - ElementTree.SubElement(element, "CastShadow").text = "true" - - if self.addresses: - addresses_element = ElementTree.SubElement(element, "Addresses") - for address in self.addresses: - address.to_xml(addresses_element) - - if self.alignments: - alignments_element = ElementTree.SubElement(element, "Alignments") - for alignment in self.alignments: - alignments_element.append(alignment.to_xml()) - - if self.custom_commands: - commands_element = ElementTree.SubElement(element, "CustomCommands") - for command in self.custom_commands: - commands_element.append(command.to_xml()) - - if self.overwrites: - overwrites_element = ElementTree.SubElement(element, "Overwrites") - for overwrite in self.overwrites: - overwrites_element.append(overwrite.to_xml()) - - if self.connections: - connections_element = ElementTree.SubElement(element, "Connections") - for connection in self.connections: - connections_element.append(connection.to_xml()) - - if self.child_list: - self.child_list.to_xml(element) - - ElementTree.SubElement(element, "FixtureID").text = str(self.fixture_id) or "0" - ElementTree.SubElement(element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) - if self.unit_number is not None: - ElementTree.SubElement(element, "UnitNumber").text = str(self.unit_number) - if self.custom_id_type is not None: - ElementTree.SubElement(element, "CustomIdType").text = str(self.custom_id_type) - if self.custom_id is not None: - ElementTree.SubElement(element, "CustomId").text = str(self.custom_id) - return element From acb64a557db14809cc1ece7033a9e7ad7ed4d17a Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 11:19:57 +0200 Subject: [PATCH 09/16] Implement missing classes --- pymvr/__init__.py | 153 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 134 insertions(+), 19 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index 6f0a252..d68ae45 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -5,6 +5,7 @@ import sys import uuid as py_uuid from .value import Matrix, Color # type: ignore +from enum import Enum __version__ = "0.5.1" @@ -186,6 +187,114 @@ def to_xml(self, parent: Element): return element +class ScaleHandelingEnum(Enum): + SCALE_KEEP_RATIO = "ScaleKeepRatio" + SCALE_IGNORE_RATIO = "ScaleIgnoreRatio" + KEEP_SIZE_CENTER = "KeepSizeCenter" + + +class ScaleHandeling(BaseNode): + def __init__( + self, + value: ScaleHandelingEnum = ScaleHandelingEnum.SCALE_KEEP_RATIO, + xml_node: "Element" = None, + *args, + **kwargs, + ): + self.value = value + super().__init__(xml_node, *args, **kwargs) + + def _read_xml(self, xml_node: "Element"): + if xml_node.text: + try: + self.value = ScaleHandelingEnum(xml_node.text) + except ValueError: + self.value = ScaleHandelingEnum.SCALE_KEEP_RATIO + else: + self.value = ScaleHandelingEnum.SCALE_KEEP_RATIO + + def to_xml(self, parent: Element): + element = ElementTree.SubElement(parent, "ScaleHandeling") + element.text = self.value.value + return element + + +class Network(BaseNode): + def __init__( + self, + geometry: str = "", + ipv4: Union[str, None] = None, + subnetmask: Union[str, None] = None, + ipv6: Union[str, None] = None, + dhcp: bool = False, + hostname: Union[str, None] = None, + xml_node: "Element" = None, + *args, + **kwargs, + ): + self.geometry = geometry + self.ipv4 = ipv4 + self.subnetmask = subnetmask + self.ipv6 = ipv6 + self.dhcp = dhcp + self.hostname = hostname + super().__init__(xml_node, *args, **kwargs) + + def _read_xml(self, xml_node: "Element"): + self.geometry = xml_node.attrib.get("geometry", "") + self.ipv4 = xml_node.attrib.get("ipv4") + self.subnetmask = xml_node.attrib.get("subnetmask") + self.ipv6 = xml_node.attrib.get("ipv6") + self.dhcp = xml_node.attrib.get("dhcp", "false").lower() in ("true", "1", "on") + self.hostname = xml_node.attrib.get("hostname") + + def to_xml(self, parent: Element): + attributes = {"geometry": self.geometry} + if self.ipv4: + attributes["ipv4"] = self.ipv4 + if self.subnetmask: + attributes["subnetmask"] = self.subnetmask + if self.ipv6: + attributes["ipv6"] = self.ipv6 + if self.dhcp: + attributes["dhcp"] = "true" + if self.hostname: + attributes["hostname"] = self.hostname + element = ElementTree.SubElement(parent, "Network", **attributes) + return element + + +class Addresses(BaseNode): + def __init__( + self, + dmx: List["Address"] = [], + network: List["Network"] = [], + xml_node: "Element" = None, + *args, + **kwargs, + ): + self.dmx = dmx if dmx is not None else [] + self.network = network if network is not None else [] + super().__init__(xml_node, *args, **kwargs) + + def _read_xml(self, xml_node: "Element"): + self.dmx = [Address(xml_node=i) for i in xml_node.findall("Address")] + self.network = [Network(xml_node=i) for i in xml_node.findall("Network")] + + def to_xml(self, parent: Element): + if not self.dmx and not self.network: + return None + element = ElementTree.SubElement(parent, "Addresses") + for dmx_address in self.dmx: + dmx_address.to_xml(element) + for network_address in self.network: + network_address.to_xml(element) + return element + + def __len__(self): + return len(self.dmx) + len(self.network) + + class BaseChildNode(BaseNode): def __init__( self, @@ -201,7 +310,7 @@ def __init__( custom_id: int = 0, custom_id_type: int = 0, cast_shadow: bool = False, - addresses: List["Address"] = [], + addresses: "Addresses" = None, alignments: List["Alignment"] = [], custom_commands: List["CustomCommand"] = [], overwrites: List["Overwrite"] = [], @@ -225,7 +334,7 @@ def __init__( self.custom_id = custom_id self.custom_id_type = custom_id_type self.cast_shadow = cast_shadow - self.addresses = addresses + self.addresses = addresses if addresses is not None else Addresses() self.alignments = alignments self.custom_commands = custom_commands self.overwrites = overwrites @@ -268,10 +377,14 @@ def _read_xml(self, xml_node: "Element"): text_value = (xml_node.find("CastShadow").text or "false").lower() self.cast_shadow = text_value in ("true", "1") - if xml_node.find("Addresses") is not None: - self.addresses = [Address(xml_node=i) for i in xml_node.find("Addresses").findall("Address")] - if not len(self.addresses): - self.addresses = [Address(dmx_break=0, universe=0, address=0)] + addresses_node = xml_node.find("Addresses") + if addresses_node is not None: + self.addresses = Addresses(xml_node=addresses_node) + else: + self.addresses = Addresses() + + if len(self.addresses) == 0: + self.addresses.dmx.append(Address(dmx_break=0, universe=0, address=0)) if xml_node.find("Alignments"): self.alignments = [Alignment(xml_node=i) for i in xml_node.find("Alignments").findall("Alignment")] @@ -301,9 +414,7 @@ def to_xml(self, element: Element): ElementTree.SubElement(element, "CastShadow").text = "true" if self.addresses: - addresses_element = ElementTree.SubElement(element, "Addresses") - for address in self.addresses: - address.to_xml(addresses_element) + self.addresses.to_xml(element) if self.alignments: alignments_element = ElementTree.SubElement(element, "Alignments") @@ -338,7 +449,6 @@ def to_xml(self, element: Element): self.child_list.to_xml(element) - class BaseChildNodeExtended(BaseChildNode): def __init__( self, @@ -434,7 +544,7 @@ def __init__( size_x: int = 0, size_y: int = 0, source=None, - scale_handling=None, + scale_handling: "ScaleHandeling" = None, *args, **kwargs, ): @@ -443,7 +553,7 @@ def __init__( self.size_x = size_x self.size_y = size_y self.source = source - self.scale_handling = scale_handling + self.scale_handling = scale_handling if scale_handling is not None else ScaleHandeling() super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): @@ -451,13 +561,17 @@ def _read_xml(self, xml_node: "Element"): self.size_x = int(xml_node.find("SizeX").text) self.size_y = int(xml_node.find("SizeY").text) self.source = xml_node.find("Source") # TODO - self.scale_handling = xml_node.find("ScaleHandeling").text # TODO ENUM + scale_handling_node = xml_node.find("ScaleHandeling") + if scale_handling_node is not None: + self.scale_handling = ScaleHandeling(xml_node=scale_handling_node) def to_xml(self): element = ElementTree.Element(type(self).__name__, name=self.name, uuid=self.uuid) ElementTree.SubElement(element, "SizeX").text = str(self.size_x) ElementTree.SubElement(element, "SizeY").text = str(self.size_y) - # TODO source and scale_handling + if self.scale_handling: + self.scale_handling.to_xml(element) + # TODO source return element @@ -1366,26 +1480,27 @@ class Projection(BaseNode): def __init__( self, source: "Source" = None, - scale_handling: Union[str, None] = None, + scale_handling: "ScaleHandeling" = None, *args, **kwargs, ): self.source = source - self.scale_handling = scale_handling + self.scale_handling = scale_handling if scale_handling is not None else ScaleHandeling() super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): if xml_node.find("Source") is not None: self.source = Source(xml_node=xml_node.find("Source")) - if xml_node.find("ScaleHandeling") is not None: - self.scale_handling = xml_node.find("ScaleHandeling").text + scale_handling_node = xml_node.find("ScaleHandeling") + if scale_handling_node is not None: + self.scale_handling = ScaleHandeling(xml_node=scale_handling_node) def to_xml(self): element = ElementTree.Element(type(self).__name__) if self.source: element.append(self.source.to_xml()) if self.scale_handling: - ElementTree.SubElement(element, "ScaleHandeling").text = self.scale_handling + self.scale_handling.to_xml(element) return element From 9efbb3bdae2f9d4345b3cd3a2e7b688b8f8716fb Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 11:35:05 +0200 Subject: [PATCH 10/16] Adjust addresses class --- pymvr/__init__.py | 14 +++++++------- tests/test_fixture_1_5.py | 4 ++-- tests/test_mvr_02_read_ours.py | 4 ++-- tests/test_mvr_03_write_ours_json.py | 3 ++- tests/test_mvr_04_read_ours_json.py | 4 ++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index d68ae45..5b91017 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -267,32 +267,32 @@ def to_xml(self, parent: Element): class Addresses(BaseNode): def __init__( self, - dmx: List["Address"] = [], + address: List["Address"] = [], network: List["Network"] = [], xml_node: "Element" = None, *args, **kwargs, ): - self.dmx = dmx if dmx is not None else [] + self.address = address if address is not None else [] self.network = network if network is not None else [] super().__init__(xml_node, *args, **kwargs) def _read_xml(self, xml_node: "Element"): - self.dmx = [Address(xml_node=i) for i in xml_node.findall("Address")] + self.address = [Address(xml_node=i) for i in xml_node.findall("Address")] self.network = [Network(xml_node=i) for i in xml_node.findall("Network")] def to_xml(self, parent: Element): - if not self.dmx and not self.network: + if not self.address and not self.network: return None element = ElementTree.SubElement(parent, "Addresses") - for dmx_address in self.dmx: + for dmx_address in self.address: dmx_address.to_xml(element) for network_address in self.network: network_address.to_xml(element) return element def __len__(self): - return len(self.dmx) + len(self.network) + return len(self.address) + len(self.network) class BaseChildNode(BaseNode): @@ -384,7 +384,7 @@ def _read_xml(self, xml_node: "Element"): self.addresses = Addresses() if len(self.addresses) == 0: - self.addresses.dmx.append(Address(dmx_break=0, universe=0, address=0)) + self.addresses.address.append(Address(dmx_break=0, universe=0, address=0)) if xml_node.find("Alignments"): self.alignments = [Alignment(xml_node=i) for i in xml_node.find("Alignments").findall("Alignment")] diff --git a/tests/test_fixture_1_5.py b/tests/test_fixture_1_5.py index 8b85116..9af5ac6 100644 --- a/tests/test_fixture_1_5.py +++ b/tests/test_fixture_1_5.py @@ -22,8 +22,8 @@ def process_mvr_child_list(child_list, mvr_scene): def process_mvr_fixture(fixture): assert fixture.gdtf_spec == "LED PAR 64 RGBW.gdtf" - assert fixture.addresses[0].universe == 1 # even though the uni is 0 in the file, 1 is by the spec - assert fixture.addresses[0].address == 1 # dtto + assert fixture.addresses.address[0].universe == 1 # even though the uni is 0 in the file, 1 is by the spec + assert fixture.addresses.address[0].address == 1 # dtto assert fixture.gdtf_mode == "Default" assert fixture.matrix.matrix[3] == [5.0, 5.0, 5.0, 0] diff --git a/tests/test_mvr_02_read_ours.py b/tests/test_mvr_02_read_ours.py index a5edab2..4802030 100644 --- a/tests/test_mvr_02_read_ours.py +++ b/tests/test_mvr_02_read_ours.py @@ -22,8 +22,8 @@ def process_mvr_child_list(child_list, mvr_scene): def process_mvr_fixture(fixture): assert fixture.gdtf_spec == "LED PAR 64 RGBW.gdtf" - assert fixture.addresses[0].universe == 1 - assert fixture.addresses[0].address == 1 + assert fixture.addresses.address[0].universe == 1 + assert fixture.addresses.address[0].address == 1 assert fixture.gdtf_mode == "Default" assert fixture.matrix.matrix[3] == [5.0, 5.0, 5.0, 0] diff --git a/tests/test_mvr_03_write_ours_json.py b/tests/test_mvr_03_write_ours_json.py index e3752a2..19e454c 100644 --- a/tests/test_mvr_03_write_ours_json.py +++ b/tests/test_mvr_03_write_ours_json.py @@ -39,13 +39,14 @@ def test_write_from_json(): new_addresses = [ pymvr.Address(dmx_break=address["dmx_break"], address=address["address"], universe=address["universe"]) for address in fixture_data["addresses"] ] + new_fixture = pymvr.Fixture( name=fixture_data["gdtf_spec"], uuid=fixture_data["uuid"], gdtf_spec=fixture_data["gdtf_spec"], gdtf_mode=fixture_data["gdtf_mode"], fixture_id=fixture_data["fixture_id"], - addresses=new_addresses, + addresses=pymvr.Addresses(address=new_addresses), ) child_list.fixtures.append(new_fixture) diff --git a/tests/test_mvr_04_read_ours_json.py b/tests/test_mvr_04_read_ours_json.py index ac1360a..e66d5d9 100644 --- a/tests/test_mvr_04_read_ours_json.py +++ b/tests/test_mvr_04_read_ours_json.py @@ -23,8 +23,8 @@ def process_mvr_child_list(child_list, mvr_scene): def process_mvr_fixture(fixture): assert fixture.gdtf_spec == "BlenderDMX@Basic_LED_Bulb@ver2.gdtf" - assert fixture.addresses[0].dmx_break == 1 - assert fixture.addresses[0].universe == 1 + assert fixture.addresses.address[0].dmx_break == 1 + assert fixture.addresses.address[0].universe == 1 assert fixture.gdtf_mode == "Standard mode" assert fixture.matrix.matrix[3] == [0.0, 0.0, 0.0, 0] From bca71f143e00ca43c10f3de82d9f9e67bb80e965 Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 11:54:04 +0200 Subject: [PATCH 11/16] Implement container classes --- pymvr/__init__.py | 131 +++++++++++++++++++++++++++++++++------------- 1 file changed, 95 insertions(+), 36 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index 5b91017..cc09c05 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -86,6 +86,77 @@ def _read_xml(self, xml_node: "Element"): pass +class ContainerNode(BaseNode): + def __init__(self, children=None, xml_node: "Element" = None, *args, **kwargs): + if children is None: + children = [] + self.children = children + super().__init__(xml_node, *args, **kwargs) + + def __iter__(self): + return iter(self.children) + + def __getitem__(self, item): + return self.children[item] + + def append(self, child): + self.children.append(child) + + def extend(self, children_list): + self.children.extend(children_list) + + def insert(self, index, child): + self.children.insert(index, child) + + def remove(self, child): + self.children.remove(child) + + def pop(self, index=-1): + return self.children.pop(index) + + def clear(self): + self.children.clear() + + def __len__(self): + return len(self.children) + + def to_xml(self, parent: Element): + element = ElementTree.SubElement(parent, type(self).__name__) + for child in self.children: + element.append(child.to_xml()) + return element + + +class Protocols(ContainerNode): + def _read_xml(self, xml_node: "Element"): + self.children = [Protocol(xml_node=i) for i in xml_node.findall("Protocol")] + + +class Alignments(ContainerNode): + def _read_xml(self, xml_node: "Element"): + self.children = [Alignment(xml_node=i) for i in xml_node.findall("Alignment")] + + +class CustomCommands(ContainerNode): + def _read_xml(self, xml_node: "Element"): + self.children = [CustomCommand(xml_node=i) for i in xml_node.findall("CustomCommand")] + + +class Overwrites(ContainerNode): + def _read_xml(self, xml_node: "Element"): + self.children = [Overwrite(xml_node=i) for i in xml_node.findall("Overwrite")] + + +class Connections(ContainerNode): + def _read_xml(self, xml_node: "Element"): + self.children = [Connection(xml_node=i) for i in xml_node.findall("Connection")] + + +class Mappings(ContainerNode): + def _read_xml(self, xml_node: "Element"): + self.children = [Mapping(xml_node=i) for i in xml_node.findall("Mapping")] + + class Scene(BaseNode): def __init__( self, @@ -311,10 +382,10 @@ def __init__( custom_id_type: int = 0, cast_shadow: bool = False, addresses: "Addresses" = None, - alignments: List["Alignment"] = [], - custom_commands: List["CustomCommand"] = [], - overwrites: List["Overwrite"] = [], - connections: List["Connection"] = [], + alignments: "Alignments" = None, + custom_commands: "CustomCommands" = None, + overwrites: "Overwrites" = None, + connections: "Connections" = None, child_list: Union["ChildList", None] = None, multipatch: Union[str, None] = None, *args, @@ -335,10 +406,10 @@ def __init__( self.custom_id_type = custom_id_type self.cast_shadow = cast_shadow self.addresses = addresses if addresses is not None else Addresses() - self.alignments = alignments - self.custom_commands = custom_commands - self.overwrites = overwrites - self.connections = connections + self.alignments = alignments if alignments is not None else Alignments() + self.custom_commands = custom_commands if custom_commands is not None else CustomCommands() + self.overwrites = overwrites if overwrites is not None else Overwrites() + self.connections = connections if connections is not None else Connections() self.child_list = child_list self.multipatch = multipatch super().__init__(*args, **kwargs) @@ -387,13 +458,13 @@ def _read_xml(self, xml_node: "Element"): self.addresses.address.append(Address(dmx_break=0, universe=0, address=0)) if xml_node.find("Alignments"): - self.alignments = [Alignment(xml_node=i) for i in xml_node.find("Alignments").findall("Alignment")] + self.alignments = Alignments(xml_node=xml_node.find("Alignments")) if xml_node.find("Connections"): - self.connections = [Connection(xml_node=i) for i in xml_node.find("Connections").findall("Connection")] + self.connections = Connections(xml_node=xml_node.find("Connections")) if xml_node.find("CustomCommands") is not None: - self.custom_commands = [CustomCommand(xml_node=i) for i in xml_node.find("CustomCommands").findall("CustomCommand")] + self.custom_commands = CustomCommands(xml_node=xml_node.find("CustomCommands")) if xml_node.find("Overwrites"): - self.overwrites = [Overwrite(xml_node=i) for i in xml_node.find("Overwrites").findall("Overwrite")] + self.overwrites = Overwrites(xml_node=xml_node.find("Overwrites")) if xml_node.find("Classing") is not None: self.classing = xml_node.find("Classing").text @@ -417,24 +488,16 @@ def to_xml(self, element: Element): self.addresses.to_xml(element) if self.alignments: - alignments_element = ElementTree.SubElement(element, "Alignments") - for alignment in self.alignments: - alignments_element.append(alignment.to_xml()) + self.alignments.to_xml(element) if self.custom_commands: - commands_element = ElementTree.SubElement(element, "CustomCommands") - for command in self.custom_commands: - commands_element.append(command.to_xml()) + self.custom_commands.to_xml(element) if self.overwrites: - overwrites_element = ElementTree.SubElement(element, "Overwrites") - for overwrite in self.overwrites: - overwrites_element.append(overwrite.to_xml()) + self.overwrites.to_xml(element) if self.connections: - connections_element = ElementTree.SubElement(element, "Connections") - for connection in self.connections: - connections_element.append(connection.to_xml()) + self.connections.to_xml(element) ElementTree.SubElement(element, "FixtureID").text = str(self.fixture_id) or "0" ElementTree.SubElement(element, "FixtureIDNumeric").text = str(self.fixture_id_numeric) @@ -585,8 +648,8 @@ def __init__( position: Union[str, None] = None, function_: Union[str, None] = None, child_position: Union[str, None] = None, - protocols: List["Protocol"] = [], - mappings: List["Mapping"] = [], + protocols: "Protocols" = None, + mappings: "Mappings" = None, gobo: Union["Gobo", None] = None, *args, **kwargs, @@ -598,8 +661,8 @@ def __init__( self.position = position self.function_ = function_ self.child_position = child_position - self.protocols = protocols - self.mappings = mappings + self.protocols = protocols if protocols is not None else Protocols() + self.mappings = mappings if mappings is not None else Mappings() self.gobo = gobo super().__init__(*args, **kwargs) @@ -630,9 +693,9 @@ def _read_xml(self, xml_node: "Element"): self.child_position = xml_node.find("ChildPosition").text if xml_node.find("Protocols"): - self.protocols = [Protocol(xml_node=i) for i in xml_node.find("Protocols").findall("Protocol")] + self.protocols = Protocols(xml_node=xml_node.find("Protocols")) if xml_node.find("Mappings") is not None: - self.mappings = [Mapping(xml_node=i) for i in xml_node.find("Mappings").findall("Mapping")] + self.mappings = Mappings(xml_node=xml_node.find("Mappings")) if xml_node.find("Gobo") is not None: self.gobo = Gobo(xml_node=xml_node.find("Gobo")) @@ -657,9 +720,7 @@ def to_xml(self): ElementTree.SubElement(element, "ChildPosition").text = self.child_position if self.protocols: - protocols_element = ElementTree.SubElement(element, "Protocols") - for protocol in self.protocols: - protocols_element.append(protocol.to_xml()) + self.protocols.to_xml(element) if isinstance(self.color, Color): self.color.to_xml(element) @@ -667,9 +728,7 @@ def to_xml(self): Color(str_repr=self.color).to_xml(element) if self.mappings: - mappings_element = ElementTree.SubElement(element, "Mappings") - for mapping in self.mappings: - mappings_element.append(mapping.to_xml()) + self.mappings.to_xml(element) if self.gobo: element.append(self.gobo.to_xml()) From 2b23c61e72beabbd1b430677879ecba05960550a Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 12:00:41 +0200 Subject: [PATCH 12/16] Add parts of missing truss definition --- pymvr/__init__.py | 59 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index cc09c05..8d024ef 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -620,10 +620,14 @@ def __init__( super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): + self.name = xml_node.attrib.get("name") + self.uuid = xml_node.attrib.get("uuid") # TODO handle missing data... self.size_x = int(xml_node.find("SizeX").text) self.size_y = int(xml_node.find("SizeY").text) - self.source = xml_node.find("Source") # TODO + source_node = xml_node.find("Source") + if source_node is not None: + self.source = Source(xml_node=source_node) scale_handling_node = xml_node.find("ScaleHandeling") if scale_handling_node is not None: self.scale_handling = ScaleHandeling(xml_node=scale_handling_node) @@ -1023,15 +1027,13 @@ def _read_xml(self, xml_node: "Element"): self.name = xml_node.attrib.get("name") self.uuid = xml_node.attrib.get("uuid") - self.symbol = [Symbol(xml_node=i) for i in xml_node.findall("Symbol")] - _geometry3d = [Geometry3D(xml_node=i) for i in xml_node.findall("Geometry3D")] - if xml_node.find("ChildList") is not None: - child_list = xml_node.find("ChildList") - - symbols = [Symbol(xml_node=i) for i in child_list.findall("Symbol")] - geometry3ds = [Geometry3D(xml_node=i) for i in child_list.findall("Geometry3D")] - self.symbol += symbols - _geometry3d += geometry3ds + child_list = xml_node.find("ChildList") + if child_list is not None: + self.symbol = [Symbol(xml_node=i) for i in child_list.findall("Symbol")] + _geometry3d = [Geometry3D(xml_node=i) for i in child_list.findall("Geometry3D")] + else: + self.symbol = [] + _geometry3d = [] # sometimes the list of geometry3d is full of duplicates, eliminate them here self.geometry3d = list(set(_geometry3d)) @@ -1192,7 +1194,42 @@ class SceneObject(BaseChildNodeExtended): class Truss(BaseChildNodeExtended): - pass + def __init__( + self, + position: Union[str, None] = None, + function_: Union[str, None] = None, + child_position: Union[str, None] = None, + *args, + **kwargs, + ): + self.position = position + self.function_ = function_ + self.child_position = child_position + super().__init__(*args, **kwargs) + + def _read_xml(self, xml_node: "Element"): + super()._read_xml(xml_node) + if xml_node.find("Position") is not None: + self.position = xml_node.find("Position").text + if xml_node.find("Function") is not None: + self.function_ = xml_node.find("Function").text + if xml_node.find("ChildPosition") is not None: + self.child_position = xml_node.find("ChildPosition").text + + def to_xml(self): + attributes = {"name": self.name, "uuid": self.uuid} + if self.multipatch: + attributes["multipatch"] = self.multipatch + element = ElementTree.Element(type(self).__name__, **attributes) + super().to_xml(element) + if self.position: + ElementTree.SubElement(element, "Position").text = self.position + if self.function_: + ElementTree.SubElement(element, "Function").text = self.function_ + if self.child_position: + ElementTree.SubElement(element, "ChildPosition").text = self.child_position + return element + class Support(BaseChildNodeExtended): From 31cb2ff0fe659dd7e14a47f74e9a5903f453410d Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 12:17:35 +0200 Subject: [PATCH 13/16] Fix errors --- pymvr/__init__.py | 49 +- spec/mvr-spec.md | 1793 --------------------------------------------- 2 files changed, 27 insertions(+), 1815 deletions(-) delete mode 100644 spec/mvr-spec.md diff --git a/pymvr/__init__.py b/pymvr/__init__.py index 8d024ef..4c93643 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -7,7 +7,7 @@ from .value import Matrix, Color # type: ignore from enum import Enum -__version__ = "0.5.1" +__version__ = "1.0.0-dev0" def _find_root(pkg: "zipfile.ZipFile") -> "ElementTree.Element": @@ -454,9 +454,6 @@ def _read_xml(self, xml_node: "Element"): else: self.addresses = Addresses() - if len(self.addresses) == 0: - self.addresses.address.append(Address(dmx_break=0, universe=0, address=0)) - if xml_node.find("Alignments"): self.alignments = Alignments(xml_node=xml_node.find("Alignments")) if xml_node.find("Connections"): @@ -544,7 +541,7 @@ class Data(BaseNode): def __init__( self, provider: str = "", - ver: str = "", + ver: str = "1", *args, **kwargs, ): @@ -655,6 +652,7 @@ def __init__( protocols: "Protocols" = None, mappings: "Mappings" = None, gobo: Union["Gobo", None] = None, + unit_number: int = 0, *args, **kwargs, ): @@ -668,6 +666,7 @@ def __init__( self.protocols = protocols if protocols is not None else Protocols() self.mappings = mappings if mappings is not None else Mappings() self.gobo = gobo + self.unit_number = unit_number super().__init__(*args, **kwargs) def _read_xml(self, xml_node: "Element"): @@ -702,6 +701,8 @@ def _read_xml(self, xml_node: "Element"): self.mappings = Mappings(xml_node=xml_node.find("Mappings")) if xml_node.find("Gobo") is not None: self.gobo = Gobo(xml_node=xml_node.find("Gobo")) + if xml_node.find("UnitNumber") is not None: + self.unit_number = int(xml_node.find("UnitNumber").text) def to_xml(self): attributes = {"name": self.name, "uuid": self.uuid} @@ -723,7 +724,7 @@ def to_xml(self): if self.child_position: ElementTree.SubElement(element, "ChildPosition").text = self.child_position - if self.protocols: + if len(self.protocols) > 0: self.protocols.to_xml(element) if isinstance(self.color, Color): @@ -731,11 +732,13 @@ def to_xml(self): elif self.color: Color(str_repr=self.color).to_xml(element) - if self.mappings: + if len(self.mappings) > 0: self.mappings.to_xml(element) if self.gobo: element.append(self.gobo.to_xml()) + ElementTree.SubElement(element, "UnitNumber").text = str(self.unit_number) + return element def __str__(self): @@ -1129,13 +1132,6 @@ def __init__( def _read_xml(self, xml_node: "Element"): self.symbol = [Symbol(xml_node=i) for i in xml_node.findall("Symbol")] self.geometry3d = [Geometry3D(xml_node=i) for i in xml_node.findall("Geometry3D")] - if xml_node.find("ChildList"): - child_list = xml_node.find("ChildList") - - symbols = [Symbol(xml_node=i) for i in child_list.findall("Symbol")] - geometry3ds = [Geometry3D(xml_node=i) for i in child_list.findall("Geometry3D")] - self.symbol += symbols # TODO remove this over time, children should only be in child_list - self.geometry3d += geometry3ds def to_xml(self, parent: Element): element = ElementTree.SubElement(parent, type(self).__name__) @@ -1231,7 +1227,6 @@ def to_xml(self): return element - class Support(BaseChildNodeExtended): def __init__( self, @@ -1343,7 +1338,7 @@ def to_xml(self): class Protocol(BaseNode): def __init__( self, - geometry: Union[str, None] = None, + geometry: Union[str, None] = "NetworkInOut_1", name: Union[str, None] = None, type_: Union[str, None] = None, version: Union[str, None] = None, @@ -1387,7 +1382,7 @@ def to_xml(self): class Alignment(BaseNode): def __init__( self, - geometry: Union[str, None] = None, + geometry: Union[str, None] = "Beam", up: Union[str, None] = "0,0,1", direction: Union[str, None] = "0,0,-1", *args, @@ -1499,11 +1494,21 @@ def __init__( def _read_xml(self, xml_node: "Element"): self.link_def = xml_node.attrib.get("linkedDef") - self.ux = int(xml_node.find("ux").text) - self.uy = int(xml_node.find("uy").text) - self.ox = int(xml_node.find("ox").text) - self.oy = int(xml_node.find("oy").text) - self.rz = float(xml_node.find("rz").text) + ux_node = xml_node.find("ux") + if ux_node is not None: + self.ux = int(ux_node.text) + uy_node = xml_node.find("uy") + if uy_node is not None: + self.uy = int(uy_node.text) + ox_node = xml_node.find("ox") + if ox_node is not None: + self.ox = int(ox_node.text) + oy_node = xml_node.find("oy") + if oy_node is not None: + self.oy = int(oy_node.text) + rz_node = xml_node.find("rz") + if rz_node is not None: + self.rz = float(rz_node.text) def __str__(self): return f"{self.link_def}" diff --git a/spec/mvr-spec.md b/spec/mvr-spec.md deleted file mode 100644 index c21644e..0000000 --- a/spec/mvr-spec.md +++ /dev/null @@ -1,1793 +0,0 @@ -MVR Version 1.6 - DIN SPEC 15801 - -# Introduction - -MVR - My Virtual Rig - specified in this DIN SPEC will unify the information exchange between different applications within the entertainment industry. Based on GDTF, as specified in DIN SPEC 15800, MVR allows the exchange of scenic and environmental information and complete show setups as planning status. Furthermore the MVR file format allows programs to share data and geometry for a scene. A scene is a set of parametric objects such as fixtures, trusses, video screens, and other objects that are used secifically in the entertainment industry. - -Typical workflow -1. Program A saves an MVR file containing a scene; -2. Program B imports this file; -3. Program B changes some parametric data in the scene; -4. Program B saves an MVR containing the scene; -5. Program A imports this file and applies the changes to the existing objects. - - -# Scope - -This document specifies "My Virtual Rig" (MVR), which is designed to provide a unified way of listing and describing the hierarchical and logical structure based on DIN SPEC 15800 "General Device Type Format" (GDTF) - and further environmental information of a show setup in the entertainment business. It will be used as a foundation for the exchange of extended device and environmental data between lighting consoles, CAD and 3D-pre-visualization applications. The purpose of an MVR-file is to reflect the real-world physical components of a show setup and the logical patch information of the devices while maintaining the kinematic chain. - - -# Normative references - -The following documents are referred to in the text in such a way that some or all of their content constitutes -requirements of this document. For dated references, only the edition cited applies. For undated references, -the latest edition of the referenced document (including any amendments) applies. - -- [DIN SPEC 15800:2022-02, Entertainment Technology— General Device Type Format (GDTF)](https://www.beuth.de/en/technical-rule/din-spec-15800/349717520) -- [ISO/IEC 21778:2017, Information technology— The JSON data interchange syntax](https://standards.iso.org/ittf/PubliclyAvailableStandards/c071616_ISO_IEC_21778_2017.zip) -- [Extensible Markup Language (XML) 1.0](https://www.w3.org/TR/2008/REC-xml-20081126/) -- [PKWARE 6.3.3](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) -- [Domain Names— Implementation and Specification](https://www.ietf.org/rfc/rfc1035.txt) -- [RFC3629, UTF-8, a transformation format of ISO 10646](https://datatracker.ietf.org/doc/html/rfc3629) -- [RFC4122, A Universally Unique IDentifier (UUID) URN Namespace](https://www.rfc-editor.org/rfc/rfc4122) -- [RFC6455, The WebSocket Protocol](https://www.ietf.org/rfc/rfc6455.txt) -- [RFC6762, Multicast DNS](https://www.ietf.org/rfc/rfc6762.txt) -- [RFC6763, DNS-Based Service Discovery](https://www.ietf.org/rfc/rfc6763.txt) - - -# Terms and definitions - -For the purposes of this document, the following terms and definitions apply. -DIN and DKE maintain terminological databases for use in standardization at the following addresses: - - - DIN-TERMinologieportal: available at - - - DKE-IEV: available at - - -### My Virtual Rig, MVR -descriptive name of the specification - -### MVR-xchange -protocol to share MVR files over the network - -### MVR-xchange client -application that participates in the MVR-xchange - -### MVR-xchange group -group of MVR-xchange clients that work on the same project and communicate together - -### TCP Mode -MVR-xchange communication via TCP packages and discovery via mDNS - -### WebSocket Mode -MVR-xchange communication via WebSockets and discovery via DNS - - - -# MVR Definitions - -## General - -MVR consists of two parts to enable any application to exchange GDTF but also general information in the same -common format. Firstly the MVR file format as described in the following section. Secondly a MVR communication format to simplify exchange between applications. - - -## File Format Definition - -To describe all information within one file, a zip file according to PKWARE 6.3.3 with the extension `*.mvr` is used. The archive shall contain one root file named `GeneralSceneDescription.xml`, along with all other resource files referenced via this Root File. The root file `GeneralSceneDescription.xml` is mandatory inside the archive to be a valid MVR file. - -UTF-8 according to RFC3629 has to be used to encode the XML file. Each XML file internally consists of XML nodes. Each XML node could have XML attributes and XML node children. Each XML attribute has a value. If a XML attribute is not specified, the default value of this XML attribute will be used. If the XML attribute value is -specified as a string, the format of the string will depend on the XML attribute type. - -- The archive shall not use encryption or password protection. -- All files referenced by the Root File shall be placed at the root level. They shall not be placed in folders. -- Files shall be placed using either STORE (uncompressed) or DEFLATE compression. No other compression algorithms are supported. -- Files may be placed into the archive in any order. -- A `Universal.gdtt` GDTF template file with a `.gdtt` extension can be added to define Gobos, Emitters, Filter and such for referencing. -- Filenames within the archive must not differ only by case. Eg it is prohibited to have the files `GEO1.glb` and `geo1.glb` within the same archive. -- The file name of the ZIP archive can be chosen freely. - -All objects used have a persistent unique ID to track changes between the different applications. If there are no changes to the original GDTF file it is mandatory to keep it in the MVR during export. If there are changes to the GDTF file it is mandatory to add a revision to the GDTF file in order to reflect it. - -Only user-intended modifications of any part of the MVR file shall be processed. This is particular important if applications in the workflow do not need or accept all data of the MVR file. Such behaviour guarantees that all later steps in the workflow have access to the original intended data. - -- EXAMPLE An example of a typical MVR archive is shown below: - -``` -GeneralSceneDescription.xml -Custom@Fixture1.gdtf -Custom@Fixture2.gdtf -geo1.3ds -geo1.glb -Textr12.png -Universal.gdtt -``` - - -## Generic Value Types - -Table 1 contains a list of the available types for node or attribute values: - -##### Table 1 — *XML Generic Value Types* - -| Value Type Name | Description | -| ------------------ |--------------------------------------------------------------------------- | -| Integer | A signed or unsigned integer value represented in base 10. Uses a dash ‘-’ (U+002D) -as a prefix to denote negative numbers. E.g. `15` or `-6`| -| Float | A floating point numeric value represented in #attrType-Bool base 10 decimal or scientific format.
Uses full stop '.' (U+002E) to delimit the whole and decimal part and 'e' or 'E' to delimit mantissa and exponent.
Implementations shall write sufficient decimal places to precisely round-trip their internal level of precision.
Infinities and not-a-number (NaN) are not permitted.
Eg `1.5`, `3.9265e+2`| -| Bool | A boolean value. When representing `true` inidcate with true, when `false` indicate with false. | -| String | Any sequence of Unicode codepoints, encoded as necessary for XML.
Eg The following XML encodings (with their meaning in brackets):
`<` (\<), `&` (&), `>` (\>), `"` ("), and `'` (') | -| Enum | Possible values are predefined | -| UUID | A UUID to RFC4122 in text representation.
The nil UUID (all zeros) is not permitted.
Formatted as `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX`.
Used to link objects. | -| Vector | Three Float values separated by ',' defining a 3D vector's X, Y, and Z components.
Eg `1.0,2.0,3.0`| -| FileName | The case-sensitive name of a file within the archive including the extension.
The filename must not contain any FAT32 or NTFS reserved characters.
The extension is delimited from the base name by full stop '.' and the base name shall not be empty.
It is recommended to limit filenames to the POSIX "Fully Portable Filenames" character set: [A-Z], [a-z], [0-9], the symbols '\_' (U+005F), '-' (U+002D) and a maximum of one '.' (U+002E)
Eg `My-Fixture_5.gdtf`| -| CIE Color | CIE 1931 xyY absolute color point.
Formatted as three Floats `x,y,Y`
Eg `0.314303,0.328065,87.699166`| -| IPv4 Address | Common IPv4 Address in the format of dotted decimal notation.
Eg `192.168.1.10` | -| IPv6 Address | Common IPv6 Address in the format of hexadecimal notation.
Eg `2001:0db8:85a3:0000:0000:8a2e:0370:7344` | - - -## Node Definiftions - -The first XML node is always the XML description node: `` - -## GeneralSceneDescription Node - -The second XML node is the mandatory GeneralSceneDescription node. The attributes of this node are listed in Table 2, the children of this node are given in Table 3. - -##### Table 2 — *GeneralSceneDescription Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------------- | --------------------------- | ------------------------------------------------------------------- | -| verMajor | [Integer](#user-content-attrtype-integer) | Not Optional | Denotes the major version of the format used when saving this file. | -| verMinor | [Integer](#user-content-attrtype-integer) | Not Optional | Denotes the minor version of the format used when saving this file. | -| provider | [String](#user-content-attrtype-string) | Not Optional | The name of the application that is generating the MVR export. This should stay the same between multiple version. | -| providerVersion| [String](#user-content-attrtype-string) | Not Optional | The version of the software that is generating the MVR export. This should be different for each version that is available. | - -The current version of MVR reflected by this document is 1.6. - - -##### Table 3 — *GeneralSceneDescription Node Children* - -| Child Node | Allowed Count | Description | -| ---------- | ------------- | ---------------------------------------------- | -| UserData | 0 or 1 | Specifies user data associated with this file. | -| Scene | 1 | Defines the scene described in this file. | - -## Node Definition: UserData - -### General - -This node contains a collection of user data nodes defined and used by provider applications if required. User data should not be expected to be preserved in the workflow of multiple applications importing and exporting the data. The defined UserData Node Children are specified in Table 4. - -Node name: `UserData` - -##### Table 4 — *UserData Node Children* - -| Child Node | Allowed Count | Description | -| ----------------------------- | ------------- | ----------------------------- | -| [Data](#node-definition-data) | 0 or many | Defines a block of user data. | - - -### Node Definition: Data - -This node contains a collection of data specified by the provider application. The defined Data Node Attributes are specified in Table 5. - -Node name: `Data` - -##### Table 5 — *Data Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------------------------------------------- | -| provider | [String](#user-content-attrtype-string) | Not Optional | Specifies the name of the provider application that created this data. | -| ver | [String](#user-content-attrtype-string) | 1 | Version information of the data as specified by the provider application. | - - -## Node Definition: Scene - -This node contains information about the scene. The defined Scene Node Children are specified in Table 6. - -Node name: `Scene` - -##### Table 6 — *Scene Node Children* - -| Child Node | Allowed Count | Description | -| ----------------------------------- | ------------- | ------------------------------------- | -| [AUXData](#node-definition-auxdata) | 0 or 1 | Defines auxiliary data for the scene. | -| [Layers](#node-definition-layers) | 1 | A list of layers in the scene. | - - -## Node Definition: AUXData - -### General - -This node contains auxiliary data for the scene node. The defined AUXData Node Children are specified in Table 7. - -Node name: `AUXData` - -##### Table 7 — *AUXData Node Children* - -| Child Node | Allowed Count | Description | -| ------------------------------------------------------- | ------------- | ---------------------------------------------------------------| -| [Symdef](#node-definition-symdef) | 0 or any | Graphical representation that will be instanced in the scene. | -| [Position](#node-definition-position) | 0 or any | Defines a logical group of lighting devices. | -| [MappingDefinition](#node-definition-mappingdefinition) | 0 or any | Defines a input source for fixture color mapping applications. | -| [Class](#node-definition-class) | 0 or any | Defines a Class for object visiblity filtering. | - - -### Node Definition: Symdef - -This node contains the graphics so the scene can refer to this, thus optimizing repetition of the geometry. The child objects are located within a local coordinate system. The defined Symdef Node Attributes are specified in Table 8. - -Node name: `Symdef` - -##### Table 8 — *Symdef Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object | - - -The Symdef node (Table 9) contains the following children. - -##### Table 9 — *Symdef Node Children* - -| Child Node | Allowed Count | Value Type | Description | -| ------------------------------------------------- | ------------- | ----------------------------------------- | --------------------------------------------------- | -| ChildList | 1 | [Integer](#user-content-attrtype-integer) | The size in x direction in pixels of the source. | - -The child list (Table 10) contains a list of the following nodes: - -##### Table 10 — *Symdef Childlist Node Children* - -| Child Node | Description | -| ----------------------------------------- | -------------------------------------------------------------------- | -| [Geometry3D](#node-definition-geometry3d) | The geometry of this definition that will be instanced in the scene. | -| [Symbol](#node-definition-symbol) | The symbol instance that will provide a geometry of this definition. | - - -### Node Definition: Position - -This node defines a logical grouping of lighting devices and trusses. The defined Position Node Attributes are specified in Table 11. - -Node name: `Position` - -##### Table 11 — *Position Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object | - - -### Node Definition: MappingDefinition - -This node specifies an input source for fixture color mapping applications. The defined MappingDefinition Node Attributes are specified in Table 12. - -Node name: `MappingDefinition` - -##### Table 12 — *MappingDefinition Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | --------------------------------------- | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | | The name of the source for the mapping. | - -The child list (Table 13) contains a list of the following nodes: - -##### Table 13 — *MappingDefinition Node Children* - -| Child Node | Allowed Count | Value Type | Description | -| ------------------------------------------------- | ------------- | ----------------------------------------- | --------------------------------------------------- | -| SizeX | 1 | [Integer](#user-content-attrtype-integer) | The size in x direction in pixels of the source. | -| SizeY | 1 | [Integer](#user-content-attrtype-integer) | The size in y direction in pixels of the source. | -| [Source](#node-definition-source) | 1 | | The video source that will be used for the Mapping. | -| [ScaleHandeling](#node-definition-scalehandeling) | 0 or 1 | | How the source will be scaled to the mapping. | - -```xml - - 1920 - 1080 - movie.mov - - UpScale - -``` - -### Node Definition: Class - -This node defines a logical grouping across different layers. Primarily used for controlling object visibility of objects across multiple Layers. The defined Class Node Attributes are specified in Table 14. - -Node name: `Class` - -##### Table 14 — *Class Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------- | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the class. | -| name | [String](#user-content-attrtype-string) | | The name of the Class. | - - -### Node Definition: Layers - -This node defines a list of layers inside the scene. The layer is a container of graphical objects defining a local coordinate system. - -Node name: `Layers` - -The child list (Table 15) contains a list of layer nodes: - -##### Table 15 — *Layers Node Childs* - -| Child Node | Description | -| ------------------------------- | ----------------------- | -| [Layer](#node-definition-layer) | A layer representation. | - - -#### Node Definition: Layer - -This node defines a layer. The layer is a spatial representation of a geometric container. The child objects are located inside a local coordinate system. The defined Layer Node Attributes are specified in Table 16. - -Node name: `Layer` - -##### Table 16 — *Layer Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object | - -The child list (Table 17) contains a list of layer nodes: - -##### Table 17 — *Layer Node Childs* - -| Child Node | Allowed Count | Description | -| --------------------------------------- | ------------- | -------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | The transformation matrix that defines the location and orientation of this the layer inside its global coordinate space. This effectively defines local coordinate space for the objects inside. The Matrix of the Layer is only allowed to have a vertical Transform (elevation). Rotation and scale must be identity. Rotation and scale must be identity, means no rotation and no scale. | -| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | - - -## Node Definition: ChildList - -This node defines a list of graphical objects. - -Node name: `ChildList` - -The child list (Table 18) contains a list of the following nodes: - -##### Table 18 — *ChildList Node Childs* - -| Child Node | Description | -| ------------------------------------------- | ---------------------------------------------------------------------------- | -| [SceneObject](#node-definition-sceneobject) | A generic graphical object from the scene. | -| [GroupObject](#node-definition-groupobject) | A grouping object of other graphical objects inside local coordinate system. | -| [FocusPoint](#node-definition-focuspoint) | A definition of a focus point. | -| [Fixture](#node-definition-fixture) | A definition of a fixture. | -| [Support](#node-definition-support) | A definition of a support. | -| [Truss](#node-definition-truss) | A definition of a truss. | -| [VideoScreen](#node-definition-videoscreen) | A definition of a video screen. | -| [Projector](#node-definition-projector) | A definition of a projector. | - - -## Node Definition for Parametric Objects - -### Node Definition: SceneObject - -This node defines a generic graphical object. The defined SceneObject Node Attributes are specified in Table 19. - -Node name: `SceneObject` - -##### Table 19 — *SceneObject Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object | -| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | - -The child list (Table 20) contains a list of one of the following nodes: - -##### Table 20 — *SceneObject Node Childs* - -| Child Node | Allowed Count | Value Type | Description | -| ------------------------------------------------- | ------------- | ------------------------------------------- | ----------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | | The location and orientation of the object inside the parent coordinate system. | -| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | -| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are part of the object. | -| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | -| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | -| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if an object cast shadows. | -| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object.| -| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | -| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object.| -| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | -| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | -| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | -| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The Custom ID is a value that can be used as a short name of the Fixture Instance. This does not have to be unique. The default value is 0. | -| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this object belongs to. | -| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | - - - -### Node Definition: GroupObject - -This node defines logical group of objects. The child objects are located inside a local coordinate system. The defined GroupObject Node Attributes are specified in Table 21. - -Node name: `GroupObject` - -##### Table 21 — *GroupObject Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object | - -The child list (Table 22) contains a list of one of the following nodes: - -##### Table 22 — *GroupObject Node Childs* - -| Child Node | Allowed Count | Value Type | Description | -| --------------------------------------- | ------------- | ----------------------------------- | ------------------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | | The location and orientation of the object inside the parent coordinate system. | -| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | -| [ChildList](#node-definition-childlist) | 0 or 1 | | A list of graphic objects that are part of the group. | - - -### Node Definition: FocusPoint - -This node defines a focus point object. The defined FocusPoint Node Attributes are specified in Table 23. - -Node name: `FocusPoint` - -##### Table 23 — *FocusPoint Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object | - -The child list (Table 24) contains a list of one of the following nodes: - -##### Table 24 — *FocusPoint Node Childs* - -| Child Node | Allowed Count | Value Type | Description | -| ----------------------------------------- | ------------- | ----------------------------------- | ------------------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | | The location and orientation of the object inside the parent coordinate system. | -| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | -| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are part of the object. | - - -### Node Definition: Fixture - -This node defines an entertainment fixture object. The defined FixtureNode Attributes are specified in Table 25. - -Node name: `Fixture` - -##### Table 25 — *Fixture Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The Name is the value that represents the fixture object. Is is not unique, and normally pairs with FID in Display | -| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | - -The child list (Table 26) contains a list of one of the following nodes: - -##### Table 26 — *Fixture Node Childs* - -| Child Node | Allowed Count | Value Type | Description | -| --------------------------------------- | ------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | -| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | -| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | -| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | -| Focus | 0 or 1 | [UUID](#user-content-attrtype-uuid) | A focus point reference that this lighting fixture aims at if this reference exists. | -| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | -| DMXInvertPan | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines of all Pan Channels of the fixture should be DMX Inverted. | -| DMXInvertTilt | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines of all Tilt Channels of the fixture should be DMX Inverted. | -| Position | 0 or 1 | [UUID](#user-content-attrtype-uuid) | A position reference that this lighting fixture belongs to if this reference exists. | -| Function | 0 or 1 | [String](#user-content-attrtype-string) | The name of the purpose this Fixture has. | -| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| UnitNumber | 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | -| ChildPosition | 0 or 1 | [String](#user-content-attrtype-string) | Node link to the geometry. Starting point is the Geometry Collect of the linked parent GDTF of this object. | -| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this fixture. | -| [Protocols](#node-definition-protocols) | 0 or 1 | | The container for protocols assignments. | -| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this fixture. | -| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this fixture. | -| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this fixture. | -| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this fixture. | -| Color | 0 or 1 | [CIE Color](#user-content-attrtype-ciecolor) | A color assigned to a fixture. If it is not defined, there is no color for the fixture. | -| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | -| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | -| [Mappings](#node-definition-mappings) | 0 or 1 | | The container for Mappings for this fixture. | -| [Gobo](#node-definition-gobo) | 0 or 1 | | The Gobo used for the fixture. The image resource must conform to the GDTF standard. | -| [ChildList](#node-definition-childlist) | 0 or 1 | | A list of graphic objects that are part of the layer. | - -Note: _The fixture has no `Geometries` node as geometry is defined in a GDTF file._ - -CustomID Types -- 0 Undefined -- 1 Fixture -- 2 Channel -- 4 Houselights -- 5 NonDim -- 6 Media -- 7 Fog -- 8 Effect -- 9 Pyro -- 10 Marker - -For further information about the difference between FixtureID and CustomID refer to Annex A. - -EXAMPLE An example of a node definition is shown below: - - -```xml - - {0.158127,-0.987419,0.000000}{0.987419,0.158127,0.000000}{0.000000,0.000000,1.000000}{6020.939200,2838.588955,4978.134459} - Custom@Robe Robin MMX WashBeam - DMX Mode - 4A B1 94 62 A6 E3 4C 3B B2 5A D8 09 9F 78 17 0C - 77 BC DE 16 95 A6 47 25 9D 04 16 A0 BD 67 CD 1A - -
45
- - - -
- - - - - - - - - Body_Pan,f 50 - Yoke_Tilt,f 50 - - - - - - - - - - 10 - 10 - 5 - 5 - 45 - - - - - - - - - 0 - Speaker 1 - 0 - 0 - true - true - 2.533316,-5.175210,3.699302 - image_file_forgobo - - - Fancy@Attachment - DMX Mode - The parent GDTF here is the one from the Robe Robin MMX WashBeam - Base.Yoke.Head - The position is now defined based on the ECS from the geometry of parents GDTF including all applied Rotation via DMX or other protocols - {0.158127,-0.987419,0.000000}{0.987419,0.158127,0.000000}{0.000000,0.000000,1.000000}{6020.939200,2838.588955,4978.134459} - - -
-``` - - -## Node Definition: Truss - -This node defines a truss object. The defined Truss Node Attributes are specified in Table 27. - -Node name: `Truss` - -##### Table 27 — *Truss Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object | -| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | - -The child list (Table 28) contains a list of one of the following nodes: - -##### Table 28 — *Truss Node Childs* - -| Child Node | Allowed Count | Value Type | Description | -| ------------------------------------------------- | ------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | -| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | -| [Position](#node-definition-position) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | A position reference that this truss belongs to if this reference exists. | -| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are a part of the object. | -| Function | 0 or 1 | [String](#user-content-attrtype-string) | The name of the function this Truss is used for. | -| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | -| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | -| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | -| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object. | -| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | -| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object. | -| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | -| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | -| ChildPosition | 0 or 1 | [String](#user-content-attrtype-string) | Node Link to the Geometry. Starting point is the Geometry Collect of the linked parent GDTF of this object. | -| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | -| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | -| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | -| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | - - - -## Node Definition: Support - -This node defines a support object. The defined Support Node Attributes are specified in Table 29. - -Node name: `Support` - -##### Table 29 — *Support Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object | -| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | - -The child list (Table 30) contains a list of one of the following nodes: - -##### Table 30 — *Support Node Childs* - -| Child Node | Allowed Count | Value Type | Description | -| ------------------------------------------------- | ------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | -| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | -| [Position](#node-definition-position) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | A position reference that this support belongs to if this reference exists. | -| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are a part of the object. | -| Function | 0 or 1 | [String](#user-content-attrtype-string) | The name of the function this support is used for. | -| ChainLength | 1 | [Float](#user-content-attrtype-float) | The chain length that will be applied to the GDTF . | -| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | -| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | -| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | -| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object. | -| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | -| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object. | -| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | -| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | -| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | -| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | -| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | -| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | - - -## Node Definition: VideoScreen - -This node defines a video screen object. The defined VideoScreen Node Attributes are specified in Table 31. - -Node name: `VideoScreen` - -##### Table 31 — *VideoScreen Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object. | -| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | - -The child list (Table 32) contains a list of one of the following nodes: - -##### Table 32 — *VideoScreen Node Childs* - -| Child Node | Allowed Count | Value Type | Description | -| ------------------------------------------------- | ------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | -| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | -| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are a part of the object. | -| [Sources](#node-definition-sources) | 0 or 1 | | A list of video input sources.. | -| Function | 0 or 1 | [String](#user-content-attrtype-string) | The name of the function this VideoScreen is used for. | -| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | -| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | -| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | -| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object. | -| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | -| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object. | -| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | -| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | -| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | -| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | -| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | -| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | - - -EXAMPLE An example of a node definition is shown below: - -```xml - - {0.158127,-0.987419,0.000000}{0.987419,0.158127,0.000000}{0.000000,0.000000,1.000000}{6020.939200,2838.588955,4978.134459} - Generic@TV - DisplayModeWideScreen - -
45
-
- 25 - 0 - 0 - - movie.mov - - -``` - - -## Node Definition: Projector - -This node defines a video projector object. The defined Projector Node Attributes are specified in Table 33. - -Node name: `Projector` - -##### Table 33 — *Projector Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------------------------- | --------------------------- | ------------------------------------ | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| name | [String](#user-content-attrtype-string) | Empty | The name of the object. | -| multipatch | [UUID](#user-content-attrtype-uuid) | Empty | The unique identifier of the parent multipatch fixture. When this value is set, you may not define a FixtureID or CustomID for this fixture. The FixtureID and CustomID from the object defined as multi parent also applies to this object. | - -The child list (Table 34) contains a list of one of the following nodes: - -##### Table 34 — *Projector Node Childs* - -| Child Node | Allowed Count | Value Type | Description | -| ------------------------------------------------- | ------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | | The location of the object inside the parent coordinate system. | -| [Classing](#node-definition-class) | 0 or 1 | [UUID](#user-content-attrtype-uuid) | The Class the object belongs to. | -| [Geometries](#node-definition-geometries) | 1 | | A list of geometrical representation objects that are a part of the object. | -| [Projections](#node-definition-projections) | 1 | | A list of video source for Beam Geometries in the GDTF file. | -| GDTFSpec | 0 or 1 | [FileName](#user-content-attrtype-filename) | The name of the file containing the GDTF information for this object, conforming to the DIN SPEC 15800. | -| GDTFMode | 0 or 1 | [String](#user-content-attrtype-string) | The name of the used DMX mode. This has to match the name of a DMXMode in the GDTF file. Mandatory when `GDTFSpec` as been defined. | -| CastShadow | 0 or 1 | [Bool](#user-content-attrtype-bool) | Defines if a Object cast Shadows. | -| [Addresses](#node-definition-addresses) | 0 or 1 | | The container for DMX Addresses for this object. | -| [Alignments](#node-definition-alignments) | 0 or 1 | | The container for Alignments for this object. | -| [CustomCommands](#node-definition-customcommands) | 0 or 1 | | The container for custom command for this object. | -| [Overwrites](#node-definition-overwrites) | 0 or 1 | | The container for overwrites for this object. | -| [Connections](#node-definition-connections) | 0 or 1 | | The container for connections for this object. | -| [ChildList](#node-definition-childlist) | 0 or 1 | A list of graphic objects that are part of the layer. | -| FixtureID | 1 | [String](#user-content-attrtype-string) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| FixtureIDNumeric | 1 | [Integer](#user-content-attrtype-integer) | The Fixture ID is an identifier for the instance of this fixture that can be used to activate / select them for programming. | -| UnitNumber | 0 or 1 | [Integer](#user-content-attrtype-integer) | The identification of a fixture on its position. Use this as an alternative numbering scheme if the planning and programming numbering is different. | -| CustomIdType | 0 or 1 | [Integer](#user-content-attrtype-integer) | Defines the CustomID Type this fixture belongs to. A Custom ID Type defines to which group of objects this objects belongs as an additional object identifier. The types for the custom ID Types are defined below. | -| CustomId | 0 or 1 | [Integer](#user-content-attrtype-integer) | The CustomId ID is an identifier for the instance of this fixture within the Custom ID Type that can be used to activate / select them for programming. | - - -EXAMPLE An example of a node definition is shown below: - - -```xml - - {0.158127,-0.987419,0.000000}{0.987419,0.158127,0.000000}{0.000000,0.000000,1.000000}{6020.939200,2838.588955,4978.134459} - Generic@Projector - Projector@ThrowRatio1_7_to_2_2 - -
45
-
- 25 - 0 - 0 - - movie.mov - - UpScale - - -
-``` - - -## Other Node Definition - -### Node Definition: Matrix - -This node contains a definition of a transformation matrix: - -- Right-handed -- Z-Up -- 1 Distance Unit equals 1 mm - -Node name: `Matrix` - -The defined Matrix Node Value Types are specified in Table 35. - -##### Table 35 — *Matrix Node Value Types* - -| Value Type | Default Value When Missing | Description | -| ---------------------- | ---------------------------- | --------------------------------------------- | -| {[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float)} {[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float)} {[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float)} {[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float),[Float](#user-content-attrtype-float)} | {1,0,0}
{0,1,0}
{0,0,1}
{0,0,0} | This node contains the array for a 4x3 transform matrix.
The order is:
`u1,u2,u3`
`v1,v2,v3`
`w1,w2,w3`
`o1,o2,o3` | - -### Node Definition: Gobo - -This node defines a Gobo. The defined Gobo Node Attributes are specified in Table 36. - -Node name: `Gobo` - -##### Table 36 — *Gobo Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ------------------------------------- | --------------------------- | ---------------------------------- | -| rotation | [Float](#user-content-attrtype-float) | 0 | The roation of the Gobo in degree. | - -The node value is the Gobo used for the fixture. The image resource shall apply to the GDTF standard. Use a FileName to specify. - - -### Node Definition: Sources - -This node defines a group of sources for VideoScreen. - -Node name: `Sources` - -The child list (Table 37) contains a list of the following nodes: - -##### Table 37 — *Sources Node Children* - -The child list contains a list of the following nodes: - -| Child Node | Description | -| --------------------------------- | --------------------------- | -| [Source](#node-definition-source) | One Source for the fixture. | - - -#### Node Definition: Source - -This node defines a Source. The defined Source Node Attributes are specified in Table 38. The defined Source Node Value Types are specified in Table 39. - -Node name: `Source` - -##### Table 38 — *Source Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | -------------------------------------- | --------------------------- | -------------------------------------------------------------- | -| linkedGeometry | [String](#user-content-attrtype-string)| Not Optional | For a Display: The GDTF Geometry Type Display whose linked texture will get replaced by the source value.

`For a Beam: Defines the source for the GDTF Geometry Type Beam. Only applicable when BeamType is "Rectangle".` | -| type | [Enum](#user-content-attrtype-enum) | Not Optional | Defines the type of source of the media resource that will be used. The currently defined types are: NDI, File, CITP, CaptureDevice | - -##### Table 39 — *Source Node Value Types* - -| Value Type | Default Value When Missing | Description | -| --------------------------------------- | -------------------------- | ------------------------------------------------------------------------------ | -| [String](#user-content-attrtype-string) | Not Optional | Based on the Attribute name `type`:
- If type is NDI or CITP use the Stream Name
- If type is File use the filename in MVR file
- If type is CaptureDevice use the CaptureDevice Name | - -### Node Definition: ScaleHandeling - -This node defines how the MappingDefinition will react if the video source has not the same resolution. The defined ScaleHandeling Node Attributes are specified in Table 40. - -Node name: `ScaleHandeling` - -##### Table 40 — *ScaleHandeling Node Attributes* - -| Value Type | Default Value When Missing | Description | -| --------------------------------------- | -------------------------- | ------------------------------------------------------------------------------- | -| [Enum](#user-content-attrtype-enum) | ScaleKeepRatio | The available value are `ScaleKeepRatio`, `ScaleIgnoreRatio`, `KeepSizeCenter`. | - -Figure 1 shows how the scaling should look like. - -##### Figure 1 — *ScaleHandeling Node Attributes* - -| a) ScaleKeepRatio | b) ScaleIgnoreRatio | c) KeepSizeCenter | -|------------------------------|------------------------------------------|-------------------------------------------------------| -| ![media/ScaleKeepRatio.png](media/ScaleKeepRatio.png) | ![media/ScaleIgnoreRatio.png](media/ScaleIgnoreRatio.png) | ![media/KeepSizeCenter.png](media/KeepSizeCenter.png) | - - - -## Node Definition: Geometries - -This node defines a group of graphical objects. - -Node name: `Geometries` - -The child list (Table 41) contains a list of the following nodes: - -##### Table 41 — *Geometries Node Childs* - -| Child Node | Description | -| ----------------------------------------- | -------------------------------------------------------------------- | -| [Geometry3D](#node-definition-geometry3d) | The geometry of this definition that will be instanced in the scene. | -| [Symbol](#node-definition-symbol) | The symbol instance that will provide a geometry of this definition. | - - -## Node Definition: Symbol - -This node specified a symbol instance (geometry insert) of the definition geometry defined by a [Symdef](#node-definition-symdef) node. The defined Symbol Node Attributes are specified in Table 42. - -Node name: `Symbol` - -##### Table 42 — *Symbol Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| uuid | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the object. | -| symdef | [UUID](#user-content-attrtype-uuid) | Not Optional | The unique identifier of the Symdef node that will be the source of geometry. | - -The child list (Table 43) contains a list of the following nodes: - -##### Table 43 — *Symbol Node Childs* - -| Child Node | Allowed Count | Description | -| -----------------------| ------------- | -------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | The transformation matrix that defines the location. orientation and scale of the geometry inside the local coordinate space of the container. Considered identity when missing. | - - -## Node Definition: Geometry3D - -This node provides geometry from another file within the archive. The defined Geometry3D Node Attributes -are specified in Table 44. - -Node name: `Geometry3D` - -##### Table 44 — *Geometry3D Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------- | --------------------------- | ------------------------------------------------------------------------- | -| fileName | [FileName](#user-content-attrtype-filename) | Not Optional | The file name, including extension, of the external file in the archive. If there is no extension, it will assume that the extension is 3ds. | - -The child list (Table 45) contains a list of the following nodes: - -##### Table 45 — *Geometry3D Node Childs* - -| Child Node | Allowed Count | Description | -| ----------------- | ------------- | ----------------------------------------------------------------------------------- | -| [Matrix](#node-definition-matrix) | 0 or 1 | The transformation matrix that defines the location, orientation and scale of the geometry inside the local coordinate space of the container. Considered identity when missing. | - - -### Supported 3D file formats - -The supported 3D file formats are specified in Table 46. - -##### Table 46 — *Supported 3D file formats* - -| Format Name | File Extensions | Requirements | Notes | -| ----------- | --------------- | ----------------------------------- | --------------------------------------------------------- | -| 3DS | 3ds | 1 unit = 1 mm | [Deprecated Discreet 3DS](https://en.wikipedia.org/wiki/.3ds) | -| gltf 2.0 | gltf, glb | `extensionsRequired` shall be empty | GLB packaging is recommended [ISO/IEC 12113 Khronos glTF 2.0](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html) | - -All referenced files (e.g. texture images, binary blobs) shall be present in the archive. - -All file references (URIs etc) shall be relative to the root of the archive. Absolute URIs and file paths are not permitted. - - -### Node Definition: Projections - -This node defines a group of Projections. - -Node name: `Projections` - -The child list (Table 47) contains a list of the following nodes: - -##### Table 47 — *Projections Node Children* - -| Child Node | Description | -| ----------------------------------------- | ----------------------- | -| [Projection](#node-definition-projection) | Defines the Projection. | - - -#### Node Definition: Projection - -This node defines a Projection. - -Node name: `Projection` - -The child list (Table 48) contains a list of the following nodes: - -##### Table 48 — *Projection Node Childs* - -| Child Node | Description | -| ------------------------------------------------- | ------------------------------------------------ | -| [Source](#node-definition-source) | Defines the source for the projection. | -| [ScaleHandeling](#node-definition-scalehandeling) | How the source will be scaled to the projection. | - - -### Node Definition: Addresses - -This node defines a group of Addresses. - -Node name: `Addresses` - -The child list (Table 49) contains a list of the following nodes: - -##### Table 49 — *Adresses Node Childs* - -| Child Node | Description | -| ----------------------------------- | ----------------------- | -| [Address](#node-definition-address) | DMX address of the fixture. | -| [Network](#node-definition-network) | Network address of the fixture. | - - -#### Node Definition: Address - -This node defines a DMX address. The defined Address Node Attributes are specified in Table 50. - -Node name: `Address` - -##### Table 50 — *Address Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | --------------------- | --------------------------- | ----------------- | -| break | [Integer](#user-content-attrtype-integer) | 0 | This is the break ident for this address. This value has to be unique for one fixture. | - -The child list (Table 51) contains a list of the following nodes: - -##### Table 51 — *Address Node Children* - -| Value Type | Default Value When Missing | Description | -| ----------- | -------------------------- | ---------------- | -| [Integer](#user-content-attrtype-integer) or [String](#user-content-attrtype-string)| Not Optional | This is the DMX address.
`Integer Format:` `Absolute DMX address;`
`String format:` `Universe - integer universe number, starting with 1; Address - address within universe from 1 to 512. `*`Universe.Address`* | - -#### Node Definition: Network - -This node defines a network IP-address according to the physical interface. The defined Network Node Attributes are specified in Table 52. - -Node name: `Network` - -##### Table 52 — *Network Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -|----------------|-------------------------------------------|-----------------------------|--------------------------------------------------------------| -| geometry | [String](#user-content-attrtype-string) | Not Optional | This is the name of the wire geometry of the linked GDTF that this information is of.. Typically used "ethernet_x", "wireless_x", "loopback_x" (x starting at 1 and incrementing) | -| ipv4 | [IPv4](#user-content-attrtype-ipv4) | Optional | This is the IPv4-address. | -| subnetmask | [IPv4](#user-content-attrtype-ipv4) | optional | This is the SubnetMask-address. Only needed for IPv4. | -| ipv6 | [IPv6](#user-content-attrtype-ipv6) | optional | This is the IPv6-address. | -| dhcp | [Bool](#user-content-attrtype-bool) | false | This is the automated-address. DHCP is considered off. If present it should be set "on" (true). | -| hostname | [hostname](#user-content-attrtype-string) | optional | This is the hostname for the device with an automated address. | - -### Node Definition: Protocols - -This node defines the supported protocols and the used interface. - -Node name: `Protocols` - -The child list (Table 53) contains a list of the following nodes: - -##### Table 53 — *Protocols Node Childs* - -| Child Node | Description | -| --------------------------------- | --------------------------- | -| [Protocol](#node-definition-protocol) | The protocol used by this instance of object. | - -### Node Definition: Protocol - -This node defines the protocol used by the instance of this object. The defined Protocol Node Attributes are specified in Table 54. - -Node name: `Protocol` - -##### Table 54 — *Protocol Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | -------------------- | --------------------------- | ------------ | -| geometry | [String](#user-content-attrtype-string) | NetworkInOut_1 | This is the interface name. | -| name | [String](#user-content-attrtype-string) | empty | Custom Name of the protocol to identify the protocol. Needs to be unique for this instance of object. | -| type | [String](#user-content-attrtype-string) | empty | Name of the protocol. | -| version | [String](#user-content-attrtype-string) | empty | This is the protocol version if available.| -| transmission | [Enum](#user-content-attrtype-enum) | undefined | Unicast, Multicast, Broadcast, Anycast | - -The following names for the `type` are predefined: -- RDMNet -- Art-Net -- sACN -- PosiStageNet -- OpenSoundControl -- CITP -- NDI - -Any other protocol can be freely defined. - - -### Node Definition: Alignments - -This node defines a group of Alignment. - -Node name: `Alignments` - -The child list (Table 55) contains a list of the following nodes: - -##### Table 55 — *Alignments Node Childs* - -| Child Node | Description | -| ------------------------------------- | ------------------------------------------------------------- | -| [Alignment](#node-definition-alignment) | Defines a custom alignment for a beam inside the linked GDTF. | - - -#### Node Definition: Alignment - -This node defines an alignment for a Beam Geometry inside the linked GDTF. The defined Alignment Node Attributes are specified in Table 56. - -Node name: `Alignment` - -##### Table 56 — *Alignment Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value | Description | -| -------------- | -------------------------------------- | --------------------------- | -------------------------------------------- | -| geometry | [String](#user-content-attrtype-string) | Beam Geometry of the first Beam in the kinematic chain of the GDTF. | Defines the Beam Geometry that gets aligned. | -| up | [String](#user-content-attrtype-string)| 0,0,1 | Defines the up vector of the direction. | -| direction | [String](#user-content-attrtype-string)| 0,0,-1 | Defines the direction vector of the lamp. | - - -### Node Definition: CustomCommands - -This node defines a group of CustomCommands. - -Node name: `CustomCommands` - -The child list (Table 57) contains a list of the following nodes: - -##### Table 57 — *CustomCommands Node Childs* - -| Child Node | Description | -| ----------------------------------------------- | ---------------------------------------------------------------------------- | -| [CustomCommand](#node-definition-customcommand) | Contains a list with custom commands that should be executed on the fixture | - - -#### Node Definition: CustomCommand - -This node defines a custom command for the linked GDTF. - -Node name: `CustomCommand` - -The Custom command contains the command that will be executed on the fixture. The definition from the syntax for the command -aligns with the DIN SPEC 15800:2022-02, 11.2.1.2.3, Channel Functions, [for command based control systems](https://github.com/mvrdevelopment/spec/blob/main/gdtf-spec.md#channel-function). - -With this feature you can also control static properties for fixture that cannot be controlled via DMX. - - -### Node Definition: Overwrites - -This node defines a group of Overwrite. - -Node name: `Overwrites` - -The child list (Table 58) contains a list of the following nodes: - -##### Table 58 — *Overwrites Node Childs* - -| Child Node | Description | -| --------------------------------------- | ----------------------------------------------------------------- | -| [Overwrite](#node-definition-overwrite) | Contains a list with overwrites for gobos, filters and emitters. | - - -#### Node Definition: Overwrite - -This node defines an overwrite with the `Universal.gdtt` GDTF template inside the MVR to overwrite Wheel Slots, Emitters and Filters for the fixture. The defined Overwrite Node Attributes are specified in Table 59. - -Node name: `Overwrite` - -##### Table 59 — *Overwrtie Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value | Description | -| -------------- | ----------------------------------------- | ------------- | -------------------------------------------------------------- | -| universal | [String](#user-content-attrtype-string) | Mandatory | Node Link to the Wheel, Emitter or Filter. Starting point is the the collect of the Universal GDTF. | -| target | [String](#user-content-attrtype-string) | Empty String | Node Link to the Wheel, Emitter or Filter. Starting point is the the collect of the linked GDTF of the fixture. When no target is given, it will be like a static gobo or filter that you attach in front of all beams. | - - -### Node Definition: Connections - -This node defines a group of Connection. - -Node name: `Connections` - -The child list (Table 60) contains a list of the following nodes: - -##### Table 61 — *Connections Node Childs* - -| Child Node | Description | -| ---------------------------------------- | --------------------------------------------------------- | -| [Connection](#node-definition-connection) | Contains an definition of an object to object connection. | - - -#### Node Definition: Connection - -This nodes defines an connection of two scene object. The connection can be an electrical or data connection. The defined Connection Node Attributes are specified in Table 61. - -Node name: `Connection` - -##### Table 61 — *Connection Node Attributes* - -| Attribute Name | Attribute Value Type | Default Value | Description | -| -------------- | ----------------------------------------- | --------------------------- | ----------------------------------------------------- | -| own | [String](#user-content-attrtype-string) | Mandatory | Node Link to the Geometry with DIN SPEC 15800 Type [Wiring Object](https://github.com/mvrdevelopment/spec/blob/main/gdtf-spec.md#geometry-type-wiring-object) . Starting point is the Geometry Collect of the linked GDTF. | -| other | [String](#user-content-attrtype-string) | Mandatory | Node Link to the Geometry with DIN SPEC 15800 Type [Wiring Object](https://github.com/mvrdevelopment/spec/blob/main/gdtf-spec.md#geometry-type-wiring-object) . Starting point is the Geometry Collect of the linked GDTF of the object defined in `toObject`. | -| toObject | [UUID](#user-content-attrtype-uuid) | Mandatory | UUID of an other object in the scene. | - -### Node Definition: Mappings - -This node defines a group of Mappings. - -Node name: `Mappings` - -The child list (Table 62) contains a list of the following nodes: - -##### Table 62 — *Mappings Node Childs* - -| Child Node | Allowed Count | Description | -| ------------------------------------- | ------------- | ---------------------------- | -| [Mapping](#node-definition-mapping) | 0 or any | One Mapping for the fixture. | - -It is only allowed to have one Mapping linked to the same Mapping Definition once per Fixture - - -#### Node Definition: Mapping - -This node defines a Mapping. The defined Mapping Node Attributes are specified in Table 63. - -Node name: `Mapping` - -##### Table 63 — *Mapping Node Attributes* - -| Attribute Name | Attribute Value Type | Description | -| -------------- | ----------------------------------- | ------------------------------------------------------------------------------------------- | -| linkedDef | [UUID](#user-content-attrtype-uuid) | The unique identifier of the MappingDefinition node that will be the source of the mapping. | - -The child list (Table 64) contains a list of the following nodes: - -##### Table 64 — *Mapping Node Childs* - -| Child Node | Allowed Count | Value Type | Description | -| ---------- | ------------- | ---------- | --------------------------------------------------------------------------------------------- | -| ux | 0 or 1 | [Integer](#user-content-attrtype-integer) | The offset in pixels in x direction from top left corner of the source that will be used for the mapped object. | -| uy | 0 or 1 | [Integer](#user-content-attrtype-integer) | The offset in pixels in y direction from top left corner of the source that will be used for the mapped object. | -| ox | 0 or 1 | [Integer](#user-content-attrtype-integer) | The size in pixels in x direction from top left of the starting point. | -| oy | 0 or 1 | [Integer](#user-content-attrtype-integer) | The size in pixels in y direction from top left of the starting point. | -| rz | 0 or 1 | [Float](#user-content-attrtype-float) | The rotation around the middle point of the defined rectangle in degree. Positive direction is counter cock wise. | - -NOTE The transformation will be applied in the following order: – Translation – Rotation - - -# Communication Format Definition - - -## General -The MVR communication format - MVR-xchange - shall support the exchange of MVR files over network without the need of an external transport device like a USB-stick. The exchange allows multiple clients within the same network to share MVR files. - -MVR-xchange defines two modes of operation (see Figure 2): -- TCP Mode, which works without configuration but does not support routing. -- WebSocket Mode, which need minimal configuration but allows for routing. - -##### Figure 2 — *MVR-xchange mode of operation* - -| a) TCP Mode of protocol | b) WebSocket Mode of protocol | -|---|---| -| ![media/MVR_LocalNetwork.png](media/MVR_LocalNetwork.png) | ![media/MVR_Websockets.png](media/MVR_Websockets.png) | - - -## TCP Mode of protocol - -The TCP Mode allows users to directly use the MVR-xchange without the need for configuration or special hardware. Discovery of available MVR-xchange clients shall be performed by mDNS (RFC6762 Multicast DNS). Every application that wants to join a MVR-xchange group, need to register a mDNS service. - -The service name shall be `_mvrxchange._tcp.local.`. The sub service name shall be `xxxx._mvrxchange._tcp.local.` where *xxxx* is the name of the group. -. Each client shall negotiate a unique hostname via the methods described in the mDNS standards. Each client shall have a PTR, SRV, TXT and A and/or AAAA -record. - -The TXT record should contain the information given in Table 65: - - -##### Table 65 — *TXT Record Attributes* - -| Attribute Name | Attribute Value Type | Description | -| -------------- | ----------------------------------- | ----------------------------------------------------------------------------- | -| StationName | [String](#user-content-attrtype-string) | The Name of the sending station to be shown on the clients UI. | -| StationUUID | [UUID](#user-content-attrtype-uuid) | UUID of sending station inside the network. This UUID should be persistent across multiple start-ups of the same software on the same computer | - -The format of the TXT record matches RFC1035. - -When a MVR-xchange client wants to join a MVR-xchange group, he needs to register the service and sub service, and send a `MVR_JOIN` message to the other stations that register this sub service name. When a MVRxchange client wants to create a MVR-xchange group, he needs to register a service name which is currently not in use and wait for other MVR-xchange clients to join. - -You can upgrade a TCP Mode MVR-xchange group to use the WebSocket Mode with sending a `MVR_NEW_SESSION_HOST` message providing the URL of the new service. - -## WebSocket Mode of protocol - -The WebSocket Mode allows users to create a routable service for the MVR-xchange. Discovery works with the normal DNS according to RFC6763. The service name needs to be a valid URL that can be resolved by the DNS server. - -The DNS entry should point to the IP of the service running the websocket server. MVR-xchange clients that want to join this MVR-xchange Group need to connect with a web socket client (RFC6455— The WebSocket Protocol). - - -## Packet & Message definition - -### General - -Packages define how the message will be send to the MVR-xchange client, while the message describes the content. All the messages are defined, unless otherwise stated, as JSON documents (ISO/IEC 21778:2017). Packages are defined based on the mode of communication. They are defined for TCP Mode and WebSocket mode differently. - -### TCP Mode - -When in TCP Mode, all messages are send via TCP directly to the client. The packet is encoded as specified in Table 66: - -##### Table 66 — *Packet & Message Definitions* - -| Type | Symbol | -|---|---| -| `MVR_PACKAGE_HEADER` | Number that defines the package. Use 778682. | -| `MVR_PACKAGE_VERSION` | Number that defines the version of the package format. Use 1. | -| `MVR_PACKAGE_COUNT` | Number that defines how many packages the current message consists of. | -| `MVR_PACKAGE_NUMBER` | Number that defines what number this package in the complete message has. Zero based. | -| `MVR_PACKAGE_TYPE` | Number that defines the package type. Use 0 for JSON UTF-8 Payload, use 1 for MVR FILES. | -| `MVR_PAYLOAD_LENGTH` | Number showing the byte-length of transferred buffer. | -| `MVR_PAYLOAD_BUFFER` | Buffer data that stores the payload encoded. | - - -The order and size is defined as follows: -``` -uint32 MVR_PACKAGE_HEADER -uint32 MVR_PACKAGE_VERSION -uint32 MVR_PACKAGE_NUMBER -uint32 MVR_PACKAGE_COUNT -uint32 MVR_PACKAGE_TYPE -uint64 MVR_PAYLOAD_LENGTH -char[] MVR_PAYLOAD_BUFFER -``` - -Where the following applies (Table 67): - -##### Table 67 — *Data Type MVR-xchange package* - -| Type | Symbol | -|---|---| -| uint32 | 32-bit unsigned integer | -| uint64 | 64-bit unsigned integer | -| char[] | 8-bit character array | - -NOTE All multi-byte fields defined shall be transmitted in network byte (big-endian) order - -### WebSocket Mode - -When in WebSocket Mode, all messages should be send as data frame Text *[RFC6455 5.6 Text 0x1](https://datatracker.ietf.org/doc/html/rfc6455#section-5.6)* unless otherwise defined. - -## `MVR_JOIN` message - -### General - -When a MVR-xchange client connects with another MVR-xchange client, the first MVR-xchange client needs to send a `MVR_JOIN` message. - -NOTE A MVR-xchange client can send multiple `MVR_JOIN` messages to the same server during the same connection to update its name or get the latest MVR file list. - -### TCP Mode - -Figure 3 shows the TCP mode for a MVR-xchange client joining MVR-xchange group. - -##### Figure 3 — *TCP mode: MVR-xchange client joining MVR-xchange group* - -| a) MVR-xchange client 2 joins the MVR-xchange Group | b) and sends to all mDNS Service a `MVR_JOIN` message | -|---|---| -| ![media/MVR_Join_mDNS_1.png](media/MVR_Join_mDNS_1.png) | ![media/MVR_Join_mDNS_2.png](media/MVR_Join_mDNS_2.png) | - - -### WebSocket Mode - -Figure 4 shows the Websocket mode for a MVR-xchange client joining MVR-xchange group. - -##### Figure 4 — *Websocket mode: MVR-xchange client joining MVR-xchange group* - -| a) 1 Is a Websocket Server and has a URL | b) MVR-xchange client 2 connects to the websocket sever and send a `MVR_JOIN` message | -|---|---| -| ![media/MVR_Join_1.png](media/MVR_Join_1.png) | ![media/MVR_Join_2.png](media/MVR_Join_2.png) | - -| c) MVR-xchange client 3 connects to the websocket sever and send a `MVR_JOIN` message | d) MVR-xchange client 3 connects to the websocket sever and send a `MVR_JOIN` message | -|---|---| -| ![media/MVR_Join_3.png](media/MVR_Join_3.png) | ![media/MVR_Join_4.png](media/MVR_Join_4.png) | - -The defined MVR_JOIN message Attributes are specified in Table 68. - -##### Table 68 — *MVR_JOIN message Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_JOIN | -| Provider | [String](#user-content-attrtype-string) | Not Optional | The application name providing MVR Import & Export | -| StationName | [String](#user-content-attrtype-string) | Not Optional | The Name of the sending station to be shown on the clients UI. | -| verMajor | [Integer](#user-content-attrtype-integer) | 0 | It is mandatory to transmit the version of the MVR file that the sender station supports. | -| verMinor | [Integer](#user-content-attrtype-integer) | 0 | It is mandatory to transmit the version of the MVR file that the sender station supports. | -| StationUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | UUID of sending station inside the network. This UUID should be persistent across multiple start-ups of the same software on the same computer | -| Commits | [Array of `MVR_COMMIT`](#user-content-attrtype-string) | Empty Array | List all available MVR files that are on sender station in the format of the `MVR_COMMIT` packet. | | - -The defined MVR_JOIN response Attributes are specified in Table 69. - -##### Table 69 — *MVR_JOIN response Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_JOIN_RET | -| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | -| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message if there is an error. | | -| Provider | [String](#user-content-attrtype-string) | Not Optional | The application name providing MVR Import & Export | -| StationName | [String](#user-content-attrtype-string) | Not Optional | The Name of the receiving station to be shown on the UI. | -| verMajor | [Integer](#user-content-attrtype-integer) | 0 | It is mandatory to transmit the version of the MVR file that the receiver station supports. | -| verMinor | [Integer](#user-content-attrtype-integer) | 0 | It is mandatory to transmit the version of the MVR file that the receiver station supports. | -| StationUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | UUID for receiving station inside the network. This UUID should be persistent across multiple start-ups of the same software on the same computer | -| Commits | [Array of `MVR_COMMIT`](#user-content-attrtype-string) | Empty Array | List all available MVR files that are on receiver station in the format of the `MVR_COMMIT` packet. | | - -EXAMPLE - ->ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. - -Request: -``` -{ - "Type": "MVR_JOIN", - "Provider":"MVRApplication", - "verMajor":1, - "verMinor":6, - "StationUUID":"4aa291a1-1a62-45fe-aabc-e90e5e2399a8", - "StationName":"MVR Application from user A at location B", - "Commits": [ - { - ...MVR_COMMIT_MESSAGE_ARGS - }, - { - ...MVR_COMMIT_MESSAGE_ARGS - }, - { - ...MVR_COMMIT_MESSAGE_ARGS - } - ] - -} -``` - -Response: -``` -{ - "Type": "MVR_JOIN_RET", - "OK": true, - "Message": "", - "verMajor":1, - "verMinor":6, - "StationUUID":"a7669ff9-bd61-4486-aea6-c190f8ba6b8c", - "StationName":"MVR Application from user A at location B", - "Commits": [ - { - ...MVR_COMMIT_MESSAGE_ARGS - }, - { - ...MVR_COMMIT_MESSAGE_ARGS - }, - { - ...MVR_COMMIT_MESSAGE_ARGS - } - ] -} -``` - - -## `MVR_LEAVE` message - -A client sends a `MVR_LEAVE` when it wants to quit an MVR-xchange Group and does not want to get updates about new MVR files anymore. - -For the WebSocket mode [Figure 5 a)]: it is not required to terminate the Websockets connection, but it can be done. For the TCP mode [Figure 5 b)]: it is not required to turn down the mDNS service, but it can be done. - -In order to join again, the client needs to send a `MVR_JOIN` message again. - -##### Figure 5 — *MVR_LEAVE message to quit MVR-xchange group* - -|a) In Webssocket mode: MVR-xchange client 4 send a `MVR_LEAVE` message to the websocket server. | b) In TCP Mode: MVR-xchange client 2 send a `MVR_LEAVE` message to all stations | -|---|---| -| ![media/MVR_Leave_1.png](media/MVR_Leave_2.png) | ![media/MVR_Leave_2.png](media/MVR_Leave_1.png) | - -The defined MVR_LEAVE message Attributes are specified in Table 70. - -##### Table 70 — *MVR_LEAVE message Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_LEAVE | -| FromStationUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | The UUID of the station. | - -The defined MVR_LEAVE response Attributes are specified in Table 71. - -##### Table 71 — *MVR_LEAVE response Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_LEAVE_RET. | -| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | -| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message when there is an error. | | - - -EXAMPLE - ->ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. - -Request: -``` -{ - "Type": "MVR_LEAVE", - "StationUUID":"", -} -``` -Response: -``` -{ - "Type": "MVR_LEAVE_RET", - "OK": true, - "Message": "" -} -``` - -## `MVR_COMMIT` message - -### General - -The MVR commit message informs all connected stations that there is a new MVR commit. This message only informs the stations about the existence of the new file. Stations needs to request the MVR file with a `MVR_REQUEST` message. - -Each MVR commit represents one revision of the project. Therefore an array of MVR commits, as found in the `MVR_JOIN` message, represents the working history of the project. It is up to the client how many commits are kept in store at any time. - -The following chart displays the process when one client sends a `MVR_COMMIT` message to the server, and the server distributes this in the session. - -### TCP Mode - -The MVR-xchange client informs all other MVR-xchange clients about the new commit (see Figure 6). Note that the client needs to respect any previous `MVR_LEAVE` messages themselves. - -##### Figure 6 — *TCP mode: MVR-xchange client commits to MVR-xchange group.* - -| MVR-xchange client sends the `MVR_COMMIT` message to the connected stations. | -|---| -| ![media/MVR_Commit_4.png](media/MVR_Commit_4.png) | - - -### WebSocket Mode - -Figure 7 shows the WebSocket mode for a MVR-xchange client that commits to MVR-xchange group. - -##### Figure 7 — *Websocket mode: MVR-xchange client commits to MVR-xchange group.* - -| a) MVR-xchange client sends message to server | b) Server sends messages to all connected MVR-xchange clients but the sender | -|---|---| -| ![media/MVR_Commit_1.png](media/MVR_Commit_1.png) | ![media/MVR_Commit_2.png](media/MVR_Commit_2.png) | - -Figure 8 displays the process when the server is the station who is providing a new MVR file. In this case the MVR info is directly transmitted to the connected stations. - -##### Figure 8 — *Server makes the MVR_COMMIT itself, and only sends it to connected MVR-xchange clients* - -| Server makes the `MVR_COMMIT` itself, and only sends it to connected MVR-xchange clients | -|---| -| ![media/MVR_Commit_3.png](media/MVR_Commit_3.png) | - -The defined MVR_COMMIT message Attributes are specified in Table 72. - -##### Table 72 — *MVR_COMMIT message Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_COMMIT. | -| verMajor | [Integer](#user-content-attrtype-integer) | Not Optional | It is mandatory to transmit the current version of the MVR file as specified in Root File. If joining as new member send "0". | -| verMinor | [Integer](#user-content-attrtype-integer) | Not Optional | It is mandatory to transmit the current version of the MVR file as specified in Root File. If joining as new member send "0". | -| FileSize | [Integer](#user-content-attrtype-integer) | Not Optional | | -| FileUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | The UUID of the MVR file. Generate a UUID using | -| StationUUID | [UUID](#user-content-attrtype-uuid) | Not Optional | UUID for the station inside the network. This UUID should be persistent across multiple start-ups of the same software on the same computer | -| ForStationsUUID | Array of [UUID](#user-content-attrtype-uuid) | [] | Array with the station UUID that this MVR should be send to. When it is an empty array, the MVR will be send to all connected *MVR-xchange clients* | -| Comment | [String](#user-content-attrtype-string) | | Describes the changes made in this version of the MVR file. | -| FileName | [String](#user-content-attrtype-string) | | Describes the file name that can be used to store the file on disk to preserve it across multiple MVR-xchange clients. The usage of this attribute is optional, when not defined, the receiving MVR-xchange client can decide which file name it uses to store it on disk. | - -The defined MVR_COMMIT response Attributes are specified in Table 73. - -##### Table 73 — *MVR_COMMIT response Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_COMMIT_RET. | -| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | -| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message when there is an error. | | - - ->ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. - -``` -Request: -{ - "Type": "MVR_COMMIT", - "verMajor":1, - "verMinor":6, - "FileUUID":"", - "ForStationsUUID":[], - "FileSize":256, - "Comment":"My complete description of what I have changed", -} -Response: -{ - "Type": "MVR_COMMIT_RET", - "OK": true, - "Message": "" -} -``` - -## `MVR_REQUEST` message - -This packet requests a MVR file from a station. You either can request a specific MVR via its UUID or get the last available MVR File by leaving the field empty. The underlying software will then generate a file based on the current state. This also triggers a `MVR_COMMIT` message to other connected stations. - -The available MVR UUIDs can be retrieved using the `MVR_COMMIT` message. - -If the station does not have the specified MVR file, it returns a MVR_REQUEST Json Response, otherwise it sends the buffer of the MVR file. - -NOTE 1 When in WebSocket Mode, the binary frame flag will be used to tell the receiver if a Buffer or JSON is sent. -NOTE 2 When in TCP Mode, the `MVR_PACKAGE_TYPE` flag will be used to tell the receiver if a Buffer or JSON was sent. - -Figure 9 shows the Websocket mode for a MVR-xchange client that is requesting a file. - -##### Figure 9 — *Websocket mode: MVR-xchange client requesting file* - -| a) Station requests a MVR from another station | b) Server sends the request to the right station | -|---|---| -| ![media/MVR_Request_1.png](media/MVR_Request_1.png) | ![media/MVR_Request_2.png](media/MVR_Request_2.png) | - -| c) Station sends the MVR file as binary data to the server | d) Server sends the MVR the MVR file as binary data to the station | -|---|---| -| ![media/MVR_Commit_3.png](media/MVR_Request_3.png) | ![media/MVR_Request_4.png](media/MVR_Request_4.png) | - -Figure 10 shows the TCP mode for a MVR-xchange client that is requesting a file. - -##### Figure 10 — *TCP mode: MVR-xchange client requesting file* - -| a) MVR-xchange client requests a MVR from another station | b) First requested station does not have the MVR and sends back a failure message, | -|---|---| -| ![media/MVR_Request_mDNS1.png](media/MVR_Request_mDNS1.png) | ![media/MVR_Request_mDNS2.png](media/MVR_Request_mDNS2.png) | - -| c) MVR-xchange client requests a MVR from another station | d) Second requested station does have the MVR and sends back the MVR file | -|---|---| -| ![media/MVR_Request_mDNS3.png](media/MVR_Request_mDNS3.png) | ![media/MVR_Request_mDNS4.png](media/MVR_Request_mDNS4.png) | - -The defined MVR_REQUEST message Attributes are specified in Table 74. - -##### Table 74 — *MVR_REQUEST message Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_REQUEST- | -| FileUUID | [UUID](#user-content-attrtype-uuid) | Last MVR File from station | The UUID of the requested MVR file. If not set, the last available file is sent. | -| FromStationUUID | Array of [UUID](#user-content-attrtype-uuid) | | The UUID of the station that you want to retrieve the MVR from. | - -The defined MVR_REQUEST error response Attributes are specified in Table 75. - -##### Table 75 — *MVR_REQUEST error response Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_REQUEST_RET | -| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | -| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message when there is an error. | | - - ->ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. - -Request: -``` -{ - "Type": "MVR_REQUEST", - "FromStationUUID":"", - "FileUUID":"", -} -``` -Response: - -binary frame - -OR -``` - -{ - "Type": "MVR_REQUEST_RET", - "OK": false, - "Message": "The MVR is not available on this client" -} -``` - -## `MVR_NEW_SESSION_HOST` message - -This message is used to inform other MVR-xchange clients of impending network configuration changes. This message is sent to all nodes in the network. - -This message type is meantfor two use cases: -- Change the Service URL (WebSocket Mode) or the ServiceName (TCP Mode) of a network -- Switch the Mode of a network - -This requires that only either `ServiceName` or `SerivceURL` are set. Setting both will return OK: false. - -### Change Service URL/Name - -This requires, that the current Network mode and the supplied message data are matching: -- If in WebSocket Mode, the **ServiceURL** shall be set -- If in TCP Mode, the **ServiceName** shall be set - -When the receiving nodes are in TCP Mode: - -Each receiver will try to connect to the mDNS service given in `ServiceName` and send a `MVR_JOIN` message. If this is successful, the nodes save the new Service Name and modify their own mDNS service. OK: true is returned. If no connection could be established, OK: false is returned. - -When the receiving nodes are in WebSocket Mode: - -Each receiver will try to connect to the URL given in `ServiceURL` and send a `MVR_JOIN` Message. If this is successful, the nodes save the URL and return OK: true. Otherwise OK: false is returned. - -## Switch Mode of a Network - -This requires, that the current Network mode and the supplied message data are **not** matching: -- If in WebSocket Mode, the **ServiceName** shall be set -- If in TCP Mode, the **ServiceURL** shall be set. - -When the receiving nodes are in TCP Mode: - -Each receiver will try to switch into WebSocket Mode by connecting to the URL given in `ServiceURL` and send a `MVR_JOIN` Message. If this is successful, then OK: true is returned and the mode is switched. If the URL is not reachable, then OK: false is returned. - -When the receiving nodes are in WebSocket Mode: - -Each receiver will try to switch into TCP Mode by connecting to the mDNS service given in `ServiceName` and send a `MVR_JOIN` Message. If this is successful, the nodes switch to TCP Mode and establish their own mDNS client as described above. OK: true is returned in this case. If the new mDNS service is not reachable OK: false is returned. - -The defined MVR_NEW_SESSION_HOST message Attributes are specified in Table 76. - -##### Table 76 — *MVR_NEW_SESSION_HOST message Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional |Defines the type of the message. Should be MVR_NEW_SESSION_HOST | -| ServiceName | [String](#user-content-attrtype-string) | Empty | New mDNS Service Name to connect to. If Empty, ignore. Cannot be set together with ServiceURL | -| ServiceURL | [String](#user-content-attrtype-string) | Empty. | New WebSocket Service URL to connect to. If Empty, ignore. Cannot be set together with ServiceURL - -The defined MVR_NEW_SESSION_HOST error response Attributes Attributes are specified in Table 77. - -##### Table 77 — *MVR_NEW_SESSION_HOST error response Attributes* - -| Attribute Name | Attribute Value Type | Default Value when Optional | Description | -| -------------- | ----------------------------------- | --------------------------- | ----------------------------------------------------------------------------- | -| Type | [String](#user-content-attrtype-string) | Not Optional | Defines the type of the message. Should be MVR_NEW_SESSION_HOST_RET | -| OK | [Bool](#user-content-attrtype-bool) | Not Optional | True when operation is successful, false when there is an error. Check the Message for more information in this case. | -| Message | [String](#user-content-attrtype-string) | Empty String | Human readable message when there is an error. | | - - ->ℹ NOTE: This example has been adjusted post-publishing to match the MVR Spec. - -Request: -``` -{ - "Type": "MVR_NEW_SESSION_HOST", - "ServiceName":"fancyProjectGroup._mvrxchange._tcp.local.", - "ServiceURL":"", -} -``` - -Response: -``` -{ - "Type": "MVR_NEW_SESSION_HOST_RET", - "OK": true, - "Message": "" - -} -``` ---- - - - -# Annex A. Object ID for Selection purposes (informative) - -In order to control or reference, all objects in the MVR Spec have human readable object IDs. For this both the FixtureIDNumeric and the CustomId is used. -The FixtureIDNumeric is a generic name pool that applies to all objects. All FixtureIDNumerics should be unique in one scene, so that objects can be selected without collisions. -The CustomId has a similar approach, but allows you define the pool type for the numbers as well. An object can so report that it is a Pyro device, and in the Pyro ID Pool it has the number 100. -Normally FixtureIDs are numeric to allow range selection. For Descriptive display on plots, some tools also append a letter like # or A before the FixtureID. A lot of tools have a concept of selecting objects with a range. Like 100 thru 200. So the Numeric portion of the FixtureID should be placed into the FixtureIDNumeric field. -A similar concept is the multipatch. Sometimes you want to group multiple objects behind the same FixtureIDNumeric or CustomId. This can be objects of the same GDTF Type, but not forced to be. When you select the FixtureIDNumeric or CustomId from the multipatch parent, all objects that reference this object in multipatch -parent should also be selected. - - -# Annex B. UUID purposes (informative) - -UUIDs are randomly generated numbers which are, practically speaking, unique and unable to conflict. The way UUIDs are designed is what allows them to uniquely identify an object with certainty. They are so unique that if you generate one today, you can be reasonably certain that this UUID has never been generated before and will never be generated by someone else in the future. This means that UUIDs in MVR will not conflict even across many files. Because it is easier to disregard data than try to derive it, MVR requires UUIDs for many things. This design and its incorporation into MVR is advantageous for many reasons, a few of which we will discuss below. -One of the most important aspects of UUIDs in MVR is that they are persistent. A UUID should identify an item throughout its entire life cycle. This means that if a document is exported, then objects should have the same UUID every time an export is performed. -One use case for UUIDs is importing or merging MVRs into an existing document. This is one reason that persistent UUIDs are valuable. If you export an MVR from one program, open it in another, and make modifications, then you may want to incorporate those changes into the original document. By cross referencing UUIDs, you can avoid creating duplicate objects and instead update existing ones. -UUIDs are also used inside of the MVR file format as a form of reference. For example, a symbol instance shall refer to a symbol definition. Because the symbol definition is given a UUID, the symbol instance can reference its symbol through the use of this UUID. - From 42116de5c2fd3cd67608521328ecc67eb53c42fa Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 14:12:51 +0200 Subject: [PATCH 14/16] Update examples in README --- README.md | 80 +++++++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index b1faad2..268f860 100644 --- a/README.md +++ b/README.md @@ -43,29 +43,49 @@ for layer_index, layer in enumerate(mvr_file.scene.layers): ### Writing ```python +import pymvr +from pathlib import Path + +# 1. Create a writer instance +mvr_writer = pymvr.GeneralSceneDescriptionWriter() -mvr = pymvr.GeneralSceneDescriptionWriter() -pymvr.UserData().to_xml(parent=mvr.xml_root) -scene = pymvr.Scene().to_xml(parent=mvr.xml_root) -pymvr.AUXData().to_xml(parent=scene) -fixtures_list = [] +# 2. Build the MVR object tree +# Create a scene object +scene_obj = pymvr.Scene() +# Create layers and add them to the scene layers = pymvr.Layers() +scene_obj.layers = layers + +# Create a layer and add it to the layers layer = pymvr.Layer(name="Test layer") -layers.layers.append(layer) +layers.append(layer) +# Create a child list for the layer child_list = pymvr.ChildList() layer.child_list = child_list -fixture = pymvr.Fixture(name="Test Fixture") # not really a valid fixture +# Create a fixture and add it to the child list +# Note: A valid fixture would require more attributes +fixture = pymvr.Fixture(name="Test Fixture") child_list.fixtures.append(fixture) -fixtures_list.append((fixture.gdtf_spec, fixture.gdtf_spec)) -layers.to_xml(parent=scene) +# 3. Serialize the scene object into the writer's XML root +scene_obj.to_xml(parent=mvr_writer.xml_root) + +# 4. Add any necessary files (like GDTF) to the MVR archive +# (This example fixture doesn't have a GDTF file, so this list will be empty) +files_to_pack = [] +if fixture.gdtf_spec: + # The list should contain tuples of (source_path, archive_name) + files_to_pack.append((fixture.gdtf_spec, fixture.gdtf_spec)) +mvr_writer.files_list = list(set(files_to_pack)) + +# 5. Write the MVR file +output_path = Path("example.mvr") +mvr_writer.write_mvr(output_path) -mvr.files_list = list(set(fixtures_list)) -test_file_path = Path(Path(__file__).parent, "example.mvr") -mvr.write_mvr(test_file_path) +print(f"MVR file written to {output_path.resolve()}") ``` See [BlenderDMX](https://github.com/open-stage/blender-dmx) and @@ -74,41 +94,7 @@ reference implementation. ## Status -- Reading: - - - Address - - Alignment - - AUXData - - ChildList - - Class - - Connection - - CustomCommand - - Data - - Fixture - - FocusPoint - - Geometries - - Geometry3D - - Gobo - - GroupObject - - Layer - - Mapping - - Overwrite - - Position - - Projector - - Protocol - - SceneObject - - Sources - - Support - - Symbol - - Symdef - - Truss - - UserData - - VideoScreen - -- Writing: - - Fixture - - Focus point - - creating MVR zip file +- Reading and Writing of all aspects of MVR should be covered. ## Development From 959cac6e61d5bb3662c8da2b4f1e834893102218 Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 14:13:48 +0200 Subject: [PATCH 15/16] Rename some to_xml to populate_xml based on function to clarify and fix inheritence --- pymvr/__init__.py | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/pymvr/__init__.py b/pymvr/__init__.py index 4c93643..114a6f0 100644 --- a/pymvr/__init__.py +++ b/pymvr/__init__.py @@ -331,7 +331,7 @@ def to_xml(self, parent: Element): attributes["dhcp"] = "true" if self.hostname: attributes["hostname"] = self.hostname - element = ElementTree.SubElement(parent, "Network", **attributes) + element = ElementTree.SubElement(parent, "Network", attributes) return element @@ -470,7 +470,7 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.name}" - def to_xml(self, element: Element): + def populate_xml(self, element: Element): Matrix(self.matrix.matrix).to_xml(element) if self.classing: ElementTree.SubElement(element, "Classing").text = self.classing @@ -531,8 +531,8 @@ def _read_xml(self, xml_node: "Element"): def __str__(self): return f"{self.name}" - def to_xml(self, element: Element): - super().to_xml(element) + def populate_xml(self, element: Element): + super().populate_xml(element) if self.geometries: self.geometries.to_xml(element) @@ -708,8 +708,8 @@ def to_xml(self): attributes = {"name": self.name, "uuid": self.uuid} if self.multipatch: attributes["multipatch"] = self.multipatch - element = ElementTree.Element(type(self).__name__, **attributes) - super().to_xml(element) + element = ElementTree.Element(type(self).__name__, attributes) + self.populate_xml(element) if self.focus: ElementTree.SubElement(element, "Focus").text = self.focus @@ -1186,7 +1186,13 @@ def to_xml(self): class SceneObject(BaseChildNodeExtended): - pass + def to_xml(self): + attributes = {"name": self.name, "uuid": self.uuid} + if self.multipatch: + attributes["multipatch"] = self.multipatch + element = ElementTree.Element(type(self).__name__, attributes) + self.populate_xml(element) + return element class Truss(BaseChildNodeExtended): @@ -1216,8 +1222,8 @@ def to_xml(self): attributes = {"name": self.name, "uuid": self.uuid} if self.multipatch: attributes["multipatch"] = self.multipatch - element = ElementTree.Element(type(self).__name__, **attributes) - super().to_xml(element) + element = ElementTree.Element(type(self).__name__, attributes) + self.populate_xml(element) if self.position: ElementTree.SubElement(element, "Position").text = self.position if self.function_: @@ -1259,8 +1265,8 @@ def to_xml(self): attributes = {"name": self.name, "uuid": self.uuid} if self.multipatch: attributes["multipatch"] = self.multipatch - element = ElementTree.Element(type(self).__name__, **attributes) - super().to_xml(element) + element = ElementTree.Element(type(self).__name__, attributes) + self.populate_xml(element) if self.position: ElementTree.SubElement(element, "Position").text = self.position @@ -1296,8 +1302,8 @@ def to_xml(self): attributes = {"name": self.name, "uuid": self.uuid} if self.multipatch: attributes["multipatch"] = self.multipatch - element = ElementTree.Element(type(self).__name__, **attributes) - super().to_xml(element) + element = ElementTree.Element(type(self).__name__, attributes) + self.populate_xml(element) if self.sources: self.sources.to_xml(element) @@ -1326,8 +1332,8 @@ def to_xml(self): attributes = {"name": self.name, "uuid": self.uuid} if self.multipatch: attributes["multipatch"] = self.multipatch - element = ElementTree.Element(type(self).__name__, **attributes) - super().to_xml(element) + element = ElementTree.Element(type(self).__name__, attributes) + self.populate_xml(element) if self.projections: self.projections.to_xml(element) @@ -1375,7 +1381,7 @@ def to_xml(self): attributes["version"] = self.version if self.transmission: attributes["transmission"] = self.transmission - element = ElementTree.Element(type(self).__name__, **attributes) + element = ElementTree.Element(type(self).__name__, attributes) return element @@ -1409,7 +1415,7 @@ def to_xml(self): attributes["up"] = self.up if self.direction: attributes["direction"] = self.direction - element = ElementTree.Element(type(self).__name__, **attributes) + element = ElementTree.Element(type(self).__name__, attributes) return element @@ -1436,7 +1442,7 @@ def to_xml(self): attributes = {"universal": self.universal} if self.target: attributes["target"] = self.target - element = ElementTree.Element(type(self).__name__, **attributes) + element = ElementTree.Element(type(self).__name__, attributes) return element @@ -1655,7 +1661,7 @@ def to_xml(self): attributes["linkedGeometry"] = self.linked_geometry if self.type_: attributes["type"] = self.type_ - element = ElementTree.Element(type(self).__name__, **attributes) + element = ElementTree.Element(type(self).__name__, attributes) element.text = self.value return element From 86a368291cf4befb287f51fe012fa919b5454d2f Mon Sep 17 00:00:00 2001 From: vanous Date: Mon, 21 Jul 2025 22:45:36 +0200 Subject: [PATCH 16/16] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8428fe0..9f9a336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ### Changelog +### 1.0.0-dev0 + +* Breaking changes! +* Big refactor of structure - ensure that XML structure is followed in code +* Implemented all MVR fields +* Big refactor for export + #### 0.5.0 * Add classing to export