/** * Objects that travel through a `Load scene` (#165): which entities of the running world are kept, * and how their objects are put into the scene loaded next. */ import type { SceneAsset } from '../core/model/Asset'; import { removeSubtrees } from '../core/model/hierarchy'; import { findAsset, type Project } from '../core/model/Project'; import type { SceneObject } from '../core/model/SceneObject'; import { KEEP_ON_SCENE_LOAD_KEY } from '../core/properties/PropertySchema'; import { resolveSceneObjectProps } from '../core/properties/resolve'; import type { Entity } from './Entity'; /** Whether the switch is on for an object as the scene stores it. */ function isKept(project: Project, object: SceneObject): boolean { const asset = findAsset(project, object.assetId); return ( asset !== undefined && resolveSceneObjectProps(project, asset, object)[KEEP_ON_SCENE_LOAD_KEY] === true ); } /** * The entities a scene swap keeps: every top-level one with the switch on, and all below it. Only a * top-level object can be kept – one further down would arrive without the parent it is placed in. */ export function keptEntities(entities: readonly Entity[]): Entity[] { const kept = new Set(); // A parent may come after its child in the list, so the chain is walked rather than the order trusted. const travels = (entity: Entity): boolean => { let top = entity; while (top.parent) top = top.parent; return top.props[KEEP_ON_SCENE_LOAD_KEY] === true; }; for (const entity of entities) if (travels(entity)) kept.add(entity); return entities.filter((e) => kept.has(e)); } /** * The stored objects of the kept entities, as copies ready for another scene: parented as the run * has them now, and in no hierarchy folder, which belongs to the scene they leave. An entity the * scene stores no object for cannot travel and is left out of `entities`. */ export function objectsToBring( scene: SceneAsset, entities: readonly Entity[], ): { entities: Entity[]; objects: SceneObject[] } { const stored = new Map(scene.objects.map((o) => [o.id, o])); const brought: Entity[] = []; const objects: SceneObject[] = []; for (const entity of entities) { const object = stored.get(entity.objectId); if (!object) continue; const copy = structuredClone(object); copy.parentId = entity.parent?.objectId ?? ''; delete copy.groupId; brought.push(entity); objects.push(copy); } return { entities: brought, objects }; } /** * Puts the brought objects into the scene, after its own. What the scene holds of the same thing is * not built, the one brought along wins: an object of the same id – the scene the object came from, * loaded again – and a top-level object of the same asset that is kept itself, which is how each * level can place its own player and still end up with one. Answers the ids of the objects left out * that way, each with the id of the object standing in for it. */ export function bringObjects( project: Project, scene: SceneAsset, brought: readonly SceneObject[], ): Map { const ids = new Set(brought.map((o) => o.id)); const tops = brought.filter((o) => o.parentId === ''); const standIns = new Map(); const leftOut: string[] = []; for (const object of scene.objects) { if (ids.has(object.id)) { leftOut.push(object.id); continue; } if (object.parentId !== '') continue; const standIn = tops.find((top) => top.assetId === object.assetId); if (!standIn || !isKept(project, object)) continue; leftOut.push(object.id); standIns.set(object.id, standIn.id); } removeSubtrees(scene, leftOut); scene.objects.push(...brought); return standIns; }