Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f08d44188 | ||
|
|
f069a8091b |
@@ -0,0 +1,837 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate shows/cabaret.qxw — grouped can RGB + per-mover position and RGB."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
OUT = ROOT / "shows" / "cabaret.qxw"
|
||||||
|
|
||||||
|
M1, M2 = 8, 9
|
||||||
|
|
||||||
|
# Can groups: 1-2 | 3-4-5-6 | 7-8
|
||||||
|
GROUP_L = [0, 1]
|
||||||
|
GROUP_C = [2, 3, 4, 5]
|
||||||
|
GROUP_R = [6, 7]
|
||||||
|
|
||||||
|
# Can 8ch: Dim=0, R=2, G=3, B=4
|
||||||
|
# Mover 43ch: Pan/Tilt Speed=4, Focus=7, Center Bright=8; RGB heads 1–7
|
||||||
|
MOVER_SPEED = 4
|
||||||
|
MOVER_FOCUS = 7
|
||||||
|
MOVER_DIM = 8
|
||||||
|
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]
|
||||||
|
FOCUS_DEFAULT = 70
|
||||||
|
CANS = list(range(8))
|
||||||
|
|
||||||
|
PALETTE: dict[str, tuple[int, int, int]] = {
|
||||||
|
"Amber": (255, 90, 0),
|
||||||
|
"Pink": (255, 50, 160),
|
||||||
|
"HotBlue": (round(0.30 * 255), 0, 255), # 30% red, 100% blue
|
||||||
|
"Red": (255, 0, 0),
|
||||||
|
"Green": (0, 255, 0),
|
||||||
|
"Blue": (0, 0, 255),
|
||||||
|
"Orange": (255, 127, 0),
|
||||||
|
"Yellow": (255, 255, 0),
|
||||||
|
"Purple": (127, 0, 255),
|
||||||
|
}
|
||||||
|
|
||||||
|
CAN_ODD = [0, 2, 4, 6] # #1,3,5,7
|
||||||
|
CAN_EVEN = [1, 3, 5, 7] # #2,4,6,8
|
||||||
|
MOVER_SOLIDS = [
|
||||||
|
"Amber",
|
||||||
|
"Pink",
|
||||||
|
"Red",
|
||||||
|
"Green",
|
||||||
|
"Blue",
|
||||||
|
"Orange",
|
||||||
|
"Yellow",
|
||||||
|
"Purple",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Pad display values. M2 (left) is hung 180°, so its pad pan is reversed:
|
||||||
|
# DMX pan = 255 - pad pan. M1 pad pan is DMX pan.
|
||||||
|
PAD_PAN = {M2: 181, M1: 80}
|
||||||
|
PAD_TILT = {M2: 145, M1: 161}
|
||||||
|
POSITION_FADE_MS = 2000
|
||||||
|
# name, M1 (pan, tilt), M2 (pan, tilt), button colour
|
||||||
|
# Same DMX pan = split (180° hang). Cross left was mirrored (108) = 180° out.
|
||||||
|
# Now = live pad aim (M2 pad 181/145 → DMX 74/145, M1 pad 80/161).
|
||||||
|
POSITIONS: list[tuple[str, tuple[int, int], tuple[int, int], str]] = [
|
||||||
|
("Now", (80, 161), (74, 145), "4278236360"),
|
||||||
|
("Here", (179, 0), (179, 0), "4282664004"),
|
||||||
|
("Down", (179, 36), (179, 36), "4284900966"),
|
||||||
|
("Mid", (164, 18), (144, 18), "4286611584"),
|
||||||
|
("Cross", (140, 26), (140, 26), "4281545523"),
|
||||||
|
("Out", (200, 22), (200, 22), "4279900698"),
|
||||||
|
]
|
||||||
|
POSITION_FUNC0 = 100
|
||||||
|
PADS_FUNC = POSITION_FUNC0 + len(POSITIONS)
|
||||||
|
PADS_BG = "4282800750" # #465A6E pad grey-blue
|
||||||
|
FIGURE8_FLOOR_FUNC = 110
|
||||||
|
FIGURE8_WALL_FUNC = 111
|
||||||
|
FIGURE8_MS = 12000
|
||||||
|
FIGURE8_FLOOR_BG = "4282664004"
|
||||||
|
FIGURE8_WALL_BG = "4286611584"
|
||||||
|
LOOK_PAIRS: list[tuple[str, str | None]] = [
|
||||||
|
("Amber", None),
|
||||||
|
("Pink", "Amber"),
|
||||||
|
("Pink", "HotBlue"),
|
||||||
|
("Red", "Green"),
|
||||||
|
("Blue", "Green"),
|
||||||
|
("Blue", "Red"),
|
||||||
|
("Orange", "Blue"),
|
||||||
|
("Yellow", "Purple"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def color_label(name: str) -> str:
|
||||||
|
return "Blue" if name == "HotBlue" else name
|
||||||
|
|
||||||
|
|
||||||
|
def qlc_color(r: int, g: int, b: int) -> str:
|
||||||
|
return str((0xFF << 24) | (r << 16) | (g << 8) | b)
|
||||||
|
|
||||||
|
|
||||||
|
def mix_color(
|
||||||
|
a: tuple[int, int, int], b: tuple[int, int, int]
|
||||||
|
) -> tuple[int, int, int]:
|
||||||
|
return ((a[0] + b[0]) // 2, (a[1] + b[1]) // 2, (a[2] + b[2]) // 2)
|
||||||
|
|
||||||
|
RGB = [
|
||||||
|
("R", 2, MOVER_R, "4294901760", "Red"),
|
||||||
|
("G", 3, MOVER_G, "4278255360", "Green"),
|
||||||
|
("B", 4, MOVER_B, "4278190335", "Blue"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
inverted: bool = False,
|
||||||
|
) -> str:
|
||||||
|
ch_xml = "\n".join(
|
||||||
|
f' <Channel Fixture="{fid}">{ch}</Channel>' for fid, ch in channels
|
||||||
|
)
|
||||||
|
inv = "true" if inverted else "false"
|
||||||
|
return f""" <Slider Caption="{caption}" ID="{wid}" WidgetStyle="Slider" InvertedAppearance="{inv}">
|
||||||
|
<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,
|
||||||
|
*,
|
||||||
|
reverse_pan: bool = False,
|
||||||
|
reverse_tilt: bool = False,
|
||||||
|
pan: int = 128,
|
||||||
|
tilt: int = 128,
|
||||||
|
) -> str:
|
||||||
|
pan_rev = "True" if reverse_pan else "False"
|
||||||
|
tilt_rev = "True" if reverse_tilt else "False"
|
||||||
|
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="{pan_rev}"/>
|
||||||
|
<Axis ID="Y" LowLimit="0" HighLimit="1" Reverse="{tilt_rev}"/>
|
||||||
|
</Fixture>
|
||||||
|
<Window hMin="0" hMax="255" vMin="0" vMax="255"/>
|
||||||
|
<Pan Position="{pan}"/>
|
||||||
|
<Tilt Position="{tilt}"/>
|
||||||
|
</XYPad>"""
|
||||||
|
|
||||||
|
|
||||||
|
def label_frame(wid: int, caption: str, x: int, y: int, w: int, h: int) -> str:
|
||||||
|
return f""" <Frame Caption="{caption}" ID="{wid}">
|
||||||
|
<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 button(
|
||||||
|
wid: int,
|
||||||
|
caption: str,
|
||||||
|
func_id: int,
|
||||||
|
x: int,
|
||||||
|
y: int,
|
||||||
|
w: int,
|
||||||
|
h: int,
|
||||||
|
bg: str,
|
||||||
|
fg: str = "4294967295",
|
||||||
|
*,
|
||||||
|
indent: str = " ",
|
||||||
|
) -> str:
|
||||||
|
inner = indent + " "
|
||||||
|
return f"""{indent}<Button Caption="{caption}" ID="{wid}" Icon="">
|
||||||
|
{inner}<WindowState Visible="True" X="{x}" Y="{y}" Width="{w}" Height="{h}"/>
|
||||||
|
{inner}<Appearance>
|
||||||
|
{inner} <FrameStyle>None</FrameStyle>
|
||||||
|
{inner} <ForegroundColor>{fg}</ForegroundColor>
|
||||||
|
{inner} <BackgroundColor>{bg}</BackgroundColor>
|
||||||
|
{inner} <BackgroundImage>None</BackgroundImage>
|
||||||
|
{inner} <Font>Default</Font>
|
||||||
|
{inner}</Appearance>
|
||||||
|
{inner}<Function ID="{func_id}"/>
|
||||||
|
{inner}<Action>Toggle</Action>
|
||||||
|
{inner}<Intensity Adjust="False">100</Intensity>
|
||||||
|
{indent}</Button>"""
|
||||||
|
|
||||||
|
|
||||||
|
def solo_frame(wid: int, caption: str, x: int, y: int, w: int, h: int, children: str) -> str:
|
||||||
|
return f""" <SoloFrame Caption="{caption}" ID="{wid}">
|
||||||
|
<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>
|
||||||
|
<Mixing>False</Mixing>
|
||||||
|
{children}
|
||||||
|
</SoloFrame>"""
|
||||||
|
|
||||||
|
|
||||||
|
def can_fixture_val(fid: int, r: int, g: int, b: int) -> str:
|
||||||
|
# Dimmer is owned by the Can Bright slider (HTP).
|
||||||
|
return (
|
||||||
|
f' <FixtureVal ID="{fid}">'
|
||||||
|
f"1,0,2,{r},3,{g},4,{b},5,0,6,0,7,0"
|
||||||
|
f"</FixtureVal>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mover_fixture_val(fid: int, r: int, g: int, b: int) -> str:
|
||||||
|
# Dimmer is owned by the Mover Bright slider (HTP).
|
||||||
|
parts: list[int] = []
|
||||||
|
for i in range(7):
|
||||||
|
base = 10 + i * 4
|
||||||
|
parts.extend([base, r, base + 1, g, base + 2, b, base + 3, 0])
|
||||||
|
return f' <FixtureVal ID="{fid}">{",".join(str(x) for x in parts)}</FixtureVal>'
|
||||||
|
|
||||||
|
|
||||||
|
def look_scene_xml(
|
||||||
|
func_id: int,
|
||||||
|
name: str,
|
||||||
|
can_colors: dict[int, tuple[int, int, int]] | None = None,
|
||||||
|
mover_colors: dict[int, tuple[int, int, int]] | None = None,
|
||||||
|
) -> str:
|
||||||
|
vals: list[str] = []
|
||||||
|
if can_colors:
|
||||||
|
vals += [can_fixture_val(fid, *rgb) for fid, rgb in can_colors.items()]
|
||||||
|
if mover_colors:
|
||||||
|
vals += [mover_fixture_val(fid, *rgb) for fid, rgb in mover_colors.items()]
|
||||||
|
return f""" <Function ID="{func_id}" Type="Scene" Name="{name}">
|
||||||
|
<Speed FadeIn="0" FadeOut="0" Duration="0"/>
|
||||||
|
{chr(10).join(vals)}
|
||||||
|
</Function>"""
|
||||||
|
|
||||||
|
|
||||||
|
def look_caption(kind: str, left_n: str, right_n: str | None) -> str:
|
||||||
|
ll, rl = color_label(left_n), color_label(right_n or left_n)
|
||||||
|
if right_n is None:
|
||||||
|
return ll
|
||||||
|
if kind == "cans":
|
||||||
|
return f"1-4 {ll} 5-8 {rl}"
|
||||||
|
return f"Odd {ll} Even {rl}"
|
||||||
|
|
||||||
|
|
||||||
|
def look_values(
|
||||||
|
kind: str, left: tuple[int, int, int], right: tuple[int, int, int]
|
||||||
|
) -> tuple[dict[int, tuple[int, int, int]] | None, dict[int, tuple[int, int, int]] | None]:
|
||||||
|
if kind == "cans":
|
||||||
|
return {**{i: left for i in range(4)}, **{i: right for i in range(4, 8)}}, None
|
||||||
|
if kind == "cans_oe":
|
||||||
|
return {**{i: left for i in CAN_ODD}, **{i: right for i in CAN_EVEN}}, None
|
||||||
|
raise ValueError(kind)
|
||||||
|
|
||||||
|
|
||||||
|
def build_looks() -> tuple[str, dict[str, list[tuple[str, int, str]]]]:
|
||||||
|
specs: dict[str, list[tuple[str, int, str]]] = {
|
||||||
|
"cans": [],
|
||||||
|
"movers": [],
|
||||||
|
"cans_oe": [],
|
||||||
|
}
|
||||||
|
scenes: list[str] = []
|
||||||
|
func_id = 0
|
||||||
|
for kind in ("cans", "cans_oe"):
|
||||||
|
for left_n, right_n in LOOK_PAIRS:
|
||||||
|
if kind == "cans_oe" and right_n is None:
|
||||||
|
continue
|
||||||
|
left = PALETTE[left_n]
|
||||||
|
right = PALETTE[right_n] if right_n else left
|
||||||
|
cans, movers = look_values(kind, left, right)
|
||||||
|
if right_n is None:
|
||||||
|
cans = {fid: left for fid in CANS}
|
||||||
|
movers = None
|
||||||
|
ll, rl = color_label(left_n), color_label(right_n or left_n)
|
||||||
|
name = f"{kind} {ll}" if right_n is None else f"{kind} {ll} {rl}"
|
||||||
|
scenes.append(look_scene_xml(func_id, name, cans, movers))
|
||||||
|
bg = qlc_color(*left) if right_n is None else qlc_color(*mix_color(left, right))
|
||||||
|
specs[kind].append((look_caption(kind, left_n, right_n), func_id, bg))
|
||||||
|
func_id += 1
|
||||||
|
for color_n in MOVER_SOLIDS:
|
||||||
|
rgb = PALETTE[color_n]
|
||||||
|
label = color_label(color_n)
|
||||||
|
scenes.append(
|
||||||
|
look_scene_xml(
|
||||||
|
func_id,
|
||||||
|
f"movers {label}",
|
||||||
|
mover_colors={M1: rgb, M2: rgb},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
specs["movers"].append((label, func_id, qlc_color(*rgb)))
|
||||||
|
func_id += 1
|
||||||
|
return "\n".join(scenes), specs
|
||||||
|
|
||||||
|
|
||||||
|
def look_scenes_xml() -> str:
|
||||||
|
xml, _ = build_looks()
|
||||||
|
return xml
|
||||||
|
|
||||||
|
|
||||||
|
def position_scene_xml() -> str:
|
||||||
|
blocks = [
|
||||||
|
f""" <Function ID="{PADS_FUNC}" Type="Scene" Name="Mover Pads">
|
||||||
|
<Speed FadeIn="0" FadeOut="0" Duration="0"/>
|
||||||
|
</Function>"""
|
||||||
|
]
|
||||||
|
for i, (name, m1, m2, _bg) in enumerate(POSITIONS):
|
||||||
|
poses = {M1: m1, M2: m2}
|
||||||
|
vals = []
|
||||||
|
for fid in (M1, M2):
|
||||||
|
pan, tilt = poses[fid]
|
||||||
|
vals.append(
|
||||||
|
f' <FixtureVal ID="{fid}">0,{pan},1,0,2,{tilt},3,0</FixtureVal>'
|
||||||
|
)
|
||||||
|
blocks.append(
|
||||||
|
f""" <Function ID="{POSITION_FUNC0 + i}" Type="Scene" Name="Mover {name}">
|
||||||
|
<Speed FadeIn="{POSITION_FADE_MS}" FadeOut="0" Duration="{POSITION_FADE_MS}"/>
|
||||||
|
{chr(10).join(vals)}
|
||||||
|
</Function>"""
|
||||||
|
)
|
||||||
|
return "\n".join(blocks)
|
||||||
|
|
||||||
|
|
||||||
|
def position_button_specs() -> list[tuple[str, int, str]]:
|
||||||
|
return (
|
||||||
|
[("Pads", PADS_FUNC, PADS_BG)]
|
||||||
|
+ [
|
||||||
|
(name, POSITION_FUNC0 + i, bg)
|
||||||
|
for i, (name, _m1, _m2, bg) in enumerate(POSITIONS)
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
("Floor 8", FIGURE8_FLOOR_FUNC, FIGURE8_FLOOR_BG),
|
||||||
|
("Wall 8", FIGURE8_WALL_FUNC, FIGURE8_WALL_BG),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def figure8_floor_center() -> tuple[int, int]:
|
||||||
|
"""Now is the live floor aim; average both heads so the 8 stays on the ground."""
|
||||||
|
now = next((m1, m2) for name, m1, m2, _bg in POSITIONS if name == "Now")
|
||||||
|
pans = [now[0][0], now[1][0]]
|
||||||
|
tilts = [now[0][1], now[1][1]]
|
||||||
|
return (pans[0] + pans[1]) // 2, (tilts[0] + tilts[1]) // 2
|
||||||
|
|
||||||
|
|
||||||
|
def figure8_wall_bounds() -> tuple[int, int, int, int]:
|
||||||
|
"""Pan/tilt centre and amplitude covering Here–Out (the wall looks)."""
|
||||||
|
pans: list[int] = []
|
||||||
|
tilts: list[int] = []
|
||||||
|
for name, m1, m2, _bg in POSITIONS:
|
||||||
|
if name == "Now":
|
||||||
|
continue
|
||||||
|
for pan, tilt in (m1, m2):
|
||||||
|
pans.append(pan)
|
||||||
|
tilts.append(tilt)
|
||||||
|
p0, p1 = min(pans), max(pans)
|
||||||
|
t0, t1 = min(tilts), max(tilts)
|
||||||
|
return (p0 + p1) // 2, (t0 + t1) // 2, (p1 - p0) // 2, (t1 - t0) // 2
|
||||||
|
|
||||||
|
|
||||||
|
def figure8_efx_block(
|
||||||
|
func_id: int,
|
||||||
|
name: str,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
rotation: int,
|
||||||
|
pan_c: int,
|
||||||
|
tilt_c: int,
|
||||||
|
) -> str:
|
||||||
|
fixtures = "\n".join(
|
||||||
|
f""" <Fixture>
|
||||||
|
<ID>{fid}</ID>
|
||||||
|
<Direction>Forward</Direction>
|
||||||
|
<Intensity>255</Intensity>
|
||||||
|
</Fixture>"""
|
||||||
|
for fid in (M1, M2)
|
||||||
|
)
|
||||||
|
return f""" <Function ID="{func_id}" Type="EFX" Name="{name}">
|
||||||
|
{fixtures}
|
||||||
|
<PropagationMode>Parallel</PropagationMode>
|
||||||
|
<Speed Duration="{FIGURE8_MS}" FadeOut="0" FadeIn="0"/>
|
||||||
|
<Direction>Forward</Direction>
|
||||||
|
<RunOrder>Loop</RunOrder>
|
||||||
|
<Algorithm>Eight</Algorithm>
|
||||||
|
<Width>{width}</Width>
|
||||||
|
<Height>{height}</Height>
|
||||||
|
<Rotation>{rotation}</Rotation>
|
||||||
|
<Axis Name="X">
|
||||||
|
<Offset>{pan_c}</Offset>
|
||||||
|
<Frequency>1</Frequency>
|
||||||
|
<Phase>0</Phase>
|
||||||
|
</Axis>
|
||||||
|
<Axis Name="Y">
|
||||||
|
<Offset>{tilt_c}</Offset>
|
||||||
|
<Frequency>1</Frequency>
|
||||||
|
<Phase>0</Phase>
|
||||||
|
</Axis>
|
||||||
|
</Function>"""
|
||||||
|
|
||||||
|
|
||||||
|
def figure8_efx_xml() -> str:
|
||||||
|
floor_pan, floor_tilt = figure8_floor_center()
|
||||||
|
wall_pan, wall_tilt, wall_pan_amp, wall_tilt_amp = figure8_wall_bounds()
|
||||||
|
# Floor 8: around Now, tilt stays on the ground. Rotation 90 = upright 8.
|
||||||
|
# Wall 8: Here–Out box (tilt 0–36). Rotation 0 = 8 across the wall.
|
||||||
|
return "\n".join(
|
||||||
|
[
|
||||||
|
figure8_efx_block(
|
||||||
|
FIGURE8_FLOOR_FUNC,
|
||||||
|
"Figure of 8 Floor",
|
||||||
|
16,
|
||||||
|
18,
|
||||||
|
90,
|
||||||
|
floor_pan,
|
||||||
|
floor_tilt,
|
||||||
|
),
|
||||||
|
figure8_efx_block(
|
||||||
|
FIGURE8_WALL_FUNC,
|
||||||
|
"Figure of 8 Walls",
|
||||||
|
wall_pan_amp,
|
||||||
|
wall_tilt_amp,
|
||||||
|
0,
|
||||||
|
wall_pan,
|
||||||
|
wall_tilt,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 fixture_groups_xml() -> str:
|
||||||
|
def heads(fids: list[int]) -> str:
|
||||||
|
return "\n".join(
|
||||||
|
f' <Head X="{i}" Y="0" Fixture="{fid}">0</Head>'
|
||||||
|
for i, fid in enumerate(fids)
|
||||||
|
)
|
||||||
|
|
||||||
|
return f""" <FixtureGroup ID="0">
|
||||||
|
<Name>Cans 1-2</Name>
|
||||||
|
<Size X="2" Y="1"/>
|
||||||
|
{heads(GROUP_L)}
|
||||||
|
</FixtureGroup>
|
||||||
|
<FixtureGroup ID="1">
|
||||||
|
<Name>Cans 3-6</Name>
|
||||||
|
<Size X="4" Y="1"/>
|
||||||
|
{heads(GROUP_C)}
|
||||||
|
</FixtureGroup>
|
||||||
|
<FixtureGroup ID="2">
|
||||||
|
<Name>Cans 7-8</Name>
|
||||||
|
<Size X="2" Y="1"/>
|
||||||
|
{heads(GROUP_R)}
|
||||||
|
</FixtureGroup>
|
||||||
|
<FixtureGroup ID="3">
|
||||||
|
<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>"""
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
pad_w, pad_h = 240, 240
|
||||||
|
sl_w, sl_h = 44, 240
|
||||||
|
gap = 8
|
||||||
|
block_gap = 28
|
||||||
|
|
||||||
|
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", inverted=False
|
||||||
|
):
|
||||||
|
nonlocal wid, max_x, max_y
|
||||||
|
widgets.append(
|
||||||
|
slider(
|
||||||
|
wid,
|
||||||
|
caption,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
channels,
|
||||||
|
value=value,
|
||||||
|
fg=fg,
|
||||||
|
cng=cng,
|
||||||
|
inverted=inverted,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
wid += 1
|
||||||
|
max_x = max(max_x, x + w)
|
||||||
|
max_y = max(max_y, y + h)
|
||||||
|
|
||||||
|
def place_rgb_block(prefix: str, x: int, y: int, dim_chs, rgb_chs_for):
|
||||||
|
"""Dim + R G B. rgb_chs_for(name, can_ch, mover_chs) -> channel list."""
|
||||||
|
place_slider(
|
||||||
|
f"{prefix} Dim", x, y, sl_w, sl_h, dim_chs, 0, "4294967295"
|
||||||
|
)
|
||||||
|
x += sl_w + 2
|
||||||
|
for name, can_ch, mover_chs, fg, cng in RGB:
|
||||||
|
place_slider(
|
||||||
|
f"{prefix} {name}",
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
sl_w,
|
||||||
|
sl_h,
|
||||||
|
rgb_chs_for(name, can_ch, mover_chs),
|
||||||
|
fg=fg,
|
||||||
|
cng=cng,
|
||||||
|
)
|
||||||
|
x += sl_w + 2
|
||||||
|
return x
|
||||||
|
|
||||||
|
def place_mover(prefix: str, fid: int, x: int, y: int, *, reverse_pan: bool = False) -> int:
|
||||||
|
nonlocal wid, max_y
|
||||||
|
widgets.append(
|
||||||
|
xypad(
|
||||||
|
wid,
|
||||||
|
f"{prefix} Position",
|
||||||
|
fid,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
pad_w,
|
||||||
|
pad_h,
|
||||||
|
reverse_pan=reverse_pan,
|
||||||
|
pan=PAD_PAN[fid],
|
||||||
|
tilt=PAD_TILT[fid],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
wid += 1
|
||||||
|
max_y = max(max_y, y + pad_h)
|
||||||
|
return x + pad_w + gap
|
||||||
|
|
||||||
|
def place_looks(
|
||||||
|
title: str,
|
||||||
|
looks: list[tuple[str, int, str]],
|
||||||
|
x: int,
|
||||||
|
y: int,
|
||||||
|
*,
|
||||||
|
cols: int = 2,
|
||||||
|
btn_w: int = 156,
|
||||||
|
) -> int:
|
||||||
|
nonlocal wid, max_x, max_y
|
||||||
|
header_h = 28
|
||||||
|
pad = 4
|
||||||
|
gap_x, gap_y = 6, 6
|
||||||
|
n = len(looks)
|
||||||
|
cols = max(1, min(cols, n))
|
||||||
|
rows = max(1, (n + cols - 1) // cols)
|
||||||
|
frame_w = pad * 2 + cols * btn_w + (cols - 1) * gap_x
|
||||||
|
frame_h = header_h + sl_h
|
||||||
|
inner_h = sl_h - pad
|
||||||
|
btn_h = (inner_h - gap_y * (rows - 1)) // rows
|
||||||
|
look_btns = []
|
||||||
|
frame_id = wid
|
||||||
|
wid += 1
|
||||||
|
for i, (caption, func_id, bg) in enumerate(looks):
|
||||||
|
col, row = i % cols, i // cols
|
||||||
|
look_btns.append(
|
||||||
|
button(
|
||||||
|
wid,
|
||||||
|
caption,
|
||||||
|
func_id,
|
||||||
|
pad + col * (btn_w + gap_x),
|
||||||
|
header_h + pad + row * (btn_h + gap_y),
|
||||||
|
btn_w,
|
||||||
|
btn_h,
|
||||||
|
bg,
|
||||||
|
indent=" ",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
wid += 1
|
||||||
|
widgets.append(
|
||||||
|
solo_frame(
|
||||||
|
frame_id,
|
||||||
|
title,
|
||||||
|
x,
|
||||||
|
y - header_h,
|
||||||
|
frame_w,
|
||||||
|
frame_h,
|
||||||
|
"\n".join(look_btns),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
max_x = max(max_x, x + frame_w)
|
||||||
|
max_y = max(max_y, y - header_h + frame_h)
|
||||||
|
return x + frame_w
|
||||||
|
|
||||||
|
_, look_specs = build_looks()
|
||||||
|
|
||||||
|
y0 = 28
|
||||||
|
x = 10
|
||||||
|
# M2 on the left, M1 on the right. Left unit is hung opposite, so pan is reversed.
|
||||||
|
x = place_mover("M2", M2, x, y0, reverse_pan=True)
|
||||||
|
x += block_gap
|
||||||
|
x = place_mover("M1", M1, x, y0)
|
||||||
|
x += block_gap
|
||||||
|
place_slider(
|
||||||
|
"M Bright",
|
||||||
|
x,
|
||||||
|
y0,
|
||||||
|
sl_w,
|
||||||
|
sl_h,
|
||||||
|
[(M1, MOVER_DIM), (M2, MOVER_DIM)],
|
||||||
|
255,
|
||||||
|
"4294967295",
|
||||||
|
)
|
||||||
|
x += sl_w + 2
|
||||||
|
place_slider(
|
||||||
|
"M Focus",
|
||||||
|
x,
|
||||||
|
y0,
|
||||||
|
sl_w,
|
||||||
|
sl_h,
|
||||||
|
[(M1, MOVER_FOCUS), (M2, MOVER_FOCUS)],
|
||||||
|
FOCUS_DEFAULT,
|
||||||
|
"4294902015",
|
||||||
|
)
|
||||||
|
x += sl_w + 2
|
||||||
|
place_slider(
|
||||||
|
"Speed",
|
||||||
|
x,
|
||||||
|
y0,
|
||||||
|
sl_w,
|
||||||
|
sl_h,
|
||||||
|
[(M1, MOVER_SPEED), (M2, MOVER_SPEED)],
|
||||||
|
0,
|
||||||
|
"4294967295",
|
||||||
|
inverted=True,
|
||||||
|
)
|
||||||
|
x += sl_w + 2
|
||||||
|
for name, _can_ch, mover_chs, fg, cng in RGB:
|
||||||
|
place_slider(
|
||||||
|
f"M {name}",
|
||||||
|
x,
|
||||||
|
y0,
|
||||||
|
sl_w,
|
||||||
|
sl_h,
|
||||||
|
[(fid, c) for fid in (M1, M2) for c in mover_chs],
|
||||||
|
fg=fg,
|
||||||
|
cng=cng,
|
||||||
|
)
|
||||||
|
x += sl_w + 2
|
||||||
|
x += block_gap - 2
|
||||||
|
x = place_looks(
|
||||||
|
"Position",
|
||||||
|
position_button_specs(),
|
||||||
|
x,
|
||||||
|
y0,
|
||||||
|
cols=2,
|
||||||
|
btn_w=72,
|
||||||
|
)
|
||||||
|
x += block_gap
|
||||||
|
x = place_looks("Movers", look_specs["movers"], x, y0)
|
||||||
|
|
||||||
|
y1 = y0 + pad_h + 40
|
||||||
|
x = 10
|
||||||
|
groups = [
|
||||||
|
("1-2", GROUP_L),
|
||||||
|
("3-6", GROUP_C),
|
||||||
|
("7-8", GROUP_R),
|
||||||
|
]
|
||||||
|
for label, fids in groups:
|
||||||
|
block_w = 4 * sl_w + 3 * 2
|
||||||
|
widgets.append(label_frame(wid, f"Cans {label}", x, y1 - 28, block_w, 24))
|
||||||
|
wid += 1
|
||||||
|
x = place_rgb_block(
|
||||||
|
label,
|
||||||
|
x,
|
||||||
|
y1,
|
||||||
|
[(f, 0) for f in fids],
|
||||||
|
lambda _n, can_ch, _m, fids=fids: [(f, can_ch) for f in fids],
|
||||||
|
)
|
||||||
|
x += block_gap
|
||||||
|
|
||||||
|
place_slider(
|
||||||
|
"C Bright",
|
||||||
|
x,
|
||||||
|
y1,
|
||||||
|
sl_w,
|
||||||
|
sl_h,
|
||||||
|
[(f, 0) for f in CANS],
|
||||||
|
255,
|
||||||
|
"4294967295",
|
||||||
|
)
|
||||||
|
x += sl_w + 2
|
||||||
|
place_slider(
|
||||||
|
"C Color",
|
||||||
|
x,
|
||||||
|
y1,
|
||||||
|
sl_w,
|
||||||
|
sl_h,
|
||||||
|
[(f, ch) for f in CANS for ch in (2, 3, 4)],
|
||||||
|
0,
|
||||||
|
"4294967295",
|
||||||
|
"RGB",
|
||||||
|
)
|
||||||
|
x += sl_w + block_gap
|
||||||
|
x = place_looks("Cans", look_specs["cans"], x, y1)
|
||||||
|
x += block_gap
|
||||||
|
place_looks("Cans Odd/Even", look_specs["cans_oe"], x, y1)
|
||||||
|
|
||||||
|
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()}
|
||||||
|
{fixture_groups_xml()}
|
||||||
|
{look_scenes_xml()}
|
||||||
|
{position_scene_xml()}
|
||||||
|
{figure8_efx_xml()}
|
||||||
|
</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()
|
||||||
+1473
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user