ttrpg-tools/src/components/md-deck/CardLayer.tsx

208 lines
7.7 KiB
TypeScript
Raw Normal View History

import { createMemo, For, Show } from "solid-js";
import { parseMarkdown } from "../../markdown";
import { getLayerStyle } from "./hooks/dimensions";
import type { CardData, CardSide, LayerConfig } from "./types";
2026-03-30 12:01:08 +08:00
import { DeckStore } from "./hooks/deckStore";
import { processVariables } from "../utils/csv-loader";
import type { LayerInteractionHandlers } from "./hooks/useLayerInteraction";
2026-02-27 21:12:23 +08:00
export interface CardLayerProps {
cardData: CardData;
store: DeckStore;
2026-03-13 17:26:00 +08:00
side?: CardSide;
2026-03-30 12:01:08 +08:00
interaction?: LayerInteractionHandlers;
2026-02-27 21:12:23 +08:00
}
export function CardLayer(props: CardLayerProps) {
const side = () => props.side || "front";
const layers = createMemo(() =>
side() === "front"
2026-03-13 17:26:00 +08:00
? props.store.state.frontLayerConfigs.filter((l) => l.visible)
: props.store.state.backLayerConfigs.filter((l) => l.visible),
2026-03-13 17:26:00 +08:00
);
const dimensions = () => props.store.state.dimensions!;
2026-03-30 12:01:08 +08:00
const selectedLayer = () => props.store.state.selectedLayer;
const draggingState = () => props.store.state.draggingState;
2026-03-13 17:26:00 +08:00
function renderLayerContent(content: string) {
return parseMarkdown(
processVariables(content, props.cardData, props.store.state.cards),
props.store.state.cards.sourcePath,
) as string;
}
2026-03-27 15:17:25 +08:00
const getAlignStyle = (align?: "l" | "c" | "r") => {
if (align === "l") return "left";
if (align === "r") return "right";
return "center";
2026-03-27 15:17:25 +08:00
};
2026-03-30 12:01:08 +08:00
const isLayerSelected = (layerIndex: number) =>
selectedLayer() === layerIndex;
2026-03-30 12:01:08 +08:00
const getFrameBounds = (layer: LayerConfig) => {
const dims = dimensions();
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 = (layerIndex: number, e: MouseEvent) => {
2026-03-30 12:01:08 +08:00
if (props.interaction) {
props.interaction.onLayerClick(layerIndex, e);
2026-03-30 12:01:08 +08:00
}
};
const handleFrameMouseDown = (
action: "drag" | "resize-corner" | "resize-edge",
anchor?: "nw" | "ne" | "sw" | "se",
edge?: "n" | "s" | "e" | "w",
e?: MouseEvent,
2026-03-30 12:01:08 +08:00
) => {
if (props.interaction) {
props.interaction.onFrameMouseDown(action, anchor, edge, e);
}
};
2026-02-27 21:12:23 +08:00
return (
<For each={layers()}>
{(layer, index) => {
2026-03-30 12:01:08 +08:00
const bounds = () => getFrameBounds(layer);
const isSelected = () => isLayerSelected(index());
2026-02-27 22:56:06 +08:00
return (
<>
<article
class="absolute flex flex-col items-stretch justify-center prose text-black prose-sm cursor-pointer"
classList={{
"ring-2 ring-blue-500 ring-offset-1":
isSelected() && !draggingState(),
2026-03-30 12:01:08 +08:00
}}
2026-02-27 22:37:11 +08:00
style={{
...getLayerStyle(layer, dimensions()),
"font-size": `${layer.fontSize || 3}mm`,
"text-align": getAlignStyle(layer.align),
2026-02-27 22:37:11 +08:00
}}
innerHTML={renderLayerContent(props.cardData[layer.prop])}
onClick={(e) => handleLayerClick(index(), e)}
2026-02-27 22:37:11 +08:00
/>
2026-03-30 12:01:08 +08:00
<Show when={isSelected()}>
<div
2026-03-30 12:33:42 +08:00
class="absolute border-2 border-blue-500 pointer-events-none z-10"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left}mm`,
top: `${bounds().top}mm`,
width: `${bounds().width}mm`,
height: `${bounds().height}mm`,
2026-03-30 12:01:08 +08:00
}}
/>
<div
2026-03-30 12:33:42 +08:00
class="absolute pointer-events-auto z-10"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left}mm`,
top: `${bounds().top}mm`,
width: `${bounds().width}mm`,
height: `${bounds().height}mm`,
2026-03-30 12:01:08 +08:00
}}
onMouseDown={(e) =>
handleFrameMouseDown("drag", undefined, undefined, e)
}
2026-03-30 12:01:08 +08:00
/>
<Show
when={!draggingState() || draggingState()?.action !== "drag"}
>
2026-03-30 12:01:08 +08:00
<div
2026-03-30 12:33:42 +08:00
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nwse-resize pointer-events-auto z-20"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left - 2}mm`,
top: `${bounds().top - 2}mm`,
2026-03-30 12:01:08 +08:00
}}
onMouseDown={(e) =>
handleFrameMouseDown("resize-corner", "nw", undefined, e)
}
2026-03-30 12:01:08 +08:00
/>
<div
2026-03-30 12:33:42 +08:00
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nesw-resize pointer-events-auto z-20"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left + bounds().width - 2}mm`,
top: `${bounds().top - 2}mm`,
2026-03-30 12:01:08 +08:00
}}
onMouseDown={(e) =>
handleFrameMouseDown("resize-corner", "ne", undefined, e)
}
2026-03-30 12:01:08 +08:00
/>
<div
2026-03-30 12:33:42 +08:00
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nesw-resize pointer-events-auto z-20"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left - 2}mm`,
top: `${bounds().top + bounds().height - 2}mm`,
2026-03-30 12:01:08 +08:00
}}
onMouseDown={(e) =>
handleFrameMouseDown("resize-corner", "sw", undefined, e)
}
2026-03-30 12:01:08 +08:00
/>
<div
2026-03-30 12:33:42 +08:00
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nwse-resize pointer-events-auto z-20"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left + bounds().width - 2}mm`,
top: `${bounds().top + bounds().height - 2}mm`,
2026-03-30 12:01:08 +08:00
}}
onMouseDown={(e) =>
handleFrameMouseDown("resize-corner", "se", undefined, e)
}
2026-03-30 12:01:08 +08:00
/>
<div
2026-03-30 12:33:42 +08:00
class="absolute h-2 cursor-ns-resize pointer-events-auto z-10"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left}mm`,
top: `${bounds().top - 1}mm`,
width: `${bounds().width}mm`,
2026-03-30 12:01:08 +08:00
}}
onMouseDown={(e) =>
handleFrameMouseDown("resize-edge", undefined, "n", e)
}
2026-03-30 12:01:08 +08:00
/>
<div
2026-03-30 12:33:42 +08:00
class="absolute h-2 cursor-ns-resize pointer-events-auto z-10"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left}mm`,
top: `${bounds().top + bounds().height - 1}mm`,
width: `${bounds().width}mm`,
2026-03-30 12:01:08 +08:00
}}
onMouseDown={(e) =>
handleFrameMouseDown("resize-edge", undefined, "s", e)
}
2026-03-30 12:01:08 +08:00
/>
<div
2026-03-30 12:33:42 +08:00
class="absolute w-2 cursor-ew-resize pointer-events-auto z-10"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left - 1}mm`,
top: `${bounds().top}mm`,
height: `${bounds().height}mm`,
2026-03-30 12:01:08 +08:00
}}
onMouseDown={(e) =>
handleFrameMouseDown("resize-edge", undefined, "w", e)
}
2026-03-30 12:01:08 +08:00
/>
<div
2026-03-30 12:33:42 +08:00
class="absolute w-2 cursor-ew-resize pointer-events-auto z-10"
2026-03-30 12:01:08 +08:00
style={{
left: `${bounds().left + bounds().width - 1}mm`,
top: `${bounds().top}mm`,
height: `${bounds().height}mm`,
2026-03-30 12:01:08 +08:00
}}
onMouseDown={(e) =>
handleFrameMouseDown("resize-edge", undefined, "e", e)
}
2026-03-30 12:01:08 +08:00
/>
</Show>
</Show>
2026-02-27 22:56:06 +08:00
</>
);
}}
2026-02-27 21:12:23 +08:00
</For>
);
}