import { schemaForAsset } from '../properties/PropertySchema'; import { type Asset, type ContainerAsset, isSceneAsset, type SceneAsset } from './Asset'; import type { SceneObject } from './SceneObject'; /** Written into every document. Reserved for format migrations – the loader does not branch on it yet. */ export const PROJECT_FORMAT_VERSION = 1; export interface Project { formatVersion: number; id: string; name: string; assets: Asset[]; /** Scene shown in the viewport and instantiated on Play. Empty string = none. */ activeSceneId: string; } export function createEmptyProject(id: string, name: string): Project { return { formatVersion: PROJECT_FORMAT_VERSION, id, name, assets: [], activeSceneId: '' }; } export function findAsset(project: Project, assetId: string): Asset | undefined { return project.assets.find((a) => a.id === assetId); } /** The scene asset with that id – undefined for other asset types as well. */ export function findScene(project: Project, sceneId: string): SceneAsset | undefined { const asset = findAsset(project, sceneId); return asset && isSceneAsset(asset) ? asset : undefined; } 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 { holder: ObjectHolder; object: SceneObject; } /** 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 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); } /** One place where an `asset`-kind property points at an asset. */ export type PropertyUsage = | { on: 'asset'; asset: Asset; key: string } | { on: 'object'; holder: ObjectHolder; object: SceneObject; key: string }; /** * Every property that references `assetId`, on assets and on the overrides of placed objects. * * This is the other half of "where is this asset used": `findUsages` finds assets that were * *placed*, which is meaningless for a texture – a texture is never placed, it is worn by * sprites through their `texture` property. Deleting one therefore has to count both, or it * reports "0 uses" for a picture that ten sprites are showing. * * A reference is deliberately left dangling rather than cleared when its asset goes: a * dangling one resolves to "no picture" everywhere it is read, and it comes back by itself * when the delete is undone. */ export function findPropertyUsages(project: Project, assetId: string): PropertyUsage[] { if (!assetId) return []; const result: PropertyUsage[] = []; const assetKeys = new Map(); for (const asset of project.assets) { const keys = schemaForAsset(asset) .filter((def) => def.kind === 'asset') .map((def) => def.key); assetKeys.set(asset.id, keys); for (const key of keys) if (asset.props[key] === assetId) result.push({ on: 'asset', asset, key }); } for (const holder of objectHolders(project)) { for (const object of holder.objects) { for (const key of assetKeys.get(object.assetId) ?? []) if (object.overrides[key] === assetId) result.push({ on: 'object', holder, object, key }); } } return result; }