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
+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()