Add a Texture tab for defining frames

A new Texture tab next to Scene/Game/Graph shows a texture sheet and lets
frames be drawn on it: drag to create, move, resize via eight handles, edit
name and rect in the inspector, Delete to remove. Every change is a command,
so one gesture is one undo step. "Slice..." cuts a grid from columns and rows
with optional offset and spacing, previewing the resulting frame count and
cell size live.

Sprites pick a frame instead of the whole texture through the same `texture`
property: it now holds either a texture id or a frame id. Frame ids come from
the same generator and are document-wide unique, so one picker, one property
and one set of instance overrides cover both cases, and every sprite written
before this still means the same thing.

A Graphics fill ignores a Pixi sub-texture's frame and draws the whole image,
because it derives its uvs from the shape bounds and the inverted style
matrix. The cutout is therefore a matrix (`viewport/textureFill.ts`), which
keeps a sheet at a single GPU upload no matter how many of its frames are on
screen at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 14:27:23 +02:00
1 parent 5a6af74e44
commit 35a1a23af8
29 files changed
+2650 -53

No files matched your search

+18 -10
View File
@@ -71,32 +71,40 @@ export type PropertyUsage =
| { on: 'object'; holder: ObjectHolder; object: SceneObject; key: string };
/**
* Every property that references `assetId`, on assets and on the overrides of placed objects.
* Every property that references one of `ids`, 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
* Several ids rather than one because a texture is referenced by more than its own id: every
* frame cut from it is a reference to the same picture (see `textureRefIds`). Deleting the
* texture has to count the sprites wearing a frame of it too, or it under-reports exactly the
* sheets that were sliced – the ones with the most users.
*
* A reference is deliberately left dangling rather than cleared when its target 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 [];
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 assetKeys = new Map<string, string[]>();
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')
.filter((def) => def.kind === 'asset' || def.kind === 'texture')
.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 });
refKeys.set(asset.id, keys);
for (const key of keys) if (matches(asset.props[key])) 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 });
for (const key of refKeys.get(object.assetId) ?? [])
if (matches(object.overrides[key])) result.push({ on: 'object', holder, object, key });
}
}
return result;