feat(viewport): add fit artboard command
This commit is contained in:
@@ -14,11 +14,13 @@ export function createImageStudioApp(options?: { documentName?: string; createDe
|
|||||||
const store = createAppStore(createInitialAppState(options?.documentName), registry);
|
const store = createAppStore(createInitialAppState(options?.documentName), registry);
|
||||||
|
|
||||||
if (options?.createDefaultArtboard !== false) {
|
if (options?.createDefaultArtboard !== false) {
|
||||||
|
const artboardId = crypto.randomUUID();
|
||||||
store.dispatch(commandIds.documentAddArtboard, {
|
store.dispatch(commandIds.documentAddArtboard, {
|
||||||
id: crypto.randomUUID(),
|
id: artboardId,
|
||||||
name: "Artboard 1",
|
name: "Artboard 1",
|
||||||
bounds: { x: -400, y: -300, w: 800, h: 600 },
|
bounds: { x: -400, y: -300, w: 800, h: 600 },
|
||||||
});
|
});
|
||||||
|
store.dispatch(commandIds.viewportFitArtboard, { artboardId });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ export const commandIds = {
|
|||||||
viewportZoomAroundPoint: "viewport.zoomAroundPoint",
|
viewportZoomAroundPoint: "viewport.zoomAroundPoint",
|
||||||
viewportSetSize: "viewport.setSize",
|
viewportSetSize: "viewport.setSize",
|
||||||
viewportReset: "viewport.reset",
|
viewportReset: "viewport.reset",
|
||||||
|
viewportFitArtboard: "viewport.fitArtboard",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { DocumentAddArtboardPayload } from "./document";
|
|||||||
import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
|
import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
|
||||||
import type { ToolSetActivePayload } from "./tool";
|
import type { ToolSetActivePayload } from "./tool";
|
||||||
import type {
|
import type {
|
||||||
|
ViewportFitArtboardPayload,
|
||||||
ViewportPanPayload,
|
ViewportPanPayload,
|
||||||
ViewportSetSizePayload,
|
ViewportSetSizePayload,
|
||||||
ViewportSetZoomPayload,
|
ViewportSetZoomPayload,
|
||||||
@@ -22,6 +23,7 @@ export type CommandPayloads = {
|
|||||||
[commandIds.viewportZoomAroundPoint]: ViewportZoomAroundPointPayload;
|
[commandIds.viewportZoomAroundPoint]: ViewportZoomAroundPointPayload;
|
||||||
[commandIds.viewportSetSize]: ViewportSetSizePayload;
|
[commandIds.viewportSetSize]: ViewportSetSizePayload;
|
||||||
[commandIds.viewportReset]: void;
|
[commandIds.viewportReset]: void;
|
||||||
|
[commandIds.viewportFitArtboard]: ViewportFitArtboardPayload | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CommandId = keyof CommandPayloads;
|
export type CommandId = keyof CommandPayloads;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createInitialAppState } from "@editor/initial-state";
|
import { createInitialAppState } from "@editor/initial-state";
|
||||||
import {
|
import {
|
||||||
|
viewportFitArtboardCommand,
|
||||||
viewportPanCommand,
|
viewportPanCommand,
|
||||||
viewportResetCommand,
|
viewportResetCommand,
|
||||||
viewportSetSizeCommand,
|
viewportSetSizeCommand,
|
||||||
@@ -34,6 +35,36 @@ describe("viewport commands", () => {
|
|||||||
expect(next.editor.viewport.center).toEqual({ x: 25, y: 0 });
|
expect(next.editor.viewport.center).toEqual({ x: 25, y: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("fits an artboard in the viewport", () => {
|
||||||
|
const state = {
|
||||||
|
...viewportSetSizeCommand.execute(context(), { w: 1000, h: 800 }),
|
||||||
|
document: {
|
||||||
|
...context().state.document,
|
||||||
|
artboards: [{ id: "artboard-1", name: "Artboard", bounds: { x: -400, y: -300, w: 800, h: 600 }, backgroundColor: "transparent", layers: [] }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = viewportFitArtboardCommand.execute({ state }, { artboardId: "artboard-1", padding: 100 });
|
||||||
|
|
||||||
|
expect(next.editor.viewport.center).toEqual({ x: 0, y: 0 });
|
||||||
|
expect(next.editor.viewport.zoom).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fit artboard centers even before viewport size is known", () => {
|
||||||
|
const state = {
|
||||||
|
...context().state,
|
||||||
|
document: {
|
||||||
|
...context().state.document,
|
||||||
|
artboards: [{ id: "artboard-1", name: "Artboard", bounds: { x: 10, y: 20, w: 100, h: 200 }, backgroundColor: "transparent", layers: [] }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = viewportFitArtboardCommand.execute({ state }, { artboardId: "artboard-1" });
|
||||||
|
|
||||||
|
expect(next.editor.viewport.center).toEqual({ x: 60, y: 120 });
|
||||||
|
expect(next.editor.viewport.zoom).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test("resets viewport but preserves size", () => {
|
test("resets viewport but preserves size", () => {
|
||||||
const sized = viewportSetSizeCommand.execute(context(), { w: 640, h: 480 });
|
const sized = viewportSetSizeCommand.execute(context(), { w: 640, h: 480 });
|
||||||
const panned = viewportPanCommand.execute({ state: sized }, { delta: { x: 4, y: 8 } });
|
const panned = viewportPanCommand.execute({ state: sized }, { delta: { x: 4, y: 8 } });
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ArtboardId } from "@core/id";
|
||||||
import type { Vec2D } from "@core/geometry";
|
import type { Vec2D } from "@core/geometry";
|
||||||
import type { Command } from "./command";
|
import type { Command } from "./command";
|
||||||
import { commandIds } from "./ids";
|
import { commandIds } from "./ids";
|
||||||
@@ -20,6 +21,11 @@ export type ViewportSetSizePayload = {
|
|||||||
h: number;
|
h: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ViewportFitArtboardPayload = {
|
||||||
|
artboardId?: ArtboardId;
|
||||||
|
padding?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const viewportPanCommand: Command<ViewportPanPayload> = {
|
export const viewportPanCommand: Command<ViewportPanPayload> = {
|
||||||
id: commandIds.viewportPan,
|
id: commandIds.viewportPan,
|
||||||
name: "Pan viewport",
|
name: "Pan viewport",
|
||||||
@@ -111,6 +117,40 @@ export const viewportSetSizeCommand: Command<ViewportSetSizePayload> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const viewportFitArtboardCommand: Command<ViewportFitArtboardPayload | undefined> = {
|
||||||
|
id: commandIds.viewportFitArtboard,
|
||||||
|
name: "Fit artboard in viewport",
|
||||||
|
execute({ state }, payload) {
|
||||||
|
const artboardId = payload?.artboardId ?? state.editor.selection.artboardId;
|
||||||
|
const artboard = artboardId
|
||||||
|
? state.document.artboards.find((candidate) => candidate.id === artboardId)
|
||||||
|
: state.document.artboards[0];
|
||||||
|
|
||||||
|
if (!artboard) return state;
|
||||||
|
|
||||||
|
const padding = Math.max(0, payload?.padding ?? 48);
|
||||||
|
const availableWidth = Math.max(0, state.editor.viewport.size.w - padding * 2);
|
||||||
|
const availableHeight = Math.max(0, state.editor.viewport.size.h - padding * 2);
|
||||||
|
const canFitZoom = availableWidth > 0 && availableHeight > 0 && artboard.bounds.w > 0 && artboard.bounds.h > 0;
|
||||||
|
const zoom = canFitZoom ? Math.max(0.01, Math.min(availableWidth / artboard.bounds.w, availableHeight / artboard.bounds.h)) : state.editor.viewport.zoom;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
editor: {
|
||||||
|
...state.editor,
|
||||||
|
viewport: {
|
||||||
|
...state.editor.viewport,
|
||||||
|
center: {
|
||||||
|
x: artboard.bounds.x + artboard.bounds.w / 2,
|
||||||
|
y: artboard.bounds.y + artboard.bounds.h / 2,
|
||||||
|
},
|
||||||
|
zoom,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const viewportResetCommand: Command = {
|
export const viewportResetCommand: Command = {
|
||||||
id: commandIds.viewportReset,
|
id: commandIds.viewportReset,
|
||||||
name: "Reset viewport",
|
name: "Reset viewport",
|
||||||
@@ -135,5 +175,6 @@ export const viewportCommands = [
|
|||||||
viewportSetZoomCommand,
|
viewportSetZoomCommand,
|
||||||
viewportZoomAroundPointCommand,
|
viewportZoomAroundPointCommand,
|
||||||
viewportSetSizeCommand,
|
viewportSetSizeCommand,
|
||||||
|
viewportFitArtboardCommand,
|
||||||
viewportResetCommand,
|
viewportResetCommand,
|
||||||
] satisfies Command<unknown>[];
|
] satisfies Command<unknown>[];
|
||||||
|
|||||||
Reference in New Issue
Block a user