1
0

feat: add character studio and aircraft foundation

This commit is contained in:
2026-08-11 19:32:32 -07:00
parent 1c37ef8f3e
commit c0c7fcf974
16 changed files with 2250 additions and 25 deletions
+188
View File
@@ -0,0 +1,188 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import {
AircraftController,
ELECTRIC_AIRCRAFT_METRICS,
aircraftChaseCameraPose,
advanceAircraftFans,
buildElectricAircraft,
createElectricAircraftMaterials,
disposeElectricAircraft,
normalizeAircraftActions,
replayAircraftInputs,
setAircraftControlSurfaces,
setAircraftFanRotation,
} from "../aircraft/index.ts";
const ROUTE = [
{ id: "los-angeles", lat: 34.0522, lng: -118.2437, altitudeM: 1_300 },
{ id: "san-francisco", lat: 37.7749, lng: -122.4194, altitudeM: 1_700 },
] as const;
describe("procedural electric aircraft", () => {
it("builds an original metre-scale fixed wing facing -Z with articulated parts", () => {
const rig = buildElectricAircraft();
assert.equal(rig.root.name, "electric-aircraft");
assert.equal(rig.fans.length, 2);
assert.equal(rig.ownsMaterials, true);
assert.ok(ELECTRIC_AIRCRAFT_METRICS.wingspan > ELECTRIC_AIRCRAFT_METRICS.length);
const bounds = new THREE.Box3().setFromObject(rig.root);
const size = bounds.getSize(new THREE.Vector3());
assert.ok(size.x > 10);
assert.ok(size.z > 7);
assert.ok(size.y > 1.5);
disposeElectricAircraft(rig);
assert.equal(rig.root.children.length, 0);
});
it("animates opposing ailerons, V-tail surfaces, and electric fans", () => {
const rig = buildElectricAircraft();
setAircraftControlSurfaces(rig, { roll: 0.8, pitch: 0.5, yaw: -0.4 });
assert.ok(rig.leftAileron.rotation.x > 0);
assert.ok(rig.rightAileron.rotation.x < 0);
assert.notEqual(rig.leftVTail.rotation.x, rig.rightVTail.rotation.x);
setAircraftFanRotation(rig, 1);
advanceAircraftFans(rig, 0.5);
assert.equal(rig.fans[0]?.rotation.z, 1.5);
assert.equal(rig.fans[1]?.rotation.z, 1.5);
disposeElectricAircraft(rig);
});
it("does not dispose caller-owned materials", () => {
const materials = createElectricAircraftMaterials();
let disposals = 0;
for (const material of Object.values(materials)) {
material.addEventListener("dispose", () => { disposals += 1; });
}
const rig = buildElectricAircraft({ materials });
assert.equal(rig.ownsMaterials, false);
disposeElectricAircraft(rig);
assert.equal(disposals, 0);
for (const material of Object.values(materials)) material.dispose();
});
});
describe("aircraft controller", () => {
it("normalizes device-neutral flight axes", () => {
assert.deepEqual(normalizeAircraftActions({
throttle: 5,
yaw: -4,
pitch: Number.NaN,
roll: 2,
modeRequest: "manual",
reset: true,
}), {
throttle: 1,
yaw: -1,
pitch: 0,
roll: 1,
modeRequest: "manual",
reset: true,
});
});
it("follows route and altitude deterministically in assisted mode", () => {
const options = {
route: ROUTE,
initialPosition: ROUTE[0],
initialHeadingDeg: 0,
initialAltitudeM: 900,
assistedAltitudeM: 1_300,
};
const a = new AircraftController(options);
const b = new AircraftController(options);
for (let index = 0; index < 600; index += 1) {
a.stepFixed();
b.stepFixed();
}
assert.deepEqual(a.snapshot(), b.snapshot());
assert.equal(a.state().mode, "assisted");
assert.equal(a.state().routeWaypointId, "san-francisco");
assert.ok(a.state().altitudeM > 900);
assert.ok(a.state().headingDeg > 180, "aircraft turns northwest toward San Francisco");
});
it("takes manual input immediately and resumes assistance without a state jump", () => {
const controller = new AircraftController({ route: ROUTE });
for (let index = 0; index < 120; index += 1) {
controller.stepFixed({ throttle: 0.8, roll: 0.7, pitch: -0.3 });
}
assert.equal(controller.state().mode, "manual");
const before = controller.snapshot();
controller.stepFixed({ modeRequest: "assisted", roll: 0.8 });
assert.equal(controller.state().mode, "manual", "manual controls beat simultaneous resume");
controller.stepFixed({ modeRequest: "assisted" });
assert.equal(controller.state().mode, "assisted");
assert.ok(Math.abs(controller.state().rollDeg - before.rollDeg) < 3);
assert.ok(Math.abs(controller.state().pitchDeg - before.pitchDeg) < 3);
});
it("enforces geographic, altitude, and speed boundaries", () => {
const controller = new AircraftController({
mode: "manual",
initialPosition: { lat: 100, lng: -200 },
initialAltitudeM: 100_000,
initialSpeedMps: 1_000,
maximumSpeedMps: 80,
});
assert.equal(controller.state().lat, 42.1);
assert.equal(controller.state().lng, -124.6);
assert.equal(controller.state().altitudeM, 6_000);
assert.equal(controller.state().speedMps, 80);
for (let index = 0; index < 180; index += 1) {
controller.stepFixed({ throttle: 1, pitch: 1, roll: 1 });
}
assert.ok(controller.state().lat >= 32.4 && controller.state().lat <= 42.1);
assert.ok(controller.state().lng >= -124.6 && controller.state().lng <= -114);
assert.ok(controller.state().altitudeM >= 75 && controller.state().altitudeM <= 6_000);
assert.ok(controller.state().speedMps <= 80);
assert.equal(controller.state().envelopeContact, true);
});
it("resets exactly, caps sleeping-tab time, and replays bit-for-bit", () => {
const options = { route: ROUTE, initialAltitudeM: 1_200, initialSpeedMps: 48 } as const;
const controller = new AircraftController(options);
const spawn = controller.snapshot();
assert.ok(controller.tick(600) <= 15);
controller.stepFixed({ throttle: 1, roll: -1 });
controller.stepFixed({ reset: true });
assert.deepEqual(controller.snapshot(), spawn);
const frames = [
{ steps: 90, actions: { throttle: 0.9, roll: 0.4 } },
{ steps: 1, actions: { modeRequest: "assisted" as const } },
{ steps: 180 },
];
assert.deepEqual(
replayAircraftInputs(options, frames),
replayAircraftInputs(options, frames),
);
});
it("derives a finite geographic chase camera behind the aircraft", () => {
const controller = new AircraftController({ initialHeadingDeg: 0 });
const camera = aircraftChaseCameraPose(controller.state());
assert.ok(camera.position.lat < controller.state().lat);
assert.ok(camera.target.lat > controller.state().lat);
assert.ok(camera.position.altitudeM > controller.state().altitudeM);
assert.ok(Object.values(camera.position).every(Number.isFinite));
assert.ok(Object.values(camera.target).every(Number.isFinite));
});
it("rejects malformed route and envelope configuration", () => {
assert.throws(() => new AircraftController({
route: [{ id: "bad", lat: Number.NaN, lng: 0, altitudeM: 1_000 }],
}), /waypoints/);
assert.throws(() => new AircraftController({
envelope: {
minLat: 40,
maxLat: 30,
minLng: -124,
maxLng: -114,
minAltitudeM: 10,
maxAltitudeM: 100,
},
}), /envelope/);
});
});
+102
View File
@@ -0,0 +1,102 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import { MaterialRegistry } from "../assets/materials.ts";
import "../assets/office/index.ts";
import { createFurnishings } from "../interiors/furnish.ts";
import { Plan } from "../interiors/plan.ts";
import type { Level, Office, Room } from "../interiors/types.ts";
import { createOfficeMediaPresentation } from "../media/presentation.ts";
const ROOM: Room = {
id: "room",
name: "Room",
floor: "floor" as never,
outline: [{ x: 0, z: 0 }, { x: 8, z: 0 }, { x: 8, z: 6 }, { x: 0, z: 6 }],
};
function fixture(depth: "full" | "public" = "full") {
const level: Level = {
id: "ground",
name: "Ground",
elevation: 0,
wallHeight: 3,
wallThickness: 0.1,
floorplan: { rooms: [ROOM], walls: [], props: [
{ id: "wall-display", kind: "tera:screen.wall-display", position: { x: 2, z: 2 }, rotation: 0 },
{ id: "desk-monitor", kind: "tera:screen.monitor", position: { x: 4, z: 2 }, rotation: 0, elevation: 0.73 },
{ id: "ordinary-desk", kind: "tera:desk.workstation", position: { x: 6, z: 2 }, rotation: 0 },
{
id: "private-display",
kind: "tera:screen.wall-display",
position: { x: 2, z: 4 },
rotation: 0,
audience: "private",
},
] },
};
const office: Office = { id: "media-office", name: "Media Office", levels: [level], viewpoints: [] };
const plan = new Plan(office, { depth, warn: false });
const materials = new MaterialRegistry({ quality: "low" });
const furnishings = createFurnishings(plan, { materials });
const presentation = createOfficeMediaPresentation(plan, furnishings.group);
return { plan, materials, furnishings, presentation };
}
describe("procedural office media presentation", () => {
it("discovers only authored screen props and keeps exact prop ids and rooms", () => {
const f = fixture();
const listed = f.presentation.list();
assert.deepEqual(listed.map((item) => item.screenId).sort(), ["desk-monitor", "private-display", "wall-display"]);
assert.ok(listed.every((item) => item.officeId === "media-office" && item.roomId === "room"));
assert.ok(!listed.some((item) => item.screenId === "ordinary-desk"));
assert.equal(f.presentation.group.children.length, 3);
f.presentation.dispose();
f.furnishings.dispose();
f.materials.dispose();
});
it("keeps public depth private-prop-free by discovering only the resolved plan", () => {
const f = fixture("public");
assert.deepEqual(f.presentation.list().map((item) => item.screenId).sort(), ["desk-monitor", "wall-display"]);
f.presentation.dispose();
f.furnishings.dispose();
f.materials.dispose();
});
it("requires authorization plus opt-in and returns to a safe placeholder", () => {
const f = fixture();
let textureDisposals = 0;
const texture = new THREE.VideoTexture({} as HTMLVideoElement);
texture.dispose = () => { textureDisposals += 1; };
assert.equal(f.presentation.bind("wall-display", { canView: true, optedIn: false }, texture), false);
assert.equal(f.presentation.list().find((item) => item.screenId === "wall-display")?.bound, false);
assert.equal(f.presentation.bind("wall-display", { canView: true, optedIn: true }, texture), true);
const mesh = f.presentation.group.getObjectByName("media-surface:wall-display") as THREE.Mesh;
assert.equal((mesh.material as THREE.MeshBasicMaterial).map, texture);
assert.equal(f.presentation.clear("wall-display"), true);
assert.equal((mesh.material as THREE.MeshBasicMaterial).map, null);
assert.equal(textureDisposals, 0);
f.presentation.dispose();
assert.equal(textureDisposals, 0, "caller-owned texture was disposed");
f.furnishings.dispose();
f.materials.dispose();
});
it("returns defensive descriptors and contains unknown/disposed operations", () => {
const f = fixture();
const leaked = f.presentation.list()[0];
assert.ok(leaked);
leaked.screenId = "changed";
assert.notEqual(f.presentation.list()[0]?.screenId, "changed");
const texture = new THREE.VideoTexture({} as HTMLVideoElement);
assert.equal(f.presentation.bind("missing", { canView: true, optedIn: true }, texture), false);
f.presentation.dispose();
f.presentation.dispose();
assert.equal(f.presentation.bind("wall-display", { canView: true, optedIn: true }, texture), false);
assert.equal(f.presentation.clear("wall-display"), false);
f.furnishings.dispose();
f.materials.dispose();
texture.dispose();
});
});
+241
View File
@@ -0,0 +1,241 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
ACCENTS,
BODY_SHAPES,
HAIR_COLORS,
OUTFITS,
SKIN_TONES,
createDefaultLocalProfile,
createProfileEditor,
type LocalProfile,
} from "../profile/index.ts";
type Listener = (event: FakeEvent) => void;
class FakeEvent {
defaultPrevented = false;
readonly type: string;
readonly target: FakeElement;
readonly key: string;
readonly ctrlKey: boolean;
readonly metaKey: boolean;
readonly shiftKey: boolean;
constructor(
type: string,
target: FakeElement,
key = "",
ctrlKey = false,
metaKey = false,
shiftKey = false,
) {
this.type = type;
this.target = target;
this.key = key;
this.ctrlKey = ctrlKey;
this.metaKey = metaKey;
this.shiftKey = shiftKey;
}
preventDefault(): void { this.defaultPrevented = true; }
}
class FakeElement {
readonly children: FakeElement[] = [];
readonly attributes = new Map<string, string>();
readonly listeners = new Map<string, Listener[]>();
parentElement: FakeElement | null = null;
className = "";
textContent = "";
hidden = false;
value = "";
id = "";
type = "";
maxLength = 0;
autocomplete = "";
disabled = false;
readonly ownerDocument: FakeDocument;
readonly tagName: string;
constructor(ownerDocument: FakeDocument, tagName: string) {
this.ownerDocument = ownerDocument;
this.tagName = tagName;
}
append(...nodes: FakeElement[]): void {
for (const node of nodes) {
node.parentElement = this;
this.children.push(node);
}
}
setAttribute(name: string, value: string): void { this.attributes.set(name, value); }
getAttribute(name: string): string | null { return this.attributes.get(name) ?? null; }
addEventListener(type: string, listener: Listener): void {
const found = this.listeners.get(type);
if (found) found.push(listener);
else this.listeners.set(type, [listener]);
}
dispatch(type: string, init: Partial<Pick<FakeEvent, "key" | "ctrlKey" | "metaKey" | "shiftKey">> = {}): FakeEvent {
const event = new FakeEvent(type, this, init.key, init.ctrlKey, init.metaKey, init.shiftKey);
for (const listener of this.listeners.get(type) ?? []) listener(event);
return event;
}
focus(): void { this.ownerDocument.activeElement = this; }
remove(): void {
if (!this.parentElement) return;
const index = this.parentElement.children.indexOf(this);
if (index >= 0) this.parentElement.children.splice(index, 1);
this.parentElement = null;
}
find(attribute: string, value: string): FakeElement {
if (this.attributes.get(attribute) === value) return this;
for (const child of this.children) {
try { return child.find(attribute, value); } catch { /* continue */ }
}
throw new Error(`missing [${attribute}=${value}]`);
}
descendants(tagName: string): FakeElement[] {
const result: FakeElement[] = [];
for (const child of this.children) {
if (child.tagName === tagName.toUpperCase()) result.push(child);
result.push(...child.descendants(tagName));
}
return result;
}
}
class FakeDocument {
activeElement: FakeElement | null = null;
createElement(tagName: string): FakeElement { return new FakeElement(this, tagName.toUpperCase()); }
}
function setup(profile = createDefaultLocalProfile("member-22", "Avery")) {
const document = new FakeDocument();
const container = document.createElement("div");
const previews: LocalProfile[] = [];
const saves: LocalProfile[] = [];
let cancels = 0;
const editor = createProfileEditor({
container: container as unknown as HTMLElement,
profile,
identityId: "member-22",
onPreview: (next) => previews.push(next),
onSave: (next) => saves.push(next),
onCancel: () => cancels++,
});
return { document, container, editor, previews, saves, cancels: () => cancels };
}
describe("profile editor DOM adapter", () => {
it("builds a closed labelled dialog with every enumerated appearance choice", () => {
const { container, editor } = setup();
const root = editor.root as unknown as FakeElement;
assert.equal(container.children.length, 1);
assert.equal(root.hidden, true);
assert.equal(root.getAttribute("role"), "dialog");
assert.equal(root.getAttribute("aria-modal"), "true");
assert.ok(root.getAttribute("aria-labelledby"));
assert.equal(root.find("data-field", "displayName").getAttribute("aria-describedby"), root.descendants("p")[1]?.id);
assert.equal(root.find("data-field", "skinTone").children.length, SKIN_TONES.length);
assert.equal(root.find("data-field", "outfit").children.length, OUTFITS.length);
assert.equal(root.find("data-field", "accent").children.length, ACCENTS.length);
assert.equal(root.find("data-field", "hair").children.length, HAIR_COLORS.length);
assert.equal(root.find("data-field", "bodyShape").children.length, BODY_SHAPES.length);
assert.equal(editor.state().open, false);
});
it("opens with focus, edits preview data, saves, and restores invoker focus", () => {
const { document, editor, previews, saves } = setup();
const trigger = document.createElement("button");
trigger.focus();
editor.open();
const root = editor.root as unknown as FakeElement;
const name = root.find("data-field", "displayName");
assert.equal(document.activeElement, name);
assert.equal(root.getAttribute("aria-hidden"), "false");
name.value = "Avery Chen";
name.dispatch("input");
const outfit = root.find("data-field", "outfit");
outfit.value = "sage";
outfit.dispatch("change");
assert.equal(editor.state().draft.displayName, "Avery Chen");
assert.equal(editor.state().draft.appearance.outfit, "sage");
assert.equal(editor.state().dirty, true);
assert.equal(previews.at(-1)?.appearance.outfit, "sage");
root.find("data-action", "save").dispatch("click");
assert.equal(saves.length, 1);
assert.equal(saves[0]?.displayName, "Avery Chen");
assert.equal(editor.state().open, false);
assert.equal(document.activeElement, trigger);
});
it("blocks invalid names and exposes accessible validation", () => {
const { document, editor, saves } = setup();
editor.open();
const root = editor.root as unknown as FakeElement;
const name = root.find("data-field", "displayName");
const save = root.find("data-action", "save");
name.value = " ";
name.dispatch("input");
assert.equal(editor.state().valid, false);
assert.equal(name.getAttribute("aria-invalid"), "true");
assert.equal(save.disabled, true);
// Form submit covers Enter/assistive submit even though a disabled click cannot fire in a browser.
root.descendants("form")[0]?.dispatch("submit");
assert.equal(saves.length, 0);
assert.equal(document.activeElement, name);
});
it("resets appearance deterministically and Escape cancels the draft", () => {
const profile = createDefaultLocalProfile("some-other-id", "River");
const { editor, previews, cancels } = setup(profile);
editor.open();
const root = editor.root as unknown as FakeElement;
const outfit = root.find("data-field", "outfit");
outfit.value = outfit.value === "ink" ? "clay" : "ink";
outfit.dispatch("change");
const name = root.find("data-field", "displayName");
name.value = "River Two";
name.dispatch("input");
root.find("data-action", "reset").dispatch("click");
assert.deepEqual(editor.state().draft.appearance, createDefaultLocalProfile("member-22", "River").appearance);
assert.equal(editor.state().draft.displayName, "River Two");
const event = root.dispatch("keydown", { key: "Escape" });
assert.equal(event.defaultPrevented, true);
assert.equal(cancels(), 1);
assert.deepEqual(editor.state().draft, profile);
assert.deepEqual(previews.at(-1), profile);
});
it("traps keyboard focus and supports command-enter save", () => {
const { document, editor, saves } = setup();
editor.open();
const root = editor.root as unknown as FakeElement;
const first = root.find("data-field", "displayName");
const last = root.find("data-action", "save");
last.focus();
assert.equal(root.dispatch("keydown", { key: "Tab" }).defaultPrevented, true);
assert.equal(document.activeElement, first);
first.focus();
assert.equal(root.dispatch("keydown", { key: "Tab", shiftKey: true }).defaultPrevented, true);
assert.equal(document.activeElement, last);
assert.equal(root.dispatch("keydown", { key: "Enter", ctrlKey: true }).defaultPrevented, true);
assert.equal(saves.length, 1);
});
it("strictly updates caller data, then disposes without leaving DOM", () => {
const { container, editor, previews } = setup();
const next = createDefaultLocalProfile("new-person", "Morgan");
const updated = editor.update(next);
assert.deepEqual(updated.profile, next);
assert.deepEqual(updated.draft, next);
assert.deepEqual(previews.at(-1), next);
assert.throws(() => editor.update({ ...next, displayName: "" }), RangeError);
editor.open();
editor.dispose();
editor.dispose();
assert.equal(container.children.length, 0);
});
});
+108
View File
@@ -0,0 +1,108 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
createWebcamFaceConsent,
createWebcamFaceTexture,
type WebcamFaceTextureState,
} from "../profile/index.ts";
function media() {
let stops = 0;
let pauses = 0;
const track = {
kind: "video",
stop: () => { stops += 1; },
} as unknown as MediaStreamTrack;
const stream = {
getVideoTracks: () => [track],
getTracks: () => [track],
} as unknown as MediaStream;
const video = {
srcObject: stream,
pause: () => { pauses += 1; },
} as unknown as HTMLVideoElement;
return { stream, video, stops: () => stops, pauses: () => pauses };
}
describe("ephemeral webcam face texture", () => {
it("is off by default and refuses media before explicit active consent", () => {
const consent = createWebcamFaceConsent();
const adapter = createWebcamFaceTexture({ consent });
const input = media();
assert.deepEqual(adapter.state(), {
status: "off",
active: false,
indicatorVisible: false,
consentRevision: 0,
ephemeral: true,
persistence: "none",
recording: false,
uploading: false,
});
assert.throws(() => adapter.bind(input), /active consent/);
assert.equal(adapter.texture(), null);
adapter.dispose();
});
it("accepts only caller-attached video after consent and owns only its texture", () => {
const consent = createWebcamFaceConsent();
consent.requestStart();
consent.start(true);
const input = media();
const wrongVideo = { srcObject: null } as HTMLVideoElement;
const adapter = createWebcamFaceTexture({ consent });
assert.throws(() => adapter.bind({ stream: input.stream, video: wrongVideo }), /attach/);
const texture = adapter.bind(input);
let textureDisposals = 0;
const dispose = texture.dispose.bind(texture);
texture.dispose = () => { textureDisposals += 1; dispose(); };
assert.equal(adapter.texture(), texture);
assert.equal(adapter.state().active, true);
adapter.clear();
assert.equal(textureDisposals, 1);
assert.equal(adapter.texture(), null);
assert.equal(input.stops(), 0, "caller-owned track was stopped");
assert.equal(input.pauses(), 0, "caller-owned video was paused");
adapter.dispose();
});
it("clears on stop/revoke and publishes defensive active-indicator transitions", () => {
const consent = createWebcamFaceConsent();
consent.requestStart();
consent.start(true);
const input = media();
const indicators: WebcamFaceTextureState[] = [];
const adapter = createWebcamFaceTexture({
consent,
onIndicatorChange: (state) => {
state.status = "disposed";
indicators.push({ ...state });
},
});
adapter.bind(input);
assert.equal(adapter.state().status, "active", "callback mutation escaped into adapter");
consent.stop();
assert.equal(adapter.sync().active, false);
assert.equal(adapter.texture(), null);
assert.equal(input.stops(), 0);
assert.deepEqual(indicators.map((state) => state.indicatorVisible), [true, false]);
adapter.dispose();
});
it("is idempotent after disposal and rejects reuse", () => {
const consent = createWebcamFaceConsent();
consent.requestStart();
consent.start(true);
const input = media();
const adapter = createWebcamFaceTexture({ consent });
adapter.bind(input);
adapter.dispose();
adapter.dispose();
assert.equal(adapter.state().status, "disposed");
assert.equal(adapter.texture(), null);
assert.throws(() => adapter.bind(input), /disposed/);
assert.equal(input.stops(), 0);
assert.equal(input.pauses(), 0);
});
});