From dc1ca9f2c6b58e7ea0959bee32cee3497d21687f Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Sun, 1 Nov 2026 14:28:16 +0100 Subject: [PATCH] 3D rooms: cabinets apart from servers, cables you can see The cabinet and the servers were both dark grey, and the cables behind an opaque cabinet could only be seen from the back. Cabinets are now light steel and see-through like a perforated door; every server has a solid dark chassis between its front and its back, so it stands out from the cabinet. Cables are thicker and lit, each switch's cables run up their own lane beside the rack instead of on top of one another, and the switch colours start away from the selection's blue. The legend says the cables are at the back. Co-Authored-By: Claude Opus 5.5 --- cmd/rollcall/web/room3d.js | 20 +++++++++++++------- cmd/rollcall/web/sites.js | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cmd/rollcall/web/room3d.js b/cmd/rollcall/web/room3d.js index 9fceb5f..da75f53 100644 --- a/cmd/rollcall/web/room3d.js +++ b/cmd/rollcall/web/room3d.js @@ -25,7 +25,7 @@ const cablesView = { get() { try { return localStorage.getItem('rollcall.cables') !== 'off'; } catch (e) { return true; } }, set(v) { try { localStorage.setItem('rollcall.cables', v ? 'on' : 'off'); } catch (e) { /* the choice is not kept */ } }, }; -const SWITCH_COLORS = ['#4f8cff', '#f2a33a', '#3fbf8f', '#c470e0', '#e05d5d', '#46b8d1', '#b5c43a', '#e07fb0']; +const SWITCH_COLORS = ['#f2a33a', '#3fbf8f', '#c470e0', '#e05d5d', '#46b8d1', '#b5c43a', '#e07fb0', '#4f8cff']; // the selection's blue last function loadThree() { if (!loadThree.p) loadThree.p = import('./vendor/three/three.module.min.js').catch(e => { loadThree.p = null; throw e; }); @@ -121,13 +121,14 @@ function build(site, room, racks, blocks) { g.position.set((r.x + f.w / 2) * TILE, 0, (r.y + f.h / 2) * TILE); g.rotation.y = { N: 0, S: Math.PI, E: -Math.PI / 2, W: Math.PI / 2 }[r.facing]; // the front is local -z const st = rackState(site, r); - const body = new T.Mesh(box(TILE - 0.02, h, 2 * TILE - 0.02), mat('rack' + r.auto, () => new T.MeshStandardMaterial({ color: 0x2a2f36, roughness: 0.8, metalness: 0.2, transparent: r.auto, opacity: r.auto ? 0.5 : 1 }))); + // the cabinet: steel, see-through like a perforated door, so the servers inside and the cables behind show + const body = new T.Mesh(box(TILE - 0.02, h, 2 * TILE - 0.02), mat('rack' + r.auto, () => new T.MeshStandardMaterial({ color: 0x7d8794, roughness: 0.6, metalness: 0.3, transparent: true, opacity: r.auto ? 0.18 : 0.32, depthWrite: false }))); body.position.y = h / 2; body.userData = { rack: r.name, tip: `${r.name} · ${st.ms.length} machines · ${st.used}/${H}U used${r.auto ? ' · not placed yet' : ''}` }; const edges = new T.LineSegments(keep(new T.EdgesGeometry(body.geometry)), lineMat(sel ? '--accent' : '--muted')); edges.position.copy(body.position); // the rails' face, lighter than the cabinet: where the front is and what is empty - const face = new T.Mesh(box(RAIL, H * U_M, 0.004), mat('face', () => new T.MeshStandardMaterial({ color: 0x3c434c, roughness: 0.9 }))); + const face = new T.Mesh(box(RAIL, H * U_M, 0.004), mat('face', () => new T.MeshStandardMaterial({ color: 0x59626d, roughness: 0.9, transparent: true, opacity: 0.55, depthWrite: false }))); face.position.set(0, PLINTH + H * U_M / 2, -TILE + 0.008); const back = face.clone(); back.position.z = TILE - 0.008; g.add(body, edges, face, back); R3.pick.push(body); @@ -162,6 +163,10 @@ function build(site, room, racks, blocks) { s.userData = { rack: r.name, id: m.id, tip: `${nameOf(m)} · ${m.state} · U${b.unit}${span > 1 ? '–' + (b.unit + span - 1) : ''}${b.known ? '' : ' (height unknown)'} · ${[m.manufacturer, m.product].filter(Boolean).join(' ')}` }; g.add(s); R3.pick.push(s); }); + // the chassis between front and back: solid and dark, apart from the cabinet + const chassis = new T.Mesh(box(RAIL - 0.004, hh - 0.002, 2 * TILE - 0.07), mat('chassis' + b.known, () => new T.MeshStandardMaterial({ color: 0x202328, roughness: 0.7, metalness: 0.4, transparent: !b.known, opacity: b.known ? 1 : 0.5 }))); + chassis.position.set(0, y, -0.006); + g.add(chassis); // the back: PSUs, the BMC port and the network cards with their ports const p = frontProfile(b, span), sleds = b.ms.slice(0, n).map(m => ({ ports: portsBy.get(m.id) || [] })); const rkey = p.style + '|' + span + '|' + JSON.stringify(sleds); @@ -248,10 +253,11 @@ function cables(cab, rackAt, portAt, keep, mat) { const W = (ra, x, y, z) => ra.g.localToWorld(new T.Vector3(x, y, z)); for (const mp of cab.machines) for (const pt of mp.ports) { const a = pt.switch && portAt.get(mp.id + '|' + pt.name), sw = byName[pt.switch]; if (!a || !sw) continue; - const ra = rackAt[a.rack], side = a.lx >= 0 ? RAIL / 2 + 0.03 : -RAIL / 2 - 0.03, lift = 0.004 * (switchPortNo(pt.name) % 5); + const lane = 0.012 * (R3.switches.indexOf(sw) % 4); // each switch's cables up their own lane, side by side + const ra = rackAt[a.rack], side = a.lx >= 0 ? RAIL / 2 + 0.03 + lane : -RAIL / 2 - 0.03 - lane, lift = 0.004 * (switchPortNo(pt.name) % 5); const pts = [W(ra, a.lx, a.ly, TILE + 0.01), W(ra, a.lx, a.ly - 0.01, OUT), W(ra, side, a.ly - 0.01, OUT)]; if (sw.at) { - const rb = rackAt[sw.at.rack], b = sw.port(pt.switch_port), sideB = b.lx >= 0 ? RAIL / 2 + 0.03 : -RAIL / 2 - 0.03; + const rb = rackAt[sw.at.rack], b = sw.port(pt.switch_port), sideB = b.lx >= 0 ? RAIL / 2 + 0.03 + lane : -RAIL / 2 - 0.03 - lane; if (rb !== ra) { pts.push(W(ra, side, ra.h + 0.08 + lift, OUT), W(rb, sideB, rb.h + 0.08 + lift, OUT)); } if (rb === ra) pts.push(W(ra, side, b.ly + 0.012, OUT)); // down its own side to the switch, then across else pts.push(W(rb, sideB, b.ly + 0.012, OUT)); @@ -259,8 +265,8 @@ function cables(cab, rackAt, portAt, keep, mat) { } else pts.push(W(ra, side, ra.h + 0.08 + lift, OUT), W(ra, side * 0.5, 2.9, OUT)); // to a switch in another room: up through the ceiling const dim = SITE.sel && lc(SITE.sel) !== a.rack && !(sw.at && lc(SITE.sel) === sw.at.rack); const col = pt.link === false ? '#7a7f86' : sw.color; - const m = new T.Mesh(keep(new T.TubeGeometry(new T.CatmullRomCurve3(pts, false, 'catmullrom', 0.02), pts.length * 12, pt.kind === 'bmc' ? 0.0025 : 0.0035, 6, false)), - mat('cab' + col + dim, () => new T.MeshStandardMaterial({ color: new T.Color(col), roughness: 0.5, transparent: dim, opacity: dim ? 0.15 : 1 }))); + const m = new T.Mesh(keep(new T.TubeGeometry(new T.CatmullRomCurve3(pts, false, 'catmullrom', 0.02), pts.length * 12, pt.kind === 'bmc' ? 0.004 : 0.0055, 6, false)), + mat('cab' + col + dim, () => new T.MeshStandardMaterial({ color: new T.Color(col), emissive: new T.Color(col), emissiveIntensity: 0.45, roughness: 0.45, transparent: dim, opacity: dim ? 0.15 : 1 }))); const m0 = S.machines.find(x => x.id === mp.id); m.userData = { rack: ra.r.name, id: mp.id, tip: `${m0 ? nameOf(m0) : mp.id} ${pt.kind === 'bmc' ? 'BMC' : pt.name}${pt.speed ? ' ' + pt.speed : ''} → ${pt.switch}${sw.role === 'oob' ? ' (OOB)' : ''} ${pt.switch_port}${pt.vlan ? ' · VLAN ' + pt.vlan : ''}${pt.link === false ? ' · no link' : ''}${sw.away ? ' (in ' + sw.away + ')' : ''} · ${pt.source === 'lldp' ? 'seen by LLDP' : pt.source === 'netbox' ? 'NetBox cable' : 'entered by hand'}${pt.netbox_says ? ' · NetBox says ' + pt.netbox_says : ''}` }; R3.scene.add(m); if (!dim) R3.pick.push(m); diff --git a/cmd/rollcall/web/sites.js b/cmd/rollcall/web/sites.js index 95b4e5c..4c5f23b 100644 --- a/cmd/rollcall/web/sites.js +++ b/cmd/rollcall/web/sites.js @@ -218,7 +218,7 @@ function pageRoom(siteName, roomArg) { ${SITE.edit ? `
` : ''} ${rackElev(selRack)}${cablingTable(site, selRack)}` : `
Click a rack for its front.${autoN ? `

${autoN} rack(s) have no place yet and are laid out automatically (${v3 ? 'see-through' : 'dashed'}): Edit layout to place them.` : ''}
${v3 ? switchTable() : ''}
`; - const legend = v3 ? `drag: turn · right or shift drag: move · wheel: zoom · double click: go to a rack · a click on a selected rack\'s server opens it · see-through: height unknown${frontView.get() === 'real' ? ' · fronts: the drive bays of the model, the strip on the left ear is the state' : ''}` + const legend = v3 ? `drag: turn · right or shift drag: move · wheel: zoom · double click: go to a rack · a click on a selected rack\'s server opens it · see-through: height unknown${frontView.get() === 'real' ? ' · fronts: the drive bays of the model, the strip on the left ear is the state' : ''}${cablesView.get() ? ' · the cables run at the back: Turn around' : ''}` : `emptythick edge: the front · bar: units used${SITE.edit ? ' · drag to move, select and Rotate' : ''}`; return `

Sites / ${esc(site.name)} / ${esc(room || '(no room)')}

${racks.length} racks · floor tiles of 600 mm