feat(input): select image layers on canvas
This commit is contained in:
@@ -35,6 +35,54 @@ describe("selection input", () => {
|
|||||||
expect(dispatched).toEqual([{ commandId: commandIds.selectionSet, payload: { artboardId: "a1", layerIds: [] } }]);
|
expect(dispatched).toEqual([{ commandId: commandIds.selectionSet, payload: { artboardId: "a1", layerIds: [] } }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("selects topmost image layer before artboard", () => {
|
||||||
|
const state = {
|
||||||
|
...createInitialAppState("Test"),
|
||||||
|
document: {
|
||||||
|
...createInitialAppState("Test").document,
|
||||||
|
assets: [{ id: "asset-1", name: "Image", mimeType: "image/png", source: "blob:test", intrinsicSize: { w: 50, h: 50 } }],
|
||||||
|
artboards: [
|
||||||
|
{
|
||||||
|
id: "a1",
|
||||||
|
name: "Artboard",
|
||||||
|
bounds: { x: -100, y: -100, w: 200, h: 200 },
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "l1",
|
||||||
|
type: "image",
|
||||||
|
name: "Image",
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
opacity: 1,
|
||||||
|
assetId: "asset-1",
|
||||||
|
transform: { position: { x: -25, y: -25 }, scale: { x: 1, y: 1 }, rotation: 0 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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: ["l1"] } }]);
|
||||||
|
});
|
||||||
|
|
||||||
test("clears selection when clicking outside artboards", () => {
|
test("clears selection when clicking outside artboards", () => {
|
||||||
const state = createInitialAppState("Test");
|
const state = createInitialAppState("Test");
|
||||||
const dispatched: unknown[] = [];
|
const dispatched: unknown[] = [];
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { commandIds } from "@commands/ids";
|
import { commandIds } from "@commands/ids";
|
||||||
import type { Dispatch } from "@commands/dispatcher";
|
import type { Dispatch } from "@commands/dispatcher";
|
||||||
import type { ImageDocument } from "@core/document";
|
import type { ImageDocument } from "@core/document";
|
||||||
|
import type { Layer } from "@core/layer";
|
||||||
|
import { resolveTransformTargetBounds } from "@editor/transform-targets";
|
||||||
import type { ViewportState } from "@editor/state";
|
import type { ViewportState } from "@editor/state";
|
||||||
import type { PointerInputEvent } from "./pointer";
|
import type { PointerInputEvent } from "./pointer";
|
||||||
|
|
||||||
@@ -13,6 +15,12 @@ export function handleArtboardSelection(options: {
|
|||||||
if (options.event.pointerType !== "mouse" || (options.event.buttons & 1) !== 1) return false;
|
if (options.event.pointerType !== "mouse" || (options.event.buttons & 1) !== 1) return false;
|
||||||
|
|
||||||
const point = viewportPointToDocumentPoint(options.event.position, options.viewport);
|
const point = viewportPointToDocumentPoint(options.event.position, options.viewport);
|
||||||
|
const layerHit = findTopmostLayerAtPoint(options.document, point);
|
||||||
|
if (layerHit) {
|
||||||
|
options.dispatch(commandIds.selectionSet, { artboardId: layerHit.artboardId, layerIds: [layerHit.layerId] });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const artboard = [...options.document.artboards].reverse().find((candidate) => {
|
const artboard = [...options.document.artboards].reverse().find((candidate) => {
|
||||||
const bounds = candidate.bounds;
|
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;
|
return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
|
||||||
@@ -27,6 +35,31 @@ export function handleArtboardSelection(options: {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findTopmostLayerAtPoint(document: ImageDocument, point: { x: number; y: number }) {
|
||||||
|
for (const artboard of [...document.artboards].reverse()) {
|
||||||
|
const layerId = findTopmostLayerInTreeAtPoint(document, [...artboard.layers].reverse(), point);
|
||||||
|
if (layerId) return { artboardId: artboard.id, layerId };
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTopmostLayerInTreeAtPoint(document: ImageDocument, layers: Layer[], point: { x: number; y: number }): string | undefined {
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (layer.type === "group") {
|
||||||
|
const childId = findTopmostLayerInTreeAtPoint(document, [...layer.children].reverse(), point);
|
||||||
|
if (childId) return childId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = resolveTransformTargetBounds(document, { type: "layer", id: layer.id });
|
||||||
|
if (bounds && point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h) {
|
||||||
|
return layer.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function viewportPointToDocumentPoint(point: PointerInputEvent["position"], viewport: ViewportState) {
|
function viewportPointToDocumentPoint(point: PointerInputEvent["position"], viewport: ViewportState) {
|
||||||
return {
|
return {
|
||||||
x: viewport.center.x + (point.x - viewport.size.w / 2) / viewport.zoom,
|
x: viewport.center.x + (point.x - viewport.size.w / 2) / viewport.zoom,
|
||||||
|
|||||||
Reference in New Issue
Block a user