feat(input): support artboard selection
This commit is contained in:
@@ -5,6 +5,7 @@ export {
|
|||||||
} from "./dom";
|
} from "./dom";
|
||||||
export type { CommandKeybind, GlobalKeybindConsumer, Keybind, KeybindEvent, KeybindMap } from "./keyboard";
|
export type { CommandKeybind, GlobalKeybindConsumer, Keybind, KeybindEvent, KeybindMap } from "./keyboard";
|
||||||
export { handleKeybind, keybindFromEvent } from "./keyboard";
|
export { handleKeybind, keybindFromEvent } from "./keyboard";
|
||||||
|
export { handleArtboardSelection } from "./selection";
|
||||||
export type { ViewportPanInputController } from "./viewport-pan";
|
export type { ViewportPanInputController } from "./viewport-pan";
|
||||||
export { createViewportPanInputController } from "./viewport-pan";
|
export { createViewportPanInputController } from "./viewport-pan";
|
||||||
export type { GlobalPointerConsumer, GlobalWheelConsumer, PointerInputEvent, WheelInputEvent } from "./pointer";
|
export type { GlobalPointerConsumer, GlobalWheelConsumer, PointerInputEvent, WheelInputEvent } from "./pointer";
|
||||||
|
|||||||
69
input/selection.test.ts
Normal file
69
input/selection.test.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { commandIds } from "@commands/ids";
|
||||||
|
import { createInitialAppState } from "@editor/initial-state";
|
||||||
|
import { handleArtboardSelection } from "./selection";
|
||||||
|
import type { PointerInputEvent } from "./pointer";
|
||||||
|
|
||||||
|
const ignoredState = undefined as never;
|
||||||
|
|
||||||
|
describe("selection input", () => {
|
||||||
|
test("selects artboard hit by left click", () => {
|
||||||
|
const state = {
|
||||||
|
...createInitialAppState("Test"),
|
||||||
|
document: {
|
||||||
|
...createInitialAppState("Test").document,
|
||||||
|
artboards: [{ id: "a1", name: "Artboard", bounds: { x: -50, y: -50, w: 100, h: 100 }, backgroundColor: "transparent", layers: [] }],
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
...createInitialAppState("Test").editor,
|
||||||
|
viewport: { center: { x: 0, y: 0 }, zoom: 1, rotation: 0, size: { w: 200, h: 200 } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dispatched: unknown[] = [];
|
||||||
|
|
||||||
|
const consumed = handleArtboardSelection({
|
||||||
|
event: pointerEvent({ position: { x: 100, y: 100 }, buttons: 1 }),
|
||||||
|
document: state.document,
|
||||||
|
viewport: state.editor.viewport,
|
||||||
|
dispatch: (commandId, payload) => {
|
||||||
|
dispatched.push({ commandId, payload });
|
||||||
|
return ignoredState;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consumed).toBe(true);
|
||||||
|
expect(dispatched).toEqual([{ commandId: commandIds.selectionSet, payload: { artboardId: "a1", layerIds: [] } }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clears selection when clicking outside artboards", () => {
|
||||||
|
const state = createInitialAppState("Test");
|
||||||
|
const dispatched: unknown[] = [];
|
||||||
|
|
||||||
|
const consumed = handleArtboardSelection({
|
||||||
|
event: pointerEvent({ position: { x: 100, y: 100 }, buttons: 1 }),
|
||||||
|
document: state.document,
|
||||||
|
viewport: { center: { x: 0, y: 0 }, zoom: 1, rotation: 0, size: { w: 200, h: 200 } },
|
||||||
|
dispatch: (commandId, payload) => {
|
||||||
|
dispatched.push({ commandId, payload });
|
||||||
|
return ignoredState;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(consumed).toBe(true);
|
||||||
|
expect(dispatched).toEqual([{ commandId: commandIds.selectionClear, payload: undefined }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function pointerEvent(overrides: Partial<PointerInputEvent>): PointerInputEvent {
|
||||||
|
return {
|
||||||
|
pointerId: 1,
|
||||||
|
pointerType: "mouse",
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
buttons: 0,
|
||||||
|
altKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
35
input/selection.ts
Normal file
35
input/selection.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { commandIds } from "@commands/ids";
|
||||||
|
import type { Dispatch } from "@commands/dispatcher";
|
||||||
|
import type { ImageDocument } from "@core/document";
|
||||||
|
import type { ViewportState } from "@editor/state";
|
||||||
|
import type { PointerInputEvent } from "./pointer";
|
||||||
|
|
||||||
|
export function handleArtboardSelection(options: {
|
||||||
|
event: PointerInputEvent;
|
||||||
|
document: ImageDocument;
|
||||||
|
viewport: ViewportState;
|
||||||
|
dispatch: Dispatch;
|
||||||
|
}): boolean {
|
||||||
|
if (options.event.pointerType !== "mouse" || (options.event.buttons & 1) !== 1) return false;
|
||||||
|
|
||||||
|
const point = viewportPointToDocumentPoint(options.event.position, options.viewport);
|
||||||
|
const artboard = [...options.document.artboards].reverse().find((candidate) => {
|
||||||
|
const bounds = candidate.bounds;
|
||||||
|
return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!artboard) {
|
||||||
|
options.dispatch(commandIds.selectionClear, undefined);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.dispatch(commandIds.selectionSet, { artboardId: artboard.id, layerIds: [] });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewportPointToDocumentPoint(point: PointerInputEvent["position"], viewport: ViewportState) {
|
||||||
|
return {
|
||||||
|
x: viewport.center.x + (point.x - viewport.size.w / 2) / viewport.zoom,
|
||||||
|
y: viewport.center.y + (point.y - viewport.size.h / 2) / viewport.zoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { isPanInteractionMode } from "@editor/tools";
|
|||||||
import type { GlobalKeybindConsumer, GlobalPointerConsumer, GlobalWheelConsumer } from "@input/index";
|
import type { GlobalKeybindConsumer, GlobalPointerConsumer, GlobalWheelConsumer } from "@input/index";
|
||||||
import {
|
import {
|
||||||
createViewportPanInputController,
|
createViewportPanInputController,
|
||||||
|
handleArtboardSelection,
|
||||||
handleViewportWheel,
|
handleViewportWheel,
|
||||||
keybindEventFromKeyboardEvent,
|
keybindEventFromKeyboardEvent,
|
||||||
pointerInputEventFromPointerEvent,
|
pointerInputEventFromPointerEvent,
|
||||||
@@ -50,12 +51,23 @@ export function useCanvasInput(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerDown = (event: PointerEvent) => {
|
const handlePointerDown = (event: PointerEvent) => {
|
||||||
const consumed = panHandler.pointerDown(pointerInputEventFromPointerEvent(event));
|
const inputEvent = pointerInputEventFromPointerEvent(event);
|
||||||
if (!consumed) return;
|
const consumed = panHandler.pointerDown(inputEvent);
|
||||||
|
if (consumed) {
|
||||||
|
canvas.setPointerCapture(event.pointerId);
|
||||||
|
setIsPanning(true);
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
canvas.setPointerCapture(event.pointerId);
|
const state = store.getState();
|
||||||
setIsPanning(true);
|
const selected = state.editor.tools.activeTool === "select" && handleArtboardSelection({
|
||||||
event.preventDefault();
|
event: inputEvent,
|
||||||
|
document: state.document,
|
||||||
|
viewport: state.editor.viewport,
|
||||||
|
dispatch: store.dispatch,
|
||||||
|
});
|
||||||
|
if (selected) event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerMove = (event: PointerEvent) => {
|
const handlePointerMove = (event: PointerEvent) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user