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

+12 -3
View File
@@ -1,6 +1,6 @@
import { type Application, type Container, Rectangle, Text } from 'pixi.js';
import { ui, Widget } from '../ui';
import type { Editor } from './Editor';
import type { CentreTab, Editor } from './Editor';
import type { EditorState } from './state';
/**
@@ -8,7 +8,7 @@ import type { EditorState } from './state';
* visible text and to read editor state. Read-only access; harmless in production.
*/
export function installDebugHook(app: Application, state: EditorState, editor: Editor): void {
const { viewport, gameView, graphEditor, hierarchy } = editor;
const { viewport, gameView, graphEditor, textureEditor, hierarchy } = editor;
const centreOf = (widget: { getGlobalPosition(): { x: number; y: number }; w: number; h: number }) => {
const p = widget.getGlobalPosition();
return { x: p.x + widget.w / 2, y: p.y + widget.h / 2 };
@@ -81,6 +81,15 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
nodeAt: (nodeId: string) => graphEditor.nodeGlobal(nodeId),
center: () => centreOf(graphEditor),
},
texture: {
current: () => textureEditor.currentAssetId,
/**
* Where an image pixel of the shown sheet sits on screen. Frames are dragged in image
* coordinates, so without this a check could only drag somewhere and hope – it could not
* aim at a known part of the picture and then assert which part came out.
*/
pointAt: (x: number, y: number) => textureEditor.imageToScreen(x, y),
},
world: () => editor.worldSnapshot(),
/** Rows the hierarchy shows – collapsed subtrees are not in here. */
hierarchyOrder: () => hierarchy.rowOrder(),
@@ -103,6 +112,6 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
},
fps: () => editor.fps,
tab: () => editor.tab,
showTab: (tab: 'scene' | 'graph' | 'game') => editor.showTab(tab),
showTab: (tab: CentreTab) => editor.showTab(tab),
};
}