Spawn node: place assets at runtime into the play copy, Spawner demo behavior
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
c3a533c4e5
commit
91ee2feadd
13 files changed
+219
-37
No files matched your search
+51
-17
@@ -1,5 +1,9 @@
|
||||
import { worldPositionOf } from '../core/graph';
|
||||
import { newId } from '../core/ids';
|
||||
import type { Asset } from '../core/model/Asset';
|
||||
import { isPlaceableType } from '../core/model/AssetTypes';
|
||||
import { findAsset, findScene, type Project } from '../core/model/Project';
|
||||
import { createSceneObject, type SceneObject } from '../core/model/SceneObject';
|
||||
import { resolveSceneObjectProps } from '../core/properties/resolve';
|
||||
import type { Entity } from './Entity';
|
||||
import type { System } from './systems/System';
|
||||
@@ -22,6 +26,8 @@ export class World {
|
||||
readonly log: string[] = [];
|
||||
private readonly systems: System[] = [];
|
||||
private elapsed = 0;
|
||||
/** Set when `spawn` added an object; reported (and cleared) by the next `syncToModel`. */
|
||||
private structureChanged = false;
|
||||
|
||||
private constructor(readonly project: Project, readonly sceneId: string) {}
|
||||
|
||||
@@ -48,21 +54,7 @@ export class World {
|
||||
for (const object of scene.objects) {
|
||||
const asset = findAsset(this.project, object.assetId);
|
||||
if (!asset || asset.type === 'scene') continue;
|
||||
let entity = this.findByObjectId(object.id);
|
||||
if (!entity) {
|
||||
entity = { id: `e-${object.id}`, objectId: object.id, assetId: asset.id, type: asset.type, name: '', x: 0, y: 0, z: 0, props: {}, parent: null };
|
||||
this.entities.push(entity);
|
||||
}
|
||||
entity.assetId = asset.id;
|
||||
entity.type = asset.type;
|
||||
entity.name = object.name || asset.name;
|
||||
entity.x = object.position.x;
|
||||
entity.y = object.position.y;
|
||||
entity.z = object.position.z;
|
||||
const resolved = resolveSceneObjectProps(asset, object);
|
||||
for (const key of Object.keys(entity.props)) if (!(key in resolved)) delete entity.props[key];
|
||||
Object.assign(entity.props, resolved);
|
||||
keep.add(entity);
|
||||
keep.add(this.materialize(object, asset));
|
||||
}
|
||||
for (let i = this.entities.length - 1; i >= 0; i--) if (!keep.has(this.entities[i]!)) this.entities.splice(i, 1);
|
||||
// Link parents once every entity exists (storage order is not guaranteed to be parent-first).
|
||||
@@ -72,14 +64,56 @@ export class World {
|
||||
}
|
||||
}
|
||||
|
||||
/** The entity for a scene object, created when missing; position, name and props re-read. */
|
||||
private materialize(object: SceneObject, asset: Asset): Entity {
|
||||
let entity = this.findByObjectId(object.id);
|
||||
if (!entity) {
|
||||
entity = { id: `e-${object.id}`, objectId: object.id, assetId: asset.id, type: asset.type, name: '', x: 0, y: 0, z: 0, props: {}, parent: null };
|
||||
this.entities.push(entity);
|
||||
}
|
||||
entity.assetId = asset.id;
|
||||
entity.type = asset.type;
|
||||
entity.name = object.name || asset.name;
|
||||
entity.x = object.position.x;
|
||||
entity.y = object.position.y;
|
||||
entity.z = object.position.z;
|
||||
const resolved = resolveSceneObjectProps(asset, object);
|
||||
for (const key of Object.keys(entity.props)) if (!(key in resolved)) delete entity.props[key];
|
||||
Object.assign(entity.props, resolved);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of an asset while running (Spawn node): the object is added to
|
||||
* the scene of the runtime copy – so the panels list it and a full sync keeps it – and its
|
||||
* entity is created right away, without touching the other entities (they may have moved
|
||||
* during this tick already). Position is local to `parent`, world when there is none.
|
||||
* Returns null for unknown or unplaceable assets.
|
||||
*/
|
||||
spawn(assetId: string, position: { x: number; y: number; z: number }, parent: Entity | null): Entity | null {
|
||||
const scene = findScene(this.project, this.sceneId);
|
||||
const asset = findAsset(this.project, assetId);
|
||||
if (!scene || !asset || !isPlaceableType(asset.type)) return null;
|
||||
const parentEntity = parent && this.entities.includes(parent) ? parent : null;
|
||||
const object = createSceneObject(newId(), asset.id, position, parentEntity?.objectId ?? '');
|
||||
scene.objects.push(object);
|
||||
const entity = this.materialize(object, asset);
|
||||
entity.parent = parentEntity;
|
||||
this.structureChanged = true;
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the entities' state into the scene: positions, property values that differ from
|
||||
* what the model resolves (stored as overrides, so the inspector marks them) and removed
|
||||
* entities (their objects are dropped). Returns true when the object list changed.
|
||||
* entities (their objects are dropped). Returns true when the object list changed – by a
|
||||
* destroyed entity here or by a `spawn` since the last call.
|
||||
*/
|
||||
syncToModel(): boolean {
|
||||
const scene = findScene(this.project, this.sceneId);
|
||||
if (!scene) return false;
|
||||
const spawned = this.structureChanged;
|
||||
this.structureChanged = false;
|
||||
const alive = new Set(this.entities.map((e) => e.objectId));
|
||||
const before = scene.objects.length;
|
||||
for (let i = scene.objects.length - 1; i >= 0; i--) {
|
||||
@@ -96,7 +130,7 @@ export class World {
|
||||
const base = resolveSceneObjectProps(asset, object);
|
||||
for (const [key, value] of Object.entries(entity.props)) if (key in base && base[key] !== value) object.overrides[key] = value;
|
||||
}
|
||||
return scene.objects.length !== before;
|
||||
return spawned || scene.objects.length !== before;
|
||||
}
|
||||
|
||||
/** World position of an entity: local position plus those of all ancestors. */
|
||||
|
||||
Reference in new issue
Block a user