Let pixel art and vector art be nine-sliced and sorted (#308)
Nine-slice and the sort line hung on the texture frame alone. Drawing and sorting already read both off the TextureRef every picture hands out, but the generated pictures always said "no slice, no line". So the feature moves from the frame to the picture, without a second copy of any rule. - PictureCut (border, fill, sortLine) is one type. TextureFrame extends it, PixelArtAsset and VectorArtAsset carry it as a whole. The texture file format does not change, and no migration is needed. - The rules in texture.ts take a cut and a size: cutSortLine, setCutSlice, rawCutSlice and the rest replace the frame* versions. serialize.ts reads all three through one readCut. - cutTarget.ts resolves a target to cut and size: a texture frame, a pixel frame's size or the vector canvas. It lives apart from texture.ts because vectorart.ts reaches texture.ts itself. - SetSliceCommand and SetSortLineCommand take that target instead of (assetId, frameId). They refuse an asset that draws no picture. - generatedRef fills slice and sortLine from the drawing. The whole strip of a pixel art asset with several frames gets none, it is no picture of that size. With one frame asset and frame are the same. - Nine-slice scale is one shared schema row, now also on pixel art and vector art. - cutSection.ts holds the NINE-SLICE and SORTING rows once. The frame inspector and the drawing inspector both build them. - SheetCanvas draws a drawing's lines that are set, dashed as in the Texture tab. They are not dragged there, the painting tools own the canvas (as proposed in the issue). - drawOrder falls back to the picture's height when a ref has no rect, or a sort line on vector art would have been ignored. Tests: - Gate: 232 files, 3542 tests (+8: drawing-cut.test.ts, and vector art in sort-by-y.test.ts). Three counter-proofs bite: no cut on a drawing, a cut on the whole strip, no height fallback. - e2e 48: 44 checks (+1, sum 1732). 48l-ii reads red and blue where 12-pixel borders at scale 2 put them. Without borders both spots read green. - Looked at: the Pixel art tab with the lines, and the sliced sprite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d9c6bbfa4d
commit
3e05cbbed5
20 files changed
+666
-300
No files matched your search
@@ -2,28 +2,12 @@ import {
|
||||
RemoveFrameCommand,
|
||||
RenameFrameCommand,
|
||||
SetFrameRectCommand,
|
||||
SetFrameSliceCommand,
|
||||
SetFrameSortLineCommand,
|
||||
} from '../../core/commands/frameCommands';
|
||||
import type { FrameBorders, TextureAsset, TextureFrame } from '../../core/model/Asset';
|
||||
import { type PropertyValue, textOf } from '../../core/model/Properties';
|
||||
import type { TextureAsset, TextureFrame } from '../../core/model/Asset';
|
||||
import { findAssetOf, type Project } from '../../core/model/Project';
|
||||
import {
|
||||
DEFAULT_FILL,
|
||||
frameBorders,
|
||||
frameFill,
|
||||
frameRect,
|
||||
type FrameRect,
|
||||
frameSlice,
|
||||
type FrameSlice,
|
||||
frameSortLine,
|
||||
normalizeFrameRect,
|
||||
normalizeSortLine,
|
||||
setFrameRect,
|
||||
setFrameSlice,
|
||||
setFrameSortLine,
|
||||
} from '../../core/model/texture';
|
||||
import { frameRect, type FrameRect, normalizeFrameRect, setFrameRect } from '../../core/model/texture';
|
||||
import { Button } from '../../ui';
|
||||
import { buildCutRows, refreshCutRows } from './cutSection';
|
||||
import { createFieldRow, type FieldHandlers } from './fieldFactory';
|
||||
import { addGap, addNameField, addNote, addTitle, type InspectorHost } from './host';
|
||||
import { previewingHandlers } from './previewHandlers';
|
||||
@@ -35,13 +19,6 @@ const RECT_COMPONENTS = [
|
||||
{ key: 'h', label: 'Height' },
|
||||
] as const;
|
||||
|
||||
const BORDER_COMPONENTS = [
|
||||
{ key: 'left', label: 'Left' },
|
||||
{ key: 'top', label: 'Top' },
|
||||
{ key: 'right', label: 'Right' },
|
||||
{ key: 'bottom', label: 'Bottom' },
|
||||
] as const;
|
||||
|
||||
/** Structure-defining facts of a frame selection: none – only the frame disappearing rebuilds. */
|
||||
export function frameSignature(project: Project, assetId: string, frameId: string): string {
|
||||
return currentFrame(project, assetId, frameId) ? 'frame' : 'missing';
|
||||
@@ -68,15 +45,7 @@ export function buildFrameSection(host: InspectorHost, assetId: string, frameId:
|
||||
for (const component of RECT_COMPONENTS)
|
||||
rectRow(host, assetId, frameId, component.key, component.label, frame[component.key]);
|
||||
|
||||
addTitle(host, 'NINE-SLICE (IMAGE PIXELS)');
|
||||
for (const component of BORDER_COMPONENTS)
|
||||
borderRow(host, assetId, frameId, component.key, component.label);
|
||||
fillRow(host, assetId, frameId);
|
||||
addNote(host, 'All four at zero: the picture is drawn over the whole box, as it always was.');
|
||||
|
||||
addTitle(host, 'SORTING (IMAGE PIXELS)');
|
||||
sortLineRow(host, assetId, frameId);
|
||||
addNote(host, 'Where a sprite of this frame stands on a layer sorted by Y, up from the bottom edge.');
|
||||
buildCutRows(host, { assetId, frameId }, 'IMAGE PIXELS');
|
||||
|
||||
addGap(host);
|
||||
host.body.add(
|
||||
@@ -93,97 +62,10 @@ export function refreshFrameSection(host: InspectorHost, assetId: string, frameI
|
||||
if (!frame) return false;
|
||||
host.nameField?.setValue(frame.name);
|
||||
for (const component of RECT_COMPONENTS) host.fields.get(component.key)?.setValue(frame[component.key]);
|
||||
const border = frameBorders(frame);
|
||||
for (const component of BORDER_COMPONENTS)
|
||||
host.fields.get(`border-${component.key}`)?.setValue(border[component.key]);
|
||||
host.fields.get('fill')?.setValue(frameFill(frame));
|
||||
host.fields.get('sortLine')?.setValue(frameSortLine(frame));
|
||||
refreshCutRows(host, { assetId, frameId });
|
||||
return true;
|
||||
}
|
||||
|
||||
function sortLineRow(host: InspectorHost, assetId: string, frameId: string): void {
|
||||
const frame = currentFrame(host.state.project, assetId, frameId);
|
||||
const row = createFieldRow(
|
||||
host,
|
||||
{ key: 'sortLine', label: 'Sort line', kind: 'number', default: 0, min: 0, step: 1, decimals: 0 },
|
||||
frame ? frameSortLine(frame) : 0,
|
||||
previewingHandlers<TextureFrame, number>(host, {
|
||||
target: () => currentFrame(host.state.project, assetId, frameId),
|
||||
read: frameSortLine,
|
||||
write: setFrameSortLine,
|
||||
toModel: (input, frame) => normalizeSortLine(Number(input), frame.h),
|
||||
commit: (line) => new SetFrameSortLineCommand(assetId, frameId, line),
|
||||
}),
|
||||
);
|
||||
host.fields.set('sortLine', row);
|
||||
host.body.add(row.row);
|
||||
}
|
||||
|
||||
/**
|
||||
* One border of the nine-slice, in pixels of the frame. The row keeps the other three and the fill
|
||||
* as they are at that moment, so two rows edited one after the other do not undo each other.
|
||||
*/
|
||||
function borderRow(
|
||||
host: InspectorHost,
|
||||
assetId: string,
|
||||
frameId: string,
|
||||
key: keyof FrameBorders,
|
||||
label: string,
|
||||
): void {
|
||||
const frame = currentFrame(host.state.project, assetId, frameId);
|
||||
const row = createFieldRow(
|
||||
host,
|
||||
{ key: `border-${key}`, label, kind: 'number', default: 0, min: 0, step: 1, decimals: 0 },
|
||||
frame ? frameBorders(frame)[key] : 0,
|
||||
sliceHandlers(host, assetId, frameId, (input, slice) => ({
|
||||
...slice,
|
||||
border: { ...slice.border, [key]: Number(input) },
|
||||
})),
|
||||
);
|
||||
host.fields.set(`border-${key}`, row);
|
||||
host.body.add(row.row);
|
||||
}
|
||||
|
||||
/** How the parts between the corners fill their space – one choice for the edges and the middle. */
|
||||
function fillRow(host: InspectorHost, assetId: string, frameId: string): void {
|
||||
const frame = currentFrame(host.state.project, assetId, frameId);
|
||||
const row = createFieldRow(
|
||||
host,
|
||||
{
|
||||
key: 'fill',
|
||||
label: 'Fill',
|
||||
kind: 'enum',
|
||||
default: DEFAULT_FILL,
|
||||
options: [
|
||||
{ value: 'repeat', label: 'Repeat' },
|
||||
{ value: 'stretch', label: 'Stretch' },
|
||||
],
|
||||
},
|
||||
frame ? frameFill(frame) : DEFAULT_FILL,
|
||||
sliceHandlers(host, assetId, frameId, (input, slice) => ({
|
||||
...slice,
|
||||
fill: textOf(input) === 'stretch' ? 'stretch' : 'repeat',
|
||||
})),
|
||||
);
|
||||
host.fields.set('fill', row);
|
||||
host.body.add(row.row);
|
||||
}
|
||||
|
||||
function sliceHandlers(
|
||||
host: InspectorHost,
|
||||
assetId: string,
|
||||
frameId: string,
|
||||
apply: (input: PropertyValue, slice: FrameSlice) => FrameSlice,
|
||||
): FieldHandlers {
|
||||
return previewingHandlers<TextureFrame, FrameSlice>(host, {
|
||||
target: () => currentFrame(host.state.project, assetId, frameId),
|
||||
read: frameSlice,
|
||||
write: setFrameSlice,
|
||||
toModel: (input, frame) => apply(input, frameSlice(frame)),
|
||||
commit: (slice) => new SetFrameSliceCommand(assetId, frameId, slice),
|
||||
});
|
||||
}
|
||||
|
||||
/** A number row editing one component of the rectangle. Whole pixels: a frame indexes a bitmap. */
|
||||
function rectRow(
|
||||
host: InspectorHost,
|
||||
|
||||
Reference in new issue
Block a user