diff --git a/plugins/highway_3d/plugin.json b/plugins/highway_3d/plugin.json index bc61390..5800bd9 100644 --- a/plugins/highway_3d/plugin.json +++ b/plugins/highway_3d/plugin.json @@ -1,7 +1,7 @@ { "id": "highway_3d", "name": "3D Highway", - "version": "3.36.0", + "version": "3.37.0", "type": "visualization", "scriptType": "module", "bundled": true, diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index 8c2b5ae..2f77983 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -6,7 +6,7 @@ // main player and per-panel in splitscreen without any architectural // changes. -import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, computeBPM, _makeGaussTex } from './src/geometry.js'; // h3d-carve-1 +import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, computeBPM, _makeGaussTex, RENDER_ORDER_LAYER_STACK, RENDER_ORDER_LAYER_INDEX, RENDER_ORDER_AT_Z_ZERO, RENDER_ORDER_FAR_CLAMP, renderOrderForLayerAtZ, _noteKey, lowerBoundT, hwyFirstRelevantFrettedTime, geoFretMid } from './src/geometry.js'; // h3d-carve-1b (function () { 'use strict'; @@ -971,58 +971,8 @@ import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, compute /** Note travel speed. */ const TS = 230 * K; - const RENDER_ORDER_LAYER_STACK = Object.freeze([ - 'CHORD_FILL', - 'CHORD_STRUM_FILL', - 'CHORD_STRUM_LINE', - 'SUSTAIN_TRAIL', - 'CHORD_FRAME', - 'CHORD_EDGE_GLOW', - 'CONNECTOR_LINE', - 'FRET_COLUMN', - 'ARP_CONNECTOR_LINE', - 'NOTE_OUTLINE', - 'NOTE_CORE', - 'TECHNIQUE_MARKER', - 'BOARD_STRING', - 'BOARD_FRET_WIRE', - 'NOTE_FRET_LABEL', - 'ARP_NOTE_FRET_LABEL', - 'CHORD_FRET_LABEL', - ]); - const RENDER_ORDER_LAYER_INDEX = Object.freeze(RENDER_ORDER_LAYER_STACK.reduce( - (indexByLayer, layerName, layerIndex) => { - indexByLayer[layerName] = layerIndex; - return indexByLayer; - }, - Object.create(null) - )); - - const RENDER_ORDER_AT_Z_ZERO = 700; - const RENDER_ORDER_FAR_CLAMP = 50; - - /** - * Computes renderOrder from world depth plus a named layer. - * Closer objects receive larger values and paint over farther objects; the - * layer stack breaks ties at the same depth, keeping labels above note gems. - * - * The layer index is added as a sub-unit fraction (< 1) so the integer - * depth bucket STRICTLY dominates: a farther object can never outrank a - * nearer one merely because it sits on a higher layer. Adding the raw index - * (0..N-1) directly would let the ~N-wide layer span leak across depth - * buckets and re-introduce far-over-near bleed for notes within ~N draw - * units of each other. Fraction granularity (1/N ≈ 0.06) stays well above - * the 0.0001 intra-element sub-increments used at some call sites. - */ - function renderOrderForLayerAtZ(worldZ, layerName) { - const layerIndex = RENDER_ORDER_LAYER_INDEX[layerName]; - if (layerIndex === undefined) throw new Error(`Unknown 3D highway depth layer: ${layerName}`); - const depthRenderOrder = Math.max( - RENDER_ORDER_FAR_CLAMP, - Math.round(RENDER_ORDER_AT_Z_ZERO + worldZ / K) - ); - return depthRenderOrder + layerIndex / RENDER_ORDER_LAYER_STACK.length; - } + // RENDER_ORDER_LAYER_STACK, RENDER_ORDER_LAYER_INDEX, RENDER_ORDER_AT_Z_ZERO, + // RENDER_ORDER_FAR_CLAMP, renderOrderForLayerAtZ — moved to src/geometry.js (h3d-carve-1b). /** Match `nextNoteByString` onset to this note (float + chart rounding; avoids ghost / glow flicker). */ const NEXT_ON_STRING_T_EPS = 0.06; @@ -1285,80 +1235,7 @@ import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, compute return cadence.slice(i0, i1); } - // Fast integer key for (t, s) pairs — avoids per-frame string allocation in - // hot-path Set lookups. Encodes chart time in 0.1 ms steps (sufficient for - // chart-format note precision) combined with the string index. - // t range 0–600 s → 0–6,000,000; * 10 + s(0–7) = max 60,000,007 < 2^53 ✓. - // The |0 truncates to int32 but the outer multiply stays in float64, so the - // key is always a safe JS integer for songs ≤ 214,748 s (well above any song). - function _noteKey(t, s) { return ((t * 10000 + 0.5) | 0) * 10 + s; } - - // Binary lower-bound: returns the first index i in arr where arr[i].t >= t. - // Assumes arr is sorted ascending by .t (bundle.notes / bundle.chords always are). - // Byte-identical to core's bundle.lowerBoundT — kept as a local because this - // plugin must run on downlevel hosts whose bundles don't carry the helper - // (it's called from ~30 sites incl. top-level helpers that don't receive a - // bundle). New code that already holds a bundle should prefer - // bundle.lowerBoundT / bundle.lowerBoundTime. - function lowerBoundT(arr, t) { - let lo = 0, hi = arr.length; - while (lo < hi) { - const mid = (lo + hi) >>> 1; - if (arr[mid].t < t) lo = mid + 1; - else hi = mid; - } - return lo; - } - - /** - * Return the chart time of the first fretted event that can still affect - * the camera at `now`, or the next fretted onset after it. - * - * This is intentionally a one-time full-chart scan. It runs only when a - * new song/arrangement's arrays first arrive, allowing the camera to frame - * the opening phrase during a silent intro instead of waiting for that - * phrase to enter the live targeting window. Open strings do not define a - * horizontal fret target, and malformed/out-of-range strings are ignored. - * - * Events already inside the behind-window, plus older sustains that are - * still ringing at `now`, return `now` so bootstrap framing matches the - * ordinary live path. Future events return their onset time. - */ - function hwyFirstRelevantFrettedTime(notes, chords, now, behind, stringCount) { - const nStrings = Number.isFinite(stringCount) ? Math.max(0, Math.floor(stringCount)) : 0; - const cameraFloor = now - Math.max(0, Number(behind) || 0); - let first = Infinity; - - const validFretted = n => n - && n.f > 0 - && Number.isInteger(n.s) - && n.s >= 0 - && n.s < nStrings; - const consider = (eventTime, sustain) => { - const t = Number(eventTime); - if (!Number.isFinite(t)) return; - const sus = Number(sustain); - const end = t + (Number.isFinite(sus) && sus > 0 ? sus : 0); - if (t < cameraFloor && end < now) return; - const relevantTime = t <= now ? now : t; - if (relevantTime < first) first = relevantTime; - }; - - if (notes) { - for (const n of notes) { - if (validFretted(n)) consider(n.t, n.sus); - } - } - if (chords) { - for (const ch of chords) { - if (!ch || !ch.notes) continue; - for (const cn of ch.notes) { - if (validFretted(cn)) consider(ch.t, cn.sus); - } - } - } - return Number.isFinite(first) ? first : null; - } + // _noteKey, lowerBoundT, hwyFirstRelevantFrettedTime — moved to src/geometry.js (h3d-carve-1b). // Last arrangement at or before chart time `t` (sorted by .time). // Mirrors static/highway.js getAnchorAt — until t reaches the first anchor’s @@ -1540,7 +1417,7 @@ import { geoFretX, dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, compute _bgEmitChange('fretSpacing'); }; - const fretMid = f => (f <= 0 ? -2 * K : (fretX(f - 1) + fretX(f)) / 2); + const fretMid = f => geoFretMid(f, _h3dFretUniform); // h3d-carve-1b: delegator (1 beyond-subst) /** World-space width of fret column (wires f−1 .. f); used to scale row markers past ~12. */ function fretColumnWorldW(f) { const fi = Math.round(Number(f)); diff --git a/plugins/highway_3d/src/geometry.js b/plugins/highway_3d/src/geometry.js index 04492e7..5f71f76 100644 --- a/plugins/highway_3d/src/geometry.js +++ b/plugins/highway_3d/src/geometry.js @@ -107,6 +107,150 @@ export function computeBPM(beats, t) { return count > 0 && sum > 0 ? 60 / (sum / count) : 120; } +// ── Render-order layer stack — h3d-carve-1b ─────────────────────────────────── + +export const RENDER_ORDER_LAYER_STACK = Object.freeze([ + 'CHORD_FILL', + 'CHORD_STRUM_FILL', + 'CHORD_STRUM_LINE', + 'SUSTAIN_TRAIL', + 'CHORD_FRAME', + 'CHORD_EDGE_GLOW', + 'CONNECTOR_LINE', + 'FRET_COLUMN', + 'ARP_CONNECTOR_LINE', + 'NOTE_OUTLINE', + 'NOTE_CORE', + 'TECHNIQUE_MARKER', + 'BOARD_STRING', + 'BOARD_FRET_WIRE', + 'NOTE_FRET_LABEL', + 'ARP_NOTE_FRET_LABEL', + 'CHORD_FRET_LABEL', +]); +export const RENDER_ORDER_LAYER_INDEX = Object.freeze(RENDER_ORDER_LAYER_STACK.reduce( + (indexByLayer, layerName, layerIndex) => { + indexByLayer[layerName] = layerIndex; + return indexByLayer; + }, + Object.create(null) +)); + +export const RENDER_ORDER_AT_Z_ZERO = 700; +export const RENDER_ORDER_FAR_CLAMP = 50; + +/** + * Computes renderOrder from world depth plus a named layer. + * Closer objects receive larger values and paint over farther objects; the + * layer stack breaks ties at the same depth, keeping labels above note gems. + * + * The layer index is added as a sub-unit fraction (< 1) so the integer + * depth bucket STRICTLY dominates: a farther object can never outrank a + * nearer one merely because it sits on a higher layer. Adding the raw index + * (0..N-1) directly would let the ~N-wide layer span leak across depth + * buckets and re-introduce far-over-near bleed for notes within ~N draw + * units of each other. Fraction granularity (1/N ≈ 0.06) stays well above + * the 0.0001 intra-element sub-increments used at some call sites. + */ +export function renderOrderForLayerAtZ(worldZ, layerName) { + const layerIndex = RENDER_ORDER_LAYER_INDEX[layerName]; + if (layerIndex === undefined) throw new Error(`Unknown 3D highway depth layer: ${layerName}`); + const depthRenderOrder = Math.max( + RENDER_ORDER_FAR_CLAMP, + Math.round(RENDER_ORDER_AT_Z_ZERO + worldZ / K) + ); + return depthRenderOrder + layerIndex / RENDER_ORDER_LAYER_STACK.length; +} + +// ── Note key and binary search — h3d-carve-1b ───────────────────────────────── + +// Fast integer key for (t, s) pairs — avoids per-frame string allocation in +// hot-path Set lookups. Encodes chart time in 0.1 ms steps (sufficient for +// chart-format note precision) combined with the string index. +// t range 0–600 s → 0–6,000,000; * 10 + s(0–7) = max 60,000,007 < 2^53 ✓. +// The |0 truncates to int32 but the outer multiply stays in float64, so the +// key is always a safe JS integer for songs ≤ 214,748 s (well above any song). +export function _noteKey(t, s) { return ((t * 10000 + 0.5) | 0) * 10 + s; } + +// Binary lower-bound: returns the first index i in arr where arr[i].t >= t. +// Assumes arr is sorted ascending by .t (bundle.notes / bundle.chords always are). +// Byte-identical to core's bundle.lowerBoundT — kept as a local because this +// plugin must run on downlevel hosts whose bundles don't carry the helper +// (it's called from ~30 sites incl. top-level helpers that don't receive a +// bundle). New code that already holds a bundle should prefer +// bundle.lowerBoundT / bundle.lowerBoundTime. +export function lowerBoundT(arr, t) { + let lo = 0, hi = arr.length; + while (lo < hi) { + const mid = (lo + hi) >>> 1; + if (arr[mid].t < t) lo = mid + 1; + else hi = mid; + } + return lo; +} + +/** + * Return the chart time of the first fretted event that can still affect + * the camera at `now`, or the next fretted onset after it. + * + * This is intentionally a one-time full-chart scan. It runs only when a + * new song/arrangement's arrays first arrive, allowing the camera to frame + * the opening phrase during a silent intro instead of waiting for that + * phrase to enter the live targeting window. Open strings do not define a + * horizontal fret target, and malformed/out-of-range strings are ignored. + * + * Events already inside the behind-window, plus older sustains that are + * still ringing at `now`, return `now` so bootstrap framing matches the + * ordinary live path. Future events return their onset time. + */ +export function hwyFirstRelevantFrettedTime(notes, chords, now, behind, stringCount) { + const nStrings = Number.isFinite(stringCount) ? Math.max(0, Math.floor(stringCount)) : 0; + const cameraFloor = now - Math.max(0, Number(behind) || 0); + let first = Infinity; + + const validFretted = n => n + && n.f > 0 + && Number.isInteger(n.s) + && n.s >= 0 + && n.s < nStrings; + const consider = (eventTime, sustain) => { + const t = Number(eventTime); + if (!Number.isFinite(t)) return; + const sus = Number(sustain); + const end = t + (Number.isFinite(sus) && sus > 0 ? sus : 0); + if (t < cameraFloor && end < now) return; + const relevantTime = t <= now ? now : t; + if (relevantTime < first) first = relevantTime; + }; + + if (notes) { + for (const n of notes) { + if (validFretted(n)) consider(n.t, n.sus); + } + } + if (chords) { + for (const ch of chords) { + if (!ch || !ch.notes) continue; + for (const cn of ch.notes) { + if (validFretted(cn)) consider(ch.t, cn.sus); + } + } + } + return Number.isFinite(first) ? first : null; +} + +// ── Fret mid — h3d-carve-1b ─────────────────────────────────────────────────── + +/** + * World-space X of the midpoint of fret column f. + * f <= 0 → nut-side sentinel (−2K). + * screen.js keeps `const fretMid = f => geoFretMid(f, _h3dFretUniform);` + * so no call-site changes in screen.js. + * @param {number} f fret number + * @param {boolean} uniform true → uniform spacing; false → logarithmic + */ +export const geoFretMid = (f, uniform) => f <= 0 ? -2 * K : (geoFretX(f - 1, uniform) + geoFretX(f, uniform)) / 2; + // ── Gaussian bloom texture ──────────────────────────────────────────────────── // Build a horizontal gaussian DataTexture for the sustain-rail bloom effect. diff --git a/tests/js/highway_3d_camera_bootstrap.test.js b/tests/js/highway_3d_camera_bootstrap.test.js index bc12193..f68e0d2 100644 --- a/tests/js/highway_3d_camera_bootstrap.test.js +++ b/tests/js/highway_3d_camera_bootstrap.test.js @@ -13,6 +13,9 @@ const path = require('node:path'); const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'); const src = fs.readFileSync(SCREEN_JS, 'utf8'); +// Since h3d-carve-1b, hwyFirstRelevantFrettedTime lives in geometry.js. +const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js'); +const geoSrc = fs.readFileSync(GEOMETRY_JS, 'utf8'); function extractFn(source, name) { const start = source.indexOf('function ' + name); @@ -36,7 +39,7 @@ function sourceBetween(startText, endText) { const hwyFirstRelevantFrettedTime = new Function( '"use strict";' - + extractFn(src, 'hwyFirstRelevantFrettedTime') + + extractFn(geoSrc, 'hwyFirstRelevantFrettedTime') + '\nreturn hwyFirstRelevantFrettedTime;', )(); diff --git a/tests/js/highway_3d_geometry.test.js b/tests/js/highway_3d_geometry.test.js index 92a98c8..23947f1 100644 --- a/tests/js/highway_3d_geometry.test.js +++ b/tests/js/highway_3d_geometry.test.js @@ -99,6 +99,67 @@ test('camLowFretPullbackU is clamped to zero — high fret gives 0 not negative' assert.strictEqual(camLowFretPullbackU(10), 0, 'fret 10: clamped to 0, not -20'); }); +// ── Cut 1b class-killers ─────────────────────────────────────────────────────── + +test('RENDER_ORDER_LAYER_STACK has 17 layers with CHORD_FILL first and CHORD_FRET_LABEL last', async () => { + const { RENDER_ORDER_LAYER_STACK } = await import(GEOMETRY_JS); + assert.strictEqual(RENDER_ORDER_LAYER_STACK.length, 17, 'stack must have exactly 17 layers'); + assert.strictEqual(RENDER_ORDER_LAYER_STACK[0], 'CHORD_FILL', 'first layer must be CHORD_FILL'); + assert.strictEqual(RENDER_ORDER_LAYER_STACK[RENDER_ORDER_LAYER_STACK.length - 1], 'CHORD_FRET_LABEL', 'last layer must be CHORD_FRET_LABEL'); +}); + +test('RENDER_ORDER_LAYER_INDEX maps CHORD_FILL to 0 and NOTE_CORE to 10', async () => { + // Mutation: wrong layer order → NOTE_CORE would not map to 10. + const { RENDER_ORDER_LAYER_INDEX } = await import(GEOMETRY_JS); + assert.strictEqual(RENDER_ORDER_LAYER_INDEX['CHORD_FILL'], 0, 'CHORD_FILL must be index 0 (bottom of stack)'); + assert.strictEqual(RENDER_ORDER_LAYER_INDEX['NOTE_CORE'], 10, 'NOTE_CORE must be index 10'); +}); + +test('renderOrderForLayerAtZ applies the far clamp — worldZ=-5 gives 50 not 33', async () => { + // Mutation: remove Math.max(RENDER_ORDER_FAR_CLAMP, ...) clamp. + // K=2.25/300=0.0075; Math.round(700+(-5)/0.0075)=Math.round(33.33)=33; max(50,33)=50. + // Without clamp: 33 + 0/17 ≈ 33. Test pins the clamped value. + const { renderOrderForLayerAtZ } = await import(GEOMETRY_JS); + assert.strictEqual(renderOrderForLayerAtZ(-5, 'CHORD_FILL'), 50, 'far objects must be clamped to RENDER_ORDER_FAR_CLAMP=50'); +}); + +test('renderOrderForLayerAtZ throws for unknown layer names', async () => { + const { renderOrderForLayerAtZ } = await import(GEOMETRY_JS); + assert.throws(() => renderOrderForLayerAtZ(0, 'NONEXISTENT'), /Unknown 3D highway depth layer/); +}); + +test('_noteKey integer-truncates float times — _noteKey(1.5, 3) is 150003 not 150008', async () => { + // Mutation: drop |0 → (15000.5)*10+3 = 150008. + const { _noteKey } = await import(GEOMETRY_JS); + assert.strictEqual(_noteKey(1.5, 3), 150003, '|0 truncation must give 150003, not float-derived 150008'); + assert.strictEqual(_noteKey(0, 0), 0); +}); + +test('lowerBoundT returns first index where arr[i].t >= t (strict lower bound)', async () => { + // Mutation: < → <= causes lowerBoundT([{t:1},{t:3},{t:5}], 3) → 2 instead of 1. + const { lowerBoundT } = await import(GEOMETRY_JS); + const arr = [{ t: 1 }, { t: 3 }, { t: 5 }]; + assert.strictEqual(lowerBoundT(arr, 3), 1, 'strict lower-bound: first index where .t >= 3 is 1 (not 2)'); + assert.strictEqual(lowerBoundT(arr, 0), 0, 'value before all: must return 0'); + assert.strictEqual(lowerBoundT(arr, 6), 3, 'value after all: must return length'); +}); + +test('hwyFirstRelevantFrettedTime returns null for empty/all-open input', async () => { + const { hwyFirstRelevantFrettedTime } = await import(GEOMETRY_JS); + assert.strictEqual(hwyFirstRelevantFrettedTime([], [], 0, 0.2, 6), null); +}); + +test('geoFretMid returns -2K sentinel for f<=0, positive for f=1', async () => { + // Mutation: drop f<=0 guard → geoFretMid(0, true) returns (0+0)/2=0, not -0.015. + const { geoFretMid } = await import(GEOMETRY_JS); + const K = 2.25 / 300; + assert.ok(Math.abs(geoFretMid(0, true) - (-2 * K)) < 1e-10, 'f=0 must return -2K sentinel (≈-0.015)'); + assert.ok(geoFretMid(1, true) > 0, 'f=1 must return positive X'); + // In uniform mode: geoFretX(0,true)=0, geoFretX(1,true)=step, so mid=step/2. + // geoFretMid(2,true) = (step+2step)/2 = 1.5step. Ratio 3 catches wrong f offset. + assert.ok(Math.abs(geoFretMid(2, true) / geoFretMid(1, true) - 3) < 1e-9, 'uniform mid(2)/mid(1) must equal 3'); +}); + test('_makeGaussTex peak alpha is 255 at the centre pixel', async () => { // Mutation: default sigma changed to 0 → (u-0.5)/0 = NaN chain → all Uint8Array writes // become 0 (TypedArray coerces NaN to 0). Test calls without explicit sigma so the diff --git a/tests/js/highway_3d_panel_controls.test.js b/tests/js/highway_3d_panel_controls.test.js index cc71427..26139f6 100644 --- a/tests/js/highway_3d_panel_controls.test.js +++ b/tests/js/highway_3d_panel_controls.test.js @@ -55,7 +55,7 @@ function loadHighway3dStatics() { }, }, // Geometry stubs — panel-controls test only reads factory statics; - // it never invokes the render path where these are called (h3d-carve-1). + // it never invokes the render path where these are called (h3d-carve-1b). geoFretX: (f, _uniform) => f * 0.1, dZ: dt => -dt, slideTrailEnd: () => null, @@ -63,6 +63,15 @@ function loadHighway3dStatics() { camLowFretPullbackU: () => 0, computeBPM: () => 120, _makeGaussTex: () => ({}), + RENDER_ORDER_LAYER_STACK: Object.freeze([]), + RENDER_ORDER_LAYER_INDEX: Object.freeze(Object.create(null)), + RENDER_ORDER_AT_Z_ZERO: 700, + RENDER_ORDER_FAR_CLAMP: 50, + renderOrderForLayerAtZ: () => 0, + _noteKey: () => 0, + lowerBoundT: () => 0, + hwyFirstRelevantFrettedTime: () => null, + geoFretMid: (f, _uniform) => f * 0.1, }; vm.createContext(sandbox); vm.runInContext(instrumented, sandbox, { filename: SCREEN_JS }); diff --git a/tests/js/highway_3d_render_order.test.js b/tests/js/highway_3d_render_order.test.js index e3bbc23..ea2bb95 100644 --- a/tests/js/highway_3d_render_order.test.js +++ b/tests/js/highway_3d_render_order.test.js @@ -41,6 +41,9 @@ const fs = require('node:fs'); const path = require('node:path'); const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'); +// Since h3d-carve-1b, RENDER_ORDER_* constants and renderOrderForLayerAtZ +// live in geometry.js; screen.js imports them. +const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js'); // --------------------------------------------------------------------------- // Helpers @@ -53,9 +56,16 @@ function src() { return _src; } -/** Parses the declared render-order layer stack from screen.js. */ +let _geo; +/** Returns the cached geometry source (render-order constants + renderOrderForLayerAtZ). */ +function geo() { + if (!_geo) _geo = fs.readFileSync(GEOMETRY_JS, 'utf8'); + return _geo; +} + +/** Parses the declared render-order layer stack from geometry.js. */ function layers() { - const match = src().match(/const\s+RENDER_ORDER_LAYER_STACK\s*=\s*Object\.freeze\(\s*\[([\s\S]*?)\]\s*\)/); + const match = geo().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]); } @@ -70,7 +80,7 @@ function layerIndex(name) { /** 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*;/); + const match = geo().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]); } @@ -301,14 +311,15 @@ test('chordFrameRenderOrder uses renderOrderForLayerAtZ(z, CHORD_FRAME)', () => /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*;/); + // renderOrderForLayerAtZ implementation lives in geometry.js since h3d-carve-1b. + assert.match(geo(), /const\s+RENDER_ORDER_LAYER_INDEX\s*=\s*Object\.freeze\(\s*RENDER_ORDER_LAYER_STACK\.reduce\(/); + assert.match(geo(), /const\s+layerIndex\s*=\s*RENDER_ORDER_LAYER_INDEX\[layerName\]\s*;/); + assert.match(geo(), /if\s*\(\s*layerIndex\s*===\s*undefined\s*\)\s*throw\s+new\s+Error\(`Unknown 3D highway depth layer: \$\{layerName\}`\)\s*;/); + assert.match(geo(), /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.match(geo(), /return\s+depthRenderOrder\s*\+\s*layerIndex\s*\/\s*RENDER_ORDER_LAYER_STACK\.length\s*;/); assert.ok(layerIndex('CHORD_FRAME') < layerIndex('NOTE_OUTLINE')); });