import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; const controller = await readFile(new URL('../src/input/CameraController.ts', import.meta.url), 'utf8'); const html = await readFile(new URL('../index.html', import.meta.url), 'utf8'); const main = await readFile(new URL('../src/main.ts', import.meta.url), 'utf8'); const historyShader = await readFile(new URL('../src/shaders/region-history.frag', import.meta.url), 'utf8'); const mosaicShader = await readFile(new URL('../src/shaders/mosaic.frag', import.meta.url), 'utf8'); assert.match(controller, /maxOffset = 0\.5/); assert.match(controller, /clamp\(value, 0, 0\.9\)/); assert.match(controller, /multiplyScalar\(sensitivity\)/); assert.match(html, /id="camera-offset" type="range" min="0" max="0\.9" step="0\.01" value="0\.5"/); assert.match(html, /id="sensitivity" type="range" min="0\.2" max="2"/); assert.match(html, /Camera Offset 0\.50/); assert.match(html, /권장 0\.35–0\.65 · 시점 이동 범위/); assert.match(mosaicShader, /edgeTaper=style<=\.5\?1\.25:\.75/); assert.match(mosaicShader, /historyTexel=clamp\(floor\(fract\(gridUv\)\*8\.\),vec2\(0\.\),vec2\(7\.\)\)/); assert.equal(Math.floor(0.45 * 8), 3, 'split sample just left of the half-cell boundary stays in texel 3'); assert.equal(Math.floor(0.50 * 8), 4, 'split sample at the half-cell boundary enters texel 4'); const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); const cameraPosition = (target, sensitivity, maxOffset) => clamp(target * sensitivity, -1, 1) * maxOffset; assert.equal(cameraPosition(1, 1, 0.5), 0.5, 'default offset reaches the configured midpoint'); assert.equal(cameraPosition(1, 1, 0.9), 0.9, 'maximum offset reaches 0.9'); assert.equal(cameraPosition(1, 2, 0.9), 0.9, 'sensitivity does not exceed the view clamp'); assert.equal(clamp(1, 0, 0.9), 0.9, 'UI maximum matches controller clamp'); const signatureStart = main.indexOf('function currentSplitHistorySignature()'); const signatureEnd = main.indexOf('\nfunction clearSplitHistory()', signatureStart); assert.ok(signatureStart >= 0 && signatureEnd > signatureStart, 'history reset signature exists'); const signature = main.slice(signatureStart, signatureEnd); for (const key of ['mode', 'effectInput.value', 'mosaicStyleInput.value', 'active.uuid', 'depth.uuid', 'uRegionAvailable', 'uGridSize', 'uStrength', 'uRegionStrength', 'uCurvature', 'uPaneSize', 'matrixWorld', 'uDepthScale', 'uDepthBias', 'renderTarget.width', 'regionAtlasTarget.width', 'regionPaletteTarget.width']) { assert.ok(signature.includes(key), `model-reset signature includes ${key}`); } assert.doesNotMatch(signature, /uContentViewProjection|cameraController|uContentCameraPosition/, 'camera movement must not reset split history'); assert.match(main, /const writeIndex = 1 - splitHistoryRead/); assert.match(main, /uPreviousHistory\.value = splitHistoryTargets\[splitHistoryRead\]\.texture/); assert.match(main, /uHistoryBlend\.value = splitHistoryInitialized \? 1 - Math\.exp\(-dt \/ 0\.07\) : 1/); assert.match(main, /updateSplitHistory\(dt\); renderer\.setRenderTarget\(null\); renderer\.render\(scene, camera\)/, 'inactive view updates the signature and clears stale history'); assert.match(main, /splitHistoryTargets\.forEach\(\(target\) => target\.dispose\(\)\)/); assert.match(main, /target\.setSize\(atlasWidth, atlasHeight\)/); assert.match(historyShader, /if\(uHistoryBlend>=\.9999\)\{gl_FragColor=current;return;\}/, 'first frame avoids sampling uninitialized history'); assert.match(historyShader, /gl_FragColor=mix\(previous,current,clamp\(uHistoryBlend,0\.,1\.\)\)/); function advance(fps, seconds, initial, target) { let value = initial; const dt = 1 / fps; for (let frame = 0; frame < Math.round(fps * seconds); frame++) { const alpha = 1 - Math.exp(-dt / 0.07); value += (target - value) * alpha; } return value; } const cadence = [30, 60, 120].map(fps => advance(fps, 0.7, 0, 1)); assert.ok(Math.max(...cadence) - Math.min(...cadence) < 1e-12, `30/60/120Hz history cadence differs: ${cadence}`); assert.ok(Math.abs(cadence[0] - (1 - Math.exp(-0.7 / 0.07))) < 1e-12, 'history follows the 70ms exponential time constant'); assert.ok(cadence[0] > 0.999, 'a settled model reaches its target after 700ms'); const color = [0.8, 0.25, 0.1], confidence = 0.6; const premultiplied = [...color.map(channel => channel * confidence), confidence]; assert.deepEqual(premultiplied.slice(0, 3).map(channel => channel / premultiplied[3]), color, 'split color is stored premultiplied by confidence'); const previous = [0, 0, 0, 0], blend = 0.25; const filtered = previous.map((channel, index) => channel + (premultiplied[index] - channel) * blend); assert.equal(filtered[3], confidence * blend); assert.deepEqual(filtered.slice(0, 3).map(channel => channel / filtered[3]), color, 'filtered premultiplied color retains its hue'); console.log(`Camera/history checks passed: default/max clamp, model reset keys, ping-pong and disposal, premultiplied color, cadence ${cadence.map(v => v.toFixed(6)).join('/')}, 70ms settling.`);