diff --git a/README.md b/README.md index 993e0f3..b3dcceb 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,9 @@ The rack front, drawn from the rack units set on cutsheet items, top first. with a DAC attached; its round dot is Gandalf's link mark for enp1s0 (the slot is PCI bus 1), not an LED on the card. UniFi desk switches (Flex Mini, Flex) on a shelf are drawn with their live faceplate at their real width. Anything else is a labelled box. +- **More drawn fronts**: the Sliger CX4712 with its red, white or blue perforated front fitted (as in + the rack photo, drive cages faint behind the mesh), and the UniFi brush panel + (UACC-Rack-Panel-Brush-1U, from Ubiquiti's product images). - **Gear resting on other gear**: a shelf item with Front drawing "No shelf: gear resting on what is below" draws no tray, so a switch lying on top of the NVR is a "shelf" in the unit above the NVR whose contents sit directly on it. diff --git a/cutsheet.py b/cutsheet.py index 9111bfd..9921c94 100644 --- a/cutsheet.py +++ b/cutsheet.py @@ -37,7 +37,11 @@ CHASSIS = { 'uacc-patch-24': 'UniFi Patch Panel Blank 24 (UACC-Rack-Panel-Patch-Blank-24)', 'usw-pro-max-24-poe': 'UniFi Switch Pro Max 24 PoE (until UniFi reports it)', 'usw-pro-max-48-poe': 'UniFi Switch Pro Max 48 PoE (until UniFi reports it)', - 'sliger-cx4712': 'Sliger CX4712 4U (10-bay)', + 'sliger-cx4712': 'Sliger CX4712 4U (10-bay, faceplate off)', + 'sliger-cx4712-red': 'Sliger CX4712 4U, red front', + 'sliger-cx4712-white': 'Sliger CX4712 4U, white front', + 'sliger-cx4712-blue': 'Sliger CX4712 4U, blue front', + 'uacc-brush-1u': 'UniFi Rack Panel Brush 1U (UACC-Rack-Panel-Brush-1U)', 'rosewill-rsv-l4500u': 'Rosewill RSV-L4500U 4U (15-bay)', 'zimaboard-832': 'IceWhale ZimaBoard 832 (shelf gear)', 'zimaboard-832-cx3': 'IceWhale ZimaBoard 832 + ConnectX-3 SFP+ in PCIe (shelf gear)', diff --git a/static/fronts.js b/static/fronts.js index f7a38b6..7fdfead 100644 --- a/static/fronts.js +++ b/static/fronts.js @@ -106,12 +106,86 @@ const RACK_FRONTS = { }, }, + // Sliger CX4712 with its coloured front fitted, as in the rack photo (wiki: rack-2026.jpg): + // a hex-perforated panel over most of the face, the drive cages faintly visible through it, + // black rack ears, and a power button over two USB ports at the top right. + 'sliger-cx4712-red': sligerFront('#c8161d', '#e9e5e0'), + 'sliger-cx4712-white': sligerFront('#eceae6', '#9aa0a6'), + 'sliger-cx4712-blue': sligerFront('#1f5fae', '#e9e5e0'), + + // Ubiquiti UACC-Rack-Panel-Brush-1U, from Ubiquiti's product images (store.ui.com, UACC-Rack-Panel + // variant Brush 1U): a light silver face with separate ear plates (two rounded slots each), a + // recessed chamfered frame around the opening (shadowed along the top, bright bevel along the + // bottom), and two rows of black bristles meeting at a seam along the middle. + 'uacc-brush-1u': { + label: 'UniFi Rack Panel Brush 1U (UACC-Rack-Panel-Brush-1U)', units: 1, width: 1100, height: 105, + draw() { + // Bristle tufts: thin strands of varying shade, deterministic so the drawing never flickers + const strands = (y0, y1, seed) => Array.from({ length: 300 }, (_, i) => { + const n = (i * 7919 + seed * 104729) % 97; + return ``; + }).join(''); + const slot = x => ` + `; + return ` + + + + + + + + + ${slot(16)}${slot(1084)} + + + + + ${strands(31, 53, 1)}${strands(54, 76, 2)} + `; + }, + }, + // Generic servers, for gear without a specific drawing 'server-1u': genericServer(1, 4), 'server-2u': genericServer(2, 12), 'server-4u': genericServer(4, 24), }; +function sligerFront(colour, hole) { + const W = 1100, H = 420; + // Hex perforations: a pattern of hexagons cut out of the panel through a mask + const hex = (cx, cy, r) => Array.from({ length: 6 }, (_, i) => { + const a = Math.PI / 6 + i * Math.PI / 3; + return `${(cx + r * Math.cos(a)).toFixed(1)},${(cy + r * Math.sin(a)).toFixed(1)}`; + }).join(' '); + // Hole size and pitch from the rack photo: about 20 rows of holes over the panel's height + const r = 6.9, dx = 18, dy = 15.6; + const holes = []; + for (let row = 0, y = 46; y < H - 34; row++, y += dy) { + for (let x = 82 + (row % 2 ? dx / 2 : 0); x < 925; x += dx) holes.push(``); + } + const bays = Array.from({ length: 10 }, (_, i) => ``).join(''); + // The cages show only faintly through the mesh + return { + label: 'Sliger CX4712 4U (10-bay)', units: 4, width: W, height: H, + draw({ status }) { + const id = `sl-${colour.slice(1)}`; + return `${holes.join('')} + ${bays} + + + + + + + + ${usbA(977, 146)}${usbA(977, 168)}${usbC(980, 192)}`; + }, + }; +} + function genericServer(units, bays) { const H = 105 * units; return { diff --git a/static/style.css b/static/style.css index 0313b95..4cbc1b2 100644 --- a/static/style.css +++ b/static/style.css @@ -1630,3 +1630,17 @@ body.fp-marks-off .fp-svg .fp-mark { display: none; } .cx-tab { fill: none; stroke: #c9ccd0; stroke-width: 9; stroke-linejoin: round; } .cx-link { stroke: #101418; stroke-width: 3; } .fp-speedbar { fill: #b3b5ba; } +.rf-sl-inside { fill: #141619; } +.rf-sl-ear { fill: #17181a; } +.rf-sl-edge { fill: none; stroke: rgba(0,0,0,.35); stroke-width: 2; } +.rf-sl-button { fill: #c9ccd0; stroke: #8a8e93; stroke-width: 2; } +.rf-sl-bay { opacity: .45; } +.rf-sl-io { fill: #0e0f11; } +.ua-ear-plate { fill: #d3d5d8; stroke: #b2b4b8; stroke-width: 1; } +.ua-ear-seam { fill: #9fa1a6; } +.ua-ear-slot { fill: #2a2c30; } +.ub-frame { fill: #bfc1c5; stroke: #a5a7ac; stroke-width: 1; } +.ub-frame-shadow { fill: #8c8e93; } +.ub-frame-bevel { fill: #f1f2f3; } +.ub-opening { fill: #0d0e10; } +.ub-seam { fill: #050506; } diff --git a/tests/test_cutsheet.py b/tests/test_cutsheet.py index e59349f..6d49bb2 100644 --- a/tests/test_cutsheet.py +++ b/tests/test_cutsheet.py @@ -370,7 +370,8 @@ class TestChassis: assert all(cs.CHASSIS.values()) assert {'sliger-cx4712', 'rosewill-rsv-l4500u', 'unvr-pro', 'uacc-patch-24', 'usw-pro-max-24-poe', 'usw-pro-max-48-poe', 'zimaboard-832', 'zimaboard-832-cx3', - 'resting'} <= set(cs.CHASSIS) + 'resting', 'uacc-brush-1u', 'sliger-cx4712-red', 'sliger-cx4712-white', + 'sliger-cx4712-blue'} <= set(cs.CHASSIS) class TestFacingAndSlots: