/** * Nine-slice and sort line on pixel art and vector art (#308): the same commands, rules and ref * fields a texture frame has, measured against a pixel frame or the vector canvas. */ import { afterEach, describe, expect, it } from 'vitest'; import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands'; import { CommandError } from '../../src/core/commands/Command'; import { SetSliceCommand, SetSortLineCommand } from '../../src/core/commands/frameCommands'; import type { PixelArtAsset, VectorArtAsset } from '../../src/core/model/Asset'; import { registerPictureSource } from '../../src/core/model/generated'; import { pixelArtPicture } from '../../src/core/model/pixelart'; import { findAsset } from '../../src/core/model/Project'; import { resolvePicture, SLICE_SCALE_KEY } from '../../src/core/model/texture'; import { VECTOR_HEIGHT_KEY, VECTOR_WIDTH_KEY, vectorArtPicture } from '../../src/core/model/vectorart'; import { deserializeProject, serializeProject } from '../../src/core/store/serialize'; import { cutLines } from '../../src/editor/texture/sheetView'; import { makeProject, roundTrip } from '../helpers'; const RING = { left: 4, top: 4, right: 4, bottom: 4 }; function drawings() { const fixture = makeProject(); const pixel = new CreateAssetCommand('pixelart', 'Hero'); const vector = new CreateAssetCommand('vectorart', 'Panel'); fixture.stack.execute(pixel); fixture.stack.execute(vector); const pixelArt = () => findAsset(fixture.project, pixel.asset.id) as PixelArtAsset; const vectorArt = () => findAsset(fixture.project, vector.asset.id) as VectorArtAsset; pixelArt().width = 16; pixelArt().height = 24; fixture.stack.execute(new SetAssetPropertyCommand(vector.asset.id, VECTOR_WIDTH_KEY, 64)); fixture.stack.execute(new SetAssetPropertyCommand(vector.asset.id, VECTOR_HEIGHT_KEY, 32)); return { ...fixture, pixelId: pixel.asset.id, vectorId: vector.asset.id, pixelArt, vectorArt }; } describe('slicing and sorting a drawing as a whole (#308)', () => { it('sets both on pixel art and vector art, and undo takes the keys away again', () => { const f = drawings(); for (const [assetId, read] of [ [f.pixelId, f.pixelArt], [f.vectorId, f.vectorArt], ] as const) { f.stack.execute(new SetSliceCommand({ assetId }, { border: RING, fill: 'stretch' })); f.stack.execute(new SetSortLineCommand({ assetId }, 6)); expect(read()).toMatchObject({ border: RING, fill: 'stretch', sortLine: 6 }); f.stack.undo(); f.stack.undo(); expect('border' in read() || 'fill' in read() || 'sortLine' in read()).toBe(false); } }); it('holds borders and line inside the frame or the canvas', () => { const f = drawings(); const wide = { left: 40, top: 0, right: 40, bottom: 0 }; f.stack.execute(new SetSliceCommand({ assetId: f.pixelId }, { border: wide, fill: 'repeat' })); f.stack.execute(new SetSortLineCommand({ assetId: f.pixelId }, 99)); // 16 wide: one pixel stays between the two; 24 tall is as high as the line goes. expect(f.pixelArt()).toMatchObject({ border: { left: 8, right: 7 }, sortLine: 24 }); f.stack.execute(new SetSliceCommand({ assetId: f.vectorId }, { border: wide, fill: 'repeat' })); f.stack.execute(new SetSortLineCommand({ assetId: f.vectorId }, 99)); expect(f.vectorArt()).toMatchObject({ border: { left: 32, right: 31 }, sortLine: 32 }); }); it('refuses an asset that draws no picture of its own', () => { const f = drawings(); const sprite = new CreateAssetCommand('sprite', 'Box'); f.stack.execute(sprite); expect(() => f.stack.execute(new SetSortLineCommand({ assetId: sprite.asset.id }, 3))).toThrow( /Box cannot be sliced/, ); expect(() => f.stack.execute(new SetSortLineCommand({ assetId: f.pixelId, frameId: 'x' }, 3))).toThrow( CommandError, ); }); it('survives a round trip, and loading holds it to the canvas', () => { const f = drawings(); f.stack.execute(new SetSliceCommand({ assetId: f.vectorId }, { border: RING, fill: 'stretch' })); f.stack.execute(new SetSortLineCommand({ assetId: f.pixelId }, 5)); const back = roundTrip(f.project); expect(findAsset(back, f.vectorId)).toMatchObject({ border: RING, fill: 'stretch' }); expect(findAsset(back, f.pixelId)).toMatchObject({ sortLine: 5 }); const saved = serializeProject(f.project) as { assets: Record[] }; const pixel = saved.assets.find((a) => a.id === f.pixelId)!; pixel.sortLine = 70; pixel.border = { left: 'x', top: 2.6, right: -1, bottom: 0 }; expect(findAsset(deserializeProject(saved), f.pixelId)).toMatchObject({ sortLine: 24, border: { left: 0, top: 3, right: 0, bottom: 0 }, }); }); }); describe('the picture a drawing hands to a sprite (#308)', () => { const removers: (() => void)[] = []; afterEach(() => removers.splice(0).forEach((remove) => remove())); it('carries the cut of a pixel frame, measured in the frame', () => { removers.push(registerPictureSource('pixelart', pixelArtPicture)); const f = drawings(); f.stack.execute(new SetSliceCommand({ assetId: f.pixelId }, { border: RING, fill: 'repeat' })); f.stack.execute(new SetSortLineCommand({ assetId: f.pixelId }, 6)); f.stack.execute(new SetAssetPropertyCommand(f.pixelId, SLICE_SCALE_KEY, 2)); const slice = { border: RING, fill: 'repeat', sourceW: 16, sourceH: 24, scale: 2 }; expect(resolvePicture(f.project, f.pixelId)).toMatchObject({ slice, sortLine: 6 }); // With a second frame the asset is the sheet of both, which is no picture of that size. f.pixelArt().frames.push({ id: 'second', cels: {} }); expect(resolvePicture(f.project, f.pixelId)).toMatchObject({ sourceWidth: 32, slice: null }); expect(resolvePicture(f.project, f.pixelId)?.sortLine).toBeUndefined(); expect(resolvePicture(f.project, 'second')).toMatchObject({ rect: { x: 16, w: 16 }, slice, sortLine: 6 }); }); it('carries the cut of vector art, measured in the canvas', () => { removers.push(registerPictureSource('vectorart', vectorArtPicture)); const f = drawings(); expect(resolvePicture(f.project, f.vectorId)).toMatchObject({ slice: null }); f.stack.execute(new SetSliceCommand({ assetId: f.vectorId }, { border: RING, fill: 'stretch' })); f.stack.execute(new SetSortLineCommand({ assetId: f.vectorId }, 8)); expect(resolvePicture(f.project, f.vectorId)).toMatchObject({ rect: null, slice: { border: RING, fill: 'stretch', sourceW: 64, sourceH: 32, scale: 1 }, sortLine: 8, }); }); }); describe('the lines a drawing tab shows (#308)', () => { it('draws only what is set, in canvas pixels', () => { expect(cutLines({}, { w: 16, h: 24 })).toEqual({ borders: [], sortLine: null }); const { borders, sortLine } = cutLines( { border: { left: 4, top: 0, right: 0, bottom: 2 }, sortLine: 6 }, { w: 16, h: 24 }, ); expect(borders).toEqual([ { x1: 4, y1: 0, x2: 4, y2: 24 }, { x1: 0, y1: 22, x2: 16, y2: 22 }, ]); expect(sortLine).toEqual({ x1: 0, y1: 18, x2: 16, y2: 18 }); }); });