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:
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user