diff --git a/config.yaml b/config.yaml index 7310869..8dbbb6a 100644 --- a/config.yaml +++ b/config.yaml @@ -1,97 +1,100 @@ locations: - - name: stage + - name: Stage 1 children: - - name: upstage - children: - - name: rack-lighting-2 - nodes: - - lighting-2 - - "48:59:00:41:00:29" # Pixie Driver 8k Port 1 - - "48:59:00:28:00:27" # Pixie Driver 8k Port 2 - - "48:59:00:3c:00:3e" # Pixie Driver 8k Port 5 + - children: + - name: Upstage + children: + - name: rack-lighting-2 + nodes: + - lighting-2 + - "48:59:00:41:00:29" # Pixie Driver 8k Port 1 + - "48:59:00:28:00:27" # Pixie Driver 8k Port 2 + - "48:59:00:3c:00:3e" # Pixie Driver 8k Port 5 - - name: downstage - children: - - name: rack-audio - nodes: - - audio - - "MICS-A" - - "00:0e:dd:a7:29:93" # MICS-A bridge interface - - "MICS-B" - - "00:0e:dd:a8:3e:b3" # MICS-B bridge interface - - "MICS-C" - - "00:0e:dd:a7:6f:55" # MICS-C bridge interface - - "MICS-D" - - "00:0e:dd:64:3d:51" # MICS-D bridge interface - - "MICS-E" - - "00:0e:dd:ac:fc:7d" # MICS-E bridge interface + - name: Under Apron + children: + - name: rack-audio + nodes: + - audio + - "MICS-A" + - "00:0e:dd:a7:29:93" # MICS-A bridge interface + - "MICS-B" + - "00:0e:dd:a8:3e:b3" # MICS-B bridge interface + - "MICS-C" + - "00:0e:dd:a7:6f:55" # MICS-C bridge interface + - "MICS-D" + - "00:0e:dd:64:3d:51" # MICS-D bridge interface + - "MICS-E" + - "00:0e:dd:ac:fc:7d" # MICS-E bridge interface - - name: rack-lighting-1 - nodes: - - lighting-1 - - "48:59:00:27:00:27" # Pixie Driver 8k Port 1 - - "48:59:00:37:00:27" # Pixie Driver 8k Port 2 - - "48:59:00:3e:00:27" # Pixie Driver 8k Port 3 - - "48:59:00:3f:00:27" # Pixie Driver 8k Port 4 - - "48:59:00:47:00:1a" # Pixie Driver 8k Port 5 - - "48:59:00:44:00:1a" # Pixie Driver 8k Port 6 - - "48:59:00:42:00:19" # Pixie Driver 8k Port 7 - - "48:59:00:44:00:19" # Pixie Driver 8k Port 8 + - name: rack-lighting-1 + nodes: + - lighting-1 + - "48:59:00:27:00:27" # Pixie Driver 8k Port 1 + - "48:59:00:37:00:27" # Pixie Driver 8k Port 2 + - "48:59:00:3e:00:27" # Pixie Driver 8k Port 3 + - "48:59:00:3f:00:27" # Pixie Driver 8k Port 4 + - "48:59:00:47:00:1a" # Pixie Driver 8k Port 5 + - "48:59:00:44:00:1a" # Pixie Driver 8k Port 6 + - "48:59:00:42:00:19" # Pixie Driver 8k Port 7 + - "48:59:00:44:00:19" # Pixie Driver 8k Port 8 - - name: rack-video - nodes: - - video - - "ATEM 2 M/E Constellation 4K" - - "HyperDeck Studio 4K Pro" - - RX-QLAB-1 - - RX-QLAB-2 - - TX-PROJ-1 - - TX-PROJ-2 - - TX-M4 - - TX-M16 - - TX-MISC - - TX-PREVIEW + - name: rack-video + nodes: + - video + - "ATEM 2 M/E Constellation 4K" + - "HyperDeck Studio 4K Pro" + - RX-QLAB-1 + - RX-QLAB-2 + - TX-PROJ-1 + - TX-PROJ-2 + - TX-M4 + - TX-M16 + - TX-MISC + - TX-PREVIEW - - name: house + - name: Mid House children: - - name: house-left + - name: House Left nodes: - satellite-2 - - name: house-right + - name: House Right nodes: - satellite-3 - - name: booth + - name: Booth children: - - name: shared - nodes: - - satellite-1 + - children: + - name: shared + nodes: + - satellite-1 - - name: qlab - nodes: - - qlab - - TX-QLAB-1 - - TX-QLAB-2 - - "SK_PTZEXTREMEV2 [457081]" - - "SK_RACKPRO2 [452514]" - - "d0:11:e5:17:03:0b" # pigeon - - - name: audio - nodes: - - SQ-7 - - - name: camera-control - nodes: - - RX-CC-PREVIEW - - RX-CC-M16 - - - name: video-control - nodes: - - RX-VC-M4 - - RX-VC-M16 - - - name: control - nodes: - - "sunset.local" - - RX-CONTROL-1 + - children: + - name: qlab + nodes: + - qlab + - TX-QLAB-1 + - TX-QLAB-2 + - "SK_PTZEXTREMEV2 [457081]" + - "SK_RACKPRO2 [452514]" + - "d0:11:e5:17:03:0b" # pigeon + + - name: audio + nodes: + - SQ-7 + + - name: camera-control + nodes: + - RX-CC-PREVIEW + - RX-CC-M16 + + - name: video-control + nodes: + - RX-VC-M4 + - RX-VC-M16 + + - name: control + nodes: + - "sunset.local" + - RX-CONTROL-1 diff --git a/static/index.html b/static/index.html index 881e429..519ecef 100644 --- a/static/index.html +++ b/static/index.html @@ -75,11 +75,19 @@ return !!(node.poe_budget); } + let anonCounter = 0; + function buildLocationIndex(locations, parentId, nodeToLocation, locationMeta, depth, orderBase) { if (!locations) return; locations.forEach((loc, idx) => { - const locId = 'loc_' + loc.name.replace(/[^a-zA-Z0-9]/g, '_'); - locationMeta.set(locId, { name: loc.name, parentId, depth, order: orderBase + idx }); + let locId; + if (loc.name) { + locId = 'loc_' + loc.name.replace(/[^a-zA-Z0-9]/g, '_'); + locationMeta.set(locId, { name: loc.name, parentId, depth, order: orderBase + idx, anonymous: false }); + } else { + locId = 'loc_anon_' + (anonCounter++); + locationMeta.set(locId, { name: '', parentId, depth, order: orderBase + idx, anonymous: true }); + } if (loc.nodes) { loc.nodes.forEach(nodeRef => { @@ -104,7 +112,8 @@ return chain; } - let topLevelOrder = []; + let locationMeta = new Map(); + let usedLocations = new Set(); function doLayout() { const layout = cy.layout({ @@ -123,58 +132,100 @@ 'elk.hierarchyHandling': 'INCLUDE_CHILDREN' }, stop: function() { - reorderTopLevel(); + reorderLocations(); cy.fit(50); } }); layout.run(); } - function reorderTopLevel() { - if (topLevelOrder.length < 2) return; + function reorderLocations() { + const locationsByParent = new Map(); + locationMeta.forEach((meta, locId) => { + if (!usedLocations.has(locId)) return; + const parentKey = meta.parentId || '__root__'; + if (!locationsByParent.has(parentKey)) { + locationsByParent.set(parentKey, []); + } + locationsByParent.get(parentKey).push({ id: locId, order: meta.order, depth: meta.depth }); + }); - const boxes = topLevelOrder.map(locId => { + const depths = new Set(); + locationMeta.forEach((meta, locId) => { + if (usedLocations.has(locId)) depths.add(meta.depth); + }); + const sortedDepths = Array.from(depths).sort((a, b) => b - a); + + sortedDepths.forEach(depth => { + const isVertical = depth % 2 === 0; + + locationsByParent.forEach((siblings, parentKey) => { + const locationsAtDepth = siblings.filter(s => s.depth === depth); + if (locationsAtDepth.length < 2) return; + + locationsAtDepth.sort((a, b) => a.order - b.order); + reorderSiblings(locationsAtDepth.map(l => l.id), isVertical); + }); + }); + } + + function reorderSiblings(locIds, isVertical) { + const boxes = locIds.map(locId => { const node = cy.getElementById(locId); if (node.empty()) return null; const bb = node.boundingBox(); return { id: locId, bb: bb, + width: bb.x2 - bb.x1, height: bb.y2 - bb.y1, - centerX: (bb.x1 + bb.x2) / 2 + centerX: (bb.x1 + bb.x2) / 2, + centerY: (bb.y1 + bb.y2) / 2 }; }).filter(b => b !== null); if (boxes.length < 2) return; - const minY = Math.min(...boxes.map(b => b.bb.y1)); const gap = 50; - // Find the widest box and use its center as the target - const widest = boxes.reduce((a, b) => (b.bb.x2 - b.bb.x1) > (a.bb.x2 - a.bb.x1) ? b : a); - const targetCenterX = widest.centerX; + if (isVertical) { + const widest = boxes.reduce((a, b) => b.width > a.width ? b : a); + const targetCenterX = widest.centerX; + const minY = Math.min(...boxes.map(b => b.bb.y1)); - let targetY = minY; - const targets = boxes.map(box => { - const result = { - id: box.id, - deltaX: targetCenterX - box.centerX, - deltaY: targetY - box.bb.y1 - }; - targetY += box.height + gap; - return result; - }); - - targets.forEach(target => { - const node = cy.getElementById(target.id); - node.descendants().filter(n => !n.isParent()).forEach(n => { - const pos = n.position(); - n.position({ x: pos.x + target.deltaX, y: pos.y + target.deltaY }); + let targetY = minY; + boxes.forEach(box => { + const deltaX = targetCenterX - box.centerX; + const deltaY = targetY - box.bb.y1; + moveLocationAndDescendants(box.id, deltaX, deltaY); + targetY += box.height + gap; }); + } else { + const tallest = boxes.reduce((a, b) => b.height > a.height ? b : a); + const targetCenterY = tallest.centerY; + const minX = Math.min(...boxes.map(b => b.bb.x1)); + + let targetX = minX; + boxes.forEach(box => { + const deltaX = targetX - box.bb.x1; + const deltaY = targetCenterY - box.centerY; + moveLocationAndDescendants(box.id, deltaX, deltaY); + targetX += box.width + gap; + }); + } + } + + function moveLocationAndDescendants(locId, deltaX, deltaY) { + if (deltaX === 0 && deltaY === 0) return; + const node = cy.getElementById(locId); + node.descendants().filter(n => !n.isParent()).forEach(n => { + const pos = n.position(); + n.position({ x: pos.x + deltaX, y: pos.y + deltaY }); }); } async function init() { + anonCounter = 0; const [statusResp, configResp] = await Promise.all([ fetch('/api/status'), fetch('/api/config') @@ -193,7 +244,7 @@ const switchIds = new Set(); const nodeToLocation = new Map(); - const locationMeta = new Map(); + locationMeta = new Map(); buildLocationIndex(config.locations || [], null, nodeToLocation, locationMeta, 0, 0); nodes.forEach((n, i) => { @@ -202,7 +253,7 @@ if (isSwitch(n)) switchIds.add(id); }); - const usedLocations = new Set(); + usedLocations = new Set(); const nodeParents = new Map(); nodes.forEach((n, i) => { @@ -231,7 +282,8 @@ id: locId, label: meta.name, parent: meta.parentId && usedLocations.has(meta.parentId) ? meta.parentId : null, - isLocation: true + isLocation: true, + isAnonymous: meta.anonymous } }); }); @@ -267,18 +319,6 @@ }); }); - // Find top-level locations and sort by config order - topLevelOrder = sortedLocations - .filter(locId => { - const meta = locationMeta.get(locId); - return !meta.parentId; - }) - .sort((a, b) => { - const metaA = locationMeta.get(a); - const metaB = locationMeta.get(b); - return metaA.order - metaB.order; - }); - cy = cytoscape({ container: document.getElementById('cy'), elements: elements, @@ -343,6 +383,14 @@ 'padding': 30 } }, + { + selector: 'node[?isAnonymous]', + style: { + 'background-opacity': 0, + 'border-width': 0, + 'padding': 10 + } + }, { selector: 'edge', style: {