summaryrefslogtreecommitdiff
path: root/functions/scene.py
blob: 958138f680eb9a9eff61513df195346ff48fd839 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""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)