mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-25 14:21:21 +00:00
462 lines
22 KiB
JavaScript
462 lines
22 KiB
JavaScript
// 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');
|
|
});
|