feat: add character studio and aircraft foundation
This commit is contained in:
@@ -11,3 +11,9 @@ export {
|
||||
type MediaSurfaceBinding,
|
||||
type MediaSurfaceLifecycle,
|
||||
} from "./lifecycle.ts";
|
||||
export {
|
||||
createOfficeMediaPresentation,
|
||||
type MediaSurfaceDescriptor,
|
||||
type MediaSurfaceGrant,
|
||||
type OfficeMediaPresentation,
|
||||
} from "./presentation.ts";
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user