Add pixel-size fit and drag-to-reorder in the hierarchy
The aspect button now carries two offers, so it becomes a menu: fit one side to the texture ratio, or set both sides to the exact pixel size as a single undo step. Dropping a row on the outer quarter of another row inserts it into that gap instead of parenting into it, reordering among the siblings there. Order is the stored order, so ReorderObjectCommand is a splice; equal z falls back to tree order, which makes this the way to draw one object in front of another. Rejected on container content like the other structural edits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f40b8a0523
commit
4568c6581b
17 files changed
+416
-85
No files matched your search
@@ -2,7 +2,12 @@ import { describe, expect, it } from 'vitest';
|
||||
import { CreateTextureCommand } from '../../src/core/commands/assetCommands';
|
||||
import { AddFrameCommand } from '../../src/core/commands/frameCommands';
|
||||
import type { PropertyBag } from '../../src/core/model/Properties';
|
||||
import { fitAxisToAspect, matchesAspect, texturePixelSize } from '../../src/core/properties/aspect';
|
||||
import {
|
||||
fitAxisToAspect,
|
||||
matchesAspect,
|
||||
matchesPixelSize,
|
||||
texturePixelSize,
|
||||
} from '../../src/core/properties/aspect';
|
||||
import { makeSpriteScene } from '../helpers';
|
||||
|
||||
const FILE = `${'a1b2c3d4'.repeat(8)}.png`;
|
||||
@@ -54,6 +59,17 @@ describe('fitAxisToAspect', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesPixelSize', () => {
|
||||
const size = { w: 64, h: 48 };
|
||||
|
||||
it('asks for both sides, unlike the proportions', () => {
|
||||
expect(matchesPixelSize(size, props({ width: 64, height: 48 }))).toBe(true);
|
||||
// Proportional, but half the size: the "set to 64 × 48 px" action still has something to do.
|
||||
expect(matchesPixelSize(size, props({ width: 32, height: 24 }))).toBe(false);
|
||||
expect(matchesAspect(size, props({ width: 32, height: 24 }))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesAspect', () => {
|
||||
it('compares proportions, not sizes', () => {
|
||||
const size = { w: 64, h: 48 };
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PlaceAssetCommand,
|
||||
RemoveSceneObjectCommand,
|
||||
RemoveSceneObjectsCommand,
|
||||
ReorderObjectCommand,
|
||||
ClearOverrideCommand,
|
||||
RenameSceneObjectCommand,
|
||||
SetOverrideCommand,
|
||||
@@ -333,6 +334,10 @@ describe('the structure of a placed container', () => {
|
||||
expect(() => stack.execute(new CreateContainerCommand(sceneId, [child(tA)]))).toThrow(
|
||||
/comes from a container template/,
|
||||
);
|
||||
// Order is structure too: it comes from the template, so it is changed there.
|
||||
expect(() => stack.execute(new ReorderObjectCommand(sceneId, child(tA), child(tB)))).toThrow(
|
||||
/comes from a container template/,
|
||||
);
|
||||
});
|
||||
|
||||
it('refuses to take an object of the scene as a child', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DeleteAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import {
|
||||
MoveSceneObjectCommand,
|
||||
RemoveSceneObjectCommand,
|
||||
ReorderObjectCommand,
|
||||
SetParentCommand,
|
||||
} from '../../src/core/commands/sceneObjectCommands';
|
||||
import { type Graph, GraphInterpreter, worldPositionOf } from '../../src/core/graph';
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
descendantsOf,
|
||||
findObject,
|
||||
localPositionFor,
|
||||
type ObjectTree,
|
||||
treeOrder,
|
||||
visibleTreeOrder,
|
||||
} from '../../src/core/model/hierarchy';
|
||||
@@ -175,6 +177,50 @@ describe('scene hierarchy', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReorderObjectCommand', () => {
|
||||
/** Ids in tree order – the order the panel lists and the tie-break for equal `z`. */
|
||||
const order = (scene: ObjectTree) => treeOrder(scene).map((e) => e.object.id);
|
||||
|
||||
it('moves an object in front of a sibling and undo puts it back', () => {
|
||||
const { stack, sceneId, a, b, c, sceneOf } = threeBoxes();
|
||||
expect(order(sceneOf())).toEqual([a, b, c]);
|
||||
stack.execute(new ReorderObjectCommand(sceneId, c, a));
|
||||
expect(order(sceneOf())).toEqual([c, a, b]);
|
||||
stack.undo();
|
||||
expect(order(sceneOf())).toEqual([a, b, c]);
|
||||
});
|
||||
|
||||
it('moves it behind the last sibling when no successor is named', () => {
|
||||
const { stack, sceneId, a, b, c, sceneOf } = threeBoxes();
|
||||
stack.execute(new ReorderObjectCommand(sceneId, a, ''));
|
||||
expect(order(sceneOf())).toEqual([b, c, a]);
|
||||
});
|
||||
|
||||
it('orders children among themselves, leaving the subtree where it is', () => {
|
||||
const f = threeBoxes();
|
||||
const { stack, sceneId, a, b, c } = f;
|
||||
const x = place(f, f.spriteId, { x: 0, y: 0 }, a);
|
||||
const y = place(f, f.spriteId, { x: 0, y: 0 }, a);
|
||||
expect(order(f.sceneOf())).toEqual([a, x, y, b, c]);
|
||||
stack.execute(new ReorderObjectCommand(sceneId, y, x));
|
||||
expect(order(f.sceneOf())).toEqual([a, y, x, b, c]);
|
||||
// The move is among siblings only: the parent keeps its own place in the list.
|
||||
expect(f.object(y).parentId).toBe(a);
|
||||
});
|
||||
|
||||
it('refuses a target that is not a sibling, so a reorder never re-parents by accident', () => {
|
||||
const f = threeBoxes();
|
||||
const child = place(f, f.spriteId, { x: 0, y: 0 }, f.a);
|
||||
expect(() => f.stack.execute(new ReorderObjectCommand(f.sceneId, f.b, child))).toThrow(
|
||||
/among its own siblings/,
|
||||
);
|
||||
expect(() => f.stack.execute(new ReorderObjectCommand(f.sceneId, f.b, f.b))).toThrow(
|
||||
/in front of itself/,
|
||||
);
|
||||
expect(order(f.sceneOf())).toEqual([f.a, child, f.b, f.c]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runtime hierarchy', () => {
|
||||
it('entities mirror parents: world positions follow the parent, Destroy takes children along', () => {
|
||||
const f = threeBoxes();
|
||||
|
||||
Reference in new issue
Block a user