refactor: layer transform controls
This commit is contained in:
parent
56cabea109
commit
ceb2da8b1a
|
|
@ -1,15 +1,17 @@
|
||||||
import {createMemo, For} from 'solid-js';
|
import { createMemo, For, Show } from 'solid-js';
|
||||||
import {parseMarkdown} from '../../markdown';
|
import { parseMarkdown } from '../../markdown';
|
||||||
import { getLayerStyle } from './hooks/dimensions';
|
import { getLayerStyle } from './hooks/dimensions';
|
||||||
import type { CardData, CardSide } from './types';
|
import type { CardData, CardSide, LayerConfig } from './types';
|
||||||
import {DeckStore} from "./hooks/deckStore";
|
import { DeckStore } from "./hooks/deckStore";
|
||||||
import {processVariables} from "../utils/csv-loader";
|
import { processVariables } from "../utils/csv-loader";
|
||||||
import {resolvePath} from "../utils/path";
|
import { resolvePath } from "../utils/path";
|
||||||
|
import type { LayerInteractionHandlers } from './hooks/useLayerInteraction';
|
||||||
|
|
||||||
export interface CardLayerProps {
|
export interface CardLayerProps {
|
||||||
cardData: CardData;
|
cardData: CardData;
|
||||||
store: DeckStore;
|
store: DeckStore;
|
||||||
side?: CardSide;
|
side?: CardSide;
|
||||||
|
interaction?: LayerInteractionHandlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardLayer(props: CardLayerProps) {
|
export function CardLayer(props: CardLayerProps) {
|
||||||
|
|
@ -21,6 +23,8 @@ export function CardLayer(props: CardLayerProps) {
|
||||||
);
|
);
|
||||||
const dimensions = () => props.store.state.dimensions!;
|
const dimensions = () => props.store.state.dimensions!;
|
||||||
const showBounds = () => props.store.state.isEditing;
|
const showBounds = () => props.store.state.isEditing;
|
||||||
|
const selectedLayer = () => props.store.state.selectedLayer;
|
||||||
|
const draggingState = () => props.store.state.draggingState;
|
||||||
|
|
||||||
function renderLayerContent(content: string) {
|
function renderLayerContent(content: string) {
|
||||||
const iconPath = resolvePath(props.store.state.cards.sourcePath, props.cardData.iconPath ?? "./assets");
|
const iconPath = resolvePath(props.store.state.cards.sourcePath, props.cardData.iconPath ?? "./assets");
|
||||||
|
|
@ -32,21 +36,67 @@ export function CardLayer(props: CardLayerProps) {
|
||||||
if (align === 'r') return 'right';
|
if (align === 'r') return 'right';
|
||||||
return 'center';
|
return 'center';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isLayerSelected = (layer: LayerConfig) => selectedLayer() === layer.prop;
|
||||||
|
|
||||||
|
const getFrameBounds = (layer: LayerConfig) => {
|
||||||
|
const dims = dimensions();
|
||||||
|
const orientation = layer.orientation || 'n';
|
||||||
|
|
||||||
|
if (orientation === 'e' || orientation === 'w') {
|
||||||
|
const left = (layer.y1 - 1) * dims.cellWidth;
|
||||||
|
const top = (layer.x1 - 1) * dims.cellHeight;
|
||||||
|
const width = (layer.y2 - layer.y1 + 1) * dims.cellWidth;
|
||||||
|
const height = (layer.x2 - layer.x1 + 1) * dims.cellHeight;
|
||||||
|
return { left, top, width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = (layer.x1 - 1) * dims.cellWidth;
|
||||||
|
const top = (layer.y1 - 1) * dims.cellHeight;
|
||||||
|
const width = (layer.x2 - layer.x1 + 1) * dims.cellWidth;
|
||||||
|
const height = (layer.y2 - layer.y1 + 1) * dims.cellHeight;
|
||||||
|
return { left, top, width, height };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLayerClick = (layerProp: string, e: MouseEvent) => {
|
||||||
|
if (props.interaction) {
|
||||||
|
props.interaction.onLayerClick(layerProp, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFrameMouseDown = (
|
||||||
|
action: 'drag' | 'resize-corner' | 'resize-edge',
|
||||||
|
anchor?: 'nw' | 'ne' | 'sw' | 'se',
|
||||||
|
edge?: 'n' | 's' | 'e' | 'w',
|
||||||
|
e?: MouseEvent
|
||||||
|
) => {
|
||||||
|
if (props.interaction) {
|
||||||
|
props.interaction.onFrameMouseDown(action, anchor, edge, e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<For each={layers()}>
|
<For each={layers()}>
|
||||||
{(layer) => {
|
{(layer) => {
|
||||||
|
const bounds = () => getFrameBounds(layer);
|
||||||
|
const isSelected = () => isLayerSelected(layer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<article
|
<article
|
||||||
class="absolute flex flex-col items-stretch justify-center prose prose-sm"
|
class="absolute flex flex-col items-stretch justify-center prose prose-sm cursor-pointer"
|
||||||
|
classList={{
|
||||||
|
'ring-2 ring-blue-500 ring-offset-1': isSelected() && !draggingState()
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
...getLayerStyle(layer, dimensions()),
|
...getLayerStyle(layer, dimensions()),
|
||||||
'font-size': `${layer.fontSize || 3}mm`,
|
'font-size': `${layer.fontSize || 3}mm`,
|
||||||
'text-align': getAlignStyle(layer.align)
|
'text-align': getAlignStyle(layer.align)
|
||||||
}}
|
}}
|
||||||
innerHTML={renderLayerContent(props.cardData[layer.prop])}
|
innerHTML={renderLayerContent(props.cardData[layer.prop])}
|
||||||
|
onClick={(e) => handleLayerClick(layer.prop, e)}
|
||||||
/>
|
/>
|
||||||
{showBounds() && (
|
<Show when={showBounds() && !isSelected()}>
|
||||||
<div
|
<div
|
||||||
class="absolute border-2 border-blue-500/50 pointer-events-none select-none"
|
class="absolute border-2 border-blue-500/50 pointer-events-none select-none"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -56,10 +106,101 @@ export function CardLayer(props: CardLayerProps) {
|
||||||
height: `${(layer.y2 - layer.y1 + 1) * dimensions().cellHeight}mm`
|
height: `${(layer.y2 - layer.y1 + 1) * dimensions().cellHeight}mm`
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Show>
|
||||||
|
<Show when={isSelected()}>
|
||||||
|
<div
|
||||||
|
class="absolute border-2 border-blue-500 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left}mm`,
|
||||||
|
top: `${bounds().top}mm`,
|
||||||
|
width: `${bounds().width}mm`,
|
||||||
|
height: `${bounds().height}mm`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute pointer-events-auto"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left}mm`,
|
||||||
|
top: `${bounds().top}mm`,
|
||||||
|
width: `${bounds().width}mm`,
|
||||||
|
height: `${bounds().height}mm`
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleFrameMouseDown('drag', undefined, undefined, e)}
|
||||||
|
/>
|
||||||
|
<Show when={!draggingState() || draggingState()?.action !== 'drag'}>
|
||||||
|
<div
|
||||||
|
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nwse-resize pointer-events-auto z-10"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left - 2}mm`,
|
||||||
|
top: `${bounds().top - 2}mm`
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleFrameMouseDown('resize-corner', 'nw', undefined, e)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nesw-resize pointer-events-auto z-10"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left + bounds().width - 2}mm`,
|
||||||
|
top: `${bounds().top - 2}mm`
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleFrameMouseDown('resize-corner', 'ne', undefined, e)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nesw-resize pointer-events-auto z-10"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left - 2}mm`,
|
||||||
|
top: `${bounds().top + bounds().height - 2}mm`
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleFrameMouseDown('resize-corner', 'sw', undefined, e)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nwse-resize pointer-events-auto z-10"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left + bounds().width - 2}mm`,
|
||||||
|
top: `${bounds().top + bounds().height - 2}mm`
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleFrameMouseDown('resize-corner', 'se', undefined, e)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute h-2 cursor-ns-resize pointer-events-auto"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left}mm`,
|
||||||
|
top: `${bounds().top - 1}mm`,
|
||||||
|
width: `${bounds().width}mm`
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleFrameMouseDown('resize-edge', undefined, 'n', e)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute h-2 cursor-ns-resize pointer-events-auto"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left}mm`,
|
||||||
|
top: `${bounds().top + bounds().height - 1}mm`,
|
||||||
|
width: `${bounds().width}mm`
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleFrameMouseDown('resize-edge', undefined, 's', e)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute w-2 cursor-ew-resize pointer-events-auto"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left - 1}mm`,
|
||||||
|
top: `${bounds().top}mm`,
|
||||||
|
height: `${bounds().height}mm`
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleFrameMouseDown('resize-edge', undefined, 'w', e)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute w-2 cursor-ew-resize pointer-events-auto"
|
||||||
|
style={{
|
||||||
|
left: `${bounds().left + bounds().width - 1}mm`,
|
||||||
|
top: `${bounds().top}mm`,
|
||||||
|
height: `${bounds().height}mm`
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleFrameMouseDown('resize-edge', undefined, 'e', e)}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,58 +1,67 @@
|
||||||
import { Show, For, createMemo } from 'solid-js';
|
import { Show, For, createMemo, onCleanup, onMount } from 'solid-js';
|
||||||
import { useCardSelection } from './hooks/useCardSelection';
|
|
||||||
import { getSelectionBoxStyle } from './hooks/useCardSelection';
|
|
||||||
import { getShapeClipPath } from './hooks/shape-styles';
|
import { getShapeClipPath } from './hooks/shape-styles';
|
||||||
import { CardLayer } from './CardLayer';
|
import { CardLayer } from './CardLayer';
|
||||||
import type { DeckStore } from './hooks/deckStore';
|
import type { DeckStore } from './hooks/deckStore';
|
||||||
|
import { useLayerInteraction } from './hooks/useLayerInteraction';
|
||||||
|
|
||||||
export interface CardPreviewProps {
|
export interface CardPreviewProps {
|
||||||
store: DeckStore;
|
store: DeckStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 卡牌预览组件
|
|
||||||
*/
|
|
||||||
export function CardPreview(props: CardPreviewProps) {
|
export function CardPreview(props: CardPreviewProps) {
|
||||||
const { store } = props;
|
const { store } = props;
|
||||||
|
|
||||||
const currentCard = createMemo(() => store.state.cards[store.state.activeTab]);
|
const currentCard = createMemo(() => store.state.cards[store.state.activeTab]);
|
||||||
const selectionStyle = createMemo(() =>
|
|
||||||
getSelectionBoxStyle(store.state.selectStart, store.state.selectEnd, store.state.dimensions)
|
|
||||||
);
|
|
||||||
const shapeClipPath = createMemo(() => {
|
const shapeClipPath = createMemo(() => {
|
||||||
const dims = store.state.dimensions;
|
const dims = store.state.dimensions;
|
||||||
if (!dims) return 'none';
|
if (!dims) return 'none';
|
||||||
return getShapeClipPath(store.state.shape, dims.cardWidth, dims.cardHeight, store.state.cornerRadius);
|
return getShapeClipPath(store.state.shape, dims.cardWidth, dims.cardHeight, store.state.cornerRadius);
|
||||||
});
|
});
|
||||||
|
|
||||||
const selection = useCardSelection(store);
|
const interaction = useLayerInteraction(store);
|
||||||
|
|
||||||
let cardRef: HTMLDivElement | undefined;
|
let cardRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
const handleGlobalMouseMove = (e: MouseEvent) => {
|
||||||
|
if (cardRef && store.state.draggingState) {
|
||||||
|
interaction.onCardMouseMove(e, cardRef);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGlobalMouseUp = () => {
|
||||||
|
if (store.state.draggingState) {
|
||||||
|
interaction.onCardMouseUp();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener('mousemove', handleGlobalMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleGlobalMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
document.removeEventListener('mousemove', handleGlobalMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex justify-center drop-shadow">
|
<div class="flex justify-center drop-shadow">
|
||||||
<Show when={store.state.activeTab < store.state.cards.length}>
|
<Show when={store.state.activeTab < store.state.cards.length}>
|
||||||
<div
|
<div
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
class="relative bg-white border border-gray-300 overflow-hidden"
|
class="relative bg-white border border-gray-300 overflow-hidden"
|
||||||
classList={{ 'select-none': store.state.isEditing }}
|
classList={{ 'select-none': !!store.state.draggingState }}
|
||||||
style={{
|
style={{
|
||||||
width: `${store.state.dimensions?.cardWidth}mm`,
|
width: `${store.state.dimensions?.cardWidth}mm`,
|
||||||
height: `${store.state.dimensions?.cardHeight}mm`,
|
height: `${store.state.dimensions?.cardHeight}mm`,
|
||||||
'clip-path': shapeClipPath() !== 'none' ? shapeClipPath() : undefined
|
'clip-path': shapeClipPath() !== 'none' ? shapeClipPath() : undefined
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => selection.onMouseDown(e, cardRef!)}
|
onClick={(e) => {
|
||||||
onMouseMove={(e) => selection.onMouseMove(e, cardRef!)}
|
if (!store.state.draggingState && cardRef) {
|
||||||
onMouseUp={selection.onMouseUp}
|
interaction.onCardClick(e, cardRef);
|
||||||
onMouseLeave={selection.onMouseLeave}
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Show when={store.state.isSelecting && selectionStyle()}>
|
|
||||||
<div
|
|
||||||
class="absolute bg-blue-500/30 border-2 border-blue-500 pointer-events-none"
|
|
||||||
style={selectionStyle()!}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute"
|
class="absolute"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -87,10 +96,11 @@ export function CardPreview(props: CardPreviewProps) {
|
||||||
cardData={currentCard()}
|
cardData={currentCard()}
|
||||||
store={store}
|
store={store}
|
||||||
side={store.state.activeSide}
|
side={store.state.activeSide}
|
||||||
|
interaction={interaction}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -80,9 +80,9 @@ function LayerEditorPanel(props: LayerEditorPanelProps) {
|
||||||
toggleFn(layerProp);
|
toggleFn(layerProp);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setEditingLayer = (layerProp: string) => {
|
const selectLayer = (layerProp: string) => {
|
||||||
store.actions.setEditingLayer(
|
store.actions.setSelectedLayer(
|
||||||
store.state.editingLayer === layerProp ? null : layerProp
|
store.state.selectedLayer === layerProp ? null : layerProp
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -118,6 +118,8 @@ function LayerEditorPanel(props: LayerEditorPanelProps) {
|
||||||
<div
|
<div
|
||||||
class={`flex items-center gap-1 py-1.5 px-1 ${
|
class={`flex items-center gap-1 py-1.5 px-1 ${
|
||||||
index() < layerCount() - 1 ? 'border-b border-gray-200' : ''
|
index() < layerCount() - 1 ? 'border-b border-gray-200' : ''
|
||||||
|
} ${
|
||||||
|
store.state.selectedLayer === layer.prop ? 'bg-blue-50' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|
@ -126,21 +128,12 @@ function LayerEditorPanel(props: LayerEditorPanelProps) {
|
||||||
onChange={() => toggleLayerVisible(layer.prop)}
|
onChange={() => toggleLayerVisible(layer.prop)}
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<span class="text-sm flex-1 truncate">{layer.prop}</span>
|
<span
|
||||||
<button
|
class="text-sm flex-1 truncate cursor-pointer hover:text-blue-600"
|
||||||
onClick={() => setEditingLayer(layer.prop)}
|
onClick={() => selectLayer(layer.prop)}
|
||||||
class={`w-7 h-7 text-xs rounded cursor-pointer flex items-center justify-center ${
|
|
||||||
!layer.visible ? 'invisible pointer-events-none' : ''
|
|
||||||
} ${
|
|
||||||
store.state.editingLayer === layer.prop
|
|
||||||
? 'bg-blue-500 text-white'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
||||||
}`}
|
|
||||||
title="框选"
|
|
||||||
>
|
>
|
||||||
{store.state.editingLayer === layer.prop ? '✓' : '框'}
|
{layer.prop}
|
||||||
</button>
|
</span>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { createStore } from 'solid-js/store';
|
import { createStore } from 'solid-js/store';
|
||||||
import { calculateDimensions } from './dimensions';
|
import { calculateDimensions } from './dimensions';
|
||||||
import { loadCSV, CSV } from '../../utils/csv-loader';
|
import { loadCSV, CSV } from '../../utils/csv-loader';
|
||||||
import { initLayerConfigs, formatLayers, initLayerConfigsForSide } from './layer-parser';
|
import { initLayerConfigs, formatLayers, initLayerConfigsForSide } from './layer-parser';
|
||||||
import type { CardData, LayerConfig, Dimensions, CardSide, CardShape } from '../types';
|
import type { CardData, LayerConfig, Dimensions, CardSide, CardShape } from '../types';
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认配置常量
|
|
||||||
*/
|
|
||||||
export const DECK_DEFAULTS = {
|
export const DECK_DEFAULTS = {
|
||||||
SIZE_W: 54,
|
SIZE_W: 54,
|
||||||
SIZE_H: 86,
|
SIZE_H: 86,
|
||||||
|
|
@ -17,8 +14,17 @@ export const DECK_DEFAULTS = {
|
||||||
CORNER_RADIUS: 3
|
CORNER_RADIUS: 3
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export interface DraggingState {
|
||||||
|
layer: string;
|
||||||
|
action: 'drag' | 'resize-corner' | 'resize-edge';
|
||||||
|
anchor?: 'nw' | 'ne' | 'sw' | 'se';
|
||||||
|
edge?: 'n' | 's' | 'e' | 'w';
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
startGrid: { x1: number; y1: number; x2: number; y2: number };
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeckState {
|
export interface DeckState {
|
||||||
// 基本属性
|
|
||||||
sizeW: number;
|
sizeW: number;
|
||||||
sizeH: number;
|
sizeH: number;
|
||||||
gridW: number;
|
gridW: number;
|
||||||
|
|
@ -29,41 +35,33 @@ export interface DeckState {
|
||||||
shape: CardShape;
|
shape: CardShape;
|
||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
src: string;
|
src: string;
|
||||||
rawSrc: string; // 原始 CSV 路径(用于生成代码时保持相对路径)
|
rawSrc: string;
|
||||||
|
|
||||||
// 解析后的尺寸
|
|
||||||
dimensions: Dimensions | null;
|
dimensions: Dimensions | null;
|
||||||
|
|
||||||
// 卡牌数据
|
|
||||||
cards: CSV<CardData>;
|
cards: CSV<CardData>;
|
||||||
activeTab: number;
|
activeTab: number;
|
||||||
|
|
||||||
// 图层配置
|
|
||||||
frontLayerConfigs: LayerConfig[];
|
frontLayerConfigs: LayerConfig[];
|
||||||
backLayerConfigs: LayerConfig[];
|
backLayerConfigs: LayerConfig[];
|
||||||
|
|
||||||
// 编辑状态
|
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
editingLayer: string | null;
|
selectedLayer: string | null;
|
||||||
activeSide: CardSide;
|
activeSide: CardSide;
|
||||||
|
|
||||||
// 框选状态
|
|
||||||
isSelecting: boolean;
|
isSelecting: boolean;
|
||||||
selectStart: { x: number; y: number } | null;
|
selectStart: { x: number; y: number } | null;
|
||||||
selectEnd: { x: number; y: number } | null;
|
selectEnd: { x: number; y: number } | null;
|
||||||
|
|
||||||
// 加载状态
|
draggingState: DraggingState | null;
|
||||||
isLoading: boolean;
|
|
||||||
|
|
||||||
// 错误状态
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
||||||
// 导出状态
|
|
||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
exportProgress: number; // 0-100
|
exportProgress: number;
|
||||||
exportError: string | null;
|
exportError: string | null;
|
||||||
|
|
||||||
// 打印设置
|
|
||||||
printOrientation: 'portrait' | 'landscape';
|
printOrientation: 'portrait' | 'landscape';
|
||||||
printFrontOddPageOffsetX: number;
|
printFrontOddPageOffsetX: number;
|
||||||
printFrontOddPageOffsetY: number;
|
printFrontOddPageOffsetY: number;
|
||||||
|
|
@ -71,7 +69,6 @@ export interface DeckState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeckActions {
|
export interface DeckActions {
|
||||||
// 基本属性设置
|
|
||||||
setSizeW: (size: number) => void;
|
setSizeW: (size: number) => void;
|
||||||
setSizeH: (size: number) => void;
|
setSizeH: (size: number) => void;
|
||||||
setGridW: (grid: number) => void;
|
setGridW: (grid: number) => void;
|
||||||
|
|
@ -81,50 +78,45 @@ export interface DeckActions {
|
||||||
setCornerRadius: (cornerRadius: number) => void;
|
setCornerRadius: (cornerRadius: number) => void;
|
||||||
setShape: (shape: CardShape) => void;
|
setShape: (shape: CardShape) => void;
|
||||||
|
|
||||||
// 数据设置
|
|
||||||
setCards: (cards: CSV<CardData>) => void;
|
setCards: (cards: CSV<CardData>) => void;
|
||||||
setActiveTab: (index: number) => void;
|
setActiveTab: (index: number) => void;
|
||||||
updateCardData: (index: number, key: string, value: string) => void;
|
updateCardData: (index: number, key: string, value: string) => void;
|
||||||
|
|
||||||
// 图层操作 - 正面
|
|
||||||
setFrontLayerConfigs: (configs: LayerConfig[]) => void;
|
setFrontLayerConfigs: (configs: LayerConfig[]) => void;
|
||||||
updateFrontLayerConfig: (prop: string, updates: Partial<LayerConfig>) => void;
|
updateFrontLayerConfig: (prop: string, updates: Partial<LayerConfig>) => void;
|
||||||
toggleFrontLayerVisible: (prop: string) => void;
|
toggleFrontLayerVisible: (prop: string) => void;
|
||||||
|
|
||||||
// 图层操作 - 背面
|
|
||||||
setBackLayerConfigs: (configs: LayerConfig[]) => void;
|
setBackLayerConfigs: (configs: LayerConfig[]) => void;
|
||||||
updateBackLayerConfig: (prop: string, updates: Partial<LayerConfig>) => void;
|
updateBackLayerConfig: (prop: string, updates: Partial<LayerConfig>) => void;
|
||||||
toggleBackLayerVisible: (prop: string) => void;
|
toggleBackLayerVisible: (prop: string) => void;
|
||||||
|
|
||||||
// 编辑状态
|
|
||||||
setIsEditing: (editing: boolean) => void;
|
setIsEditing: (editing: boolean) => void;
|
||||||
setEditingLayer: (layer: string | null) => void;
|
setSelectedLayer: (layer: string | null) => void;
|
||||||
updateLayerPosition: (x1: number, y1: number, x2: number, y2: number) => void;
|
|
||||||
setActiveSide: (side: CardSide) => void;
|
setActiveSide: (side: CardSide) => void;
|
||||||
|
|
||||||
// 框选操作
|
|
||||||
setIsSelecting: (selecting: boolean) => void;
|
setIsSelecting: (selecting: boolean) => void;
|
||||||
setSelectStart: (pos: { x: number; y: number } | null) => void;
|
setSelectStart: (pos: { x: number; y: number } | null) => void;
|
||||||
setSelectEnd: (pos: { x: number; y: number } | null) => void;
|
setSelectEnd: (pos: { x: number; y: number } | null) => void;
|
||||||
cancelSelection: () => void;
|
cancelSelection: () => void;
|
||||||
|
|
||||||
// 数据加载
|
setDraggingState: (state: DraggingState | null) => void;
|
||||||
|
moveLayer: (layerProp: string, dxGrid: number, dyGrid: number, startGrid?: { x1: number; y1: number; x2: number; y2: number }) => void;
|
||||||
|
resizeLayerCorner: (layerProp: string, anchor: 'nw' | 'ne' | 'sw' | 'se', dxGrid: number, dyGrid: number, startGrid: { x1: number; y1: number; x2: number; y2: number }) => void;
|
||||||
|
resizeLayerEdge: (layerProp: string, edge: 'n' | 's' | 'e' | 'w', delta: number, startGrid: { x1: number; y1: number; x2: number; y2: number }) => void;
|
||||||
|
|
||||||
loadCardsFromPath: (path: string, rawSrc: string, layersStr?: string, backLayersStr?: string) => Promise<void>;
|
loadCardsFromPath: (path: string, rawSrc: string, layersStr?: string, backLayersStr?: string) => Promise<void>;
|
||||||
setError: (error: string | null) => void;
|
setError: (error: string | null) => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
|
|
||||||
// 生成代码
|
|
||||||
generateCode: (backLayersStr?: string) => string;
|
generateCode: (backLayersStr?: string) => string;
|
||||||
copyCode: (backLayersStr?: string) => Promise<void>;
|
copyCode: (backLayersStr?: string) => Promise<void>;
|
||||||
|
|
||||||
// 导出操作
|
|
||||||
setExporting: (exporting: boolean) => void;
|
setExporting: (exporting: boolean) => void;
|
||||||
exportDeck: () => void;
|
exportDeck: () => void;
|
||||||
setExportProgress: (progress: number) => void;
|
setExportProgress: (progress: number) => void;
|
||||||
setExportError: (error: string | null) => void;
|
setExportError: (error: string | null) => void;
|
||||||
clearExportError: () => void;
|
clearExportError: () => void;
|
||||||
|
|
||||||
// 打印设置
|
|
||||||
setPrintOrientation: (orientation: 'portrait' | 'landscape') => void;
|
setPrintOrientation: (orientation: 'portrait' | 'landscape') => void;
|
||||||
setPrintFrontOddPageOffsetX: (offset: number) => void;
|
setPrintFrontOddPageOffsetX: (offset: number) => void;
|
||||||
setPrintFrontOddPageOffsetY: (offset: number) => void;
|
setPrintFrontOddPageOffsetY: (offset: number) => void;
|
||||||
|
|
@ -136,9 +128,6 @@ export interface DeckStore {
|
||||||
actions: DeckActions;
|
actions: DeckActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 deck store
|
|
||||||
*/
|
|
||||||
export function createDeckStore(
|
export function createDeckStore(
|
||||||
initialSrc: string = '',
|
initialSrc: string = '',
|
||||||
): DeckStore {
|
): DeckStore {
|
||||||
|
|
@ -160,11 +149,12 @@ export function createDeckStore(
|
||||||
frontLayerConfigs: [],
|
frontLayerConfigs: [],
|
||||||
backLayerConfigs: [],
|
backLayerConfigs: [],
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
editingLayer: null,
|
selectedLayer: null,
|
||||||
activeSide: 'front',
|
activeSide: 'front',
|
||||||
isSelecting: false,
|
isSelecting: false,
|
||||||
selectStart: null,
|
selectStart: null,
|
||||||
selectEnd: null,
|
selectEnd: null,
|
||||||
|
draggingState: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
isExporting: false,
|
isExporting: false,
|
||||||
|
|
@ -176,7 +166,6 @@ export function createDeckStore(
|
||||||
printDoubleSided: false
|
printDoubleSided: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新尺寸并重新计算 dimensions
|
|
||||||
const updateDimensions = () => {
|
const updateDimensions = () => {
|
||||||
const dims = calculateDimensions({
|
const dims = calculateDimensions({
|
||||||
sizeW: state.sizeW,
|
sizeW: state.sizeW,
|
||||||
|
|
@ -226,7 +215,6 @@ export function createDeckStore(
|
||||||
setState('cards', index, key, value);
|
setState('cards', index, key, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 正面图层操作
|
|
||||||
const setFrontLayerConfigs = (configs: LayerConfig[]) => setState({ frontLayerConfigs: configs });
|
const setFrontLayerConfigs = (configs: LayerConfig[]) => setState({ frontLayerConfigs: configs });
|
||||||
const updateFrontLayerConfig = (prop: string, updates: Partial<LayerConfig>) => {
|
const updateFrontLayerConfig = (prop: string, updates: Partial<LayerConfig>) => {
|
||||||
setState('frontLayerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, ...updates } : config));
|
setState('frontLayerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, ...updates } : config));
|
||||||
|
|
@ -237,7 +225,6 @@ export function createDeckStore(
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 背面图层操作
|
|
||||||
const setBackLayerConfigs = (configs: LayerConfig[]) => setState({ backLayerConfigs: configs });
|
const setBackLayerConfigs = (configs: LayerConfig[]) => setState({ backLayerConfigs: configs });
|
||||||
const updateBackLayerConfig = (prop: string, updates: Partial<LayerConfig>) => {
|
const updateBackLayerConfig = (prop: string, updates: Partial<LayerConfig>) => {
|
||||||
setState('backLayerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, ...updates } : config));
|
setState('backLayerConfigs', (prev) => prev.map((config) => config.prop === prop ? { ...config, ...updates } : config));
|
||||||
|
|
@ -249,20 +236,8 @@ export function createDeckStore(
|
||||||
};
|
};
|
||||||
|
|
||||||
const setIsEditing = (editing: boolean) => setState({ isEditing: editing });
|
const setIsEditing = (editing: boolean) => setState({ isEditing: editing });
|
||||||
const setEditingLayer = (layer: string | null) => setState({ editingLayer: layer });
|
const setSelectedLayer = (layer: string | null) => setState({ selectedLayer: layer });
|
||||||
const setActiveSide = (side: CardSide) => setState({ activeSide: side });
|
const setActiveSide = (side: CardSide) => setState({ activeSide: side });
|
||||||
|
|
||||||
const updateLayerPosition = (x1: number, y1: number, x2: number, y2: number) => {
|
|
||||||
const layer = state.editingLayer;
|
|
||||||
if (!layer) return;
|
|
||||||
const currentSide = state.activeSide;
|
|
||||||
const configs = currentSide === 'front' ? state.frontLayerConfigs : state.backLayerConfigs;
|
|
||||||
const setter = currentSide === 'front' ? setFrontLayerConfigs : setBackLayerConfigs;
|
|
||||||
setter(configs.map((config) =>
|
|
||||||
config.prop === layer ? { ...config, x1, y1, x2, y2 } : config
|
|
||||||
));
|
|
||||||
setState({ editingLayer: null });
|
|
||||||
};
|
|
||||||
|
|
||||||
const setIsSelecting = (selecting: boolean) => setState({ isSelecting: selecting });
|
const setIsSelecting = (selecting: boolean) => setState({ isSelecting: selecting });
|
||||||
const setSelectStart = (pos: { x: number; y: number } | null) => setState({ selectStart: pos });
|
const setSelectStart = (pos: { x: number; y: number } | null) => setState({ selectStart: pos });
|
||||||
|
|
@ -271,7 +246,128 @@ export function createDeckStore(
|
||||||
setState({ isSelecting: false, selectStart: null, selectEnd: null });
|
setState({ isSelecting: false, selectStart: null, selectEnd: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载卡牌数据(核心逻辑)
|
const setDraggingState = (draggingState: DraggingState | null) => setState({ draggingState });
|
||||||
|
|
||||||
|
const getLayerConfig = (layerProp: string): LayerConfig | undefined => {
|
||||||
|
const configs = state.activeSide === 'front' ? state.frontLayerConfigs : state.backLayerConfigs;
|
||||||
|
return configs.find(c => c.prop === layerProp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLayerConfig = (layerProp: string, updates: Partial<LayerConfig>) => {
|
||||||
|
if (state.activeSide === 'front') {
|
||||||
|
updateFrontLayerConfig(layerProp, updates);
|
||||||
|
} else {
|
||||||
|
updateBackLayerConfig(layerProp, updates);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveLayer = (layerProp: string, dxGrid: number, dyGrid: number, startGrid?: { x1: number; y1: number; x2: number; y2: number }) => {
|
||||||
|
const layer = getLayerConfig(layerProp);
|
||||||
|
if (!layer) return;
|
||||||
|
|
||||||
|
const grid = startGrid ?? { x1: layer.x1, y1: layer.y1, x2: layer.x2, y2: layer.y2 };
|
||||||
|
const orientation = layer.orientation || 'n';
|
||||||
|
|
||||||
|
if (orientation === 'e' || orientation === 'w') {
|
||||||
|
updateLayerConfig(layerProp, {
|
||||||
|
x1: grid.x1 + dyGrid,
|
||||||
|
x2: grid.x2 + dyGrid,
|
||||||
|
y1: grid.y1 + dxGrid,
|
||||||
|
y2: grid.y2 + dxGrid
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateLayerConfig(layerProp, {
|
||||||
|
x1: grid.x1 + dxGrid,
|
||||||
|
x2: grid.x2 + dxGrid,
|
||||||
|
y1: grid.y1 + dyGrid,
|
||||||
|
y2: grid.y2 + dyGrid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeLayerCorner = (layerProp: string, anchor: 'nw' | 'ne' | 'sw' | 'se', dxGrid: number, dyGrid: number, startGrid: { x1: number; y1: number; x2: number; y2: number }) => {
|
||||||
|
const layer = getLayerConfig(layerProp);
|
||||||
|
if (!layer) return;
|
||||||
|
|
||||||
|
const orientation = layer.orientation || 'n';
|
||||||
|
|
||||||
|
if (orientation === 'e') {
|
||||||
|
const gridAnchorMap: Record<string, { xKey: string; yKey: string }> = {
|
||||||
|
'nw': { xKey: 'y1', yKey: 'x1' },
|
||||||
|
'ne': { xKey: 'y1', yKey: 'x2' },
|
||||||
|
'sw': { xKey: 'y2', yKey: 'x1' },
|
||||||
|
'se': { xKey: 'y2', yKey: 'x2' }
|
||||||
|
};
|
||||||
|
const { xKey, yKey } = gridAnchorMap[anchor];
|
||||||
|
const updates: Partial<LayerConfig> = {};
|
||||||
|
if (xKey === 'y1') updates.y1 = Math.min(startGrid.y2, startGrid.y1 + dxGrid);
|
||||||
|
if (xKey === 'y2') updates.y2 = Math.max(startGrid.y1, startGrid.y2 + dxGrid);
|
||||||
|
if (yKey === 'x1') updates.x1 = Math.min(startGrid.x2, startGrid.x1 + dyGrid);
|
||||||
|
if (yKey === 'x2') updates.x2 = Math.max(startGrid.x1, startGrid.x2 + dyGrid);
|
||||||
|
updateLayerConfig(layerProp, updates);
|
||||||
|
} else if (orientation === 'w') {
|
||||||
|
const gridAnchorMap: Record<string, { xKey: string; yKey: string }> = {
|
||||||
|
'nw': { xKey: 'y2', yKey: 'x2' },
|
||||||
|
'ne': { xKey: 'y2', yKey: 'x1' },
|
||||||
|
'sw': { xKey: 'y1', yKey: 'x2' },
|
||||||
|
'se': { xKey: 'y1', yKey: 'x1' }
|
||||||
|
};
|
||||||
|
const { xKey, yKey } = gridAnchorMap[anchor];
|
||||||
|
const updates: Partial<LayerConfig> = {};
|
||||||
|
if (xKey === 'y1') updates.y1 = Math.max(1, Math.min(startGrid.y2, startGrid.y1 - dxGrid));
|
||||||
|
if (xKey === 'y2') updates.y2 = Math.max(startGrid.y1, Math.min(state.gridH, startGrid.y2 - dxGrid));
|
||||||
|
if (yKey === 'x1') updates.x1 = Math.max(1, Math.min(startGrid.x2, startGrid.x1 - dyGrid));
|
||||||
|
if (yKey === 'x2') updates.x2 = Math.max(startGrid.x1, Math.min(state.gridW, startGrid.x2 - dyGrid));
|
||||||
|
updateLayerConfig(layerProp, updates);
|
||||||
|
} else {
|
||||||
|
const updates: Partial<LayerConfig> = {};
|
||||||
|
if (anchor === 'nw') {
|
||||||
|
updates.x1 = Math.min(startGrid.x2, startGrid.x1 + dxGrid);
|
||||||
|
updates.y1 = Math.min(startGrid.y2, startGrid.y1 + dyGrid);
|
||||||
|
} else if (anchor === 'ne') {
|
||||||
|
updates.x2 = Math.max(startGrid.x1, startGrid.x2 + dxGrid);
|
||||||
|
updates.y1 = Math.min(startGrid.y2, startGrid.y1 + dyGrid);
|
||||||
|
} else if (anchor === 'sw') {
|
||||||
|
updates.x1 = Math.min(startGrid.x2, startGrid.x1 + dxGrid);
|
||||||
|
updates.y2 = Math.max(startGrid.y1, startGrid.y2 + dyGrid);
|
||||||
|
} else if (anchor === 'se') {
|
||||||
|
updates.x2 = Math.max(startGrid.x1, startGrid.x2 + dxGrid);
|
||||||
|
updates.y2 = Math.max(startGrid.y1, startGrid.y2 + dyGrid);
|
||||||
|
}
|
||||||
|
updateLayerConfig(layerProp, updates);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeLayerEdge = (layerProp: string, edge: 'n' | 's' | 'e' | 'w', delta: number, startGrid: { x1: number; y1: number; x2: number; y2: number }) => {
|
||||||
|
const layer = getLayerConfig(layerProp);
|
||||||
|
if (!layer) return;
|
||||||
|
|
||||||
|
const orientation = layer.orientation || 'n';
|
||||||
|
|
||||||
|
if (orientation === 'e' || orientation === 'w') {
|
||||||
|
const edgeMap: Record<string, { key: string }> = {
|
||||||
|
'n': { key: 'y1' },
|
||||||
|
's': { key: 'y2' },
|
||||||
|
'e': { key: 'x2' },
|
||||||
|
'w': { key: 'x1' }
|
||||||
|
};
|
||||||
|
const { key } = edgeMap[edge];
|
||||||
|
const updates: Partial<LayerConfig> = {};
|
||||||
|
if (key === 'y1') updates.y1 = Math.min(startGrid.y2, Math.max(1, startGrid.y1 + delta));
|
||||||
|
if (key === 'y2') updates.y2 = Math.max(startGrid.y1, Math.min(state.gridH, startGrid.y2 + delta));
|
||||||
|
if (key === 'x1') updates.x1 = Math.min(startGrid.x2, Math.max(1, startGrid.x1 + delta));
|
||||||
|
if (key === 'x2') updates.x2 = Math.max(startGrid.x1, Math.min(state.gridW, startGrid.x2 + delta));
|
||||||
|
updateLayerConfig(layerProp, updates);
|
||||||
|
} else {
|
||||||
|
const updates: Partial<LayerConfig> = {};
|
||||||
|
if (edge === 'n') updates.y1 = Math.min(startGrid.y2, Math.max(1, startGrid.y1 + delta));
|
||||||
|
if (edge === 's') updates.y2 = Math.max(startGrid.y1, Math.min(state.gridH, startGrid.y2 + delta));
|
||||||
|
if (edge === 'w') updates.x1 = Math.min(startGrid.x2, Math.max(1, startGrid.x1 + delta));
|
||||||
|
if (edge === 'e') updates.x2 = Math.max(startGrid.x1, Math.min(state.gridW, startGrid.x2 + delta));
|
||||||
|
updateLayerConfig(layerProp, updates);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadCardsFromPath = async (path: string, rawSrc: string, layersStr: string = '', backLayersStr: string = '') => {
|
const loadCardsFromPath = async (path: string, rawSrc: string, layersStr: string = '', backLayersStr: string = '') => {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
setState({ error: '未指定 CSV 文件路径' });
|
setState({ error: '未指定 CSV 文件路径' });
|
||||||
|
|
@ -319,7 +415,6 @@ export function createDeckStore(
|
||||||
`grid="${state.gridW}x${state.gridH}" `
|
`grid="${state.gridW}x${state.gridH}" `
|
||||||
];
|
];
|
||||||
|
|
||||||
// 仅在非默认值时添加 bleed 和 padding
|
|
||||||
if (state.bleed !== DECK_DEFAULTS.BLEED) {
|
if (state.bleed !== DECK_DEFAULTS.BLEED) {
|
||||||
parts.push(`bleed="${state.bleed}" `);
|
parts.push(`bleed="${state.bleed}" `);
|
||||||
}
|
}
|
||||||
|
|
@ -396,13 +491,16 @@ export function createDeckStore(
|
||||||
updateBackLayerConfig,
|
updateBackLayerConfig,
|
||||||
toggleBackLayerVisible,
|
toggleBackLayerVisible,
|
||||||
setIsEditing,
|
setIsEditing,
|
||||||
setEditingLayer,
|
setSelectedLayer,
|
||||||
updateLayerPosition,
|
|
||||||
setActiveSide,
|
setActiveSide,
|
||||||
setIsSelecting,
|
setIsSelecting,
|
||||||
setSelectStart,
|
setSelectStart,
|
||||||
setSelectEnd,
|
setSelectEnd,
|
||||||
cancelSelection,
|
cancelSelection,
|
||||||
|
setDraggingState,
|
||||||
|
moveLayer,
|
||||||
|
resizeLayerCorner,
|
||||||
|
resizeLayerEdge,
|
||||||
loadCardsFromPath,
|
loadCardsFromPath,
|
||||||
setError,
|
setError,
|
||||||
clearError,
|
clearError,
|
||||||
|
|
@ -420,4 +518,4 @@ export function createDeckStore(
|
||||||
};
|
};
|
||||||
|
|
||||||
return { state, actions };
|
return { state, actions };
|
||||||
}
|
}
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
import type { DeckStore } from './deckStore';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 框选相关的操作(已整合到 deckStore)
|
|
||||||
* 此 hook 用于处理卡牌预览区域的鼠标交互
|
|
||||||
*/
|
|
||||||
export function useCardSelection(store: DeckStore) {
|
|
||||||
const calculateGridCoords = (e: MouseEvent, cardEl: HTMLElement, dimensions: DeckStore['state']['dimensions']) => {
|
|
||||||
if (!dimensions) return { gridX: 1, gridY: 1 };
|
|
||||||
|
|
||||||
const rect = cardEl.getBoundingClientRect();
|
|
||||||
|
|
||||||
const offsetX = (e.clientX - rect.left) / rect.width * dimensions.cardWidth;
|
|
||||||
const offsetY = (e.clientY - rect.top) / rect.height * dimensions.cardHeight;
|
|
||||||
|
|
||||||
const gridX = Math.max(1, Math.floor((offsetX - dimensions.gridOriginX) / dimensions.cellWidth) + 1);
|
|
||||||
const gridY = Math.max(1, Math.floor((offsetY - dimensions.gridOriginY) / dimensions.cellHeight) + 1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
gridX: Math.max(1, Math.min(dimensions.gridW, gridX)),
|
|
||||||
gridY: Math.max(1, Math.min(dimensions.gridH, gridY))
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseDown = (e: MouseEvent, cardEl: HTMLElement) => {
|
|
||||||
if (!store.state.isEditing || !store.state.editingLayer) return;
|
|
||||||
|
|
||||||
const { gridX, gridY } = calculateGridCoords(e, cardEl, store.state.dimensions);
|
|
||||||
|
|
||||||
store.actions.setSelectStart({ x: gridX, y: gridY });
|
|
||||||
store.actions.setSelectEnd({ x: gridX, y: gridY });
|
|
||||||
store.actions.setIsSelecting(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent, cardEl: HTMLElement) => {
|
|
||||||
if (!store.state.isSelecting) return;
|
|
||||||
|
|
||||||
const { gridX, gridY } = calculateGridCoords(e, cardEl, store.state.dimensions);
|
|
||||||
|
|
||||||
store.actions.setSelectEnd({ x: gridX, y: gridY });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
if (!store.state.isSelecting || !store.state.editingLayer) return;
|
|
||||||
|
|
||||||
const start = store.state.selectStart!;
|
|
||||||
const end = store.state.selectEnd!;
|
|
||||||
|
|
||||||
const x1 = Math.min(start.x, end.x);
|
|
||||||
const y1 = Math.min(start.y, end.y);
|
|
||||||
const x2 = Math.max(start.x, end.x);
|
|
||||||
const y2 = Math.max(start.y, end.y);
|
|
||||||
|
|
||||||
store.actions.updateLayerPosition(x1, y1, x2, y2);
|
|
||||||
store.actions.cancelSelection();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
onMouseDown: handleMouseDown,
|
|
||||||
onMouseMove: handleMouseMove,
|
|
||||||
onMouseUp: handleMouseUp,
|
|
||||||
onMouseLeave: handleMouseUp
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算框选区域的样式
|
|
||||||
*/
|
|
||||||
export function getSelectionBoxStyle(
|
|
||||||
selectStart: { x: number; y: number } | null,
|
|
||||||
selectEnd: { x: number; y: number } | null,
|
|
||||||
dims: { gridOriginX: number; gridOriginY: number; cellWidth: number; cellHeight: number } | null
|
|
||||||
): { left: string; top: string; width: string; height: string } | null {
|
|
||||||
if (!selectStart || !selectEnd || !dims) return null;
|
|
||||||
|
|
||||||
const x1 = Math.min(selectStart.x, selectEnd.x);
|
|
||||||
const y1 = Math.min(selectStart.y, selectEnd.y);
|
|
||||||
const x2 = Math.max(selectStart.x, selectEnd.x);
|
|
||||||
const y2 = Math.max(selectStart.y, selectEnd.y);
|
|
||||||
|
|
||||||
const left = dims.gridOriginX + (x1 - 1) * dims.cellWidth;
|
|
||||||
const top = dims.gridOriginY + (y1 - 1) * dims.cellHeight;
|
|
||||||
const width = (x2 - x1 + 1) * dims.cellWidth;
|
|
||||||
const height = (y2 - y1 + 1) * dims.cellHeight;
|
|
||||||
|
|
||||||
return {
|
|
||||||
left: `${left}mm`,
|
|
||||||
top: `${top}mm`,
|
|
||||||
width: `${width}mm`,
|
|
||||||
height: `${height}mm`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
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: (layerProp: string, 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: string[]; 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 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 => {
|
||||||
|
const orientation = layer.orientation || 'n';
|
||||||
|
|
||||||
|
if (orientation === 'e' || orientation === 'w') {
|
||||||
|
const visualX1 = layer.y1;
|
||||||
|
const visualX2 = layer.y2;
|
||||||
|
const visualY1 = layer.x1;
|
||||||
|
const visualY2 = layer.x2;
|
||||||
|
return gridX >= visualX1 && gridX <= visualX2 && gridY >= visualY1 && gridY <= visualY2;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = (layerProp: string, e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const currentlySelected = store.state.selectedLayer;
|
||||||
|
|
||||||
|
if (currentlySelected === layerProp) {
|
||||||
|
store.actions.setSelectedLayer(null);
|
||||||
|
} else {
|
||||||
|
store.actions.setSelectedLayer(layerProp);
|
||||||
|
}
|
||||||
|
|
||||||
|
clickStack = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCardClick = (e: MouseEvent, cardEl: HTMLElement) => {
|
||||||
|
if (store.state.draggingState) return;
|
||||||
|
|
||||||
|
const { gridX, gridY } = calculateGridCoords(e, cardEl);
|
||||||
|
const overlapping = getOverlappingLayers(gridX, gridY);
|
||||||
|
|
||||||
|
if (overlapping.length === 0) {
|
||||||
|
store.actions.setSelectedLayer(null);
|
||||||
|
clickStack = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlappingProps = overlapping.map(l => l.prop);
|
||||||
|
|
||||||
|
if (clickStack && clickStack.gridX === gridX && clickStack.gridY === gridY) {
|
||||||
|
clickStack.currentIndex = (clickStack.currentIndex + 1) % overlappingProps.length;
|
||||||
|
const nextLayer = overlappingProps[clickStack.currentIndex];
|
||||||
|
store.actions.setSelectedLayer(nextLayer);
|
||||||
|
} else {
|
||||||
|
clickStack = {
|
||||||
|
gridX,
|
||||||
|
gridY,
|
||||||
|
layers: overlappingProps,
|
||||||
|
currentIndex: 0
|
||||||
|
};
|
||||||
|
store.actions.setSelectedLayer(overlappingProps[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFrameMouseDown = (
|
||||||
|
action: 'drag' | 'resize-corner' | 'resize-edge',
|
||||||
|
anchor?: 'nw' | 'ne' | 'sw' | 'se',
|
||||||
|
edge?: 'n' | 's' | 'e' | 'w',
|
||||||
|
e?: MouseEvent
|
||||||
|
) => {
|
||||||
|
if (!store.state.selectedLayer) return;
|
||||||
|
if (e) e.stopPropagation();
|
||||||
|
|
||||||
|
const layer = currentLayerConfigs().find(l => l.prop === store.state.selectedLayer);
|
||||||
|
if (!layer) return;
|
||||||
|
|
||||||
|
const startX = e?.clientX ?? 0;
|
||||||
|
const startY = e?.clientY ?? 0;
|
||||||
|
|
||||||
|
const draggingState: DraggingState = {
|
||||||
|
layer: store.state.selectedLayer,
|
||||||
|
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 layer = currentLayerConfigs().find(l => l.prop === dragging.layer);
|
||||||
|
if (!layer) return;
|
||||||
|
|
||||||
|
const orientation = layer.orientation || 'n';
|
||||||
|
const startGrid = dragging.startGrid;
|
||||||
|
|
||||||
|
if (dragging.action === 'drag') {
|
||||||
|
const moveDx = Math.round(deltaGridX);
|
||||||
|
const moveDy = Math.round(deltaGridY);
|
||||||
|
store.actions.moveLayer(dragging.layer, moveDx, moveDy, 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 = orientation === 'e' || orientation === 'w'
|
||||||
|
? (dragging.edge === 'n' || dragging.edge === 's' ? Math.round(deltaGridX) : Math.round(deltaGridY))
|
||||||
|
: (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 layer = store.state.draggingState.layer;
|
||||||
|
const dragging = store.state.draggingState;
|
||||||
|
|
||||||
|
store.actions.setDraggingState(null);
|
||||||
|
|
||||||
|
const newLayerConfig = currentLayerConfigs().find(l => l.prop === 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
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue