Files
jimmyandCursor 5eb623d6af 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>
2026-09-08 00:59:36 +12:00

269 lines
8.8 KiB
Python

#!/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()