import assert from "node:assert/strict"; import { describe, it } from "node:test"; import * as THREE from "three"; import { DOG_METRICS, buildDog, cloneDog, disposeDog, dogPoseStateForSpeed, poseDog, poseDogAttention, type DogRig, } from "../assets/actors/index.ts"; function meshes(root: THREE.Object3D): THREE.Mesh[] { const result: THREE.Mesh[] = []; root.traverse((object) => { if (object instanceof THREE.Mesh) result.push(object); }); return result; } function transformSnapshot(rig: DogRig): readonly number[][] { const result: number[][] = []; rig.root.traverse((object) => { result.push([ ...object.position.toArray(), object.rotation.x, object.rotation.y, object.rotation.z, ...object.scale.toArray(), ]); }); return result; } describe("procedural dog v2", () => { it("builds a floor-centred articulated silhouette within the peer/mobile budget", () => { const rig = buildDog(); assert.equal(rig.root.userData.rigVersion, 2); for (const leg of [rig.joints.frontLeft, rig.joints.frontRight, rig.joints.rearLeft, rig.joints.rearRight]) { assert.equal(leg.lower.parent, leg.upper); assert.equal(leg.paw.parent, leg.lower); } for (const name of [ "dog.shoulder-mass", "dog.hip-mass", "dog.neck", "dog.chest", "dog.muzzle", "dog.collar", "dog.tag", "dog.tail.tip", ]) assert.ok(rig.root.getObjectByName(name), `missing ${name}`); const box = new THREE.Box3().setFromObject(rig.root); const size = box.getSize(new THREE.Vector3()); assert.ok(box.min.y > -0.02, `floor ${box.min.y}`); assert.ok(Math.abs(size.y - DOG_METRICS.height) < 0.08, `height ${size.y}`); assert.ok(Math.abs(size.z - DOG_METRICS.length) < 0.08, `length ${size.z}`); const allMeshes = meshes(rig.root); const triangles = allMeshes.reduce((sum, mesh) => { const geometry = mesh.geometry; return sum + (geometry.index?.count ?? geometry.getAttribute("position").count) / 3; }, 0); assert.ok(allMeshes.length <= 34, `${allMeshes.length} meshes`); assert.ok(triangles <= 3_600, `${triangles} triangles`); disposeDog(rig); }); it("separates idle, attention, walk, trot, run, turn and start/stop poses", () => { const rig = buildDog(); poseDog(rig, { state: "idle", phase: Math.PI / 2, amount: 1 }); const idleChest = rig.joints.chest.scale.y; assert.equal(rig.root.userData.pose, "idle"); assert.equal(rig.joints.frontLeft.upper.rotation.x, 0); poseDog(rig, { state: "attention", phase: Math.PI / 2, amount: 1, attentionYaw: 0.35 }); assert.equal(rig.root.userData.pose, "attention"); assert.equal(rig.joints.head.rotation.y, 0.35); assert.notEqual(rig.joints.earLeft.rotation.z, -rig.joints.earRight.rotation.z); poseDog(rig, { state: "walk", phase: Math.PI / 2, amount: 1 }); const walkStride = Math.abs(rig.joints.frontLeft.upper.rotation.x); assert.ok(walkStride > 0.3); assert.equal(rig.joints.frontLeft.upper.rotation.x, rig.joints.rearRight.upper.rotation.x); assert.equal(rig.joints.frontRight.upper.rotation.x, rig.joints.rearLeft.upper.rotation.x); assert.notEqual(rig.joints.chest.scale.y, idleChest); poseDog(rig, { state: "trot", phase: Math.PI / 2, amount: 1 }); const trotStride = Math.abs(rig.joints.frontLeft.upper.rotation.x); assert.ok(trotStride > walkStride); poseDog(rig, { state: "run", phase: 0.8, amount: 1 }); assert.equal(rig.root.userData.pose, "run"); assert.notEqual(rig.joints.frontLeft.upper.rotation.x, rig.joints.frontRight.upper.rotation.x, "a gathered run offsets the front pair rather than using a rigid scissor"); poseDog(rig, { state: "trot", phase: 0.7, amount: 1, turn: 0.8, acceleration: 0.7 }); assert.ok(rig.joints.body.rotation.z < -0.09, "turn banks through the chest"); assert.ok(rig.joints.body.rotation.x < -0.05, "starting lowers the chest into motion"); poseDog(rig, { state: "idle", phase: 0.7, amount: 1, acceleration: -0.8 }); assert.ok(rig.joints.body.rotation.x > 0.06, "stopping braces the chest"); disposeDog(rig); }); it("selects gait thresholds and repeats an exact pose without accumulating transforms", () => { assert.equal(dogPoseStateForSpeed(Number.NaN), "idle"); assert.equal(dogPoseStateForSpeed(0.07), "idle"); assert.equal(dogPoseStateForSpeed(0.5), "walk"); assert.equal(dogPoseStateForSpeed(1.5), "trot"); assert.equal(dogPoseStateForSpeed(3), "run"); const rig = buildDog(); const pose = { state: "trot" as const, phase: 2.17, amount: 0.83, turn: -0.42, acceleration: -0.3, attentionYaw: 0.11, attentionPitch: -0.08, }; poseDog(rig, pose); const first = transformSnapshot(rig); poseDog(rig, pose); assert.deepEqual(transformSnapshot(rig), first); poseDogAttention(rig, 0.2, 1.7); const attention = transformSnapshot(rig); poseDogAttention(rig, 0.2, 1.7); assert.deepEqual(transformSnapshot(rig), attention, "legacy attention remains idempotent"); disposeDog(rig); }); it("clones shared resources into independent joints and respects material ownership", () => { const source = buildDog(); const clone = cloneDog(source); const sourceMeshes = meshes(source.root); const cloneMeshes = meshes(clone.root); assert.equal(cloneMeshes.length, sourceMeshes.length); for (let index = 0; index < sourceMeshes.length; index += 1) { assert.equal(cloneMeshes[index]!.geometry, sourceMeshes[index]!.geometry); assert.equal(cloneMeshes[index]!.material, sourceMeshes[index]!.material); } poseDog(clone, { state: "run", phase: 1.3, amount: 1 }); assert.notEqual(clone.joints.frontLeft.upper.rotation.x, source.joints.frontLeft.upper.rotation.x); assert.equal(clone.ownsMaterials, false); const shared = sourceMeshes[0]!.material as THREE.Material; let disposals = 0; shared.addEventListener("dispose", () => disposals++); const external = { coat: shared, markings: shared, nose: shared, collar: shared }; const externalRig = buildDog({ materials: external }); disposeDog(externalRig); assert.equal(disposals, 0); disposeDog(source); assert.equal(disposals, 1); }); });