A frame that keeps its corners can be stretched to any size (#41)
game-maker / suite (push) Successful in 6m21s

TextureFrame carries optional `border` (left/right/top/bottom) and
`fill` (stretch | repeat); absent keys mean "not sliced", so no format
bump and undo restores the plain frame instead of four zeroes.

The sliced sprite builds its own geometry instead of the shared
UNIT_QUAD: a 4x4 grid, and for `repeat` one quad per tile carrying the
frame cell's real UVs -- tiling in the geometry, not in the sampler, so
a frame out of a sheet cannot pull its neighbour in. Above 64 tiles per
axis it stretches, to bound the buffer.

A sprite narrower than left+right scales both borders down together
until the middle is exactly 0, so the corners meet at a seam and never
overlap.

The texture editor gets four draggable border lines plus the same four
numbers in the inspector. Hit order is resize handles, then the lines,
then the frame body -- handleAt reports 'move' anywhere inside a frame,
so lines placed after it were unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-16 12:04:09 +02:00
1 parent b9f1a9d319
commit be8f2331de
17 files changed
+1091 -31

No files matched your search

+33
View File
@@ -1,6 +1,9 @@
import { describe, expect, it } from 'vitest';
import type { FrameRect } from '../../src/core/model/texture';
import {
borderAt,
borderLine,
dragBorder,
dragFrame,
fitView,
type FrameHandle,
@@ -138,3 +141,33 @@ describe('dragging a frame', () => {
expect(dragFrame(dragFrame(RECT, handle, 3, 4), handle, -3, -4)).toEqual(RECT);
});
});
describe('nine-slice border lines', () => {
const BORDER = { left: 4, top: 6, right: 8, bottom: 10 };
it('runs each line beside its own edge, the far ones measured from the far side', () => {
expect(borderLine(RECT, BORDER, 'left')).toEqual({ x1: 14, y1: 20, x2: 14, y2: 50 });
expect(borderLine(RECT, BORDER, 'right')).toEqual({ x1: 42, y1: 20, x2: 42, y2: 50 });
expect(borderLine(RECT, BORDER, 'top')).toEqual({ x1: 10, y1: 26, x2: 50, y2: 26 });
expect(borderLine(RECT, BORDER, 'bottom')).toEqual({ x1: 10, y1: 40, x2: 50, y2: 40 });
});
it('grabs a line only near it and only within the frame', () => {
expect(borderAt(RECT, BORDER, { x: 14, y: 35 }, 2)).toBe('left');
expect(borderAt(RECT, BORDER, { x: 30, y: 40 }, 2)).toBe('bottom');
expect(borderAt(RECT, BORDER, { x: 30, y: 35 }, 2)).toBeNull();
// Far outside the frame the lines are not offered, however close the axis would be.
expect(borderAt(RECT, BORDER, { x: 14, y: 200 }, 2)).toBeNull();
});
it('moves the near border with the pointer and the far one against it', () => {
// Both grow when the pointer goes towards the middle, which is what the eye expects of a line
// that is dragged inwards.
expect(dragBorder(BORDER, 'left', 3, 0).left).toBe(7);
expect(dragBorder(BORDER, 'right', -3, 0).right).toBe(11);
expect(dragBorder(BORDER, 'top', 0, 3).top).toBe(9);
expect(dragBorder(BORDER, 'bottom', 0, -3).bottom).toBe(13);
// And each line moves only its own border.
expect(dragBorder(BORDER, 'left', 3, 3)).toEqual({ ...BORDER, left: 7 });
});
});