97 lines
4.0 KiB
TypeScript
97 lines
4.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import * as THREE from "three";
|
|
import {
|
|
MODEL_X_METRICS,
|
|
LUMBRIDGE_EV_METRICS,
|
|
advanceModelXWheels,
|
|
buildModelX,
|
|
buildLumbridgeEV,
|
|
cloneModelX,
|
|
disposeModelX,
|
|
modelXInstanceParts,
|
|
setModelXSteering,
|
|
setModelXWheelRotation,
|
|
} from "../assets/vehicles/index.ts";
|
|
|
|
function meshes(root: THREE.Object3D): THREE.Mesh[] {
|
|
const found: THREE.Mesh[] = [];
|
|
root.traverse((object) => {
|
|
if (object instanceof THREE.Mesh) found.push(object);
|
|
});
|
|
return found;
|
|
}
|
|
|
|
describe("procedural Model X vehicle asset", () => {
|
|
it("publishes the original unbranded Lumbridge EV identity at both LODs", () => {
|
|
const follow = buildLumbridgeEV({ detail: "follow" });
|
|
const corridor = buildLumbridgeEV({ detail: "corridor" });
|
|
assert.equal(LUMBRIDGE_EV_METRICS, MODEL_X_METRICS);
|
|
assert.equal(follow.root.userData.design, "lumbridge-ev-01");
|
|
assert.equal(follow.root.userData.unbranded, true);
|
|
assert.equal(follow.root.userData.lod, "follow");
|
|
assert.equal(corridor.root.userData.lod, "corridor");
|
|
const followVertices = meshes(follow.root).reduce((sum, mesh) => sum + mesh.geometry.getAttribute("position").count, 0);
|
|
const corridorVertices = meshes(corridor.root).reduce((sum, mesh) => sum + mesh.geometry.getAttribute("position").count, 0);
|
|
assert.ok(followVertices > corridorVertices, "follow LOD includes a readable interior and denser wheels");
|
|
disposeModelX(follow);
|
|
disposeModelX(corridor);
|
|
});
|
|
it("has a metre-scale crossover silhouette and faces -Z", () => {
|
|
const rig = buildModelX({ detail: "corridor" });
|
|
const size = new THREE.Box3().setFromObject(rig.root).getSize(new THREE.Vector3());
|
|
|
|
assert.ok(Math.abs(size.x - MODEL_X_METRICS.width) < 0.12, `width ${size.x}`);
|
|
assert.ok(Math.abs(size.y - MODEL_X_METRICS.height) < 0.08, `height ${size.y}`);
|
|
assert.ok(Math.abs(size.z - MODEL_X_METRICS.length) < 0.08, `length ${size.z}`);
|
|
assert.equal(rig.root.userData.forwardAxis, "-Z");
|
|
assert.ok(rig.wheels.frontLeft.steering.position.z < 0);
|
|
assert.ok(rig.wheels.rearLeft.steering.position.z > 0);
|
|
disposeModelX(rig);
|
|
});
|
|
|
|
it("exposes independent steering and rolling joints", () => {
|
|
const rig = buildModelX();
|
|
setModelXWheelRotation(rig, 1.25);
|
|
for (const wheel of Object.values(rig.wheels)) assert.equal(wheel.spin.rotation.x, 1.25);
|
|
|
|
setModelXSteering(rig, 99);
|
|
assert.equal(rig.wheels.frontLeft.steering.rotation.y, MODEL_X_METRICS.maxSteeringAngle);
|
|
assert.equal(rig.wheels.frontRight.steering.rotation.y, MODEL_X_METRICS.maxSteeringAngle);
|
|
assert.equal(rig.wheels.rearLeft.steering.rotation.y, 0);
|
|
|
|
advanceModelXWheels(rig, MODEL_X_METRICS.wheelRadius);
|
|
assert.equal(rig.wheels.frontLeft.spin.rotation.x, 0.25);
|
|
disposeModelX(rig);
|
|
});
|
|
|
|
it("clones cheaply while keeping its pose independent", () => {
|
|
const original = buildModelX();
|
|
const clone = cloneModelX(original);
|
|
const sourceMeshes = meshes(original.root);
|
|
const clonedMeshes = meshes(clone.root);
|
|
|
|
assert.equal(clonedMeshes.length, sourceMeshes.length);
|
|
for (let i = 0; i < sourceMeshes.length; i++) {
|
|
assert.equal(clonedMeshes[i]!.geometry, sourceMeshes[i]!.geometry);
|
|
assert.equal(clonedMeshes[i]!.material, sourceMeshes[i]!.material);
|
|
}
|
|
|
|
setModelXSteering(clone, -0.3);
|
|
assert.equal(clone.wheels.frontLeft.steering.rotation.y, -0.3);
|
|
assert.equal(original.wheels.frontLeft.steering.rotation.y, 0);
|
|
assert.equal(clone.ownsMaterials, false);
|
|
disposeModelX(original);
|
|
});
|
|
|
|
it("publishes stable neutral-pose pieces for instanced traffic", () => {
|
|
const rig = buildModelX({ detail: "corridor" });
|
|
const parts = modelXInstanceParts(rig);
|
|
assert.equal(parts.length, meshes(rig.root).length);
|
|
assert.ok(parts.some((part) => part.name === "frontLeft.tire"));
|
|
assert.ok(parts.some((part) => part.name.startsWith("model-x.body:")));
|
|
assert.ok(parts.every((part) => Number.isFinite(part.matrix.determinant())));
|
|
disposeModelX(rig);
|
|
});
|
|
});
|