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:
rootandClaude Opus 5 committed 2026-09-08 22:20:46 +02:00
1 parent f40b8a0523
commit 4568c6581b
17 files changed
+416 -85

No files matched your search

+44
View File
@@ -208,6 +208,50 @@ export class ClearOverrideCommand extends OverrideCommand {
* Re-parents an object ('' = top level). It keeps its world position: the local one is recomputed
* for the new parent.
*/
/**
* Moves an object among its siblings. Sibling order is the order the stored list happens to have
* (`treeOrder`), so the move is a splice – and because objects sharing a `z` fall back to that
* order, it is also how two overlapping objects are put in front of each other.
*/
export class ReorderObjectCommand implements Command {
readonly label = 'Reorder object';
private previousIndex = -1;
/** `beforeId` empty means "behind its last sibling". */
constructor(
private readonly holderId: string,
private readonly objectId: string,
private readonly beforeId: string,
) {}
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
requireStructural(project, holder, this.objectId);
const object = requireObject(project, this.holderId, this.objectId);
if (this.beforeId === this.objectId)
throw new CommandError('An object cannot be moved in front of itself');
let at = holder.objects.length;
if (this.beforeId) {
requireStructural(project, holder, this.beforeId);
const before = requireObject(project, this.holderId, this.beforeId);
if (before.parentId !== object.parentId)
throw new CommandError('An object can only be reordered among its own siblings');
at = holder.objects.indexOf(before);
}
this.previousIndex = holder.objects.indexOf(object);
holder.objects.splice(this.previousIndex, 1);
// The index was read before the removal, so anything behind it has moved up by one.
holder.objects.splice(at > this.previousIndex ? at - 1 : at, 0, object);
}
undo(project: Project): void {
const holder = requireHolder(project, this.holderId);
const object = requireObject(project, this.holderId, this.objectId);
holder.objects.splice(holder.objects.indexOf(object), 1);
holder.objects.splice(this.previousIndex, 0, object);
}
}
export class SetParentCommand implements Command {
readonly label = 'Set parent';
private previous: { parentId: string; position: Position } = {
+5
View File
@@ -19,6 +19,11 @@ function childrenOf(tree: ObjectTree, parentId: string): SceneObject[] {
return tree.objects.filter((o) => o.parentId === parentId);
}
/** Children of `parentId` (`''` = top level) in sibling order – what a reorder rearranges. */
export function childIdsOf(tree: ObjectTree, parentId: string): string[] {
return childrenOf(tree, parentId).map((o) => o.id);
}
function parentOf(tree: ObjectTree, object: SceneObject): SceneObject | undefined {
return object.parentId ? findObject(tree, object.parentId) : undefined;
}
+9
View File
@@ -33,6 +33,15 @@ export function fitAxisToAspect(size: PixelSize, props: PropertyBag, axis: SizeA
return Number.isFinite(fitted) && fitted > 0 ? fitted : null;
}
/**
* Whether the sprite is drawn at the picture's own pixel size – one image pixel per world unit.
* Exact, unlike `matchesAspect`: these two numbers are written as they are, with no division in
* between that could cost a digit.
*/
export function matchesPixelSize(size: PixelSize, props: PropertyBag): boolean {
return Number(props.width) === size.w && Number(props.height) === size.h;
}
/**
* Whether the sprite already has the picture's proportions. Compared as a cross product with a
* relative tolerance, because fitting one side and then asking about the other must not answer