#!/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""" {frame} {fg} {bg} None Default """ 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' {ch}' for fid, ch in channels ) inv = "true" if inverted else "false" return f""" {appearance(fg=fg)} Level {ch_xml} 4294967295 """ 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""" {appearance()} """ def label_frame(wid: int, caption: str, x: int, y: int, w: int, h: int) -> str: return f""" Sunken Default Default None Default True True True False False False """ 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}""" def solo_frame(wid: int, caption: str, x: int, y: int, w: int, h: int, children: str) -> str: return f""" Sunken Default Default None Default True True True False False {children} """ def can_fixture_val(fid: int, r: int, g: int, b: int) -> str: # Dimmer is owned by the Can Bright slider (HTP). return ( f' ' f"1,0,2,{r},3,{g},4,{b},5,0,6,0,7,0" f"" ) 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' {",".join(str(x) for x in parts)}' 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""" {chr(10).join(vals)} """ 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""" """ ] 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' 0,{pan},1,0,2,{tilt},3,0' ) blocks.append( f""" {chr(10).join(vals)} """ ) 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""" {fid} Forward 255 """ for fid in (M1, M2) ) return f""" {fixtures} Parallel Forward Loop Eight {width} {height} {rotation} {pan_c} 1 0 {tilt_c} 1 0 """ 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""" beamZ BCC145 8 Channel {i} BCC145 #{i + 1} 0
{87 + i * 8}
8
""" ) for i, addr in enumerate((0, 44)): parts.append( f""" Small Eye 7 Mover 43 Channel {8 + i} Small Eye 7 #{i + 1} 0
{addr}
43
""" ) return "\n".join(parts) def fixture_groups_xml() -> str: def heads(fids: list[int]) -> str: return "\n".join( f' 0' for i, fid in enumerate(fids) ) return f""" Cans 1-2 {heads(GROUP_L)} Cans 3-6 {heads(GROUP_C)} Cans 7-8 {heads(GROUP_R)} Small Eye Movers 0 0 """ 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""" Q Light Controller Plus 4.12.7 Jimmy {fixture_xml()} {fixture_groups_xml()} {look_scenes_xml()} {position_scene_xml()} {figure8_efx_xml()} None Default Default None Default {"\n".join(widgets)} """ 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()