feat(canvas): 多角度贴节点、宫格切分菜单与多选虚线框跟随

把多角度改成贴在图片节点下方的 3D 编辑器并跟随拖动;宫格切分并入图片工具,自定义面板不关菜单;切分后子图按网格排开;多选虚线框随拖动预览平移。
This commit is contained in:
soonstudio-ai committed 2026-09-14 17:19:56 +08:00
1 parent 9727656b2b
commit 8e82e67cb3
26 files changed
+1431 -153

No files matched your search

@@ -0,0 +1,99 @@
import { useState } from "react";
import { ChevronRight } from "lucide-react";
import { CANVAS_GRID_SPLIT_MAX, CANVAS_GRID_SPLIT_PRESETS, clampGridSplitSize, isValidGridSplit } from "@/lib/canvas/canvas-grid-split";
import type { ImageSplitParams } from "@/lib/canvas/canvas-image-data";
import "./canvas-grid-split-picker.css";
function MiniGridIcon({ n }: { n: number }) {
return (
<span className="canvas-grid-split-mini" style={{ gridTemplateColumns: `repeat(${n}, minmax(0, 1fr))` }} aria-hidden>
{Array.from({ length: n * n }, (_, index) => <span key={index} />)}
</span>
);
}
export function CanvasGridSplitPicker({ onPick }: { onPick: (params: ImageSplitParams) => void }) {
const [customOpen, setCustomOpen] = useState(false);
const [hoverRows, setHoverRows] = useState(2);
const [hoverCols, setHoverCols] = useState(2);
const pick = (rows: number, columns: number) => {
const params = { rows: clampGridSplitSize(rows), columns: clampGridSplitSize(columns) };
if (!isValidGridSplit(params)) return;
onPick(params);
};
return (
<div
className="canvas-grid-split-picker"
data-canvas-no-zoom
role="dialog"
aria-label="宫格切分"
onPointerDown={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
onClick={(event) => event.stopPropagation()}
>
<div className="canvas-grid-split-presets">
{CANVAS_GRID_SPLIT_PRESETS.map((preset) => (
<button key={preset.label} type="button" className="canvas-grid-split-item" onMouseDown={(event) => event.preventDefault()} onClick={() => pick(preset.rows, preset.columns)}>
<span className="canvas-grid-split-icon"><MiniGridIcon n={preset.rows} /></span>
<span className="canvas-grid-split-item-label">{preset.label}</span>
</button>
))}
<button
type="button"
className={`canvas-grid-split-item${customOpen ? " is-active" : ""}`}
aria-expanded={customOpen}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
setCustomOpen((current) => !current);
}}
>
<span className="canvas-grid-split-icon"><MiniGridIcon n={3} /></span>
<span className="canvas-grid-split-item-label">自定义</span>
<ChevronRight className="canvas-grid-split-chevron" strokeWidth={2} />
</button>
</div>
{customOpen ? (
<div className="canvas-grid-split-custom">
<div className="canvas-grid-split-custom-head">
<span>自定义宫格</span>
<span className="canvas-grid-split-custom-size">{hoverCols} × {hoverRows}</span>
</div>
<div
className="canvas-grid-split-board"
style={{ gridTemplateColumns: `repeat(${CANVAS_GRID_SPLIT_MAX}, minmax(0, 1fr))` }}
onPointerLeave={() => {
setHoverRows(2);
setHoverCols(2);
}}
>
{Array.from({ length: CANVAS_GRID_SPLIT_MAX * CANVAS_GRID_SPLIT_MAX }, (_, index) => {
const row = Math.floor(index / CANVAS_GRID_SPLIT_MAX) + 1;
const col = (index % CANVAS_GRID_SPLIT_MAX) + 1;
const active = row <= hoverRows && col <= hoverCols;
return (
<button
key={`${row}-${col}`}
type="button"
className={`canvas-grid-split-cell${active ? " is-active" : ""}`}
aria-label={`${col} × ${row}`}
onMouseDown={(event) => event.preventDefault()}
onPointerEnter={() => {
setHoverRows(row);
setHoverCols(col);
}}
onClick={() => pick(row, col)}
/>
);
})}
</div>
</div>
) : null}
</div>
);
}