From f0db8aa5cd6913665dc805c9431fa0c835149ee9 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 18 Feb 2026 22:00:30 -0700 Subject: [PATCH] Switch to typeids, struct triggers, block types, implicit cue track, ALL CAPS signals --- cmd/qrunweb/static/index.html | 150 +++++++++++++++++----------------- cmd/qrunweb/static/show.json | 144 ++++++++++++++++++++++---------- 2 files changed, 179 insertions(+), 115 deletions(-) diff --git a/cmd/qrunweb/static/index.html b/cmd/qrunweb/static/index.html index ab842df..4872ac8 100644 --- a/cmd/qrunweb/static/index.html +++ b/cmd/qrunweb/static/index.html @@ -166,38 +166,44 @@ fetch('show.json').then(r => r.json()).then(render); function render(data) { const blockMap = new Map(data.blocks.map(b => [b.id, b])); - const trackIds = data.tracks.map(t => t.id); - const trackType = new Map(data.tracks.map(t => [t.id, t.type])); + const CUE_TRACK = '_cue'; + const trackIds = [CUE_TRACK, ...data.tracks.map(t => t.id)]; + + function ref(block, signal) { return block + ':' + signal; } const triggerTargetSet = new Set(); const triggerSourceSet = new Set(); data.triggers.forEach(t => { - triggerSourceSet.add(t.source); - t.targets.forEach(tgt => triggerTargetSet.add(tgt)); + triggerSourceSet.add(ref(t.source.block, t.source.signal)); + t.targets.forEach(tgt => triggerTargetSet.add(ref(tgt.block, tgt.hook))); }); const trackBlocks = new Map(); trackIds.forEach(id => trackBlocks.set(id, [])); - data.blocks.forEach(b => trackBlocks.get(b.track).push(b.id)); + data.blocks.forEach(b => { + const tid = b.type === 'cue' ? CUE_TRACK : b.track; + trackBlocks.get(tid).push(b.id); + }); + + function blockTrack(bid) { + const b = blockMap.get(bid); + return b.type === 'cue' ? CUE_TRACK : b.track; + } document.getElementById('header-status').innerHTML = 'QLab Connected' + 'Show: ' + data.show + '' + - 'Cue: ' + data.blocks.filter(b => b.track === 'cue').length + ''; + 'Cue: ' + data.blocks.filter(b => b.type === 'cue').length + ''; const active = new Map(); const pendingEnds = new Set(); const pendingTitles = new Set(); const rows = []; - function btype(blockId) { - const b = blockMap.get(blockId); - return b.type || trackType.get(b.track); - } - function nextInTrack(blockId) { - const b = blockMap.get(blockId); - const seq = trackBlocks.get(b.track); - const idx = seq.indexOf(blockId); + function nextInTrack(bid) { + const tid = blockTrack(bid); + const seq = trackBlocks.get(tid); + const idx = seq.indexOf(bid); return idx < seq.length - 1 ? seq[idx + 1] : null; } @@ -211,14 +217,13 @@ function render(data) { } function canAutoStart(bid) { - const ref = bid + ':start'; - return !triggerTargetSet.has(ref) && !triggerSourceSet.has(ref); + return !triggerTargetSet.has(ref(bid, 'START')) && !triggerSourceSet.has(ref(bid, 'START')); } function emitTitles() { if (pendingTitles.size === 0) return; const cells = trackIds.map(tid => { - const t = [...pendingTitles].find(bid => blockMap.get(bid).track === tid); + const t = [...pendingTitles].find(bid => blockTrack(bid) === tid); return t ? { blockId: t, segment: 'mid', title: blockMap.get(t).name } : mid(tid); }); addRow(cells); @@ -233,25 +238,25 @@ function render(data) { }); if (starts.length === 0) return; const cells = trackIds.map(tid => { - const s = starts.find(bid => blockMap.get(bid).track === tid); + const s = starts.find(bid => blockTrack(bid) === tid); if (s) { active.set(tid, s); pendingTitles.add(s); - return { blockId: s, segment: 'start', event: 'start' }; + return { blockId: s, segment: 'start', event: 'START' }; } return mid(tid); }); addRow(cells); } - function flush(upcomingSource) { + function flush(upcomingSrc) { emitTitles(); if (pendingEnds.size === 0) return; const holdBack = new Set(); - if (upcomingSource) { + if (upcomingSrc) { for (const bid of pendingEnds) { - if (upcomingSource === bid + ':end') holdBack.add(bid); + if (upcomingSrc.block === bid && upcomingSrc.signal === 'END') holdBack.add(bid); } } @@ -259,36 +264,35 @@ function render(data) { if (toEnd.length === 0) return; const cells = trackIds.map(tid => { - const e = toEnd.find(bid => blockMap.get(bid).track === tid); - return e ? { blockId: e, segment: 'end', event: 'end' } : mid(tid); + const e = toEnd.find(bid => blockTrack(bid) === tid); + return e ? { blockId: e, segment: 'end', event: 'END' } : mid(tid); }); addRow(cells); - toEnd.forEach(bid => { active.delete(blockMap.get(bid).track); pendingEnds.delete(bid); }); + toEnd.forEach(bid => { active.delete(blockTrack(bid)); pendingEnds.delete(bid); }); doAutoStarts(toEnd); emitTitles(); } function processTrigger(trigger) { - const source = trigger.source; - const isCue = !source.includes(':'); + const src = trigger.source; + const isCue = src.signal === 'GO'; - flush(source); + flush(src); if (!isCue) { - const [srcBlock, srcEvent] = source.split(':'); - const b = blockMap.get(srcBlock); - if (srcEvent === 'start') { - const cur = active.get(b.track); - if (cur && cur !== srcBlock) { + const tid = blockTrack(src.block); + if (src.signal === 'START') { + const cur = active.get(tid); + if (cur && cur !== src.block) { pendingEnds.delete(cur); - const cells = trackIds.map(tid => - tid === b.track ? { blockId: cur, segment: 'end', event: 'end' } : mid(tid) + const cells = trackIds.map(t => + t === tid ? { blockId: cur, segment: 'end', event: 'END' } : mid(t) ); addRow(cells); - active.delete(b.track); + active.delete(tid); } - active.set(b.track, srcBlock); + active.set(tid, src.block); } } @@ -297,41 +301,35 @@ function render(data) { const rowClass = isCue ? 'cue-row' : 'sig-row'; const targetMap = new Map(); - trigger.targets.forEach(t => { - const p = t.split(':'); - targetMap.set(p[0], p[1]); - }); + trigger.targets.forEach(t => targetMap.set(t.block, t.hook)); const directEnds = []; const cells = trackIds.map(tid => { - if (isCue && tid === 'cue') - return { cueLabel: blockMap.get(source).name }; + if (isCue && tid === CUE_TRACK) + return { cueLabel: blockMap.get(src.block).name }; - if (!isCue) { - const [srcBlock, srcEvent] = source.split(':'); - if (blockMap.get(srcBlock).track === tid) { - const seg = srcEvent === 'start' ? 'start' : srcEvent === 'end' ? 'end' : 'mid'; - return { blockId: srcBlock, segment: seg, event: srcEvent, isSignal: true }; - } + if (!isCue && blockTrack(src.block) === tid) { + const seg = src.signal === 'START' ? 'start' : src.signal === 'END' ? 'end' : 'mid'; + return { blockId: src.block, segment: seg, event: src.signal, isSignal: true }; } - const entry = [...targetMap.entries()].find(([bid]) => blockMap.get(bid).track === tid); + const entry = [...targetMap.entries()].find(([bid]) => blockTrack(bid) === tid); if (entry) { - const [bid, evt] = entry; + const [bid, hook] = entry; const isHook = !isCue; - if (evt === 'start') { + if (hook === 'START') { active.set(tid, bid); pendingTitles.add(bid); - return { blockId: bid, segment: 'start', event: 'start', isHook }; + return { blockId: bid, segment: 'start', event: 'START', isHook }; } - if (evt === 'end') { + if (hook === 'END') { directEnds.push(bid); - return { blockId: bid, segment: 'end', event: 'end', isHook: isHook }; + return { blockId: bid, segment: 'end', event: 'END', isHook }; } - if (evt === 'fade_out') { + if (hook === 'FADE_OUT') { pendingEnds.add(bid); - return { blockId: bid, segment: 'mid', event: 'fade_out' }; + return { blockId: bid, segment: 'mid', event: 'FADE_OUT' }; } } @@ -340,17 +338,16 @@ function render(data) { addRow(cells, rowClass); - directEnds.forEach(bid => active.delete(blockMap.get(bid).track)); + directEnds.forEach(bid => active.delete(blockTrack(bid))); doAutoStarts(directEnds); if (!isCue) { - const [srcBlock, srcEvent] = source.split(':'); - if (srcEvent === 'start') pendingTitles.add(srcBlock); - if (srcEvent === 'fade_out') pendingEnds.add(srcBlock); - if (srcEvent === 'end') { - active.delete(blockMap.get(srcBlock).track); - pendingEnds.delete(srcBlock); - doAutoStarts([srcBlock]); + if (src.signal === 'START') pendingTitles.add(src.block); + if (src.signal === 'FADE_OUT') pendingEnds.add(src.block); + if (src.signal === 'END') { + active.delete(blockTrack(src.block)); + pendingEnds.delete(src.block); + doAutoStarts([src.block]); } } } @@ -359,22 +356,27 @@ function render(data) { flush(null); emitTitles(); - const infBlocks = data.blocks.filter(b => b.infinity); - if (infBlocks.length > 0) { + const stillActive = []; + for (const [tid, bid] of active) { + if (tid !== CUE_TRACK) stillActive.push(bid); + } + if (stillActive.length > 0) { const cells = trackIds.map(tid => { - const inf = infBlocks.find(b => b.track === tid); - return inf ? { blockId: inf.id, segment: 'mid', infinity: true } : mid(tid); + const bid = stillActive.find(b => blockTrack(b) === tid); + return bid ? { blockId: bid, segment: 'mid', infinity: true } : mid(tid); }); addRow(cells); } const timeline = document.getElementById('timeline'); + const trackNames = { [CUE_TRACK]: 'Cue' }; + data.tracks.forEach(t => { trackNames[t.id] = t.name; }); timeline.style.gridTemplateColumns = 'repeat(' + trackIds.length + ', 140px)'; - data.tracks.forEach(t => { + trackIds.forEach(tid => { const th = document.createElement('div'); th.className = 'track-header'; - th.textContent = t.name; + th.textContent = trackNames[tid]; timeline.appendChild(th); }); @@ -395,7 +397,7 @@ function render(data) { } else if (cell.title) { div.className = cls; const block = document.createElement('div'); - block.className = 'block block-mid ' + btype(cell.blockId); + block.className = 'block block-mid ' + blockMap.get(cell.blockId).type; const t = document.createElement('div'); t.className = 'title'; t.textContent = cell.title; @@ -404,7 +406,7 @@ function render(data) { } else if (cell.infinity) { div.className = cls + ' infinity-cell'; const block = document.createElement('div'); - block.className = 'block block-mid ' + btype(cell.blockId); + block.className = 'block block-mid ' + blockMap.get(cell.blockId).type; div.appendChild(block); const marker = document.createElement('div'); marker.className = 'infinity-marker'; @@ -414,7 +416,7 @@ function render(data) { div.className = cls; const seg = cell.segment || 'mid'; const block = document.createElement('div'); - block.className = 'block block-' + seg + ' ' + btype(cell.blockId); + block.className = 'block block-' + seg + ' ' + blockMap.get(cell.blockId).type; if (cell.event) { const hook = document.createElement('div'); let hookCls = 'hook'; diff --git a/cmd/qrunweb/static/show.json b/cmd/qrunweb/static/show.json index 264502b..c04aeb0 100644 --- a/cmd/qrunweb/static/show.json +++ b/cmd/qrunweb/static/show.json @@ -1,50 +1,112 @@ { "show": "The Tempest", - "currentCue": "q12", "tracks": [ - {"id": "cue", "name": "Cue", "type": "cue"}, - {"id": "light-a", "name": "Lighting A", "type": "light"}, - {"id": "light-b", "name": "Lighting B", "type": "light"}, - {"id": "video", "name": "Video", "type": "video"}, - {"id": "video-ovl", "name": "Video OVL", "type": "overlay"}, - {"id": "audio", "name": "Audio", "type": "audio"} + {"id": "track_01kht419m1e65bfgqzj28se459", "name": "Lighting A"}, + {"id": "track_01kht419n7esr929mxvsacsy0k", "name": "Lighting B"}, + {"id": "track_01kht419pbfx2992sjpagc0syy", "name": "Video"}, + {"id": "track_01kht419qdeb1bjm9qe5acas2f", "name": "Video OVL"}, + {"id": "track_01kht419rffhnayzmgqnnkddtz", "name": "Audio"} ], "blocks": [ - {"id": "q10", "track": "cue", "name": "Q10 Preshow"}, - {"id": "preshow-wash", "track": "light-a", "name": "Preshow Wash"}, - {"id": "warm-70", "track": "light-b", "name": "Warm 70%"}, - {"id": "preshow-loop", "track": "video", "name": "Preshow Loop"}, - {"id": "preshow-music", "track": "audio", "name": "Preshow Music"}, - {"id": "q11", "track": "cue", "name": "Q11 House Open"}, - {"id": "q12", "track": "cue", "name": "Q12 Top of Show"}, - {"id": "cool-50", "track": "light-b", "name": "Cool 50%"}, - {"id": "delay-3s", "track": "video", "name": "3s Delay", "type": "delay"}, - {"id": "sc1-focus", "track": "light-a", "name": "SC1 Focus"}, - {"id": "sc1-blue", "track": "light-b", "name": "SC1 Blue 80%"}, - {"id": "sc1-projection", "track": "video", "name": "Sc1 Projection"}, - {"id": "lightning-flash", "track": "video-ovl", "name": "Lightning Flash"}, - {"id": "storm-ambience", "track": "audio", "name": "Storm Ambience"}, - {"id": "q13", "track": "cue", "name": "Q13 Sc1 Dialog"}, - {"id": "wave-overlay", "track": "video-ovl", "name": "Wave Overlay"}, - {"id": "dialog-spots", "track": "light-a", "name": "Dialog Spots"}, - {"id": "warm-90", "track": "light-b", "name": "Warm 90%"}, - {"id": "dialog-underscore", "track": "audio", "name": "Dialog Underscore"}, - {"id": "q14", "track": "cue", "name": "Q14 Sc2 Trans"}, - {"id": "sc2-focus", "track": "light-a", "name": "SC2 Focus", "infinity": true}, - {"id": "sc2-amber", "track": "light-b", "name": "SC2 Amber 60%", "infinity": true}, - {"id": "sc2-background", "track": "video", "name": "Sc2 Background", "infinity": true}, - {"id": "sc2-atmos", "track": "audio", "name": "SC2 Atmos", "infinity": true} + {"id": "block_01kht41ax6f0ntbmqc5jxfda2m", "type": "cue", "name": "Q10 Preshow"}, + {"id": "block_01kht41aygemwtrw590cet1en9", "type": "light", "track": "track_01kht419m1e65bfgqzj28se459", "name": "Preshow Wash"}, + {"id": "block_01kht41azrenbaxyc2vebv7s4q", "type": "light", "track": "track_01kht419n7esr929mxvsacsy0k", "name": "Warm 70%"}, + {"id": "block_01kht41b10emgt1gfvg7dy60ws", "type": "video", "track": "track_01kht419pbfx2992sjpagc0syy", "name": "Preshow Loop", "loop": true}, + {"id": "block_01kht41b29eyes3b6neh4h56mn", "type": "audio", "track": "track_01kht419rffhnayzmgqnnkddtz", "name": "Preshow Music", "loop": true}, + {"id": "block_01kht41b3ge6ntqwjqrjq4wsxm", "type": "cue", "name": "Q11 House Open"}, + {"id": "block_01kht41b4rfnhvk83p9afhp08y", "type": "cue", "name": "Q12 Top of Show"}, + {"id": "block_01kht41b5zf4ya2044tczm8tz6", "type": "light", "track": "track_01kht419n7esr929mxvsacsy0k", "name": "Cool 50%"}, + {"id": "block_01kht41b76ebtraravpknavdm5", "type": "delay", "track": "track_01kht419pbfx2992sjpagc0syy", "name": "3s Delay"}, + {"id": "block_01kht41b8deg8ae996tzqay0rg", "type": "light", "track": "track_01kht419m1e65bfgqzj28se459", "name": "SC1 Focus"}, + {"id": "block_01kht41b9neqf84ap7dm3mey9r", "type": "light", "track": "track_01kht419n7esr929mxvsacsy0k", "name": "SC1 Blue 80%"}, + {"id": "block_01kht41bawfbhr38a49tjvyahy", "type": "video", "track": "track_01kht419pbfx2992sjpagc0syy", "name": "Sc1 Projection"}, + {"id": "block_01kht41bc3evybtn5hqnk6p2f7", "type": "overlay", "track": "track_01kht419qdeb1bjm9qe5acas2f", "name": "Lightning Flash"}, + {"id": "block_01kht41bdafggbbm8959svkm4n", "type": "audio", "track": "track_01kht419rffhnayzmgqnnkddtz", "name": "Storm Ambience", "loop": true}, + {"id": "block_01kht41bejev2997rfps2kpbat", "type": "cue", "name": "Q13 Sc1 Dialog"}, + {"id": "block_01kht41bfsfhkvtvze4rh7k7z6", "type": "overlay", "track": "track_01kht419qdeb1bjm9qe5acas2f", "name": "Wave Overlay"}, + {"id": "block_01kht41bgzfjcrkwrvrrjkqs0f", "type": "light", "track": "track_01kht419m1e65bfgqzj28se459", "name": "Dialog Spots"}, + {"id": "block_01kht41bj7ea89xn71nn5h400a", "type": "light", "track": "track_01kht419n7esr929mxvsacsy0k", "name": "Warm 90%"}, + {"id": "block_01kht41bkee1n81gnfq6bydmm2", "type": "audio", "track": "track_01kht419rffhnayzmgqnnkddtz", "name": "Dialog Underscore"}, + {"id": "block_01kht41bmnfdbvfr4d08brj19k", "type": "cue", "name": "Q14 Sc2 Trans"}, + {"id": "block_01kht41bnxfme95cspptf87j9b", "type": "light", "track": "track_01kht419m1e65bfgqzj28se459", "name": "SC2 Focus"}, + {"id": "block_01kht41bq2ekwswsf51b0h1bd2", "type": "light", "track": "track_01kht419n7esr929mxvsacsy0k", "name": "SC2 Amber 60%"}, + {"id": "block_01kht41br4eajaj99tmey3eph0", "type": "video", "track": "track_01kht419pbfx2992sjpagc0syy", "name": "Sc2 Background", "loop": true}, + {"id": "block_01kht41bs5fxrr0a7mznd026v4", "type": "audio", "track": "track_01kht419rffhnayzmgqnnkddtz", "name": "SC2 Atmos", "loop": true} ], "triggers": [ - {"source": "q10", "targets": ["preshow-wash:start", "warm-70:start", "preshow-loop:start", "preshow-music:start"]}, - {"source": "q11", "targets": ["warm-70:fade_out"]}, - {"source": "q12", "targets": ["preshow-wash:fade_out", "preshow-loop:fade_out", "preshow-music:fade_out"]}, - {"source": "sc1-projection:start", "targets": ["sc1-focus:start", "cool-50:end", "storm-ambience:start"]}, - {"source": "sc1-blue:start", "targets": ["lightning-flash:start"]}, - {"source": "sc1-projection:fade_out", "targets": ["lightning-flash:end"]}, - {"source": "q13", "targets": ["sc1-focus:fade_out", "sc1-blue:fade_out", "storm-ambience:fade_out", "wave-overlay:start"]}, - {"source": "dialog-underscore:start", "targets": ["dialog-spots:start"]}, - {"source": "q14", "targets": ["dialog-spots:fade_out", "warm-90:fade_out", "sc2-background:start", "wave-overlay:fade_out", "dialog-underscore:end"]}, - {"source": "wave-overlay:end", "targets": ["sc2-atmos:start"]} + { + "source": {"block": "block_01kht41ax6f0ntbmqc5jxfda2m", "signal": "GO"}, + "targets": [ + {"block": "block_01kht41aygemwtrw590cet1en9", "hook": "START"}, + {"block": "block_01kht41azrenbaxyc2vebv7s4q", "hook": "START"}, + {"block": "block_01kht41b10emgt1gfvg7dy60ws", "hook": "START"}, + {"block": "block_01kht41b29eyes3b6neh4h56mn", "hook": "START"} + ] + }, + { + "source": {"block": "block_01kht41b3ge6ntqwjqrjq4wsxm", "signal": "GO"}, + "targets": [ + {"block": "block_01kht41azrenbaxyc2vebv7s4q", "hook": "FADE_OUT"} + ] + }, + { + "source": {"block": "block_01kht41b4rfnhvk83p9afhp08y", "signal": "GO"}, + "targets": [ + {"block": "block_01kht41aygemwtrw590cet1en9", "hook": "FADE_OUT"}, + {"block": "block_01kht41b10emgt1gfvg7dy60ws", "hook": "FADE_OUT"}, + {"block": "block_01kht41b29eyes3b6neh4h56mn", "hook": "FADE_OUT"} + ] + }, + { + "source": {"block": "block_01kht41bawfbhr38a49tjvyahy", "signal": "START"}, + "targets": [ + {"block": "block_01kht41b8deg8ae996tzqay0rg", "hook": "START"}, + {"block": "block_01kht41b5zf4ya2044tczm8tz6", "hook": "END"}, + {"block": "block_01kht41bdafggbbm8959svkm4n", "hook": "START"} + ] + }, + { + "source": {"block": "block_01kht41b9neqf84ap7dm3mey9r", "signal": "START"}, + "targets": [ + {"block": "block_01kht41bc3evybtn5hqnk6p2f7", "hook": "START"} + ] + }, + { + "source": {"block": "block_01kht41bawfbhr38a49tjvyahy", "signal": "FADE_OUT"}, + "targets": [ + {"block": "block_01kht41bc3evybtn5hqnk6p2f7", "hook": "END"} + ] + }, + { + "source": {"block": "block_01kht41bejev2997rfps2kpbat", "signal": "GO"}, + "targets": [ + {"block": "block_01kht41b8deg8ae996tzqay0rg", "hook": "FADE_OUT"}, + {"block": "block_01kht41b9neqf84ap7dm3mey9r", "hook": "FADE_OUT"}, + {"block": "block_01kht41bdafggbbm8959svkm4n", "hook": "FADE_OUT"}, + {"block": "block_01kht41bfsfhkvtvze4rh7k7z6", "hook": "START"} + ] + }, + { + "source": {"block": "block_01kht41bkee1n81gnfq6bydmm2", "signal": "START"}, + "targets": [ + {"block": "block_01kht41bgzfjcrkwrvrrjkqs0f", "hook": "START"} + ] + }, + { + "source": {"block": "block_01kht41bmnfdbvfr4d08brj19k", "signal": "GO"}, + "targets": [ + {"block": "block_01kht41bgzfjcrkwrvrrjkqs0f", "hook": "FADE_OUT"}, + {"block": "block_01kht41bj7ea89xn71nn5h400a", "hook": "FADE_OUT"}, + {"block": "block_01kht41br4eajaj99tmey3eph0", "hook": "START"}, + {"block": "block_01kht41bfsfhkvtvze4rh7k7z6", "hook": "FADE_OUT"}, + {"block": "block_01kht41bkee1n81gnfq6bydmm2", "hook": "END"} + ] + }, + { + "source": {"block": "block_01kht41bfsfhkvtvze4rh7k7z6", "signal": "END"}, + "targets": [ + {"block": "block_01kht41bs5fxrr0a7mznd026v4", "hook": "START"} + ] + } ] }