Replace project with current server release and Windows setup

Import the 2026-10-09 source release, Fastify/SQLite backend, restored source
assets, and analytics-enabled server build. Replace files from the previous
release while retaining the existing repository history.

Make builds portable across Windows and Linux, configure local startup,
normalize Windows static asset paths, and fix test fixture cleanup and file
URL handling. Exclude local environment settings, databases, dependencies,
validation output, and TypeScript build cache from version control.

Validation: npm ci, static and server builds, 106 arrival tests, and actual
local HTTP checks passed. The full suite passed 467 of 470 tests; three tests
still require QA fixtures omitted from the supplied release, documented in
README-서버실행.md.
This commit is contained in:
2026-10-09 16:43:35 +09:00
parent 71c8d3b7ff
commit e358e24436
642 changed files with 35202 additions and 17642 deletions
+144
View File
@@ -0,0 +1,144 @@
import {test} from 'node:test';
/** CPU geometry/ray contract checks; these do not execute GLSL or verify WebGL pixels. */
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
import * as THREE from 'three';
const source = new URL('../src/objects/ExperimentalCourtyard.ts', import.meta.url);
assert.ok(existsSync(source), 'the opt-in courtyard module must exist');
const { ExperimentalCourtyard, createCourtyardUniforms, createCourtyardLayout, courtyardShader, courtyardSurfaceShader } = await import(source);
assert.equal(typeof createCourtyardUniforms, 'function', 'B needs independent default-disabled uniforms before a courtyard exists');
const defaults = createCourtyardUniforms();
assert.equal(defaults.uYardEnabled.value, 0);
assert.notEqual(defaults.uYardBoxMin.value, createCourtyardUniforms().uYardBoxMin.value);
// Independent slab traversal matching the shader, compared with actual raster triangles.
function intersectBoxes(layout, origin, ray) {
let closest = null;
for (const box of layout) {
let near = -1e20, far = 1e20, nearAxis = -1, farAxis = -1;
let missed = false;
for (let axis = 0; axis < 3; axis++) {
const p = origin.getComponent(axis), d = ray.getComponent(axis);
if (Math.abs(d) < 1e-8) {
if (p < box.min[axis] || p > box.max[axis]) missed = true;
continue;
}
const a = (box.min[axis] - p) / d, b = (box.max[axis] - p) / d;
if (Math.min(a, b) > near) { near = Math.min(a, b); nearAxis = axis; }
if (Math.max(a, b) < far) { far = Math.max(a, b); farAxis = axis; }
}
if (missed || near > far || far < 1e-5) continue;
const outside = near >= 1e-5, distance = outside ? near : far;
const axis = outside ? nearAxis : farAxis;
const normal = new THREE.Vector3().setComponent(axis, Math.sign(ray.getComponent(axis)) * (outside ? -1 : 1));
if (!closest || distance < closest.distance) closest = { distance, normal, box };
}
return closest;
}
const yard = new ExperimentalCourtyard();
assert.equal(yard.group.visible, false, 'ordinary photo mode must not show the courtyard');
assert.equal(yard.uniforms.uYardEnabled.value, 0);
assert.equal(yard.group.children.length, 1, 'all courtyard surfaces must share one draw');
const mesh = yard.group.children[0];
assert.ok(mesh.isMesh);
assert.equal(mesh.material.uniforms, yard.uniforms, 'raster and B must share live uniform objects');
assert.equal(mesh.material.side, THREE.DoubleSide, 'inside-box hits must match the double-sided raster geometry');
assert.equal(mesh.material.lights, false);
assert.ok(mesh.material.fragmentShader.includes(courtyardSurfaceShader));
assert.ok(courtyardShader.includes(courtyardSurfaceShader), 'direct and refracted colors need identical source');
assert.match(courtyardShader, /bool bCourtyardHit\(vec3 origin,\s*vec3 ray,\s*out float distance,\s*out vec3 color\)/);
assert.ok(!yard.group.children.some(object => object.isLight));
const uniformIdentities = Object.fromEntries(Object.entries(yard.uniforms).map(([key, uniform]) => [key, uniform]));
let seed = 291;
const random = () => ((seed = Math.imul(seed, 1664525) + 1013904223 | 0) >>> 0) / 4294967296;
let rays = 0, hits = 0, maxError = 0;
for (const [width, height] of [[.9, 1.4], [1.8, 1.1], [2.8, .35], [.3, 2.2]]) {
const glassZ = .6;
yard.resize(width, height, glassZ);
yard.setEnabled(true);
yard.group.updateMatrixWorld(true);
const layout = createCourtyardLayout(width, height, glassZ);
assert.ok(layout.length <= 16);
assert.equal(mesh.geometry.getAttribute('position').count, layout.length * 36);
assert.equal(mesh.geometry.groups.length, 0, 'merged geometry must not preserve per-box draw groups');
const floor = layout.find(box => box.name === 'ground');
assert.ok(Math.abs(floor.max[1] - (-height / 2 - .1 * Math.min(width, height))) < 1e-10);
assert.ok(Math.abs(layout.find(box => box.name === 'boundary-wall').max[2] - (glassZ - 3 * height)) < 1e-10);
assert.ok(layout.find(box => box.name === 'wood-platform').max[0] < 0, 'the dog foreground on the right stays clear');
for (let boxIndex = 0; boxIndex < layout.length; boxIndex++) {
const box = layout[boxIndex];
assert.deepEqual(yard.uniforms.uYardBoxMin.value[boxIndex].toArray(), box.min);
assert.deepEqual(yard.uniforms.uYardBoxMax.value[boxIndex].toArray(), box.max);
assert.equal(yard.uniforms.uYardBoxKind.value[boxIndex], box.kind);
assert.ok(box.max[2] < glassZ, `${box.name} must be outside the glass`);
assert.ok(box.min.every((min, axis) => min < box.max[axis]));
}
const cases = [];
// Exit rays from glass, side/under views, both signs and exactly parallel slabs.
for (let i = 0; i < 1600; i++) {
cases.push([
new THREE.Vector3((random() - .5) * width * 4, (random() - .5) * height * 3, glassZ + (random() - .7) * height * 5),
new THREE.Vector3(random() - .5, random() - .5, random() - .5).normalize()
]);
}
for (const box of layout) {
const center = new THREE.Vector3().fromArray(box.min).add(new THREE.Vector3().fromArray(box.max)).multiplyScalar(.5);
for (let axis = 0; axis < 3; axis++) for (const sign of [-1, 1]) {
cases.push([center, new THREE.Vector3().setComponent(axis, sign)]);
const surface = center.clone().setComponent(axis, sign < 0 ? box.min[axis] : box.max[axis]);
cases.push([surface.clone().addScaledVector(new THREE.Vector3().setComponent(axis, sign), height), new THREE.Vector3().setComponent(axis, -sign)]);
}
}
for (const [origin, direction] of cases) {
const intersections = new THREE.Raycaster(origin, direction, 1e-5).intersectObject(mesh, false);
const expected = intersections[0];
const actual = intersectBoxes(layout, origin, direction);
assert.equal(Boolean(actual), Boolean(expected), `hit mismatch at ray ${rays}`);
if (expected) {
const error = Math.abs(expected.distance - actual.distance);
maxError = Math.max(error, maxError);
assert.ok(error < 5e-5, `nearest distance mismatch ${error} at ray ${rays}`);
// Box junctions have coincident inward/outward faces. Float32 mesh bounds can reverse their tie order.
assert.ok(intersections.some(hit => Math.abs(hit.distance - actual.distance) < 5e-5 && actual.normal.dot(hit.face.normal) > .999), `outward normal mismatch at ray ${rays}`);
hits++;
}
rays++;
}
for (const [key, uniform] of Object.entries(uniformIdentities)) assert.equal(yard.uniforms[key], uniform, `${key} identity must survive resize`);
}
const previousGeometry = mesh.geometry;
let replacedDisposed = 0;
previousGeometry.addEventListener('dispose', () => replacedDisposed++);
yard.resize(1, 1, .6);
assert.equal(replacedDisposed, 1, 'resize must release replaced geometry');
yard.setEnabled(false);
assert.equal(yard.group.visible, false);
assert.equal(yard.uniforms.uYardEnabled.value, 0);
yard.setEnabled(true);
assert.equal(yard.group.visible, true);
assert.equal(yard.uniforms.uYardEnabled.value, 1);
assert.throws(() => yard.resize(0, 1, .6), /positive/);
assert.throws(() => yard.resize(1, NaN, .6), /finite/);
assert.throws(() => yard.resize(1, 1, Infinity), /finite/);
const scene = new THREE.Scene();
scene.add(yard.group);
let geometryDisposed = 0, materialDisposed = 0;
mesh.geometry.addEventListener('dispose', () => geometryDisposed++);
mesh.material.addEventListener('dispose', () => materialDisposed++);
yard.dispose();
yard.dispose();
assert.equal(geometryDisposed, 1);
assert.equal(materialDisposed, 1);
assert.equal(yard.group.parent, null);
assert.equal(yard.uniforms.uYardEnabled.value, 0);
console.log(JSON.stringify({ test: 'ExperimentalCourtyard CPU geometry/analytic ray contract', rays, hits, maxError, boxes: yard.uniforms.uYardBoxMin.value.length, draws: 1, webglPixels: 'not tested' }));
test('optional dog contact shadow uses the shared yard shader without another mesh or pass',()=>{
const yard=new ExperimentalCourtyard();yard.setDogContact(.2,.35,.12,.6);
assert.deepEqual(yard.uniforms.uYardDogContact.value.toArray(),[.2,.35,.12,.6]);
assert.equal(yard.group.children.length,1);assert.match(yard.group.children[0].material.fragmentShader,/uYardDogContact/);assert.match(courtyardShader,/uYardDogContact/);yard.dispose();
});