/** * "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 { isKindWith } from '../properties/kinds'; import { schemaForAsset } from '../properties/PropertySchema'; import { referencesAny, trackDef } from './animation'; import type { AnimationAsset, Asset, TileAsset } 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; } 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(); const matches = (value: unknown): boolean => typeof value === 'string' && wanted.has(value); for (const asset of project.assets) { const keys = schemaForAsset(project, asset) .filter((def) => isKindWith('reference', def.kind)) .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; } /** The tiles drawn from any of these pictures – intrinsic data, like an animation's keys. */ export function tilesUsing(project: Project, ids: readonly string[]): TileAsset[] { const wanted = new Set(ids); return project.assets.filter( (a): a is TileAsset => a.type === 'tile' && a.pictures.some((id) => wanted.has(id)), ); } /** The cells a tile is painted on, and on how many tilemaps – the cells are the maps' own data. */ export function cellsPainted(project: Project, tileId: string): { tilemaps: number; cells: number } { let tilemaps = 0; let cells = 0; for (const asset of project.assets) { if (asset.type !== 'tilemap') continue; const here = asset.layers.reduce( (sum, layer) => sum + Object.values(layer.cells).filter((id) => id === tileId).length, 0, ); if (here > 0) tilemaps++; cells += here; } return { tilemaps, cells }; } /** The animations keying any of these ids – intrinsic data, which `findPropertyUsages` does not see. */ export function animationsUsing(project: Project, ids: readonly string[]): AnimationAsset[] { const wanted = new Set(ids); return project.assets.filter( (a): a is AnimationAsset => a.type === 'animation' && a.tracks.some((t) => referencesAny(t, trackDef(project, a, t)?.kind, wanted)), ); }