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
+6
View File
@@ -11,3 +11,9 @@ export {
type MediaSurfaceBinding,
type MediaSurfaceLifecycle,
} from "./lifecycle.ts";
export {
createOfficeMediaPresentation,
type MediaSurfaceDescriptor,
type MediaSurfaceGrant,
type OfficeMediaPresentation,
} from "./presentation.ts";
+163
View File
@@ -0,0 +1,163 @@
/**
* Render hooks for authored office screens.
*
* Screen faces remain batched in `Furnishings`; this layer adds one thin overlay
* only for each interactive monitor/display. Each overlay has an independent
* material, so replacing one screen never splits every other furniture batch.
* It consumes an already-authorized caller-owned `VideoTexture`, never a source
* locator, URL, stream, or identity record.
*/
import * as THREE from "three";
import type { Plan } from "../interiors/plan.ts";
const SCREEN_KINDS = new Set(["tera:screen.monitor", "tera:screen.wall-display"]);
const PLACEHOLDER_COLOR = 0x101820;
export interface MediaSurfaceDescriptor {
screenId: string;
officeId: string;
levelId: string;
roomId: string | null;
kind: string;
bound: boolean;
}
/** Minimal output of server authorization; source locators never enter this layer. */
export interface MediaSurfaceGrant {
canView: boolean;
optedIn: boolean;
}
export interface OfficeMediaPresentation {
group: THREE.Group;
list(): MediaSurfaceDescriptor[];
bind(screenId: string, grant: MediaSurfaceGrant, texture: THREE.VideoTexture): boolean;
clear(screenId: string): boolean;
dispose(): void;
}
interface Surface {
descriptor: MediaSurfaceDescriptor;
mesh: THREE.Mesh;
material: THREE.MeshBasicMaterial;
/** Caller-owned. Kept only while bound and never disposed here. */
texture: THREE.VideoTexture | null;
}
export function createOfficeMediaPresentation(
plan: Plan,
furnishings: THREE.Object3D,
): OfficeMediaPresentation {
const group = new THREE.Group();
group.name = "office-media-surfaces";
const surfaces = new Map<string, Surface>();
let disposed = false;
furnishings.traverse((object) => {
if (!(object instanceof THREE.InstancedMesh)) return;
const separator = object.name.indexOf(":screenDisplay");
if (separator < 0) return;
const kind = object.name.slice(0, separator);
if (!SCREEN_KINDS.has(kind)) return;
const ids = object.userData.props as string[] | undefined;
if (!ids || ids.length !== object.count) return;
for (let index = 0; index < object.count; index += 1) {
const screenId = ids[index];
if (!screenId || surfaces.has(screenId)) continue;
const prop = plan.prop(screenId);
if (!prop) continue;
const level = plan.level(prop.levelId);
if (!level) continue;
const material = placeholderMaterial(screenId);
const mesh = new THREE.Mesh(object.geometry, material);
mesh.name = `media-surface:${screenId}`;
mesh.matrixAutoUpdate = false;
object.getMatrixAt(index, mesh.matrix);
mesh.matrixWorldNeedsUpdate = true;
mesh.castShadow = false;
mesh.receiveShadow = false;
mesh.renderOrder = 1;
const roomId = plan.roomAt(prop.levelId, prop.position)?.id ?? null;
const descriptor: MediaSurfaceDescriptor = {
screenId,
officeId: plan.office.id,
levelId: prop.levelId,
roomId,
kind,
bound: false,
};
mesh.userData.mediaSurface = { ...descriptor };
group.add(mesh);
surfaces.set(screenId, { descriptor, mesh, material, texture: null });
}
});
function clearSurface(surface: Surface): void {
if (surface.material.map !== null) {
surface.material.map = null;
surface.material.color.setHex(PLACEHOLDER_COLOR);
surface.material.needsUpdate = true;
}
surface.texture = null;
surface.descriptor.bound = false;
surface.mesh.userData.mediaSurface = { ...surface.descriptor };
}
return {
group,
list() {
return [...surfaces.values()].map(({ descriptor }) => ({ ...descriptor }));
},
bind(screenId, grant, texture) {
if (disposed) return false;
const surface = surfaces.get(screenId);
if (!surface) return false;
if (!grant.canView || !grant.optedIn) {
clearSurface(surface);
return false;
}
surface.texture = texture;
surface.material.map = texture;
surface.material.color.setHex(0xffffff);
surface.material.needsUpdate = true;
surface.descriptor.bound = true;
surface.mesh.userData.mediaSurface = { ...surface.descriptor };
return true;
},
clear(screenId) {
if (disposed) return false;
const surface = surfaces.get(screenId);
if (!surface) return false;
clearSurface(surface);
return true;
},
dispose() {
if (disposed) return;
disposed = true;
for (const surface of surfaces.values()) {
clearSurface(surface);
// Geometry belongs to the original furniture batch; the small overlay
// owns only its cloned material.
surface.material.dispose();
surface.mesh.removeFromParent();
}
surfaces.clear();
group.clear();
},
};
}
function placeholderMaterial(screenId: string): THREE.MeshBasicMaterial {
return new THREE.MeshBasicMaterial({
name: `media-placeholder:${screenId}`,
color: PLACEHOLDER_COLOR,
toneMapped: false,
polygonOffset: true,
polygonOffsetFactor: -1,
polygonOffsetUnits: -1,
});
}