Organize the lighting project and ship the combined XCHC show.

Group workspaces, generators, stage map, and tests into clear folders, and add the unified cans+movers show plus a simple shared/individual RGBW control board.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-08 00:59:36 +12:00
co-authored by Cursor
parent c92094cc26
commit 5eb623d6af
17 changed files with 7457 additions and 1195 deletions
+366
View File
@@ -0,0 +1,366 @@
#!/usr/bin/env python3
"""Rewrite xchc pattern scenes so the U of cans + centre movers act as one.
Building 7 m wide. Coordinates in metres:
X = 0 at centre line, +X to the right
Y = 0 at the row of 4, +Y toward audience (in front of the 4)
Audience
1 8 ← wall-mounted sides @ 2.4 m in front of the 4
2 M1 M2 7 ← wall-mounted sides @ 1.2 m; movers @ 1.1 m, ±0.3 m
3 — 4 — 5 — 6 ← row of 4 at ±0.6 m and ±1.8 m from centre
Building 7 m → side walls at ±3.5 m. #1,#2 left wall; #7,#8 right wall.
#1 front left. Movers 1.1 m in front of the row of 4, 0.3 m off centre.
"""
from __future__ import annotations
import re
from pathlib import Path
CANS = list(range(8))
MOVERS = (8, 9)
WALL = 3.5 # building half-width
# Fixture ID → (x_m, y_m) — Y=0 = row of 4; +Y toward audience
LAYOUT_M = {
0: (-WALL, 2.4), # #1 front left — left wall
1: (-WALL, 1.2), # #2 — left wall
2: (-1.8, 0.0), # #3 row of 4
3: (-0.6, 0.0), # #4 row of 4
4: (0.6, 0.0), # #5 row of 4
5: (1.8, 0.0), # #6 row of 4
6: (WALL, 1.2), # #7 — right wall
7: (WALL, 2.4), # #8 front right — right wall
8: (-0.3, 1.1), # Small Eye #1 — 0.3 m off centre, 1.1 m in front of row of 4
9: (0.3, 1.1), # Small Eye #2 — 0.3 m off centre, 1.1 m in front of row of 4
}
# Walk U: #1 → #2 → along row of 4 L→R → #7 → #8
U_WALK = [0, 1, 2, 3, 4, 5, 6, 7]
LEFT_SIDE = {0, 1}
RIGHT_SIDE = {6, 7}
ROW4 = {2, 3, 4, 5}
RINGS = [
{8, 9},
{8, 9, 3, 4}, # movers + centre of row of 4
{8, 9, *ROW4},
{8, 9, *CANS},
]
LEFT = LEFT_SIDE | {2, 3, 8}
RIGHT = RIGHT_SIDE | {4, 5, 9}
GRID_W = 7.0
GRID_H = 3.2
X_ORIGIN = -3.5
def m_to_grid(x_m: float, y_m: float) -> tuple[float, float]:
gx = round(x_m - X_ORIGIN, 2)
# Row of 4 (y=0) toward top; audience / front sides toward bottom
gy = round(y_m, 2)
return gx, gy
COLORS = {
"Red": (255, 0, 0),
"Orange": (255, 127, 0),
"Yellow": (255, 255, 0),
"Green": (0, 255, 0),
"Cyan": (0, 255, 255),
"Blue": (0, 0, 255),
"Violet": (127, 0, 255),
"Magenta": (255, 0, 255),
"White": (255, 255, 255),
}
RAINBOW = [
(255, 0, 0),
(255, 127, 0),
(255, 255, 0),
(0, 255, 0),
(0, 255, 255),
(0, 0, 255),
(127, 0, 255),
(255, 0, 255),
]
def can_channels(r: int, g: int, b: int, *, white: int = 0, on: bool = True) -> str:
if not on:
return "0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0"
if white:
return f"1,0,2,0,3,0,4,0,5,{white},6,0,7,0"
return f"1,0,2,{r},3,{g},4,{b},5,0,6,0,7,0"
def mover_channels(r: int, g: int, b: int, *, on: bool = True, dim: int = 255) -> str:
if not on or (r == g == b == 0):
dim = 0
r = g = b = 0
parts = [
0, 128, 2, 80, 4, 0, 5, 0, 6, 191, 7, 70, 8, dim, 9, 0,
10, r, 11, g, 12, b, 13, 0,
]
for pixel in range(1, 7):
base = 10 + pixel * 4
parts.extend([base, 0, base + 1, 0, base + 2, 0, base + 3, 0])
return ",".join(str(x) for x in parts)
def fixture_vals(
lit: dict[int, tuple[int, int, int] | None], *, white_ids: set[int] | None = None
) -> str:
white_ids = white_ids or set()
lines = []
for fid in list(CANS) + list(MOVERS):
rgb = lit.get(fid)
if rgb is None:
ch = (
mover_channels(0, 0, 0, on=False)
if fid in MOVERS
else can_channels(0, 0, 0, on=False)
)
lines.append(f' <FixtureVal ID="{fid}">{ch}</FixtureVal>')
continue
r, g, b = rgb
if fid in MOVERS:
ch = mover_channels(r, g, b)
elif fid in white_ids:
ch = can_channels(0, 0, 0, white=255)
else:
ch = can_channels(r, g, b)
lines.append(f' <FixtureVal ID="{fid}">{ch}</FixtureVal>')
return "\n".join(lines)
def scene(
fid: int,
name: str,
lit: dict,
*,
white_ids: set[int] | None = None,
fade: int = 0,
) -> str:
return f""" <Function ID="{fid}" Type="Scene" Name="{name}">
<Speed FadeIn="{fade}" FadeOut="0" Duration="{fade}" />
{fixture_vals(lit, white_ids=white_ids)}
</Function>"""
def replace_function(text: str, function_id: int, new_xml: str) -> str:
pattern = re.compile(
rf' <Function ID="{function_id}"[\s\S]*?</Function>\n',
re.MULTILINE,
)
if not pattern.search(text):
raise SystemExit(f"Function ID {function_id} not found")
return pattern.sub(new_xml.rstrip() + "\n", text, count=1)
def all_color(rgb: tuple[int, int, int]) -> dict:
lit = {i: rgb for i in CANS}
lit[8] = rgb
lit[9] = rgb
return lit
def monitor_xml() -> str:
items = []
for fid, (xm, ym) in LAYOUT_M.items():
gx, gy = m_to_grid(xm, ym)
items.append(f' <FxItem ID="{fid}" XPos="{gx}" YPos="{gy}" />')
return f""" <Monitor DisplayMode="1" ShowLabels="1">
<Font>Arial,12,-1,5,50,0,0,0,0,0</Font>
<ChannelStyle>0</ChannelStyle>
<ValueStyle>0</ValueStyle>
<Grid Width="{GRID_W}" Height="{GRID_H}" Depth="5" Units="0" />
{chr(10).join(items)}
</Monitor>"""
def main():
path = Path(__file__).resolve().parents[1] / "shows" / "xchc.qxw"
text = path.read_text()
for i, (name, rgb) in enumerate(COLORS.items()):
white_ids = set(CANS) if name == "White" else None
text = replace_function(text, i, scene(i, name, all_color(rgb), white_ids=white_ids))
for step in range(8):
lit = {}
for ci, can in enumerate(U_WALK):
lit[can] = RAINBOW[(ci + step) % 8]
lit[8] = RAINBOW[(3 + step) % 8]
lit[9] = RAINBOW[(4 + step) % 8]
text = replace_function(text, 10 + step, scene(10 + step, f"Rainbow {step + 1}", lit))
blue, red = (0, 0, 255), (255, 0, 0)
odd_blue, odd_red = {}, {}
for i in CANS:
if i % 2 == 0:
odd_blue[i], odd_red[i] = blue, red
else:
odd_blue[i], odd_red[i] = red, blue
odd_blue[8], odd_blue[9] = blue, red
odd_red[8], odd_red[9] = red, blue
text = replace_function(text, 20, scene(20, "Odd Blue Even Red", odd_blue))
text = replace_function(text, 21, scene(21, "Odd Red Even Blue", odd_red))
off = {i: None for i in list(CANS) + list(MOVERS)}
text = replace_function(text, 23, scene(23, "Test Off", off))
text = replace_function(text, 72, scene(72, "Soft Blackout", off, fade=4000))
for i, can in enumerate(U_WALK):
lit = {fid: None for fid in list(CANS) + list(MOVERS)}
lit[can] = (255, 255, 255)
text = replace_function(
text, 33 + i, scene(33 + i, f"Scanner {i + 1}", lit, white_ids={can})
)
for mid, name, ids in ((99, "Scanner Movers", {8, 9}), (100, "Scanner Off", set())):
lit = {fid: None for fid in list(CANS) + list(MOVERS)}
for m in ids:
lit[m] = (255, 255, 255)
block = scene(mid, name, lit)
if f'Function ID="{mid}"' in text:
text = replace_function(text, mid, block)
else:
text = text.replace(
' <Function ID="41" Type="Chaser" Name="Scanner">',
block + '\n <Function ID="41" Type="Chaser" Name="Scanner">',
1,
)
scanner_steps = "\n".join(
f' <Step Number="{i}" FadeIn="0" Hold="400" FadeOut="0">{sid}</Step>'
for i, sid in enumerate([*range(33, 41), 99, 100])
)
text = replace_function(
text,
41,
f""" <Function ID="41" Type="Chaser" Name="Scanner">
<Speed FadeIn="0" FadeOut="0" Duration="400" />
<Direction>Forward</Direction>
<RunOrder>Loop</RunOrder>
<SpeedModes FadeIn="Common" FadeOut="Common" Duration="Common" />
{scanner_steps}
</Function>""",
)
for n in range(1, 9):
lit = {fid: None for fid in list(CANS) + list(MOVERS)}
for can in U_WALK[:n]:
lit[can] = (255, 255, 255)
if n >= 3:
lit[8] = lit[9] = (255, 255, 255)
white = {c for c in U_WALK[:n]}
text = replace_function(
text, 41 + n, scene(41 + n, f"Fill {n}", lit, white_ids=white)
)
for i, ring in enumerate(RINGS):
lit = {fid: None for fid in list(CANS) + list(MOVERS)}
white = {f for f in ring if f in CANS}
for fid in ring:
lit[fid] = (255, 255, 255)
text = replace_function(
text, 51 + i, scene(51 + i, f"Center {i + 1}", lit, white_ids=white)
)
for i, (ids, name) in enumerate(
[
({0, 7}, "Pairs 1"),
({1, 6}, "Pairs 2"),
({2, 5}, "Pairs 3"),
({3, 4, 8, 9}, "Pairs 4"),
]
):
lit = {fid: None for fid in list(CANS) + list(MOVERS)}
white = {f for f in ids if f in CANS}
for fid in ids:
lit[fid] = (255, 255, 255)
text = replace_function(text, 56 + i, scene(56 + i, name, lit, white_ids=white))
lbrr = {fid: None for fid in list(CANS) + list(MOVERS)}
lrbr = {fid: None for fid in list(CANS) + list(MOVERS)}
for fid in LEFT:
lbrr[fid], lrbr[fid] = (0, 0, 255), (255, 0, 0)
for fid in RIGHT:
lbrr[fid], lrbr[fid] = (255, 0, 0), (0, 0, 255)
text = replace_function(text, 61, scene(61, "Left Blue Right Red", lbrr))
text = replace_function(text, 62, scene(62, "Left Red Right Blue", lrbr))
for step in range(3):
lit = {fid: None for fid in list(CANS) + list(MOVERS)}
white = set()
for can in CANS:
if can % 3 == step:
lit[can] = (255, 255, 255)
white.add(can)
if step == 0:
lit[8] = (255, 255, 255)
elif step == 1:
lit[9] = (255, 255, 255)
else:
lit[8] = lit[9] = (255, 200, 140)
text = replace_function(
text, 64 + step, scene(64 + step, f"Theater {step + 1}", lit, white_ids=white)
)
collapse_steps = "\n".join(
f' <Step Number="{i}" FadeIn="200" Hold="300" FadeOut="0">{sid}</Step>'
for i, sid in enumerate([54, 53, 52, 51, 23])
)
text = replace_function(
text,
68,
f""" <Function ID="68" Type="Chaser" Name="Collapse In">
<Speed FadeIn="200" FadeOut="0" Duration="500" />
<Direction>Forward</Direction>
<RunOrder>SingleShot</RunOrder>
<SpeedModes FadeIn="Common" FadeOut="Common" Duration="Common" />
{collapse_steps}
</Function>""",
)
chase_steps = "\n".join(
f' <Step Number="{i}" FadeIn="0" Hold="300" FadeOut="0">{sid}</Step>'
for i, sid in enumerate([*range(33, 41), 99, 23])
)
text = replace_function(
text,
69,
f""" <Function ID="69" Type="Chaser" Name="Chase Out">
<Speed FadeIn="0" FadeOut="0" Duration="400" />
<Direction>Forward</Direction>
<RunOrder>SingleShot</RunOrder>
<SpeedModes FadeIn="Common" FadeOut="Common" Duration="Common" />
{chase_steps}
</Function>""",
)
text = replace_function(
text, 70, scene(70, "Home Soft", all_color((255, 180, 80)), fade=2500)
)
text = re.sub(
r' <Monitor DisplayMode="1" ShowLabels="1">[\s\S]*?</Monitor>',
lambda _m: monitor_xml(),
text,
count=1,
)
text = text.replace("<Name>BCC145 Line</Name>", "<Name>BCC145 U</Name>", 1)
path.write_text(text)
print("Updated", path)
print("Y=0 row of 4; +Y toward audience (in front of the 4)")
for fid in sorted(LAYOUT_M):
x, y = LAYOUT_M[fid]
label = f"Can #{fid + 1}" if fid < 8 else f"Mover #{fid - 7}"
print(f" {label:10} x={x:+.1f} y={y:.1f}")
if __name__ == "__main__":
main()