Take the cross-cutting queries out of the model's base module

findPropertyUsages walks override sites, so Project.ts imported instance.ts
while instance.ts imported Project.ts – the repo's only value-level import
cycle. The queries now sit in usages.ts and Project.ts is a base type module
again. requireScene has had no caller since holderId replaced sceneId.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-10 07:14:28 +02:00
1 parent b13b130da7
commit a2f2f8465c
7 files changed
+70 -76

No files matched your search

+2 -8
View File
@@ -1,10 +1,10 @@
/** Lookups that throw a `CommandError` when the target does not exist. */
import { findNode, type GraphNode } from '../graph/Graph';
import { type NodeDef, nodeDef } from '../graph/NodeRegistry';
import type { Asset, BehaviorAsset, SceneAsset } from '../model/Asset';
import type { Asset, BehaviorAsset } from '../model/Asset';
import { findObject, type ObjectTree } from '../model/hierarchy';
import { isTemplateContent, objectRef, type ObjectRef } from '../model/instance';
import { findAsset, findObjectHolder, findScene, type ObjectHolder, type Project } from '../model/Project';
import { findAsset, findObjectHolder, type ObjectHolder, type Project } from '../model/Project';
import type { SceneObject } from '../model/SceneObject';
import { propDef, type PropertyDef } from '../properties/PropertySchema';
import { CommandError } from './Command';
@@ -15,12 +15,6 @@ export function requireAsset(project: Project, assetId: string): Asset {
return asset;
}
export function requireScene(project: Project, sceneId: string): SceneAsset {
const scene = findScene(project, sceneId);
if (!scene) throw new CommandError(`Scene ${sceneId} not found`);
return scene;
}
export function requireHolder(project: Project, holderId: string): ObjectHolder {
const holder = findObjectHolder(project, holderId);
if (!holder) throw new CommandError(`Scene or container ${holderId} not found`);
-55
View File
@@ -1,7 +1,4 @@
import { schemaForAsset } from '../properties/PropertySchema';
import { type Asset, type ContainerAsset, isSceneAsset, type SceneAsset } from './Asset';
import { overrideSites } from './instance';
import type { SceneObject } from './SceneObject';
/**
* 2 since a placed container stopped copying its template into the scene. Old files are not
@@ -62,55 +59,3 @@ export function findObjectHolder(project: Project, holderId: string): ObjectHold
export function listContainers(project: Project): ContainerAsset[] {
return project.assets.filter((a): a is ContainerAsset => a.type === 'container');
}
export interface SceneObjectRef {
holder: ObjectHolder;
object: SceneObject;
}
export function objectsWhere(
project: Project,
predicate: (object: SceneObject) => boolean,
): SceneObjectRef[] {
const result: SceneObjectRef[] = [];
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, in scenes and in container templates alike. */
export function findUsages(project: Project, assetId: string): SceneObjectRef[] {
return objectsWhere(project, (o) => o.assetId === assetId);
}
export type PropertyUsage =
| { on: 'asset'; asset: Asset; key: string }
/** `path` is empty for a placed object's own override, set for one a container instance keeps. */
| { on: 'object'; holder: ObjectHolder; object: SceneObject; path: string; key: string };
/**
* The other half of "where is this asset used": a texture is never placed, it is worn through a
* sprite's `texture` property, so counting uses needs this as well as `findUsages`. Several ids
* because every frame cut from a texture is a reference to the same picture (see `textureRefIds`).
*/
export function findPropertyUsages(project: Project, ids: string | readonly string[]): PropertyUsage[] {
const wanted = new Set((typeof ids === 'string' ? [ids] : ids).filter(Boolean));
if (wanted.size === 0) return [];
const result: PropertyUsage[] = [];
const refKeys = new Map<string, string[]>();
const matches = (value: unknown): boolean => typeof value === 'string' && wanted.has(value);
for (const asset of project.assets) {
const keys = schemaForAsset(asset)
.filter((def) => def.kind === 'asset' || def.kind === 'texture')
.map((def) => def.key);
refKeys.set(asset.id, keys);
for (const key of keys) if (matches(asset.props[key])) result.push({ on: 'asset', asset, key });
}
for (const site of overrideSites(project, () => true)) {
for (const key of refKeys.get(site.assetId) ?? [])
if (matches(site.bag[key]))
result.push({ on: 'object', holder: site.holder, object: site.object, path: site.path, key });
}
return result;
}
-4
View File
@@ -256,8 +256,6 @@ export function instanceTargetOf(
};
}
// --- writing -----------------------------------------------------------------------------------
/**
* Read/write access to a placed object, whether it is stored in the holder or resolved from a
* container template. `object` is the current state either way; the setters keep an instance edit
@@ -421,8 +419,6 @@ export function overrideSites(project: Project, wanted: (assetId: string) => boo
return sites;
}
// --- unpacking ---------------------------------------------------------------------------------
/**
* The resolved content of an instance as objects that can be stored. The derived ids are kept:
* they are unique already, and anything pointing at one – the selection, a running game, a check
+62
View File
@@ -0,0 +1,62 @@
/**
* "Where is this asset used" – across scenes and container templates alike. Kept out of
* `Project.ts` so that module stays free of the property schema and of container instancing:
* both of those already import it, and answering this question needs both.
*/
import { schemaForAsset } from '../properties/PropertySchema';
import type { Asset } from './Asset';
import { overrideSites } from './instance';
import { type ObjectHolder, objectHolders, type Project } from './Project';
import type { SceneObject } from './SceneObject';
export interface SceneObjectRef {
holder: ObjectHolder;
object: SceneObject;
}
export function objectsWhere(
project: Project,
predicate: (object: SceneObject) => boolean,
): SceneObjectRef[] {
const result: SceneObjectRef[] = [];
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, in scenes and in container templates alike. */
export function findUsages(project: Project, assetId: string): SceneObjectRef[] {
return objectsWhere(project, (o) => o.assetId === assetId);
}
export type PropertyUsage =
| { on: 'asset'; asset: Asset; key: string }
/** `path` is empty for a placed object's own override, set for one a container instance keeps. */
| { on: 'object'; holder: ObjectHolder; object: SceneObject; path: string; key: string };
/**
* The other half of "where is this asset used": a texture is never placed, it is worn through a
* sprite's `texture` property, so counting uses needs this as well as `findUsages`. Several ids
* because every frame cut from a texture is a reference to the same picture (see `textureRefIds`).
*/
export function findPropertyUsages(project: Project, ids: string | readonly string[]): PropertyUsage[] {
const wanted = new Set((typeof ids === 'string' ? [ids] : ids).filter(Boolean));
if (wanted.size === 0) return [];
const result: PropertyUsage[] = [];
const refKeys = new Map<string, string[]>();
const matches = (value: unknown): boolean => typeof value === 'string' && wanted.has(value);
for (const asset of project.assets) {
const keys = schemaForAsset(asset)
.filter((def) => def.kind === 'asset' || def.kind === 'texture')
.map((def) => def.key);
refKeys.set(asset.id, keys);
for (const key of keys) if (matches(asset.props[key])) result.push({ on: 'asset', asset, key });
}
for (const site of overrideSites(project, () => true)) {
for (const key of refKeys.get(site.assetId) ?? [])
if (matches(site.bag[key]))
result.push({ on: 'object', holder: site.holder, object: site.object, path: site.path, key });
}
return result;
}
+2 -1
View File
@@ -8,8 +8,9 @@ import { CompoundCommand } from '../core/commands/CompoundCommand';
import { CreateContainerCommand } from '../core/commands/containerCommands';
import { RemoveNodeCommand } from '../core/commands/graphCommands';
import { RemoveSceneObjectsCommand } from '../core/commands/sceneObjectCommands';
import { findAsset, findPropertyUsages, findUsages } from '../core/model/Project';
import { findAsset } from '../core/model/Project';
import { textureRefIds } from '../core/model/texture';
import { findPropertyUsages, findUsages } from '../core/model/usages';
import { Dialog } from '../ui';
import { type EditorState, objectSelection } from './state';
+2 -7
View File
@@ -9,14 +9,9 @@ import { CreateContainerCommand } from '../../src/core/commands/containerCommand
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import { RemoveVariableCommand, RenameVariableCommand } from '../../src/core/commands/variableCommands';
import type { ContainerAsset } from '../../src/core/model/Asset';
import {
findAsset,
findObjectHolder,
findUsages,
listContainers,
objectHolders,
} from '../../src/core/model/Project';
import { findAsset, findObjectHolder, listContainers, objectHolders } from '../../src/core/model/Project';
import { instanceChildId } from '../../src/core/model/instance';
import { findUsages } from '../../src/core/model/usages';
import { makeBehavior, makeSpriteScene, place } from '../helpers';
/** A behavior with a `speed` variable, placed and overridden to 42, then wrapped in a container. */
+2 -1
View File
@@ -1,7 +1,8 @@
import { describe, expect, it } from 'vitest';
import { CreateTextureCommand } from '../../src/core/commands/assetCommands';
import type { TextureAsset } from '../../src/core/model/Asset';
import { findAsset, findPropertyUsages, findUsages } from '../../src/core/model/Project';
import { findAsset } from '../../src/core/model/Project';
import { findPropertyUsages, findUsages } from '../../src/core/model/usages';
import {
DEFAULT_SLICE,
isTextureFile,