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
File diff suppressed because it is too large Load Diff
+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()
+305
View File
@@ -0,0 +1,305 @@
#!/usr/bin/env python3
"""Generate shows/simple.qxw — per-mover position + shared and individual RGBW."""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "shows" / "simple.qxw"
# Fixture IDs match shows/xchc.qxw
CANS = list(range(8)) # BCC145 #1–#8
M1, M2 = 8, 9
MOVERS = [M1, M2]
# Can 8ch: Dim=0, R=2, G=3, B=4, W=5
# Mover 43ch: Center Bright=8; RGBW heads 1–7
MOVER_R = [10, 14, 18, 22, 26, 30, 34]
MOVER_G = [11, 15, 19, 23, 27, 31, 35]
MOVER_B = [12, 16, 20, 24, 28, 32, 36]
MOVER_W = [13, 17, 21, 25, 29, 33, 37]
COLORS = [
("R", 2, MOVER_R, "4294901760", "Red"),
("G", 3, MOVER_G, "4278255360", "Green"),
("B", 4, MOVER_B, "4278190335", "Blue"),
("W", 5, MOVER_W, "4294967295", "None"),
]
def appearance(
fg: str = "Default",
bg: str = "Default",
frame: str = "Sunken",
) -> str:
return f""" <Appearance>
<FrameStyle>{frame}</FrameStyle>
<ForegroundColor>{fg}</ForegroundColor>
<BackgroundColor>{bg}</BackgroundColor>
<BackgroundImage>None</BackgroundImage>
<Font>Default</Font>
</Appearance>"""
def slider(
wid: int,
caption: str,
x: int,
y: int,
w: int,
h: int,
channels: list[tuple[int, int]],
*,
value: int = 0,
fg: str = "Default",
cng: str = "None",
) -> str:
ch_xml = "\n".join(
f' <Channel Fixture="{fid}">{ch}</Channel>' for fid, ch in channels
)
return f""" <Slider Caption="{caption}" ID="{wid}" WidgetStyle="Slider" InvertedAppearance="false">
<WindowState Visible="True" X="{x}" Y="{y}" Width="{w}" Height="{h}"/>
{appearance(fg=fg)}
<SliderMode ValueDisplayStyle="Exact" ClickAndGoType="{cng}" Monitor="true">Level</SliderMode>
<Reset/>
<Level LowLimit="0" HighLimit="255" Value="{value}">
{ch_xml}
</Level>
<Playback>
<Function>4294967295</Function>
</Playback>
</Slider>"""
def xypad(wid: int, caption: str, fid: int, x: int, y: int, w: int, h: int) -> str:
return f""" <XYPad Caption="{caption}" ID="{wid}" InvertedAppearance="0">
<WindowState Visible="True" X="{x}" Y="{y}" Width="{w}" Height="{h}"/>
{appearance()}
<Fixture ID="{fid}" Head="0">
<Axis ID="X" LowLimit="0" HighLimit="1" Reverse="False"/>
<Axis ID="Y" LowLimit="0" HighLimit="1" Reverse="False"/>
</Fixture>
<Window hMin="0" hMax="255" vMin="0" vMax="255"/>
<Pan Position="128"/>
<Tilt Position="128"/>
</XYPad>"""
def label_frame(caption: str, x: int, y: int, w: int, h: int) -> str:
return f""" <Frame Caption="{caption}">
<Appearance>
<FrameStyle>Sunken</FrameStyle>
<ForegroundColor>Default</ForegroundColor>
<BackgroundColor>Default</BackgroundColor>
<BackgroundImage>None</BackgroundImage>
<Font>Default</Font>
</Appearance>
<WindowState Visible="True" X="{x}" Y="{y}" Width="{w}" Height="{h}"/>
<AllowChildren>True</AllowChildren>
<AllowResize>True</AllowResize>
<ShowHeader>True</ShowHeader>
<ShowEnableButton>False</ShowEnableButton>
<ShowIntensityButton>False</ShowIntensityButton>
<Mixing>False</Mixing>
</Frame>"""
def fixture_xml() -> str:
parts = []
for i in range(8):
parts.append(
f""" <Fixture>
<Manufacturer>beamZ</Manufacturer>
<Model>BCC145</Model>
<Mode>8 Channel</Mode>
<ID>{i}</ID>
<Name>BCC145 #{i + 1}</Name>
<Universe>0</Universe>
<Address>{87 + i * 8}</Address>
<Channels>8</Channels>
</Fixture>"""
)
for i, addr in enumerate((0, 44)):
parts.append(
f""" <Fixture>
<Manufacturer>Small Eye 7</Manufacturer>
<Model>Mover</Model>
<Mode>43 Channel</Mode>
<ID>{8 + i}</ID>
<Name>Small Eye 7 #{i + 1}</Name>
<Universe>0</Universe>
<Address>{addr}</Address>
<Channels>43</Channels>
</Fixture>"""
)
return "\n".join(parts)
def main() -> None:
pad_w, pad_h = 240, 240
sl_w, sl_h = 44, 240
ind_w, ind_h = 36, 160
gap = 8
widgets: list[str] = []
wid = 0
max_x = 0
max_y = 0
def place_slider(caption, x, y, w, h, channels, value=0, fg="Default", cng="None"):
nonlocal wid, max_x, max_y
widgets.append(
slider(wid, caption, x, y, w, h, channels, value=value, fg=fg, cng=cng)
)
wid += 1
max_x = max(max_x, x + w)
max_y = max(max_y, y + h)
y0 = 28
x = 10
widgets.append(xypad(wid, "M1 Position", M1, x, y0, pad_w, pad_h))
wid += 1
x += pad_w + gap
widgets.append(xypad(wid, "M2 Position", M2, x, y0, pad_w, pad_h))
wid += 1
x += pad_w + gap + 12
max_x = max(max_x, x)
max_y = max(max_y, y0 + pad_h)
place_slider("C Dim", x, y0, sl_w, sl_h, [(f, 0) for f in CANS], 255, "4294967295")
x += sl_w + 2
for name, can_ch, _mover_chs, fg, cng in COLORS:
place_slider(
f"C {name}",
x,
y0,
sl_w,
sl_h,
[(f, can_ch) for f in CANS],
fg=fg,
cng=cng,
)
x += sl_w + 2
x += 14
place_slider("M Dim", x, y0, sl_w, sl_h, [(f, 8) for f in MOVERS], 255, "4294967295")
x += sl_w + 2
for name, _can_ch, mover_chs, fg, cng in COLORS:
place_slider(
f"M {name}",
x,
y0,
sl_w,
sl_h,
[(f, c) for f in MOVERS for c in mover_chs],
fg=fg,
cng=cng,
)
x += sl_w + 2
y1 = y0 + pad_h + 36
x = 10
widgets.append(label_frame("Cans individual RGBW", x, y1 - 28, 200, 24))
for i, fid in enumerate(CANS):
col_x = x + i * (4 * (ind_w + 2) + 10)
for j, (name, can_ch, _m, fg, cng) in enumerate(COLORS):
place_slider(
f"{i + 1}{name}",
col_x + j * (ind_w + 2),
y1,
ind_w,
ind_h,
[(fid, can_ch)],
fg=fg,
cng=cng,
)
mx = x + 8 * (4 * (ind_w + 2) + 10) + 16
widgets.append(label_frame("Movers individual RGBW", mx, y1 - 28, 220, 24))
for mi, (fid, label) in enumerate(((M1, "M1"), (M2, "M2"))):
col_x = mx + mi * (4 * (ind_w + 2) + 14)
for j, (name, _can_ch, mover_chs, fg, cng) in enumerate(COLORS):
place_slider(
f"{label}{name}",
col_x + j * (ind_w + 2),
y1,
ind_w,
ind_h,
[(fid, c) for c in mover_chs],
fg=fg,
cng=cng,
)
total_w = max_x + 16
total_h = max_y + 16
qxw = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Workspace>
<Workspace xmlns="http://www.qlcplus.org/Workspace" CurrentWindow="VirtualConsole">
<Creator>
<Name>Q Light Controller Plus</Name>
<Version>4.12.7</Version>
<Author>Jimmy</Author>
</Creator>
<Engine>
<InputOutputMap>
<Universe Name="Universe 1" ID="0">
<Output Plugin="ArtNet" UID="192.168.1.92" Line="7">
<PluginParameters outputIP="192.168.1.228" outputUni="0"/>
</Output>
</Universe>
<Universe Name="Universe 2" ID="1"/>
<Universe Name="Universe 3" ID="2"/>
<Universe Name="Universe 4" ID="3"/>
</InputOutputMap>
{fixture_xml()}
<FixtureGroup ID="0">
<Name>BCC145 U</Name>
<Size X="8" Y="1"/>
<Head X="0" Y="0" Fixture="0">0</Head>
<Head X="1" Y="0" Fixture="1">0</Head>
<Head X="2" Y="0" Fixture="2">0</Head>
<Head X="3" Y="0" Fixture="3">0</Head>
<Head X="4" Y="0" Fixture="4">0</Head>
<Head X="5" Y="0" Fixture="5">0</Head>
<Head X="6" Y="0" Fixture="6">0</Head>
<Head X="7" Y="0" Fixture="7">0</Head>
</FixtureGroup>
<FixtureGroup ID="1">
<Name>Small Eye Movers</Name>
<Size X="2" Y="1"/>
<Head X="0" Y="0" Fixture="8">0</Head>
<Head X="1" Y="0" Fixture="9">0</Head>
</FixtureGroup>
</Engine>
<VirtualConsole>
<Frame Caption="">
<Appearance>
<FrameStyle>None</FrameStyle>
<ForegroundColor>Default</ForegroundColor>
<BackgroundColor>Default</BackgroundColor>
<BackgroundImage>None</BackgroundImage>
<Font>Default</Font>
</Appearance>
{"\n".join(widgets)}
</Frame>
<Properties>
<Size Width="{total_w}" Height="{total_h}"/>
<GrandMaster ChannelMode="Intensity" ValueMode="Reduce" SliderMode="Normal"/>
</Properties>
</VirtualConsole>
<SimpleDesk>
<Engine/>
</SimpleDesk>
</Workspace>
"""
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(qxw)
print(f"Wrote {OUT} ({total_w}x{total_h}, {wid} widgets)")
if __name__ == "__main__":
main()
+268
View File
@@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""Merge Patterns + Movers into one SoloFrame and one Speed dial in xchc.qxw."""
from __future__ import annotations
import re
from pathlib import Path
def btn(
widget_id: int,
caption: str,
func_id: int,
x: int,
y: int,
w: int,
h: int,
bg: str,
fg: str = "4294967295",
) -> str:
return f""" <Button Caption="{caption}" ID="{widget_id}" Icon="">
<WindowState Visible="True" X="{x}" Y="{y}" Width="{w}" Height="{h}"/>
<Appearance>
<FrameStyle>None</FrameStyle>
<ForegroundColor>{fg}</ForegroundColor>
<BackgroundColor>{bg}</BackgroundColor>
<BackgroundImage>None</BackgroundImage>
<Font>Default</Font>
</Appearance>
<Function ID="{func_id}"/>
<Action>Toggle</Action>
<Intensity Adjust="False">100</Intensity>
</Button>"""
ROOT = Path(__file__).resolve().parents[1]
def main():
path = ROOT / "shows" / "xchc.qxw"
text = path.read_text()
# Combined looks: can colour chase + figure-of-8 movement
combos = [
(101, "Colour + F8", 9, 77),
(102, "Rainbow + F8", 18, 77),
(103, "OddEven + F8", 22, 77),
(104, "Center + F8", 55, 77),
(105, "Theater + F8", 67, 77),
]
combo_xml = []
for fid, name, a, b in combos:
block = f""" <Function ID="{fid}" Type="Collection" Name="{name}">
<Step Number="0">{a}</Step>
<Step Number="1">{b}</Step>
</Function>
"""
if f'Function ID="{fid}"' in text:
text = re.sub(
rf' <Function ID="{fid}"[\s\S]*?</Function>\n',
block,
text,
count=1,
)
else:
combo_xml.append(block.rstrip())
if combo_xml:
text = text.replace(
' <Monitor DisplayMode="1"',
"\n".join(combo_xml) + "\n <Monitor DisplayMode=\"1\"",
1,
)
# Build unified Show SoloFrame
# Row 0: colour / spatial patterns (cans+movers already in scenes)
# Row 1: combined flight looks
# Row 2: mover-only + exits
w, h = 150, 56
gap_x, gap_y = 8, 8
x0, y0 = 10, 28
def cell(col, row):
return x0 + col * (w + gap_x), y0 + row * (h + gap_y)
buttons = []
# Row 0 — parcans + movers colour/spatial
row0 = [
(0, "Colour", 9, "4282664004"),
(28, "Rainbow", 18, "4280427042"),
(37, "Odd Even", 22, "4284752486"),
(47, "Scanner", 41, "4292033564"),
(48, "Fill", 50, "4278218076"),
(49, "Center Out", 55, "4280817043"),
]
for i, (wid, cap, fid, bg) in enumerate(row0):
x, y = cell(i, 0)
buttons.append(btn(wid, cap, fid, x, y, w, h, bg))
row1 = [
(50, "Pairs", 60, "4294946560", "4278190080"),
(51, "Left Right", 63, "4279648704", "4294967295"),
(52, "Theater", 67, "4285143962", "4294967295"),
(200, "Colour+F8", 101, "4282664004", "4294967295"),
(201, "Rainbow+F8", 102, "4280427042", "4294967295"),
(202, "Center+F8", 104, "4280817043", "4294967295"),
]
for i, (wid, cap, fid, bg, fg) in enumerate(row1):
x, y = cell(i, 1)
buttons.append(btn(wid, cap, fid, x, y, w, h, bg, fg))
row2 = [
(100, "Figure of 8", 77, "4282664004"),
(203, "OddEven+F8", 103, "4284752486"),
(204, "Theater+F8", 105, "4285143962"),
(53, "Collapse In", 68, "4281545523"),
(54, "Chase Out", 69, "4284900966"),
(55, "Home Fade", 71, "4286611584"),
]
for i, (wid, cap, fid, bg) in enumerate(row2):
x, y = cell(i, 2)
buttons.append(btn(wid, cap, fid, x, y, w, h, bg))
# Home Fade for whole stage uses can Home Fade 71; keep mover Home Fade via MIB/Soft BO
row3 = [
(101, "Ring Collapse", 85, "4281545523"),
(114, "Ring Chase", 94, "4284900966"),
(116, "Move in Black", 98, "4279900698"),
(56, "Soft Blackout", 72, "4278190080"),
(38, "Red Test", 32, "4286578688"),
]
for i, (wid, cap, fid, bg) in enumerate(row3):
x, y = cell(i, 3)
buttons.append(btn(wid, cap, fid, x, y, w, h, bg))
frame_w = x0 + 6 * (w + gap_x) + 10
frame_h = y0 + 4 * (h + gap_y) + 16
show_frame = f""" <SoloFrame Caption="Show">
<Appearance>
<FrameStyle>Sunken</FrameStyle>
<ForegroundColor>Default</ForegroundColor>
<BackgroundColor>Default</BackgroundColor>
<BackgroundImage>None</BackgroundImage>
<Font>Default</Font>
</Appearance>
<WindowState Visible="True" X="10" Y="10" Width="{frame_w}" Height="{frame_h}"/>
<AllowChildren>True</AllowChildren>
<AllowResize>True</AllowResize>
<ShowHeader>True</ShowHeader>
<Mixing>False</Mixing>
{chr(10).join(buttons)}
</SoloFrame>"""
# Replace Patterns SoloFrame
text = re.sub(
r' <SoloFrame Caption="Patterns">[\s\S]*?</SoloFrame>',
show_frame,
text,
count=1,
)
# Remove Movers SoloFrame (buttons now in Show)
text = re.sub(
r' <SoloFrame Caption="Movers">[\s\S]*?</SoloFrame>\n',
"",
text,
count=1,
)
# Merge mover speed targets into main Speed dial; drop Mover Speed dial
text = re.sub(
r' <SpeedDial Caption="Mover Speed" ID="102">[\s\S]*?</SpeedDial>\n',
"",
text,
count=1,
)
speed_extra = """ <Function FadeIn="6" FadeOut="0" Duration="6">74</Function>
<Function FadeIn="0" FadeOut="0" Duration="6">76</Function>
<Function FadeIn="6" FadeOut="0" Duration="6">101</Function>
<Function FadeIn="6" FadeOut="0" Duration="6">102</Function>
<Function FadeIn="6" FadeOut="0" Duration="6">103</Function>
<Function FadeIn="6" FadeOut="0" Duration="6">104</Function>
<Function FadeIn="0" FadeOut="0" Duration="6">105</Function>
"""
if 'Function FadeIn="6" FadeOut="0" Duration="6">74</Function>' not in text:
text = text.replace(
' <Function FadeIn="0" FadeOut="0" Duration="6">69</Function>\n </SpeedDial>',
' <Function FadeIn="0" FadeOut="0" Duration="6">69</Function>\n'
+ speed_extra
+ " </SpeedDial>",
1,
)
# Make Speed dial visible next to Show
text = text.replace(
'<SpeedDial Caption="Speed" ID="3">\n <WindowState Visible="False" X="720" Y="20" Width="90" Height="250"/>',
f'<SpeedDial Caption="Speed" ID="3">\n <WindowState Visible="True" X="{frame_w + 20}" Y="10" Width="100" Height="{frame_h}"/>',
1,
)
# All Dim also drives mover dimmers
if "<Channel Fixture=\"8\">8</Channel>" not in text.split('Caption="All Dim"', 1)[1][:800]:
text = text.replace(
""" <Level LowLimit="0" HighLimit="255" Value="174">
<Channel Fixture="0">0</Channel>
<Channel Fixture="1">0</Channel>
<Channel Fixture="2">0</Channel>
<Channel Fixture="3">0</Channel>
<Channel Fixture="4">0</Channel>
<Channel Fixture="5">0</Channel>
<Channel Fixture="6">0</Channel>
<Channel Fixture="7">0</Channel>
</Level>
<Playback>
<Function>4294967295</Function>
</Playback>
</Slider>
<Slider Caption="1 R\"""",
""" <Level LowLimit="0" HighLimit="255" Value="174">
<Channel Fixture="0">0</Channel>
<Channel Fixture="1">0</Channel>
<Channel Fixture="2">0</Channel>
<Channel Fixture="3">0</Channel>
<Channel Fixture="4">0</Channel>
<Channel Fixture="5">0</Channel>
<Channel Fixture="6">0</Channel>
<Channel Fixture="7">0</Channel>
<Channel Fixture="8">8</Channel>
<Channel Fixture="9">8</Channel>
</Level>
<Playback>
<Function>4294967295</Function>
</Playback>
</Slider>
<Slider Caption="1 R\"""",
1,
)
# Relabel / place XY pad under Show
text = text.replace(
'Caption="Mover Position"',
'Caption="Position"',
1,
)
text = text.replace(
'<XYPad Caption="Position" ID="103" InvertedAppearance="0">\n <WindowState Visible="True" X="720" Y="180" Width="320" Height="320"/>',
f'<XYPad Caption="Position" ID="103" InvertedAppearance="0">\n <WindowState Visible="True" X="10" Y="{frame_h + 20}" Width="320" Height="300"/>',
1,
)
# Shift mover sliders under Show, right of XY pad
slider_y = frame_h + 20
old_xs = [1050 + i * 46 for i in range(10)]
new_xs = [340 + i * 46 for i in range(10)]
for ox, nx in zip(old_xs, new_xs):
text = text.replace(
f'Visible="True" X="{ox}" Y="180" Width="44" Height="320"',
f'Visible="True" X="{nx}" Y="{slider_y}" Width="44" Height="300"',
1,
)
path.write_text(text)
print(f"Unified Show controls in {path}")
print(f" SoloFrame {frame_w}x{frame_h} with pattern + F8 combos + exits")
print(" One Speed dial; Position pad + mover sliders below")
if __name__ == "__main__":
main()