Fix findUsages/undo/node-lookup bugs, split Viewport and Editor, share command and inspector helpers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1eb28314cf
commit
f140c2fd77
58 files changed
+1969
-1171
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { Asset, SceneAsset } from './Asset';
|
||||
import type { Asset, ContainerAsset, SceneAsset } from './Asset';
|
||||
import { isSceneAsset } from './Asset';
|
||||
import type { SceneObject } from './SceneObject';
|
||||
|
||||
@@ -32,21 +32,32 @@ export function listScenes(project: Project): SceneAsset[] {
|
||||
return project.assets.filter(isSceneAsset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything that holds placed objects. Container assets do so just like scenes: their
|
||||
* `objects` are the template a placed container is instantiated from. Anything that walks
|
||||
* "all placed objects" has to cover both, or it silently skips the templates.
|
||||
*/
|
||||
export type ObjectHolder = SceneAsset | ContainerAsset;
|
||||
|
||||
export function objectHolders(project: Project): ObjectHolder[] {
|
||||
return project.assets.filter((a): a is ObjectHolder => a.type === 'scene' || a.type === 'container');
|
||||
}
|
||||
|
||||
export interface SceneObjectRef {
|
||||
scene: SceneAsset;
|
||||
holder: ObjectHolder;
|
||||
object: SceneObject;
|
||||
}
|
||||
|
||||
/** Objects of all scenes that satisfy `predicate`, each with the scene it belongs to. */
|
||||
export function sceneObjectsWhere(project: Project, predicate: (object: SceneObject) => boolean): SceneObjectRef[] {
|
||||
/** Objects of all scenes and container templates that satisfy `predicate`, each with its holder. */
|
||||
export function objectsWhere(project: Project, predicate: (object: SceneObject) => boolean): SceneObjectRef[] {
|
||||
const result: SceneObjectRef[] = [];
|
||||
for (const scene of listScenes(project)) {
|
||||
for (const object of scene.objects) if (predicate(object)) result.push({ scene, object });
|
||||
for (const holder of objectHolders(project)) {
|
||||
for (const object of holder.objects) if (predicate(object)) result.push({ holder, object });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Every placed instance of an asset, across all scenes. */
|
||||
/** Every placed instance of an asset, in scenes and in container templates alike. */
|
||||
export function findUsages(project: Project, assetId: string): SceneObjectRef[] {
|
||||
return sceneObjectsWhere(project, (o) => o.assetId === assetId);
|
||||
return objectsWhere(project, (o) => o.assetId === assetId);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { newId } from '../ids';
|
||||
import type { ContainerAsset } from './Asset';
|
||||
import { chainOf, type ObjectTree } from './hierarchy';
|
||||
import { findAsset, type Project, sceneObjectsWhere, type SceneObjectRef } from './Project';
|
||||
import { findAsset, objectsWhere, type Project, type SceneObjectRef } from './Project';
|
||||
import type { SceneObject } from './SceneObject';
|
||||
|
||||
/** What a placed container's content object is linked to: the container and its template object. */
|
||||
@@ -27,9 +27,13 @@ export function templateLinkOf(project: Project, tree: ObjectTree, object: Scene
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Every placed copy of a template object, across all scenes (template ids are unique per project). */
|
||||
/**
|
||||
* Every placed copy of a template object (template ids are unique per project). Template
|
||||
* objects themselves never carry a template id, so a template is never mistaken for a copy
|
||||
* of itself even though the search covers container assets too.
|
||||
*/
|
||||
export function instancesOf(project: Project, templateId: string): SceneObjectRef[] {
|
||||
return sceneObjectsWhere(project, (o) => o.templateId === templateId);
|
||||
return objectsWhere(project, (o) => o.templateId === templateId);
|
||||
}
|
||||
|
||||
/** Nesting depth at which a container inside a container inside … stops being expanded. */
|
||||
|
||||
+17
-29
@@ -1,6 +1,7 @@
|
||||
import { type Removed, removeWhere } from '../util/list';
|
||||
import type { Position } from './Properties';
|
||||
import type { SceneObject } from './SceneObject';
|
||||
import { descendantsBelow, parentChain, sumAlongChain } from './walk';
|
||||
|
||||
/**
|
||||
* Parent/child structure of a scene. `scene.objects` stays a flat list (storage order =
|
||||
@@ -19,21 +20,18 @@ export function findObject(tree: ObjectTree, id: string): SceneObject | undefine
|
||||
return tree.objects.find((o) => o.id === id);
|
||||
}
|
||||
|
||||
export function childrenOf(tree: ObjectTree, parentId: string): SceneObject[] {
|
||||
function childrenOf(tree: ObjectTree, parentId: string): SceneObject[] {
|
||||
return tree.objects.filter((o) => o.parentId === parentId);
|
||||
}
|
||||
|
||||
/** The parent of an object, or undefined for a top-level one (and for a dangling link). */
|
||||
function parentOf(tree: ObjectTree, object: SceneObject): SceneObject | undefined {
|
||||
return object.parentId ? findObject(tree, object.parentId) : undefined;
|
||||
}
|
||||
|
||||
/** All objects below `id`, depth-first, parents before children. */
|
||||
export function descendantsOf(tree: ObjectTree, id: string): SceneObject[] {
|
||||
const result: SceneObject[] = [];
|
||||
const visit = (parentId: string) => {
|
||||
for (const child of childrenOf(tree, parentId)) {
|
||||
result.push(child);
|
||||
visit(child.id);
|
||||
}
|
||||
};
|
||||
visit(id);
|
||||
return result;
|
||||
return descendantsBelow(id, (parentId) => childrenOf(tree, parentId), (o) => o.id);
|
||||
}
|
||||
|
||||
/** The ids of `rootIds` and of everything below them – what group operations act on. */
|
||||
@@ -54,15 +52,7 @@ export function removeSubtrees(tree: ObjectTree, rootIds: readonly string[]): Re
|
||||
|
||||
/** An object and every parent above it, nearest first. Stops at a cycle (defensive). */
|
||||
export function chainOf(tree: ObjectTree, object: SceneObject): SceneObject[] {
|
||||
const chain = [object];
|
||||
const seen = new Set<string>([object.id]);
|
||||
let parent = object.parentId ? findObject(tree, object.parentId) : undefined;
|
||||
while (parent && !seen.has(parent.id)) {
|
||||
chain.push(parent);
|
||||
seen.add(parent.id);
|
||||
parent = parent.parentId ? findObject(tree, parent.parentId) : undefined;
|
||||
}
|
||||
return chain;
|
||||
return [...parentChain(object, (o) => parentOf(tree, o))];
|
||||
}
|
||||
|
||||
/** Ids of the ancestors of `id`, nearest parent first. Empty for a top-level object. */
|
||||
@@ -86,19 +76,17 @@ export function subtreeRoots(tree: ObjectTree, ids: readonly string[]): string[]
|
||||
return ids.filter((id) => !ancestorIdsOf(tree, id).some((ancestorId) => set.has(ancestorId)));
|
||||
}
|
||||
|
||||
function sumAlongChain(tree: ObjectTree, object: SceneObject, withEditorOffset: boolean): Position {
|
||||
const result: Position = { x: 0, y: 0, z: 0 };
|
||||
for (const o of chainOf(tree, object)) {
|
||||
result.x += o.position.x + (withEditorOffset ? o.editorOffset.x : 0);
|
||||
result.y += o.position.y + (withEditorOffset ? o.editorOffset.y : 0);
|
||||
result.z += o.position.z;
|
||||
}
|
||||
return result;
|
||||
function positionAlongChain(tree: ObjectTree, object: SceneObject, withEditorOffset: boolean): Position {
|
||||
return sumAlongChain(
|
||||
object,
|
||||
(o) => parentOf(tree, o),
|
||||
(o) => (withEditorOffset ? { x: o.position.x + o.editorOffset.x, y: o.position.y + o.editorOffset.y, z: o.position.z } : o.position),
|
||||
);
|
||||
}
|
||||
|
||||
/** Position of an object in world units (all local positions along the chain summed up). */
|
||||
export function worldPosition(tree: ObjectTree, object: SceneObject): Position {
|
||||
return sumAlongChain(tree, object, false);
|
||||
return positionAlongChain(tree, object, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +95,7 @@ export function worldPosition(tree: ObjectTree, object: SceneObject): Position {
|
||||
* in the game, in collisions or in the graph ever sees it (they all use `worldPosition`).
|
||||
*/
|
||||
export function editorPosition(tree: ObjectTree, object: SceneObject): Position {
|
||||
return sumAlongChain(tree, object, true);
|
||||
return positionAlongChain(tree, object, true);
|
||||
}
|
||||
|
||||
/** The local position that puts an object at `world` under the given parent ('' = top level). */
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Walking a parent/child structure, generic over the shape. Two hierarchies exist side by
|
||||
* side – scene objects (`parentId` + lookup in a flat list) and runtime entities (direct
|
||||
* `parent` pointer) – and both walk the same way. Callers pass in how to get from an item to
|
||||
* its parent (or children); everything else lives here, so the cycle guard exists once.
|
||||
*/
|
||||
import type { Position } from './Properties';
|
||||
|
||||
/**
|
||||
* `start` and every ancestor above it, nearest first. An item that was already visited ends
|
||||
* the walk instead of looping forever: a cyclic parent chain is always a bug (loading repairs
|
||||
* one, see `store/serialize`), but no walk at runtime may hang because of it.
|
||||
*/
|
||||
export function* parentChain<T>(start: T, parentOf: (item: T) => T | null | undefined): Generator<T> {
|
||||
const seen = new Set<T>();
|
||||
for (let current: T | null | undefined = start; current && !seen.has(current); current = parentOf(current)) {
|
||||
seen.add(current);
|
||||
yield current;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the parents of `start` lead into a cycle instead of up to a root. */
|
||||
export function hasParentCycle<T>(start: T, parentOf: (item: T) => T | null | undefined): boolean {
|
||||
let last = start;
|
||||
for (const item of parentChain(start, parentOf)) last = item;
|
||||
// The walk stops either above a root or at an already visited item – only the latter still has a parent.
|
||||
return Boolean(parentOf(last));
|
||||
}
|
||||
|
||||
/**
|
||||
* The local positions of `start` and all its ancestors summed up – the world position, as
|
||||
* long as the hierarchy has no rotation or scale (it has none).
|
||||
*/
|
||||
export function sumAlongChain<T>(start: T, parentOf: (item: T) => T | null | undefined, positionOf: (item: T) => Position): Position {
|
||||
const total: Position = { x: 0, y: 0, z: 0 };
|
||||
for (const item of parentChain(start, parentOf)) {
|
||||
const position = positionOf(item);
|
||||
total.x += position.x;
|
||||
total.y += position.y;
|
||||
total.z += position.z;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything below `rootKey`, depth-first, parents before children. `keyOf` turns a found item
|
||||
* back into the key its own children are looked up by, which is what lets ids (scene objects)
|
||||
* and object identity (entities) use the same walk.
|
||||
*/
|
||||
export function descendantsBelow<T, K>(rootKey: K, childrenOf: (key: K) => readonly T[], keyOf: (item: T) => K): T[] {
|
||||
const result: T[] = [];
|
||||
const visit = (key: K) => {
|
||||
for (const child of childrenOf(key)) {
|
||||
result.push(child);
|
||||
visit(keyOf(child));
|
||||
}
|
||||
};
|
||||
visit(rootKey);
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user