feat: nsew layers
This commit is contained in:
parent
c204dc695c
commit
5bc77a2291
|
|
@ -29,29 +29,32 @@ function renderLayerContent(layer: { prop: string }, cardData: CardData): string
|
||||||
export function CardLayer(props: CardLayerProps) {
|
export function CardLayer(props: CardLayerProps) {
|
||||||
return (
|
return (
|
||||||
<For each={props.layers}>
|
<For each={props.layers}>
|
||||||
{(layer) => (
|
{(layer) => {
|
||||||
<>
|
const layerStyle = getLayerStyle(layer, props.dimensions);
|
||||||
<article
|
return (
|
||||||
class="absolute flex items-center justify-center text-center prose prose-sm"
|
<>
|
||||||
style={{
|
<article
|
||||||
...getLayerStyle(layer, props.dimensions),
|
class="absolute flex items-center justify-center text-center prose prose-sm"
|
||||||
'font-size': `${props.dimensions.fontSize}mm`
|
|
||||||
}}
|
|
||||||
innerHTML={renderLayerContent(layer, props.cardData)}
|
|
||||||
/>
|
|
||||||
{props.showBounds && (
|
|
||||||
<div
|
|
||||||
class="absolute border-2 border-blue-500/50 pointer-events-none select-none"
|
|
||||||
style={{
|
style={{
|
||||||
left: `${(layer.x1 - 1) * props.dimensions.cellWidth}mm`,
|
...layerStyle,
|
||||||
top: `${(layer.y1 - 1) * props.dimensions.cellHeight}mm`,
|
'font-size': `${props.dimensions.fontSize}mm`
|
||||||
width: `${(layer.x2 - layer.x1 + 1) * props.dimensions.cellWidth}mm`,
|
|
||||||
height: `${(layer.y2 - layer.y1 + 1) * props.dimensions.cellHeight}mm`
|
|
||||||
}}
|
}}
|
||||||
|
innerHTML={renderLayerContent(layer, props.cardData)}
|
||||||
/>
|
/>
|
||||||
)}
|
{props.showBounds && (
|
||||||
</>
|
<div
|
||||||
)}
|
class="absolute border-2 border-blue-500/50 pointer-events-none select-none"
|
||||||
|
style={{
|
||||||
|
left: `${(layer.x1 - 1) * props.dimensions.cellWidth}mm`,
|
||||||
|
top: `${(layer.y1 - 1) * props.dimensions.cellHeight}mm`,
|
||||||
|
width: `${(layer.x2 - layer.x1 + 1) * props.dimensions.cellWidth}mm`,
|
||||||
|
height: `${(layer.y2 - layer.y1 + 1) * props.dimensions.cellHeight}mm`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,26 @@ export interface LayerEditorPanelProps {
|
||||||
store: DeckStore;
|
store: DeckStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ORIENTATION_OPTIONS = [
|
||||||
|
{ value: 'n', label: '↑ 北' },
|
||||||
|
{ value: 'e', label: '→ 东' },
|
||||||
|
{ value: 's', label: '↓ 南' },
|
||||||
|
{ value: 'w', label: '← 西' }
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图层编辑面板:图层可见性切换、位置编辑、复制代码
|
* 图层编辑面板:图层可见性切换、位置编辑、复制代码
|
||||||
*/
|
*/
|
||||||
export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
||||||
const { store } = props;
|
const { store } = props;
|
||||||
|
|
||||||
|
const updateLayerOrientation = (layerProp: string, orientation: 'n' | 's' | 'e' | 'w') => {
|
||||||
|
const layer = store.state.layerConfigs.find(l => l.prop === layerProp);
|
||||||
|
if (layer) {
|
||||||
|
store.actions.updateLayerConfig(layerProp, { ...layer, orientation });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="w-64 flex-shrink-0">
|
<div class="w-64 flex-shrink-0">
|
||||||
<h3 class="font-bold mb-2 mt-0">图层</h3>
|
<h3 class="font-bold mb-2 mt-0">图层</h3>
|
||||||
|
|
@ -18,24 +32,39 @@ export function LayerEditorPanel(props: LayerEditorPanelProps) {
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<For each={store.state.layerConfigs}>
|
<For each={store.state.layerConfigs}>
|
||||||
{(layer) => (
|
{(layer) => (
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex flex-col gap-1 p-2 bg-gray-50 rounded">
|
||||||
<input
|
<div class="flex items-center gap-2">
|
||||||
type="checkbox"
|
<input
|
||||||
checked={layer.visible}
|
type="checkbox"
|
||||||
onChange={() => store.actions.toggleLayerVisible(layer.prop)}
|
checked={layer.visible}
|
||||||
class="cursor-pointer"
|
onChange={() => store.actions.toggleLayerVisible(layer.prop)}
|
||||||
/>
|
class="cursor-pointer"
|
||||||
<span class="text-sm flex-1">{layer.prop}</span>
|
/>
|
||||||
<button
|
<span class="text-sm flex-1">{layer.prop}</span>
|
||||||
onClick={() => store.actions.setEditingLayer(store.state.editingLayer === layer.prop ? null : layer.prop)}
|
</div>
|
||||||
class={`text-xs px-2 py-0.5 rounded cursor-pointer ${
|
<div class="flex items-center gap-2">
|
||||||
store.state.editingLayer === layer.prop
|
<select
|
||||||
? 'bg-blue-500 text-white'
|
value={layer.orientation || 'n'}
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
onChange={(e) => updateLayerOrientation(layer.prop, e.target.value as 'n' | 's' | 'e' | 'w')}
|
||||||
}`}
|
class="text-xs px-2 py-1 rounded border border-gray-300 bg-white cursor-pointer"
|
||||||
>
|
>
|
||||||
{store.state.editingLayer === layer.prop ? '✓ 框选' : '编辑'}
|
<For each={ORIENTATION_OPTIONS}>
|
||||||
</button>
|
{(opt) => (
|
||||||
|
<option value={opt.value}>{opt.label}</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
onClick={() => store.actions.setEditingLayer(store.state.editingLayer === layer.prop ? null : layer.prop)}
|
||||||
|
class={`text-xs px-2 py-1 rounded cursor-pointer ${
|
||||||
|
store.state.editingLayer === layer.prop
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{store.state.editingLayer === layer.prop ? '✓ 框选' : '框选'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export interface DeckState {
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
fixed: boolean;
|
fixed: boolean;
|
||||||
src: string;
|
src: string;
|
||||||
|
rawSrc: string; // 原始 CSV 路径(用于生成代码时保持相对路径)
|
||||||
|
|
||||||
// 解析后的尺寸
|
// 解析后的尺寸
|
||||||
dimensions: Dimensions | null;
|
dimensions: Dimensions | null;
|
||||||
|
|
@ -97,7 +98,7 @@ export interface DeckActions {
|
||||||
cancelSelection: () => void;
|
cancelSelection: () => void;
|
||||||
|
|
||||||
// 数据加载
|
// 数据加载
|
||||||
loadCardsFromPath: (path: string, layersStr?: string) => Promise<void>;
|
loadCardsFromPath: (path: string, rawSrc: string, layersStr?: string) => Promise<void>;
|
||||||
setError: (error: string | null) => void;
|
setError: (error: string | null) => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
|
|
||||||
|
|
@ -140,6 +141,7 @@ export function createDeckStore(
|
||||||
fontSize: DECK_DEFAULTS.FONT_SIZE,
|
fontSize: DECK_DEFAULTS.FONT_SIZE,
|
||||||
fixed: false,
|
fixed: false,
|
||||||
src: initialSrc,
|
src: initialSrc,
|
||||||
|
rawSrc: initialSrc,
|
||||||
dimensions: null,
|
dimensions: null,
|
||||||
cards: [],
|
cards: [],
|
||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
|
|
@ -237,13 +239,13 @@ export function createDeckStore(
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载卡牌数据(核心逻辑)
|
// 加载卡牌数据(核心逻辑)
|
||||||
const loadCardsFromPath = async (path: string, layersStr: string = '') => {
|
const loadCardsFromPath = async (path: string, rawSrc: string, layersStr: string = '') => {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
setState({ error: '未指定 CSV 文件路径' });
|
setState({ error: '未指定 CSV 文件路径' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState({ isLoading: true, error: null, src: path });
|
setState({ isLoading: true, error: null, src: path, rawSrc: rawSrc });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await loadCSV(path);
|
const data = await loadCSV(path);
|
||||||
|
|
@ -277,9 +279,9 @@ export function createDeckStore(
|
||||||
const generateCode = () => {
|
const generateCode = () => {
|
||||||
const layersStr = state.layerConfigs
|
const layersStr = state.layerConfigs
|
||||||
.filter(l => l.visible)
|
.filter(l => l.visible)
|
||||||
.map(l => `${l.prop}:${l.x1},${l.y1}-${l.x2},${l.y2}`)
|
.map(l => `${l.prop}:${l.x1},${l.y1}-${l.x2},${l.y2}${l.orientation && l.orientation !== 'n' ? `${l.orientation}` : ''}`)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
return `:md-deck[${state.src}]{size="${state.sizeW}x${state.sizeH}" grid="${state.gridW}x${state.gridH}" bleed="${state.bleed}" padding="${state.padding}" font-size="${state.fontSize}" layers="${layersStr}"}`;
|
return `:md-deck[${state.rawSrc || state.src}]{size="${state.sizeW}x${state.sizeH}" grid="${state.gridW}x${state.gridH}" bleed="${state.bleed}" padding="${state.padding}" font-size="${state.fontSize}" layers="${layersStr}"}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyCode = async () => {
|
const copyCode = async () => {
|
||||||
|
|
|
||||||
|
|
@ -47,20 +47,89 @@ export function calculateDimensions(options: DimensionOptions): Dimensions {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算 layer 位置样式(单位:mm)
|
* 计算 layer 位置样式(单位:mm)
|
||||||
|
* 根据 orientation 旋转元素,同时保持覆盖相同的区域
|
||||||
|
*
|
||||||
|
* orientation 含义:
|
||||||
|
* - n (0°): 正常方向,内容从上到下阅读
|
||||||
|
* - e (90°): 顺时针旋转 90°,内容从右到左阅读(右侧为顶部)
|
||||||
|
* - s (180°): 旋转 180°,内容从下到上阅读
|
||||||
|
* - w (270°): 顺时针旋转 270°,内容从左到右阅读(左侧为顶部)
|
||||||
*/
|
*/
|
||||||
export function getLayerStyle(
|
export function getLayerStyle(
|
||||||
layer: { x1: number; y1: number; x2: number; y2: number },
|
layer: { x1: number; y1: number; x2: number; y2: number; orientation?: 'n' | 's' | 'e' | 'w' },
|
||||||
dims: Dimensions
|
dims: Dimensions
|
||||||
): { left: string; top: string; width: string; height: string } {
|
): {
|
||||||
const left = (layer.x1 - 1) * dims.cellWidth;
|
left: string;
|
||||||
const top = (layer.y1 - 1) * dims.cellHeight;
|
top: string;
|
||||||
const width = (layer.x2 - layer.x1 + 1) * dims.cellWidth;
|
width: string;
|
||||||
const height = (layer.y2 - layer.y1 + 1) * dims.cellHeight;
|
height: string;
|
||||||
|
transform?: string;
|
||||||
|
'transform-origin'?: string;
|
||||||
|
} {
|
||||||
|
const cellWidth = dims.cellWidth;
|
||||||
|
const cellHeight = dims.cellHeight;
|
||||||
|
|
||||||
|
// 计算原始矩形的边界(相对于网格区域起点)
|
||||||
|
const baseLeft = (layer.x1 - 1) * cellWidth;
|
||||||
|
const baseTop = (layer.y1 - 1) * cellHeight;
|
||||||
|
const baseWidth = (layer.x2 - layer.x1 + 1) * cellWidth;
|
||||||
|
const baseHeight = (layer.y2 - layer.y1 + 1) * cellHeight;
|
||||||
|
|
||||||
|
const orientation = layer.orientation ?? 'n';
|
||||||
|
|
||||||
|
// 根据方向决定旋转角度
|
||||||
|
// n: 0°, e: 90°, s: 180°, w: 270°
|
||||||
|
const rotationMap: Record<'n' | 's' | 'e' | 'w', number> = {
|
||||||
|
n: 0,
|
||||||
|
e: 90,
|
||||||
|
s: 180,
|
||||||
|
w: 270
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotation = rotationMap[orientation];
|
||||||
|
|
||||||
|
// 对于 e/w 方向,需要交换宽高
|
||||||
|
if (orientation === 'e' || orientation === 'w') {
|
||||||
|
// 计算旋转后的位置,使得元素覆盖相同的区域
|
||||||
|
// 旋转 90°/270°后,原来的宽度变成高度,高度变成宽度
|
||||||
|
// 需要调整 left/top 使得旋转后的元素中心与原始矩形中心重合
|
||||||
|
|
||||||
|
// 元素尺寸交换
|
||||||
|
const width = baseHeight;
|
||||||
|
const height = baseWidth;
|
||||||
|
|
||||||
|
// 计算新起点,使得旋转后覆盖相同区域
|
||||||
|
// 旋转中心设为元素中心,旋转后元素覆盖的区域与原始区域相同
|
||||||
|
const left = baseLeft + (baseWidth - width) / 2;
|
||||||
|
const top = baseTop + (baseHeight - height) / 2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: `${left}mm`,
|
||||||
|
top: `${top}mm`,
|
||||||
|
width: `${width}mm`,
|
||||||
|
height: `${height}mm`,
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
|
'transform-origin': 'center center'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// n 或 s 方向,宽高不变
|
||||||
|
if (orientation === 's') {
|
||||||
|
return {
|
||||||
|
left: `${baseLeft}mm`,
|
||||||
|
top: `${baseTop}mm`,
|
||||||
|
width: `${baseWidth}mm`,
|
||||||
|
height: `${baseHeight}mm`,
|
||||||
|
transform: `rotate(180deg)`,
|
||||||
|
'transform-origin': 'center center'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// n 方向(默认)
|
||||||
return {
|
return {
|
||||||
left: `${left}mm`,
|
left: `${baseLeft}mm`,
|
||||||
top: `${top}mm`,
|
top: `${baseTop}mm`,
|
||||||
width: `${width}mm`,
|
width: `${baseWidth}mm`,
|
||||||
height: `${height}mm`
|
height: `${baseHeight}mm`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import type { Layer, LayerConfig } from '../types';
|
import type { Layer, LayerConfig } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 layers 字符串 "body:1,7-5,8 title:1,1-5,1"
|
* 解析 layers 字符串 "body:1,7-5,8 title:1,1-5,1" 或 "body:1,7-5,8,s title:1,1-5,1,e"
|
||||||
*/
|
*/
|
||||||
export function parseLayers(layersStr: string): Layer[] {
|
export function parseLayers(layersStr: string): Layer[] {
|
||||||
if (!layersStr) return [];
|
if (!layersStr) return [];
|
||||||
|
|
||||||
const layers: Layer[] = [];
|
const layers: Layer[] = [];
|
||||||
const regex = /(\w+):(\d+),(\d+)-(\d+),(\d+)/g;
|
const regex = /(\w+):(\d+),(\d+)-(\d+),(\d+)([nsew])?/g;
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = regex.exec(layersStr)) !== null) {
|
while ((match = regex.exec(layersStr)) !== null) {
|
||||||
|
|
@ -16,7 +16,8 @@ export function parseLayers(layersStr: string): Layer[] {
|
||||||
x1: parseInt(match[2]),
|
x1: parseInt(match[2]),
|
||||||
y1: parseInt(match[3]),
|
y1: parseInt(match[3]),
|
||||||
x2: parseInt(match[4]),
|
x2: parseInt(match[4]),
|
||||||
y2: parseInt(match[5])
|
y2: parseInt(match[5]),
|
||||||
|
orientation: match[6] as 'n' | 's' | 'e' | 'w' | undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,7 +30,7 @@ export function parseLayers(layersStr: string): Layer[] {
|
||||||
export function formatLayers(layers: LayerConfig[]): string {
|
export function formatLayers(layers: LayerConfig[]): string {
|
||||||
return layers
|
return layers
|
||||||
.filter(l => l.visible)
|
.filter(l => l.visible)
|
||||||
.map(l => `${l.prop}:${l.x1},${l.y1}-${l.x2},${l.y2}`)
|
.map(l => `${l.prop}:${l.x1},${l.y1}-${l.x2},${l.y2}${l.orientation && l.orientation !== 'n' ? `${l.orientation}` : ''}`)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +52,8 @@ export function initLayerConfigs(
|
||||||
x1: existing?.x1 || 1,
|
x1: existing?.x1 || 1,
|
||||||
y1: existing?.y1 || 1,
|
y1: existing?.y1 || 1,
|
||||||
x2: existing?.x2 || 2,
|
x2: existing?.x2 || 2,
|
||||||
y2: existing?.y2 || 2
|
y2: existing?.y2 || 2,
|
||||||
|
orientation: existing?.orientation
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ customElement<DeckProps>('md-deck', {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载 CSV 数据
|
// 加载 CSV 数据
|
||||||
store.actions.loadCardsFromPath(resolvedSrc, (props.layers as string) || '');
|
store.actions.loadCardsFromPath(resolvedSrc, csvPath, (props.layers as string) || '');
|
||||||
|
|
||||||
// 清理函数
|
// 清理函数
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export interface Layer {
|
||||||
y1: number;
|
y1: number;
|
||||||
x2: number;
|
x2: number;
|
||||||
y2: number;
|
y2: number;
|
||||||
|
orientation?: 'n' | 's' | 'e' | 'w';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LayerConfig {
|
export interface LayerConfig {
|
||||||
|
|
@ -17,6 +18,7 @@ export interface LayerConfig {
|
||||||
y1: number;
|
y1: number;
|
||||||
x2: number;
|
x2: number;
|
||||||
y2: number;
|
y2: number;
|
||||||
|
orientation?: 'n' | 's' | 'e' | 'w';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Dimensions {
|
export interface Dimensions {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue