Guard layer selection, cursor, and resize interactions behind the editing state to avoid unintended modifications in view mode.
263 lines
7.1 KiB
TypeScript
263 lines
7.1 KiB
TypeScript
import { createMemo } from "solid-js";
|
|
import type { DeckStore, DraggingState } from "./deckStore";
|
|
import type { LayerConfig } from "../types";
|
|
|
|
export interface LayerClickInfo {
|
|
layerProp: string;
|
|
gridX: number;
|
|
gridY: number;
|
|
}
|
|
|
|
export interface LayerInteractionHandlers {
|
|
onLayerClick: (index: number, e: MouseEvent) => void;
|
|
onFrameMouseDown: (
|
|
action: "drag" | "resize-corner" | "resize-edge",
|
|
anchor?: "nw" | "ne" | "sw" | "se",
|
|
edge?: "n" | "s" | "e" | "w",
|
|
e?: MouseEvent,
|
|
) => void;
|
|
onCardMouseMove: (e: MouseEvent, cardEl: HTMLElement) => void;
|
|
onCardMouseUp: () => void;
|
|
onCardClick: (e: MouseEvent, cardEl: HTMLElement) => void;
|
|
getOverlappingLayers: (gridX: number, gridY: number) => LayerConfig[];
|
|
}
|
|
|
|
export function useLayerInteraction(
|
|
store: DeckStore,
|
|
): LayerInteractionHandlers {
|
|
let clickStack: {
|
|
gridX: number;
|
|
gridY: number;
|
|
layers: number[];
|
|
currentIndex: number;
|
|
} | null = null;
|
|
|
|
const currentLayerConfigs = () =>
|
|
store.state.activeSide === "front"
|
|
? store.state.frontLayerConfigs.filter((l) => l.visible)
|
|
: store.state.backLayerConfigs.filter((l) => l.visible);
|
|
|
|
const allLayerConfigs = () =>
|
|
store.state.activeSide === "front"
|
|
? store.state.frontLayerConfigs
|
|
: store.state.backLayerConfigs;
|
|
|
|
const calculateGridCoords = (e: MouseEvent, cardEl: HTMLElement) => {
|
|
const dims = store.state.dimensions;
|
|
if (!dims) return { gridX: 1, gridY: 1 };
|
|
|
|
const rect = cardEl.getBoundingClientRect();
|
|
const mmToPxRatio = rect.width / dims.cardWidth;
|
|
|
|
const offsetX = (e.clientX - rect.left) / mmToPxRatio;
|
|
const offsetY = (e.clientY - rect.top) / mmToPxRatio;
|
|
|
|
const gridX = Math.floor((offsetX - dims.gridOriginX) / dims.cellWidth) + 1;
|
|
const gridY =
|
|
Math.floor((offsetY - dims.gridOriginY) / dims.cellHeight) + 1;
|
|
|
|
return {
|
|
gridX: Math.max(1, Math.min(dims.gridW, gridX)),
|
|
gridY: Math.max(1, Math.min(dims.gridH, gridY)),
|
|
};
|
|
};
|
|
|
|
const isPointInLayer = (
|
|
gridX: number,
|
|
gridY: number,
|
|
layer: LayerConfig,
|
|
): boolean => {
|
|
return (
|
|
gridX >= layer.x1 &&
|
|
gridX <= layer.x2 &&
|
|
gridY >= layer.y1 &&
|
|
gridY <= layer.y2
|
|
);
|
|
};
|
|
|
|
const getOverlappingLayers = (
|
|
gridX: number,
|
|
gridY: number,
|
|
): LayerConfig[] => {
|
|
return currentLayerConfigs().filter((layer) =>
|
|
isPointInLayer(gridX, gridY, layer),
|
|
);
|
|
};
|
|
|
|
const handleLayerClick = (index: number, e: MouseEvent) => {
|
|
e.stopPropagation();
|
|
|
|
if (!store.state.isEditing || store.state.fixed) return;
|
|
|
|
const currentlySelected = store.state.selectedLayer;
|
|
|
|
if (currentlySelected === index) {
|
|
store.actions.setSelectedLayer(null);
|
|
} else {
|
|
store.actions.setSelectedLayer(index);
|
|
}
|
|
|
|
clickStack = null;
|
|
};
|
|
|
|
const handleCardClick = (e: MouseEvent, cardEl: HTMLElement) => {
|
|
if (store.state.draggingState) return;
|
|
if (!store.state.isEditing || store.state.fixed) return;
|
|
|
|
const { gridX, gridY } = calculateGridCoords(e, cardEl);
|
|
const overlapping = getOverlappingLayers(gridX, gridY);
|
|
|
|
if (overlapping.length === 0) {
|
|
store.actions.setSelectedLayer(null);
|
|
clickStack = null;
|
|
return;
|
|
}
|
|
|
|
const allConfigs = allLayerConfigs();
|
|
const overlappingIndices = overlapping
|
|
.map((l) => allConfigs.findIndex((c) => c.prop === l.prop && c.visible))
|
|
.filter((i) => i !== -1);
|
|
|
|
if (overlappingIndices.length === 0) {
|
|
store.actions.setSelectedLayer(null);
|
|
clickStack = null;
|
|
return;
|
|
}
|
|
|
|
if (
|
|
clickStack &&
|
|
clickStack.gridX === gridX &&
|
|
clickStack.gridY === gridY
|
|
) {
|
|
clickStack.currentIndex =
|
|
(clickStack.currentIndex + 1) % overlappingIndices.length;
|
|
const nextIndex = overlappingIndices[clickStack.currentIndex];
|
|
store.actions.setSelectedLayer(nextIndex);
|
|
} else {
|
|
clickStack = {
|
|
gridX,
|
|
gridY,
|
|
layers: overlappingIndices,
|
|
currentIndex: 0,
|
|
};
|
|
store.actions.setSelectedLayer(overlappingIndices[0]);
|
|
}
|
|
};
|
|
|
|
const handleFrameMouseDown = (
|
|
action: "drag" | "resize-corner" | "resize-edge",
|
|
anchor?: "nw" | "ne" | "sw" | "se",
|
|
edge?: "n" | "s" | "e" | "w",
|
|
e?: MouseEvent,
|
|
) => {
|
|
if (!store.state.isEditing || store.state.fixed) return;
|
|
if (store.state.selectedLayer === null) return;
|
|
if (e) e.stopPropagation();
|
|
|
|
const index = store.state.selectedLayer;
|
|
const layer = allLayerConfigs()[index];
|
|
if (!layer) return;
|
|
|
|
const startX = e?.clientX ?? 0;
|
|
const startY = e?.clientY ?? 0;
|
|
|
|
const draggingState: DraggingState = {
|
|
layer: index,
|
|
action,
|
|
anchor,
|
|
edge,
|
|
startX,
|
|
startY,
|
|
startGrid: {
|
|
x1: layer.x1,
|
|
y1: layer.y1,
|
|
x2: layer.x2,
|
|
y2: layer.y2,
|
|
},
|
|
};
|
|
|
|
store.actions.setDraggingState(draggingState);
|
|
};
|
|
|
|
const handleCardMouseMove = (e: MouseEvent, cardEl: HTMLElement) => {
|
|
const dragging = store.state.draggingState;
|
|
if (!dragging) return;
|
|
|
|
const dims = store.state.dimensions;
|
|
if (!dims) return;
|
|
|
|
const rect = cardEl.getBoundingClientRect();
|
|
const mmToPxRatio = rect.width / dims.cardWidth;
|
|
|
|
const deltaX = e.clientX - dragging.startX;
|
|
const deltaY = e.clientY - dragging.startY;
|
|
|
|
const deltaMmX = deltaX / mmToPxRatio;
|
|
const deltaMmY = deltaY / mmToPxRatio;
|
|
|
|
const deltaGridX = deltaMmX / dims.cellWidth;
|
|
const deltaGridY = deltaMmY / dims.cellHeight;
|
|
|
|
const startGrid = dragging.startGrid;
|
|
|
|
if (dragging.action === "drag") {
|
|
store.actions.moveLayer(
|
|
dragging.layer,
|
|
Math.round(deltaGridX),
|
|
Math.round(deltaGridY),
|
|
startGrid,
|
|
);
|
|
} else if (dragging.action === "resize-corner" && dragging.anchor) {
|
|
store.actions.resizeLayerCorner(
|
|
dragging.layer,
|
|
dragging.anchor,
|
|
Math.round(deltaGridX),
|
|
Math.round(deltaGridY),
|
|
startGrid,
|
|
);
|
|
} else if (dragging.action === "resize-edge" && dragging.edge) {
|
|
const delta =
|
|
dragging.edge === "n" || dragging.edge === "s"
|
|
? Math.round(deltaGridY)
|
|
: Math.round(deltaGridX);
|
|
store.actions.resizeLayerEdge(
|
|
dragging.layer,
|
|
dragging.edge,
|
|
delta,
|
|
startGrid,
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleCardMouseUp = () => {
|
|
if (store.state.draggingState) {
|
|
const dragging = store.state.draggingState;
|
|
|
|
store.actions.setDraggingState(null);
|
|
|
|
const newLayerConfig = allLayerConfigs()[dragging.layer];
|
|
if (newLayerConfig && dragging.startGrid) {
|
|
store.actions.setDraggingState({
|
|
...dragging,
|
|
startGrid: {
|
|
x1: newLayerConfig.x1,
|
|
y1: newLayerConfig.y1,
|
|
x2: newLayerConfig.x2,
|
|
y2: newLayerConfig.y2,
|
|
},
|
|
});
|
|
store.actions.setDraggingState(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
onLayerClick: handleLayerClick,
|
|
onFrameMouseDown: handleFrameMouseDown,
|
|
onCardMouseMove: handleCardMouseMove,
|
|
onCardMouseUp: handleCardMouseUp,
|
|
onCardClick: handleCardClick,
|
|
getOverlappingLayers,
|
|
};
|
|
}
|