Add a cabaret QLC+ workspace for the cans and Small Eyes.
Generate the board from a script so looks, positions, and figure-8s stay in one place instead of being edited by hand in QLC+. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,832 @@
|
||||
#!/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: Focus=7, Center Bright=8; RGB heads 1–7
|
||||
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_FUNC = 110
|
||||
FIGURE8_SIDE_FUNC = 111
|
||||
FIGURE8_MS = 12000
|
||||
FIGURE8_BG = "4282664004"
|
||||
FIGURE8_SIDE_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",
|
||||
) -> 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 speed_dial(
|
||||
wid: int,
|
||||
caption: str,
|
||||
x: int,
|
||||
y: int,
|
||||
w: int,
|
||||
h: int,
|
||||
func_ids: list[int],
|
||||
*,
|
||||
time_ms: int = FIGURE8_MS,
|
||||
min_ms: int = 500,
|
||||
max_ms: int = 30000,
|
||||
) -> str:
|
||||
fn_xml = "\n".join(
|
||||
f' <Function FadeIn="0" FadeOut="0" Duration="6">{fid}</Function>'
|
||||
for fid in func_ids
|
||||
)
|
||||
return f""" <SpeedDial Caption="{caption}" ID="{wid}">
|
||||
<WindowState Visible="True" X="{x}" Y="{y}" Width="{w}" Height="{h}"/>
|
||||
{appearance()}
|
||||
<Visibility>2</Visibility>
|
||||
<AbsoluteValue Minimum="{min_ms}" Maximum="{max_ms}"/>
|
||||
<Time>{time_ms}</Time>
|
||||
{fn_xml}
|
||||
</SpeedDial>"""
|
||||
|
||||
|
||||
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)
|
||||
]
|
||||
+ [("Fig 8", FIGURE8_FUNC, FIGURE8_BG), ("Side 8", FIGURE8_SIDE_FUNC, FIGURE8_SIDE_BG)]
|
||||
)
|
||||
|
||||
|
||||
def figure8_bounds() -> tuple[int, int, int, int]:
|
||||
"""Pan/tilt centre and amplitude covering Here–Out (not the live Now aim)."""
|
||||
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:
|
||||
pan_c, tilt_c, pan_amp, tilt_amp = figure8_bounds()
|
||||
# Sideways ∞ is rotation 0. 90° is a vertical 8 (tilt 0 → 36 down);
|
||||
# width/height are swapped so both still fill the Here–Out box.
|
||||
return "\n".join(
|
||||
[
|
||||
figure8_efx_block(
|
||||
FIGURE8_FUNC, "Figure of 8", tilt_amp, pan_amp, 90, pan_c, tilt_c
|
||||
),
|
||||
figure8_efx_block(
|
||||
FIGURE8_SIDE_FUNC,
|
||||
"Figure of 8 Side",
|
||||
pan_amp,
|
||||
tilt_amp,
|
||||
0,
|
||||
pan_c,
|
||||
tilt_c,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
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"):
|
||||
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)
|
||||
|
||||
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
|
||||
widgets.append(
|
||||
speed_dial(
|
||||
wid,
|
||||
"Speed",
|
||||
x,
|
||||
y0,
|
||||
sl_w,
|
||||
sl_h,
|
||||
[FIGURE8_FUNC, FIGURE8_SIDE_FUNC],
|
||||
)
|
||||
)
|
||||
wid += 1
|
||||
max_x = max(max_x, x + sl_w)
|
||||
max_y = max(max_y, y0 + sl_h)
|
||||
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()
|
||||
Reference in New Issue
Block a user