// Pins the renderOrder hierarchy in plugins/highway_3d/screen.js. // // Three.js renders transparent objects by renderOrder first, then back-to-front // Z sort within the same renderOrder. All 3D-highway materials use depthTest:false, so // renderOrder is the *only* draw-order control — getting it wrong silently // causes one layer to bleed through another (gems clipping through chord frames, // strings buried under notes, etc.). // // Full hierarchy bottom → top: // // -1 background stage traversal // 1 lane quads // 2 fret dividers // 4 sus-rail bloom (pSusRailBloom seed) ← highway_3d_sustain_bloom.test.js // 5 sus-rail core (pSusRail seed) ← highway_3d_sustain_rail.test.js // 7 string-line glows (in-lane glow lines) // 14 board-projection frame // [renderOrderForLayerAtZ(z, FRET_COLUMN)] fret-column markers (pFretColMarker) — between chord frame and gem // [layered below chordFrameRenderOrder] chord fill / PM-FH fill / PM-FH lines // [chordFrameRenderOrder] chord frame edges = renderOrderForLayerAtZ(z, CHORD_FRAME) // [layered above chordFrameRenderOrder] chord-frame glow, connector/drop lines // [below chordFrameRenderOrder] sustain-trail strip segments (Z-proportional, always < frame) // [renderOrderForLayerAtZ(noteZ, NOTE_OUTLINE)] note gem outline // [renderOrderForLayerAtZ(noteZ, NOTE_CORE)] note gem core // [techniqueMarkerRenderOrder] technique markers // [after board wire layers] note fret labels, above gem symbols and fret wires // [renderOrderForLayerAtZ(0, BOARD_STRING)] string mesh (drawn over gems but under fret wires) // [renderOrderForLayerAtZ(0, BOARD_FRET_WIRE)] static fret wires (above strings, as on a real guitar) // 1000 technique labels, ghost-fret overlay // // Tests are source-level regex checks — no need to load Three.js or a DOM. // // Any PR that changes a renderOrder value must update the relevant test(s) here // and provide a visual justification in the PR description. const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- let _src; /** Returns the cached 3D highway screen source under test. */ function src() { if (!_src) _src = fs.readFileSync(SCREEN_JS, 'utf8'); return _src; } /** Parses the declared render-order layer stack from screen.js. */ function layers() { const match = src().match(/const\s+RENDER_ORDER_LAYER_STACK\s*=\s*Object\.freeze\(\s*\[([\s\S]*?)\]\s*\)/); assert.ok(match, 'RENDER_ORDER_LAYER_STACK must be declared'); return Array.from(match[1].matchAll(/'([^']+)'/g), m => m[1]); } /** Returns the position of a named layer in the render-order stack. */ function layerIndex(name) { const ordered = layers(); const idx = ordered.indexOf(name); assert.ok(idx !== -1, `${name} must be present in RENDER_ORDER_LAYER_STACK`); return idx; } /** Reads the render-order base used for objects at z = 0. */ function zZeroRenderOrder() { const match = src().match(/const\s+RENDER_ORDER_AT_Z_ZERO\s*=\s*(-?\d+(?:\.\d+)?)\s*;/); assert.ok(match, 'RENDER_ORDER_AT_Z_ZERO must be declared'); return Number(match[1]); } // --------------------------------------------------------------------------- // Static / fixed renderOrder values // --------------------------------------------------------------------------- test('lane quads use renderOrder 1', () => { assert.match( src(), /lane\.renderOrder\s*=\s*1\s*;/, 'lane quads must use renderOrder = 1 (bottom-most visible layer)', ); }); test('fret dividers use renderOrder 2', () => { assert.match( src(), /div\.renderOrder\s*=\s*2\s*;/, 'fret dividers must use renderOrder = 2, above lane (1)', ); }); test('fret inlay dots use renderOrder 3, above lane (1) and dividers (2)', () => { // The translucent lane would otherwise paint over and hide the inlay. // The dots must draw after the lane/dividers but stay below the depth-layer stack. assert.match( src(), /d\.renderOrder\s*=\s*3\s*;/, 'fret inlay dots must use renderOrder = 3 so the lane no longer hides them', ); }); test('string-line glows use renderOrder 7, above sus-rails (4/5)', () => { // The in-lane string glow lines sit at 7 — above sus-rail bloom (4) and // core (5) so the glow is visible, but below chord fill (chordFrameRenderOrder-4, // min=44) so chord interiors don't disappear behind glow overdraw. assert.match( src(), /line\.renderOrder\s*=\s*7\s*;/, 'string glow lines must use renderOrder = 7', ); }); test('board-projection frame mesh uses renderOrder 14', () => { // The fretboard projection plane sits above string glows (7) but below // chord fill (min 44). Value 14 keeps it sandwiched cleanly. // Anchor to the board-projection pool (projMeshArr = activePalette.map(...)) // so the assertion only passes when THAT block seeds renderOrder = 14 — // not any unrelated renderOrder = 14 elsewhere in the source. const boardProjRO = /projMeshArr\s*=\s*activePalette\.map\b[\s\S]{0,1200}?m\.renderOrder\s*=\s*14\s*;/; assert.match( src(), boardProjRO, 'board-projection pool (projMeshArr) must seed meshes with renderOrder = 14', ); const boardMatch = src().match(boardProjRO); assert.ok(boardMatch, 'board projection mesh must be assigned renderOrder = 14'); }); test('string mesh in buildBoard uses the named board-string layer', () => { // The physical string cylinders/planes rendered on the fretboard sit above // the note-gem layers but below fret wires. assert.match( src(), /mesh\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*0\s*,\s*'BOARD_STRING'\s*\)\s*;/, 'buildBoard string mesh must use BOARD_STRING', ); assert.ok(layerIndex('BOARD_STRING') > layerIndex('TECHNIQUE_MARKER')); assert.ok(layerIndex('BOARD_STRING') < layerIndex('BOARD_FRET_WIRE')); }); test('static fret wires use bowed TubeGeometry + MeshStandardMaterial, named board-fret-wire layer, depthTest+depthWrite false, default gray 0x666688', () => { // Fret wires are a single shared, bowed TubeGeometry (backported from // highway_babylon): a CatmullRom curve whose middle pushes away from the // camera by FRET_BOW_DZ so the row of frets reads as wrapping a cylindrical // neck. T.Line is avoided — WebGL ignores linewidth > 1px so a Line always // renders as a hairline. The lit MeshStandardMaterial lets scene light glint // across the rounded surface (gold in-anchor → brass). depthTest:false is // required: the string BoxGeometry (MeshStandardMaterial, depthWrite:true) // writes depth at Z = +STR_THICK/2, so fret wires near Z=0 would fail the // depth test at string pixels despite the higher layer; depthWrite:false // keeps the transparent fret from polluting depth for later overlays. const s = src(); assert.match( s, /new\s+T\.TubeGeometry\(\s*tubeCurve\s*,\s*FRET_TUBE_SEG\s*,\s*FRET_TUBE_RADIUS\s*,\s*FRET_TUBE_RADIAL\s*,\s*false\s*,?\s*\)/, 'buildBoard fret wires must use a TubeGeometry built from tubeCurve + FRET_TUBE_* params', ); assert.match( s, /new\s+T\.CatmullRomCurve3\(\s*tubePath\s*\)/, 'buildBoard fret tube must follow a CatmullRomCurve3 through the bowed path', ); assert.match( s, /FRET_BOW_DZ\s*\*\s*zm/, 'fret tube path must bow in Z by FRET_BOW_DZ so the neck reads as curved', ); assert.match( s, /new\s+T\.Mesh\(\s*fretTubeGeo\s*,\s*mat\s*\)/, 'buildBoard fret wires must reuse the shared fretTubeGeo (not T.Line)', ); assert.match( s, /fw\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*0\s*,\s*'BOARD_FRET_WIRE'\s*\)\s*;/, 'buildBoard fret wire mesh must use BOARD_FRET_WIRE', ); assert.match( s, /new\s+T\.MeshStandardMaterial\(/, 'fret wires must use MeshStandardMaterial so scene light shades the metal', ); assert.match( s, /color\s*:\s*0x666688/, 'fret wire material must have default gray color 0x666688', ); // Both depth flags asserted independently so the test doesn't pin property // order in the material literal. assert.match( s, /depthTest\s*:\s*false/, 'fret wire material must set depthTest: false', ); assert.match( s, /depthWrite\s*:\s*false/, 'fret wire material must set depthWrite: false (no z-buffer pollution)', ); assert.match( s, /fretWireMats\s*\[\s*f\s*\]\s*=\s*mat\s*;/, 'buildBoard must store each wire material in fretWireMats[f]', ); }); test('update() sets fret wire gold (0xD8A636) for in-anchor frets, gray (0x666688) otherwise', () => { // Uses anchorLaneBoundsAt() — the same helper the dynamic lane uses — // so fret wire highlight aligns exactly with the lane edges: // dMin = fret - 1, dMax = fret + width - 1 // Example: { fret: 3, width: 4 } → dMin=2, dMax=6 → wires 2..6 gold. const s = src(); assert.match( s, /fretWireMats\.length/, 'update() must guard the per-frame fret wire loop on fretWireMats.length', ); assert.match( s, /anchorLaneBoundsAt\(\s*anchors\s*,\s*now\s*\)/, 'update() must use anchorLaneBoundsAt(anchors, now) to get fret wire range', ); assert.match( s, /_m\.color\.setHex\(\s*0xD8A636\s*\)/, 'update() must set gold 0xD8A636 for in-anchor fret wires', ); assert.match( s, /_m\.color\.setHex\(\s*0x666688\s*\)/, 'update() must set gray 0x666688 for out-of-anchor fret wires', ); assert.match( s, /_fwBounds\.dMin/, 'update() must use dMin from anchorLaneBoundsAt (= fret - 1)', ); assert.match( s, /_fwBounds\.dMax/, 'update() must use dMax from anchorLaneBoundsAt (= fret + width - 1)', ); }); test('fret-column markers use Z-proportional renderOrder between chord frame and gem', () => { // pFretColMarker labels use the named stack: one step above chord frame // and one step below note gems at the same depth. // This ensures chord frame borders never overdraw the label and the label // never overdraws gems, at every Z position across the lookahead window. assert.match( src(), /sp\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*z\s*,\s*'FRET_COLUMN'\s*\)\s*;/, 'pFretColMarker renderOrder must use renderOrderForLayerAtZ(z, FRET_COLUMN)', ); assert.ok(layerIndex('FRET_COLUMN') > layerIndex('CHORD_FRAME')); assert.ok(layerIndex('FRET_COLUMN') < layerIndex('NOTE_OUTLINE')); }); test('technique labels and ghost-fret overlay use renderOrder 1000', () => { // 1000 is well above the entire Z-proportional range and the // string/cadence layer — labels must always be readable. const matches = src().match(/m\.renderOrder\s*=\s*1000\s*;/g) || []; assert.ok( matches.length >= 2, 'at least two renderOrder = 1000 assignments must exist (technique labels + ghost fret)', ); }); // --------------------------------------------------------------------------- // Z-proportional formulas — chord frame / note gem / technique marker // --------------------------------------------------------------------------- test('chordFrameRenderOrder uses renderOrderForLayerAtZ(z, CHORD_FRAME)', () => { // Per-chord frame renderOrder mirrors the note-gem scale with an earlier // layer from RENDER_ORDER_LAYER_STACK. assert.match( src(), /const\s+chordFrameRenderOrder\s*=\s*renderOrderForLayerAtZ\(\s*z\s*,\s*'CHORD_FRAME'\s*\)\s*;/, 'chordFrameRenderOrder must use renderOrderForLayerAtZ(z, CHORD_FRAME)', ); assert.match(src(), /const\s+RENDER_ORDER_LAYER_INDEX\s*=\s*Object\.freeze\(\s*RENDER_ORDER_LAYER_STACK\.reduce\(/); assert.match(src(), /const\s+layerIndex\s*=\s*RENDER_ORDER_LAYER_INDEX\[layerName\]\s*;/); assert.match(src(), /if\s*\(\s*layerIndex\s*===\s*undefined\s*\)\s*throw\s+new\s+Error\(`Unknown 3D highway depth layer: \$\{layerName\}`\)\s*;/); assert.match(src(), /const\s+depthRenderOrder\s*=\s*Math\.max\(\s*RENDER_ORDER_FAR_CLAMP\s*,\s*Math\.round\(\s*RENDER_ORDER_AT_Z_ZERO\s*\+\s*worldZ\s*\/\s*K\s*\)\s*\)\s*;/); // Layer is a sub-unit fraction so the integer depth bucket strictly // dominates (a farther object can't outrank a nearer one via a higher // layer); the layer only breaks ties within the same depth bucket. assert.match(src(), /return\s+depthRenderOrder\s*\+\s*layerIndex\s*\/\s*RENDER_ORDER_LAYER_STACK\.length\s*;/); assert.ok(layerIndex('CHORD_FRAME') < layerIndex('NOTE_OUTLINE')); }); test('note outline uses renderOrderForLayerAtZ(noteZ, NOTE_OUTLINE)', () => { // Per-note gem renderOrder. noteZ is negative (ahead of hit line → negative // Z in world space). At noteZ=0 (on the hit line), the note outline uses // the near render-order base plus its layer index; far notes clamp to the // far render-order base plus that same layer index. // The ordered layer list keeps gems above chord frames everywhere. assert.match( src(), /outline\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*noteZ\s*,\s*'NOTE_OUTLINE'\s*\)\s*;/, 'note outline must use renderOrderForLayerAtZ(noteZ, NOTE_OUTLINE)', ); assert.strictEqual(layerIndex('CHORD_FILL'), 0); }); test('techniqueMarkerRenderOrder uses the named technique marker layer above gem core', () => { // Technique markers (PM cross, bend arrow, H/P chevron, etc.) must overlay // the gem itself. assert.match( src(), /const\s+techniqueMarkerRenderOrder\s*=\s*renderOrderForLayerAtZ\(\s*noteZ\s*,\s*'TECHNIQUE_MARKER'\s*\)/, 'techniqueMarkerRenderOrder must use TECHNIQUE_MARKER', ); assert.ok(layerIndex('TECHNIQUE_MARKER') > layerIndex('NOTE_CORE')); }); // --------------------------------------------------------------------------- // Intra-chord layering (chord fill < PM/FH fill < PM/FH lines < frame edge) // --------------------------------------------------------------------------- test('chord fill interior uses the named layer below chord frame', () => { // The translucent chord-box fill sits below the frame edge so the edge // always wins when both cover the same pixel. assert.match( src(), /fill\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*z\s*,\s*'CHORD_FILL'\s*\)\s*;/, 'chord fill must use CHORD_FILL', ); assert.ok(layerIndex('CHORD_FILL') < layerIndex('CHORD_FRAME')); }); test('PM/FH X fill (pPMXFill / pFHXFill) uses its ordered layer', () => { // The black background fill of the muted-note X symbol is above chord fill // but below the X lines — same chord, so same chord-frame renderOrder base. const matches = src().match(/xf\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*z\s*,\s*'CHORD_STRUM_FILL'\s*\)\s*;/g) || []; assert.ok( matches.length >= 2, 'both PM and FH X-fill meshes must use CHORD_STRUM_FILL (found ' + matches.length + ')', ); assert.ok(layerIndex('CHORD_FILL') < layerIndex('CHORD_STRUM_FILL')); assert.ok(layerIndex('CHORD_STRUM_FILL') < layerIndex('CHORD_STRUM_LINE')); }); test('PM/FH X lines (pMuteXLines / pFHXLines) use their ordered layer', () => { // The coloured X stroke lines are above the black fill but below // the chord frame border edge, so they don't escape the box. const matches = src().match(/xl\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*z\s*,\s*'CHORD_STRUM_LINE'\s*\)\s*;/g) || []; assert.ok( matches.length >= 2, 'both PM and FH X-line meshes must use CHORD_STRUM_LINE (found ' + matches.length + ')', ); assert.ok(layerIndex('CHORD_STRUM_LINE') < layerIndex('CHORD_FRAME')); }); test('chord frame glow uses the layer after chord frame', () => { // Accent glow draws after the frame while still remaining below connectors // and note symbols in the ordered layer list. assert.match( src(), /b\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*z\s*,\s*'CHORD_EDGE_GLOW'\s*\)\s*;/, 'chord frame edge slabs must use CHORD_EDGE_GLOW', ); assert.ok(layerIndex('CHORD_EDGE_GLOW') > layerIndex('CHORD_FRAME')); assert.ok(layerIndex('CHORD_EDGE_GLOW') < layerIndex('CONNECTOR_LINE')); }); // --------------------------------------------------------------------------- // Sustain-trail strip & ribbon — always below chord frame of same depth // --------------------------------------------------------------------------- test('sus-trail strip renderOrder formula keeps trails strictly below chord frames at same Z', () => { // Sustain trails use the ordered layer immediately below chord frames at // the same depth. assert.match( src(), /const\s+trailRenderOrder\s*=\s*renderOrderForLayerAtZ\(\s*Math\.min\(\s*0\s*,\s*zCenter\s*\)\s*,\s*'SUSTAIN_TRAIL'\s*\)\s*;/, 'sus-trail strip renderOrder must use renderOrderForLayerAtZ(min zCenter, SUSTAIN_TRAIL)', ); assert.ok(layerIndex('SUSTAIN_TRAIL') < layerIndex('CHORD_FRAME')); }); test('sus-trail ribbon renderOrder formula mirrors strip formula using time-based depth', () => { // Ribbons use _ribDt (time from now to ribbon midpoint) converted to the // same Z scale as dZ() on the sustain-trail layer. assert.match( src(), /const\s+ribbonRenderOrder\s*=\s*renderOrderForLayerAtZ\(\s*-\s*_ribDt\s*\*\s*TS\s*,\s*'SUSTAIN_TRAIL'\s*\)\s*;/, 'sus-trail ribbon renderOrder must use renderOrderForLayerAtZ on the sustain-trail layer', ); }); // --------------------------------------------------------------------------- // Note gem ordering (outline < core, both driven by named depth layers) // --------------------------------------------------------------------------- test('note gem outline uses the named outline layer', () => { assert.match( src(), /outline\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*noteZ\s*,\s*'NOTE_OUTLINE'\s*\)\s*;/, 'note gem outline must use NOTE_OUTLINE', ); assert.ok(layerIndex('NOTE_OUTLINE') > layerIndex('FRET_COLUMN')); }); test('note gem core uses the named layer above outline', () => { assert.match( src(), /core\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*noteZ\s*,\s*'NOTE_CORE'\s*\)\s*;/, 'note gem core must use NOTE_CORE', ); assert.ok(layerIndex('NOTE_CORE') > layerIndex('NOTE_OUTLINE')); }); // --------------------------------------------------------------------------- // Key relative-ordering invariants (derived constants) // --------------------------------------------------------------------------- test('chord frame layer is below note outline layer', () => { // Chord frames must always render below note gems, even at maximum depth // (far end of the lookahead). // assert.ok(layerIndex('CHORD_FRAME') < layerIndex('NOTE_OUTLINE')); }); test('fret labels are above note symbols in the named stack', () => { assert.ok(layerIndex('NOTE_FRET_LABEL') > layerIndex('NOTE_CORE'), 'note fret labels must draw above gem core'); assert.ok(layerIndex('NOTE_FRET_LABEL') > layerIndex('TECHNIQUE_MARKER'), 'note fret labels must draw above technique markers'); assert.ok(layerIndex('ARP_NOTE_FRET_LABEL') > layerIndex('NOTE_FRET_LABEL'), 'arp labels retain a one-layer tie-breaker'); assert.ok(layerIndex('CHORD_FRET_LABEL') > layerIndex('NOTE_CORE'), 'chord-loop fret labels must draw above gem core at the same depth'); assert.ok(layerIndex('NOTE_FRET_LABEL') > layerIndex('BOARD_FRET_WIRE'), 'note fret labels must clear static fret wires'); assert.ok(layerIndex('CHORD_FRET_LABEL') > layerIndex('BOARD_FRET_WIRE'), 'chord fret labels must clear static fret wires'); }); test('string mesh layer is above note symbols and below labels', () => { // Board strings are never occluded by flying gems, but labels still appear above strings. const s = src(); assert.match(s, /mesh\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*0\s*,\s*'BOARD_STRING'\s*\)\s*;/, 'string mesh must use BOARD_STRING'); // Confirm 1000 also exists (labels above strings) assert.match(s, /m\.renderOrder\s*=\s*1000\s*;/, 'technique label renderOrder 1000 must exist'); assert.ok(layerIndex('BOARD_STRING') > layerIndex('TECHNIQUE_MARKER'), 'string mesh layer must be above note symbols'); assert.ok(layerIndex('BOARD_STRING') < layerIndex('NOTE_FRET_LABEL'), 'string mesh layer must be below fret labels'); }); test('fret-column marker layer is above chord frame and below gem outline', () => { assert.ok(layerIndex('FRET_COLUMN') > layerIndex('CHORD_FRAME'), 'fret-column marker layer must be above chord frame'); assert.ok(layerIndex('FRET_COLUMN') < layerIndex('NOTE_OUTLINE'), 'fret-column marker layer must be below gem outline'); assert.match(src(), /renderOrderForLayerAtZ\(\s*z\s*,\s*'FRET_COLUMN'\s*\)/); }); test('static fret wire layer is above string mesh and note symbols', () => { // Structural invariant: fret wires must always draw after (on top of) strings. assert.ok(layerIndex('BOARD_FRET_WIRE') > layerIndex('BOARD_STRING'), 'fret wire must be above string mesh'); assert.ok(layerIndex('BOARD_FRET_WIRE') > layerIndex('TECHNIQUE_MARKER'), 'fret wire must be above note symbols'); assert.ok(zZeroRenderOrder() + layerIndex('BOARD_FRET_WIRE') < 1000, 'fret wire must be below technique labels (1000)'); assert.match(src(), /fw\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*0\s*,\s*'BOARD_FRET_WIRE'\s*\)\s*;/, 'buildBoard fret wire must use BOARD_FRET_WIRE'); assert.match(src(), /mesh\.renderOrder\s*=\s*renderOrderForLayerAtZ\(\s*0\s*,\s*'BOARD_STRING'\s*\)\s*;/, 'string mesh must use BOARD_STRING'); });