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:
2026-09-10 00:35:28 +12:00
co-authored by Cursor
parent 5eb623d6af
commit f069a8091b
2 changed files with 2301 additions and 0 deletions
+832
View File
@@ -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()
+1469
View File
File diff suppressed because it is too large Load Diff