1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/test/dogAssetV2.test.ts
T

160 lines
6.2 KiB
TypeScript

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);
});
});