Files
feuerwerk/src/core/model/usages.ts
T
adminandClaude Opus 5.5 9b5eeaab57 Empty a deleted tile's cells on every tilemap (#268)
Deleting a tile left the cells painted with it in the tilemaps. They
drew nothing, blocked nothing and did not show in the delete dialog,
but still read as a tile for "Tile at" and stopped a fill.

- DeleteAssetCommand empties the cells of a deleted tile on every
  layer of every tilemap in the same step. Undo puts the tile and its
  cells back together.
- The delete dialog counts them, for example "3 cell(s) on 1
  tilemap(s) are painted with it and become empty." (cellsPainted).
- presentTile treats a cell naming a missing tile as empty. "Tile at"
  (World.tileAt) and floodFill use it, so cells already in existing
  files read and fill as empty. There is no migration.
- The brush, the rectangle and the fill check the tile in hand. A
  tile deleted after it was picked no longer paints new invisible
  cells, the editor says "Choose a tile to paint with." instead.
- Unchanged on purpose: the picker still reports such a cell as
  naming a tile that no longer exists, and erasing removes it.

Tests:
- ./check.sh: 195 files, 2823 tests, CHECK EXIT 0 (+6).
- GM_STEPS=39,40: ALL PASSED, 100 checks (4 new in 40t).
- Full e2e sum is now 1590 (1586 confirmed by CI on round 8, plus 4).
- Counter-checks, all fail as expected: 5 in unit tests (delete
  without emptying, undo without cells, fill and "Tile at" without
  presentTile, wrong map count) and 3 in the browser (dialog without
  count, delete without emptying, brush without the check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:00:22 +02:00

95 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* "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<string, string[]>();
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)),
);
}