import { test } from 'node:test' import assert from 'node:assert/strict' import * as THREE from 'three' import './test-support.mjs' const { drawingPixelRatio, FramePacing, GpuTimer } = await import('../src/renderDiagnostics.ts') const { createCardMaterial, applyEdgeMaterialControls } = await import('../src/cardMaterial.ts') const { createCardGeometry } = await import('../src/cardGeometry.ts') test('full-resolution rendering retains native pixel ratio up to the existing 2x cap', () => { for (const dpr of [1, 1.25, 2, 3]) { assert.equal(drawingPixelRatio(dpr), Math.min(dpr, 2)) } }) test('callback telemetry distinguishes idle pacing from intervals after drawing', () => { const pacing = new FramePacing() pacing.observe(0) pacing.didDraw(false) pacing.observe(16) pacing.didDraw(true) pacing.observe(49) assert.deepEqual(pacing.idle, [16]) assert.deepEqual(pacing.drawing, [33]) pacing.reset() pacing.observe(5000) assert.deepEqual(pacing.idle, []) assert.deepEqual(pacing.drawing, []) }) function fakeGL(supported = true) { const ext = { TIME_ELAPSED_EXT: 1, GPU_DISJOINT_EXT: 2, QUERY_COUNTER_BITS_EXT: 3 } const reads = [] const deleted = [] const queries = [] let disjoint = false const gl = { QUERY_RESULT_AVAILABLE: 4, QUERY_RESULT: 5, getExtension() { return supported ? ext : null }, getQuery() { return 64 }, createQuery() { const q = { available: false, ns: 4000000 }; queries.push(q); return q }, beginQuery() {}, endQuery() {}, isContextLost() { return false }, getParameter() { return disjoint }, getQueryParameter(query, parameter) { reads.push(parameter) return parameter === this.QUERY_RESULT_AVAILABLE ? query.available : query.ns }, deleteQuery(query) { deleted.push(query) }, } return { gl, queries, reads, deleted, disjoint() { disjoint = true } } } test('GPU timing reads results only after availability, with bounded sampling', () => { const { gl, queries, reads, deleted } = fakeGL() const timer = new GpuTimer(gl) const delivered = [] timer.onSample = (timestampMs, elapsedMs) => delivered.push([timestampMs, elapsedMs]) timer.begin(0) timer.end() timer.poll() assert.deepEqual(reads, [gl.QUERY_RESULT_AVAILABLE]) assert.deepEqual(timer.samples, []) timer.begin(100) timer.end() assert.equal(queries.length, 1) queries[0].available = true timer.poll() assert.deepEqual(timer.samples, [4]) assert.deepEqual(delivered, [[0, 4]]) assert.deepEqual(deleted, [queries[0]]) }) test('GPU disjoint results are discarded and outstanding query count is bounded', () => { const { gl, queries, reads, deleted, disjoint } = fakeGL() const timer = new GpuTimer(gl) for (let i = 0; i < 20; i++) { timer.begin(i * 200); timer.end() } assert.equal(queries.length, 8) queries.forEach(q => { q.available = true }) disjoint() timer.poll() assert.deepEqual(reads, []) assert.deepEqual(timer.samples, []) assert.equal(deleted.length, 8) }) test('unsupported GPU timing adds no queries and reset releases pending work', () => { const unsupported = fakeGL(false) const timer = new GpuTimer(unsupported.gl) timer.begin(0); timer.end(); timer.poll() assert.equal(timer.supported, false) assert.equal(unsupported.queries.length, 0) const supported = fakeGL() const active = new GpuTimer(supported.gl) active.begin(0) active.reset() assert.equal(supported.deleted.length, 1) active.poll() assert.deepEqual(supported.reads, []) }) test('uniform-only edge changes reuse the shader; clearcoat feature changes invalidate it', () => { const edge = new THREE.MeshPhysicalMaterial() applyEdgeMaterialControls(edge, { substrate: 'Paper', condition: 1 }) const version = edge.version applyEdgeMaterialControls(edge, { substrate: 'Metal', condition: 0.5 }) assert.equal(edge.version, version) applyEdgeMaterialControls(edge, { substrate: 'Linen', condition: 1 }) assert.equal(edge.version, version + 1) edge.dispose() }) test('card artwork faces point outward on both sides with backface culling', () => { const textures = Array.from({ length: 3 }, () => new THREE.Texture()) const front = createCardMaterial(...textures, new THREE.CubeTexture(), new THREE.Vector3()) const back = new THREE.MeshPhysicalMaterial() // Default FrontSide, as in the app. const edge = new THREE.MeshPhysicalMaterial() const card = createCardGeometry(front, back, edge) card.updateMatrixWorld(true) const frontFace = card.getObjectByName('CARD_FRONT') const backFace = card.getObjectByName('CARD_BACK') assert.equal(front.side, THREE.FrontSide) assert.equal(back.side, THREE.FrontSide) const normal = new THREE.Vector3(0, 0, 1) assert.ok(normal.clone().transformDirection(frontFace.matrixWorld).z > 0.999) assert.ok(normal.clone().transformDirection(backFace.matrixWorld).z < -0.999) const geometries = new Set([frontFace.geometry, backFace.geometry, card.getObjectByName('CARD_EDGE').geometry]) geometries.forEach(g => g.dispose()) textures.forEach(t => t.dispose()) front.uniforms.studioEnvironment.value.dispose() front.dispose(); back.dispose(); edge.dispose() })