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