feat(tools): add crop tool
This commit is contained in:
@@ -4,8 +4,8 @@ import { toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiv
|
|||||||
|
|
||||||
describe("tool commands", () => {
|
describe("tool commands", () => {
|
||||||
test("sets active tool", () => {
|
test("sets active tool", () => {
|
||||||
const next = toolSetActiveCommand.execute({ state: createInitialAppState("Test") }, { tool: "pan" });
|
const next = toolSetActiveCommand.execute({ state: createInitialAppState("Test") }, { tool: "crop" });
|
||||||
expect(next.editor.tools).toEqual({ activeTool: "pan", interactionMode: { type: "tool", tool: "pan" } });
|
expect(next.editor.tools).toEqual({ activeTool: "crop", interactionMode: { type: "tool", tool: "crop" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("enters and exits temporary pan", () => {
|
test("enters and exits temporary pan", () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const availableToolIds = ["select", "pan"] as const;
|
export const availableToolIds = ["select", "crop", "pan"] as const;
|
||||||
|
|
||||||
export type ToolId = (typeof availableToolIds)[number];
|
export type ToolId = (typeof availableToolIds)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,34 @@ describe("transform controls input", () => {
|
|||||||
expect(dispatched).toEqual([]);
|
expect(dispatched).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("crop tool resizes from handles but does not move body", () => {
|
||||||
|
const state = {
|
||||||
|
...createInitialAppState("Test"),
|
||||||
|
document: {
|
||||||
|
...createInitialAppState("Test").document,
|
||||||
|
artboards: [{ id: "a1", name: "Artboard", bounds: { x: -50, y: -50, w: 100, h: 100 }, backgroundColor: "transparent", visible: true, locked: false, layers: [] }],
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
...createInitialAppState("Test").editor,
|
||||||
|
viewport: { center: { x: 0, y: 0 }, zoom: 1, rotation: 0, size: { w: 200, h: 200 } },
|
||||||
|
selection: { artboardId: "a1", layerIds: [] },
|
||||||
|
tools: { activeTool: "crop" as const, interactionMode: { type: "tool" as const, tool: "crop" as const } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dispatched: unknown[] = [];
|
||||||
|
const controller = createTransformControlsInputController({
|
||||||
|
getDocument: () => state.document,
|
||||||
|
getEditor: () => state.editor,
|
||||||
|
dispatch: (commandId, payload) => {
|
||||||
|
dispatched.push({ commandId, payload });
|
||||||
|
return ignoredState;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(controller.pointerDown(pointerEvent({ position: { x: 100, y: 100 }, buttons: 1 }))).toBe(false);
|
||||||
|
expect(controller.pointerDown(pointerEvent({ position: { x: 150, y: 150 }, buttons: 1 }))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("dispatches transform lifecycle for selected artboard", () => {
|
test("dispatches transform lifecycle for selected artboard", () => {
|
||||||
let state = {
|
let state = {
|
||||||
...createInitialAppState("Test"),
|
...createInitialAppState("Test"),
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export function createTransformControlsInputController(options: {
|
|||||||
if (event.pointerType !== "mouse" || (event.buttons & 1) !== 1) return false;
|
if (event.pointerType !== "mouse" || (event.buttons & 1) !== 1) return false;
|
||||||
|
|
||||||
const editor = options.getEditor();
|
const editor = options.getEditor();
|
||||||
if (editor.tools.activeTool !== "select" || isPanInteractionMode(editor.tools.interactionMode)) return false;
|
const cropToolActive = editor.tools.activeTool === "crop";
|
||||||
|
if ((editor.tools.activeTool !== "select" && !cropToolActive) || isPanInteractionMode(editor.tools.interactionMode)) return false;
|
||||||
|
|
||||||
const document = options.getDocument();
|
const document = options.getDocument();
|
||||||
const target = selectedTransformTarget(document, editor.selection);
|
const target = selectedTransformTarget(document, editor.selection);
|
||||||
@@ -35,7 +36,7 @@ export function createTransformControlsInputController(options: {
|
|||||||
if (!bounds) return false;
|
if (!bounds) return false;
|
||||||
|
|
||||||
const handle = hitTestArtboardTransformHandle(event.position, bounds, editor.viewport);
|
const handle = hitTestArtboardTransformHandle(event.position, bounds, editor.viewport);
|
||||||
if (!handle) return false;
|
if (!handle || (cropToolActive && handle === "body")) return false;
|
||||||
|
|
||||||
options.dispatch(commandIds.transformBegin, {
|
options.dispatch(commandIds.transformBegin, {
|
||||||
target,
|
target,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Cursor, Hand } from "@phosphor-icons/react";
|
import { Crop, Cursor, Hand } from "@phosphor-icons/react";
|
||||||
import { commandIds } from "@commands/ids";
|
import { commandIds } from "@commands/ids";
|
||||||
import type { AppStore } from "@editor/store";
|
import type { AppStore } from "@editor/store";
|
||||||
import type { InteractionMode, ToolId } from "@editor/tools";
|
import type { InteractionMode, ToolId } from "@editor/tools";
|
||||||
@@ -41,6 +41,8 @@ export function ToolOverlay({ activeTool, interactionMode, dispatch }: ToolOverl
|
|||||||
|
|
||||||
function iconForTool(tool: ToolId) {
|
function iconForTool(tool: ToolId) {
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
|
case "crop":
|
||||||
|
return Crop;
|
||||||
case "pan":
|
case "pan":
|
||||||
return Hand;
|
return Hand;
|
||||||
case "select":
|
case "select":
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import type { CanvasInputState } from "./useCanvasInput";
|
|||||||
export function canvasCursorClass(interactionMode: InteractionMode, input: CanvasInputState) {
|
export function canvasCursorClass(interactionMode: InteractionMode, input: CanvasInputState) {
|
||||||
if (input.isPanning) return "cursor-grabbing";
|
if (input.isPanning) return "cursor-grabbing";
|
||||||
if (isPanInteractionMode(interactionMode)) return "cursor-grab";
|
if (isPanInteractionMode(interactionMode)) return "cursor-grab";
|
||||||
|
if (interactionMode.type === "tool" && interactionMode.tool === "crop") return "cursor-crosshair";
|
||||||
return "cursor-default";
|
return "cursor-default";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ export function useCanvasInput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const selected = state.editor.tools.activeTool === "select" && handleArtboardSelection({
|
const selectionToolActive = state.editor.tools.activeTool === "select" || state.editor.tools.activeTool === "crop";
|
||||||
|
const selected = selectionToolActive && handleArtboardSelection({
|
||||||
event: inputEvent,
|
event: inputEvent,
|
||||||
document: state.document,
|
document: state.document,
|
||||||
viewport: state.editor.viewport,
|
viewport: state.editor.viewport,
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import type { ToolId } from "@editor/tools";
|
import type { ToolId } from "@editor/tools";
|
||||||
|
|
||||||
export function labelForTool(tool: ToolId): string {
|
export function labelForTool(tool: ToolId): string {
|
||||||
return tool === "pan" ? "Pan" : "Select";
|
switch (tool) {
|
||||||
|
case "crop":
|
||||||
|
return "Crop";
|
||||||
|
case "pan":
|
||||||
|
return "Pan";
|
||||||
|
case "select":
|
||||||
|
return "Select";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user