"""Scene function module. Contains the definition of the Scene, the lighting primitive. """ import xml.etree.ElementTree as et from typing import Mapping from .function import Function from ..topology import Fixture from ..constants import INFTY, SCENE, BXW from ..exceptions import LoadError class Scene(Function): """Class representing a lighting scene. This is the primitive for lighting, and all light cues must be based in some manner off of Scene. Scenes are simple, having no fading and infinite duration. Modifying the scene can be done in a few ways: the easiest is using the dictionary- style accesses, e.g. setting the values using: scene[channel] = value Values may be removed from the scene using the ``del`` operator and retrieved in the same fashion. Alternatively, values can be updated in bulk using ``Scene.update`` or set in bulk with ``Scene.set``. """ audio_scope = () scope = frozenset() actual_duration = INFTY values = frozenset() def __init__(self, w, id_ = None, name = None, values: Mapping[Fixture.Channel, int] = None): super().__init__(w=w, type_=SCENE, id_=id_, name=name, duration=INFTY, fade_in=0, fade_out=0) self._values = {} self._render = () self._update(values, changed=False) def _update_render(self, changed=True): self._render = tuple(self._values.items()) self.scope = frozenset(self._values.keys()) self.values = self._render if changed: self.w.function_changed(self) def set(self, v): """Set the scene's values to the given ones. Equivalent to update, except that all non-given values are deleted from the scene. """ self._values = {} self.update(v) def _update(self, v, changed=True): if isinstance(v, dict): v = v.items() vn = [] for c, val in v: val = int(val) if val < 0 or val > 255: raise ValueError("Values must be integers on [0,256)") vn.append((c, val)) for c, val in vn: self._values[c] = val self._update_render(changed=changed) def update(self, v): """Update the scene's values.""" self._update(v=v, changed=True) def __getitem__(self, c: Fixture.Channel): return self._values[c] if c in self._values else None def __setitem__(self, c: Fixture.Channel, v: int): self.update(((c, v),)) def __delitem__(self, c: Fixture.Channel): if c in self._values: del self._values[c] self._update_render() def get_data(self): return None def copy_data(self, data): return None def render(self, t, data = None): return (self._render, (), None) def serialize(self): e = et.Element(BXW+"function") e.set("type", self.type) e.set("id", str(self.id)) e.set("name", self.name) for c, v in self.values: ce = et.SubElement(e, BXW+"value") ce.set("fixture", str(c.f.id)) ce.set("channel", str(c.id)) ce.text = str(v) return e @classmethod def deserialize(cls, w, e): if e.tag != BXW+"function": raise LoadError("Invalid function tag") elif e.get("type") != SCENE: raise LoadError("Load delegated to wrong class (this is a bug)") id_ = cls.int_or_none(e.get("id")) if id_ is None: raise LoadError("Function tag has invalid/missing ID") name = e.get("name") values = {} for ve in e: if ve.tag != BXW+"value": raise LoadError("Invalid value tag") fixture = cls.int_or_none(ve.get("fixture")) channel = cls.int_or_none(ve.get("channel")) if None in (fixture, channel): raise LoadError("Missing/invalid fixture/channel value") elif fixture not in w.fixtures: raise LoadError("Missing fixture ID %d" % fixture) elif channel >= len(w.fixtures[fixture].channels): raise LoadError("Fixture %d missing channel ID %d" % (fixture, channel)) channel = w.fixtures[fixture].channels[channel] value = cls.int_or_none(ve.text) if value is None: raise LoadError("Missing/invalid value for channel") values[channel] = value return cls(w=w, id_=id_, name=name, values=values)