Files
yawn/addons/handles/test/handles.test.ts
T
Ampandheaust 6153223fd5 Add FxNode render graph playground
Add persistent render graph loadouts, grouped node authoring, code-appending apply behavior, explicit playground lighting, and resilient worker startup.

Amp-Thread-ID: https://ampcode.com/threads/T-01a033d4-0e44-708e-af0e-6166283416ff
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-08-24 17:45:48 +00:00

251 lines
7.7 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { Mesh } from "../src/Mesh";
import { Node } from "../src/Node";
import { ArcRotateCamera } from "../src/camera/ArcRotateCamera";
import { PointLight } from "../src/lights/PointLight";
import { PBRMaterial } from "../src/materials/PBRMaterial";
import type { Texture } from "../src/materials/Texture";
import { createScene, testCore, triangle } from "./scene-helpers";
function close(actual: ArrayLike<number>, expected: ArrayLike<number>) {
assert.equal(actual.length, expected.length);
for (let index = 0; index < actual.length; index++)
assert.ok(
Math.abs(actual[index] - expected[index]) < 1e-5,
`${actual[index]} != ${expected[index]} at ${index}`,
);
}
test("PBRMaterial packs factors, extensions, texture pointers, and UV transforms", async () => {
const scene = await createScene();
const textures = Array.from(
{ length: 13 },
(_, id) => ({ id, ready: Promise.resolve() }) as unknown as Texture,
);
const material = new PBRMaterial(scene, {
baseColor: [0.1, 0.2, 0.3, 0.4],
metallic: 0.5,
roughness: 0.6,
emissive: [0.7, 0.8, 0.9],
normalScale: 0.75,
alphaCutoff: 0.25,
flags: 6,
baseColorTexture: textures[0],
metallicRoughnessTexture: textures[1],
normalTexture: textures[2],
emissiveTexture: textures[3],
occlusionTexture: textures[4],
transmissionTexture: textures[5],
thicknessTexture: textures[6],
clearcoatTexture: textures[7],
iridescenceTexture: textures[8],
iridescenceThicknessTexture: textures[9],
anisotropyTexture: textures[10],
specularTexture: textures[11],
specularColorTexture: textures[12],
transmission: 0.8,
ior: 1.6,
thickness: 0.4,
attenuationDistance: 3,
attenuationColor: [0.1, 0.2, 0.3],
emissiveStrength: 4,
clearcoat: 0.5,
clearcoatRoughness: 0.6,
specular: 0.7,
occlusionStrength: 0.8,
sheenColor: [0.9, 0.1, 0.2],
sheenRoughness: 0.3,
iridescence: 0.4,
iridescenceIor: 1.4,
iridescenceThicknessMinimum: 120,
iridescenceThicknessMaximum: 380,
anisotropy: 0.5,
anisotropyRotation: 0.6,
specularExtensionColor: [0.7, 0.8, 0.9],
textureInfos: [
{
offset: [0.1, 0.2],
scale: [0.3, 0.4],
rotation: 0.5,
texCoord: 1,
},
],
});
await material.ready;
close(
scene.array("materials").row(material.id),
[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.75, 0.25, 6],
);
assert.deepEqual(
Array.from(scene.array("materialTextures").row(material.id)),
[...Array.from({ length: 13 }, (_, index) => index + 1), 0, 0, 0],
);
close(
scene.array("materialExtensions").row(material.id),
[
0.8, 1.6, 0.4, 3, 0.1, 0.2, 0.3, 4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2,
0.3, 0.4, 1.4, 120, 380, 0.5, 0.6, 0, 0, 0.7, 0.8, 0.9, 0,
],
);
const transforms = scene.array("materialTextureInfo").row(material.id);
close(transforms.subarray(0, 8), [
0.1,
0.2,
0.3,
0.4,
Math.sin(0.5),
Math.cos(0.5),
1,
0,
]);
close(transforms.subarray(8, 16), [0, 0, 1, 1, 0, 1, 0, 0]);
});
test("hot material setters write shared rows without rebuilding the graph", async () => {
const scene = await createScene();
const material = new PBRMaterial(scene);
await material.ready;
const core = testCore(scene);
const compiled = core.graphs.length;
core.array("signals").row(0).fill(0);
material.baseColor = [0.2, 0.3, 0.4, 0.5];
material.metallic = 0.6;
material.roughness = 0.7;
material.emissive = [0.8, 0.9, 1];
close(
scene.array("materials").row(material.id),
[0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1, 0.5, 0],
);
assert.equal(core.array("signals").row(0)[5], 1);
assert.equal(core.graphs.length, compiled);
});
test("Mesh packs geometry and bounds, then clones with copy-on-write", async () => {
const scene = await createScene();
const firstMaterial = new PBRMaterial(scene);
const secondMaterial = new PBRMaterial(scene, { baseColor: [1, 0, 0, 1] });
await Promise.all([firstMaterial.ready, secondMaterial.ready]);
const mesh = new Mesh(scene, {
material: firstMaterial,
vertexData: triangle,
});
await mesh.ready;
assert.equal(mesh.vertexCount, 3);
assert.equal(mesh.indexCount, 3);
close(scene.geometryData(mesh.geometryId, "positions")!, triangle.positions);
close(scene.array("bounds").row(mesh.id), [-1, -1, 0, 0, 1, 1, 0, 0]);
const clone = mesh.clone({ position: [2, 0, 0] });
await clone.ready;
assert.equal(clone.geometryId, mesh.geometryId);
await clone.setVertexData("positions", [0, 0, 0, 2, 0, 0, 0, 3, 0]);
assert.notEqual(clone.geometryId, mesh.geometryId);
close(scene.geometryData(mesh.geometryId, "positions")!, triangle.positions);
close(
scene.geometryData(clone.geometryId, "positions")!,
[0, 0, 0, 2, 0, 0, 0, 3, 0],
);
clone.material = secondMaterial;
await clone.setMaterialForFaces(firstMaterial, 0);
assert.equal(clone.materialId, secondMaterial.id);
assert.equal(clone.faceMaterials.get(0), firstMaterial.id);
assert.equal(
scene.array(`mesh.${clone.id}.faceMaterials`).row(0)[0],
firstMaterial.id + 1,
);
});
test("Node, ArcRotateCamera, and light handles update their shared rows", async () => {
const scene = await createScene();
const target = new Node(scene, { position: [1, 2, 3], scale: [2, 3, 4] });
await target.ready;
target.translate([1, -1, 2]).rotateZ(Math.PI);
close(target.position, [2, 1, 5]);
close(target.scale, [2, 3, 4]);
close(target.rotor, [0, 0, 1, 0]);
const child = new Node(scene, { parent: target });
await child.ready;
assert.equal(child.parentId, target.id);
child.enabled = false;
assert.equal(scene.array("nodes").row(child.id)[0], 0);
const camera = new ArcRotateCamera(scene, {
target,
alpha: 0,
beta: Math.PI / 2,
radius: 5,
});
await camera.ready;
close(camera.position, [2, 1, 10]);
assert.equal(scene.array("cameraMatrices").row(camera.cameraId)[19], 1);
camera.radius = 3;
close(camera.position, [2, 1, 8]);
const light = new PointLight(scene, {
position: [4, 5, 6],
color: [0.2, 0.4, 0.6],
intensity: 7,
range: 8,
radius: 2,
});
await light.ready;
close(
scene.array("pointLights").row(light.id),
[0.2, 0.4, 0.6, 7, 8, 1, 2, 0],
);
light.intensity = 9;
light.range = 10;
light.radius = 3;
close(scene.array("pointLights").row(light.id).subarray(3, 5), [9, 10]);
assert.equal(scene.array("pointLights").row(light.id)[6], 3);
});
test("ArcRotateCamera pans one projected world pixel per pointer pixel", async () => {
const listeners = new Map<string, Set<(event: any) => void>>();
const element = {
clientWidth: 400,
clientHeight: 200,
addEventListener(type: string, listener: (event: any) => void) {
const registered = listeners.get(type) ?? new Set();
registered.add(listener);
listeners.set(type, registered);
},
removeEventListener(type: string, listener: (event: any) => void) {
listeners.get(type)?.delete(listener);
},
setPointerCapture() {},
} as unknown as HTMLElement;
const dispatch = (type: string, event: Record<string, unknown>) => {
for (const listener of listeners.get(type) ?? [])
listener({ preventDefault() {}, ...event });
};
const scene = await createScene();
const camera = new ArcRotateCamera(scene, {
alpha: 0,
beta: Math.PI / 2,
radius: 10,
fov: Math.PI / 2,
aspect: 2,
controls: { element },
});
await camera.ready;
dispatch("pointerdown", {
button: 2,
clientX: 100,
clientY: 100,
pointerId: 1,
});
dispatch("pointermove", { clientX: 120, clientY: 110 });
close(camera.position, [-2, 1, 10]);
camera.detachControls();
});