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:
1 parent
5a6af74e44
commit
35a1a23af8
29 files changed
+2650
-53
No files matched your search
@@ -1,11 +1,11 @@
|
||||
// The graph package index registers the built-in nodes; unknown node types are dropped below.
|
||||
import { createEmptyGraph, type Graph, type GraphEdge, type GraphNode, nodeDef } from '../graph';
|
||||
import type { Asset } from '../model/Asset';
|
||||
import type { Asset, TextureFrame } from '../model/Asset';
|
||||
import { isAssetType } from '../model/AssetTypes';
|
||||
import type { PropertyBag, PropertyValue } from '../model/Properties';
|
||||
import { PROJECT_FORMAT_VERSION, type Project } from '../model/Project';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { isTextureFile } from '../model/texture';
|
||||
import { isTextureFile, normalizeFrameRect } from '../model/texture';
|
||||
import { hasParentCycle } from '../model/walk';
|
||||
import {
|
||||
coerceValue,
|
||||
@@ -43,8 +43,10 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: build assets.
|
||||
// Pass 2: build assets. Frame ids live in the same reference space as asset ids (a sprite's
|
||||
// `texture` may name either), so they are handed the ids already spoken for.
|
||||
const assets: Asset[] = [];
|
||||
const takenIds = new Set(schemas.keys());
|
||||
for (const a of rawAssets) {
|
||||
const id = typeof a.id === 'string' ? a.id : '';
|
||||
const schema = schemas.get(id);
|
||||
@@ -64,7 +66,7 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
graph: normalizeGraph(a.graph),
|
||||
});
|
||||
} else if (type === 'texture') {
|
||||
assets.push({ id, type, name, props, ...normalizeTexture(a) });
|
||||
assets.push({ id, type, name, props, ...normalizeTexture(a, takenIds) });
|
||||
} else {
|
||||
assets.push({ id, type, name, props });
|
||||
}
|
||||
@@ -189,11 +191,31 @@ function normalizeGraph(raw: unknown): Graph {
|
||||
* is dropped rather than kept: it can only have been hand-written, and a request built from it
|
||||
* would 404. Such a texture loads as "no picture", which draws as the sprite's plain colour.
|
||||
*/
|
||||
function normalizeTexture(raw: Record<string, unknown>): { file: string; width: number; height: number } {
|
||||
function normalizeTexture(
|
||||
raw: Record<string, unknown>,
|
||||
takenIds: Set<string>,
|
||||
): { file: string; width: number; height: number; frames: TextureFrame[] } {
|
||||
const file = typeof raw.file === 'string' && isTextureFile(raw.file) ? raw.file : '';
|
||||
const size = (value: unknown): number =>
|
||||
typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
||||
return { file, width: size(raw.width), height: size(raw.height) };
|
||||
const width = size(raw.width);
|
||||
const height = size(raw.height);
|
||||
const frames: TextureFrame[] = [];
|
||||
for (const item of Array.isArray(raw.frames) ? raw.frames : []) {
|
||||
const f = asRecord(item);
|
||||
const id = typeof f.id === 'string' ? f.id : '';
|
||||
// A frame id shares the reference space with asset ids (a sprite may name either), so a
|
||||
// duplicate would make one reference mean two pictures. Drop it rather than resolve it.
|
||||
if (!id || takenIds.has(id)) continue;
|
||||
takenIds.add(id);
|
||||
const rect = normalizeFrameRect(
|
||||
{ x: toNumber(f.x), y: toNumber(f.y), w: toNumber(f.w), h: toNumber(f.h) },
|
||||
width,
|
||||
height,
|
||||
);
|
||||
frames.push({ id, name: typeof f.name === 'string' ? f.name : '', ...rect });
|
||||
}
|
||||
return { file, width, height, frames };
|
||||
}
|
||||
|
||||
function normalizeBag(raw: unknown, schema: PropertySchema, sparse: boolean): PropertyBag {
|
||||
|
||||
Reference in new issue
Block a user