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: