@arronqzy/react-view 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/pnpm.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
- package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
- package/.turbo/turbo-lint.log +4 -0
- package/dist/styles.css +2624 -0
- package/env.d.ts +11 -0
- package/operation-guide.md +275 -0
- package/package.json +43 -0
- package/postcss.config.cjs +7 -0
- package/readme.md +21 -0
- package/src/core/createAction.ts +5 -0
- package/src/core/createView.ts +28 -0
- package/src/core/instructions.tsx +57 -0
- package/src/core/nodePool.ts +127 -0
- package/src/core/signal.ts +130 -0
- package/src/core/template.tsx +25 -0
- package/src/index.ts +7 -0
- package/src/panel/ReactViewOnlinePreview.tsx +331 -0
- package/src/panel/ReactViewPanel.tsx +3221 -0
- package/src/panel/components/ElementsLayer.tsx +891 -0
- package/src/panel/components/MaterialSidebar.tsx +1142 -0
- package/src/panel/components/MoveableLayer.tsx +803 -0
- package/src/panel/components/PanelCanvas.tsx +660 -0
- package/src/panel/components/PanelConfigSidebar.tsx +3703 -0
- package/src/panel/components/PanelRulers.tsx +89 -0
- package/src/panel/components/SelectLayer.tsx +106 -0
- package/src/panel/components/ViewElementScopePanel.tsx +79 -0
- package/src/panel/components/WorkspaceConfigSidebar.tsx +158 -0
- package/src/panel/components/WorkspaceProjectNav.tsx +220 -0
- package/src/panel/components/WorkspaceStageSplit.tsx +179 -0
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +170 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +250 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +59 -0
- package/src/panel/constants/messages.ts +34 -0
- package/src/panel/constants/zIndex.ts +6 -0
- package/src/panel/hooks/usePanelElements.ts +1108 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +278 -0
- package/src/panel/index.tsx +3 -0
- package/src/panel/library/workspace-project-cache.ts +23 -0
- package/src/panel/library/workspace-project-db.ts +112 -0
- package/src/panel/library/workspace-project-sync.ts +41 -0
- package/src/panel/library/workspace-snapshot.ts +27 -0
- package/src/panel/scope/view-scope-store.ts +55 -0
- package/src/panel/types.ts +125 -0
- package/src/panel/utils/chartOptionBuilder.ts +316 -0
- package/src/panel/utils/gridPlacement.ts +189 -0
- package/src/panel/utils/mappingLayerOps.ts +142 -0
- package/src/panel/utils/panelElementDefaults.ts +161 -0
- package/src/panel/utils/panelElementNodes.ts +35 -0
- package/src/panel/utils/panelStateIO.ts +124 -0
- package/src/panel/utils/scope-autocomplete.ts +114 -0
- package/src/panel/utils/scope-field-labels.ts +45 -0
- package/src/panel/utils/scope-template-warnings.ts +135 -0
- package/src/panel/utils/scope-template.ts +75 -0
- package/src/panel/utils/updateElementDraft.ts +221 -0
- package/src/panel/viewportZoom.ts +26 -0
- package/src/tailwind.css +58 -0
- package/src/types/react-infinite-viewer.d.ts +39 -0
- package/tailwind.config.ts +7 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import Moveable from "react-moveable";
|
|
4
|
+
|
|
5
|
+
import type { PanelElement } from "../types";
|
|
6
|
+
import { uniformViewportZoom } from "../viewportZoom";
|
|
7
|
+
import { notifyPreviewLayoutChanged } from "../utils/panelStateIO";
|
|
8
|
+
|
|
9
|
+
function LockGlyph({ className = "h-3.5 w-3.5" }: { className?: string }) {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
fill="none"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
strokeWidth="1.9"
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
|
+
className={className}
|
|
19
|
+
aria-hidden="true"
|
|
20
|
+
>
|
|
21
|
+
<rect x="4.5" y="10.5" width="15" height="10" rx="2.5" />
|
|
22
|
+
<path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" />
|
|
23
|
+
<circle cx="12" cy="15.5" r="1.2" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type MoveableLayerProps = {
|
|
29
|
+
zoomX: number;
|
|
30
|
+
zoomY: number;
|
|
31
|
+
/** 带 scale 的画布根节点(data-panel-canvas),与节点同一坐标系 */
|
|
32
|
+
canvasContainer: HTMLElement | null;
|
|
33
|
+
/** 滚动视口,用于拖拽/框选指针坐标 */
|
|
34
|
+
dragContainer?: HTMLElement | null;
|
|
35
|
+
selectedTargets: HTMLElement[];
|
|
36
|
+
elementsById: Map<string, PanelElement>;
|
|
37
|
+
updateElement: (
|
|
38
|
+
id: string,
|
|
39
|
+
patch: Partial<PanelElement>,
|
|
40
|
+
options?: { batchId?: string; meta?: Record<string, unknown> }
|
|
41
|
+
) => void;
|
|
42
|
+
/** 画布平移/缩放等视口变化时递增或变化,用于触发控制框重算 */
|
|
43
|
+
refreshToken?: number | string;
|
|
44
|
+
/** InfiniteViewer 虚拟滚动位置(无原生 scroll 事件) */
|
|
45
|
+
viewportScroll?: { left: number; top: number };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function getGridSlotLayout(grid: PanelElement) {
|
|
49
|
+
const rows = Math.max(1, Math.floor(grid.gridRows ?? 2));
|
|
50
|
+
const cols = Math.max(1, Math.floor(grid.gridCols ?? 3));
|
|
51
|
+
const gap = Math.max(0, grid.gridGap ?? 8);
|
|
52
|
+
const padding = Math.max(0, grid.gridPadding ?? 10);
|
|
53
|
+
const innerWidth = Math.max(1, grid.width - padding * 2);
|
|
54
|
+
const innerHeight = Math.max(1, grid.height - padding * 2);
|
|
55
|
+
const cellWidth = Math.max(1, (innerWidth - gap * (cols - 1)) / cols);
|
|
56
|
+
const cellHeight = Math.max(1, (innerHeight - gap * (rows - 1)) / rows);
|
|
57
|
+
const slots: Array<{ index: number; row: number; col: number; centerX: number; centerY: number; x: number; y: number }> = [];
|
|
58
|
+
for (let r = 0; r < rows; r++) {
|
|
59
|
+
for (let c = 0; c < cols; c++) {
|
|
60
|
+
const x = grid.x + padding + c * (cellWidth + gap);
|
|
61
|
+
const y = grid.y + padding + r * (cellHeight + gap);
|
|
62
|
+
slots.push({
|
|
63
|
+
index: r * cols + c,
|
|
64
|
+
row: r,
|
|
65
|
+
col: c,
|
|
66
|
+
x,
|
|
67
|
+
y,
|
|
68
|
+
centerX: x + cellWidth / 2,
|
|
69
|
+
centerY: y + cellHeight / 2,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { slots, cellWidth, cellHeight, rows, cols, gap };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inferSpanBySize(size: number, cellSize: number, gap: number, maxSpan: number) {
|
|
77
|
+
const safeMax = Math.max(1, Math.floor(maxSpan));
|
|
78
|
+
const unit = Math.max(1, cellSize + gap);
|
|
79
|
+
const ratio = (Math.max(1, size) + gap) / unit;
|
|
80
|
+
const whole = Math.floor(ratio);
|
|
81
|
+
const fraction = ratio - whole;
|
|
82
|
+
const promoted = fraction >= 0.72 ? whole + 1 : Math.max(1, whole);
|
|
83
|
+
return Math.max(1, Math.min(safeMax, promoted));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getGridSpanCells(
|
|
87
|
+
rows: number,
|
|
88
|
+
cols: number,
|
|
89
|
+
slotIndex: number,
|
|
90
|
+
colSpan: number,
|
|
91
|
+
rowSpan: number
|
|
92
|
+
) {
|
|
93
|
+
const total = rows * cols;
|
|
94
|
+
const safeIndex = Math.max(0, Math.min(total - 1, Math.floor(slotIndex || 0)));
|
|
95
|
+
const startRow = Math.floor(safeIndex / cols);
|
|
96
|
+
const startCol = safeIndex % cols;
|
|
97
|
+
const safeColSpan = Math.max(1, Math.min(cols - startCol, Math.floor(colSpan || 1)));
|
|
98
|
+
const safeRowSpan = Math.max(1, Math.min(rows - startRow, Math.floor(rowSpan || 1)));
|
|
99
|
+
const indices: number[] = [];
|
|
100
|
+
for (let r = startRow; r < startRow + safeRowSpan; r++) {
|
|
101
|
+
for (let c = startCol; c < startCol + safeColSpan; c++) {
|
|
102
|
+
indices.push(r * cols + c);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
safeIndex,
|
|
107
|
+
startRow,
|
|
108
|
+
startCol,
|
|
109
|
+
colSpan: safeColSpan,
|
|
110
|
+
rowSpan: safeRowSpan,
|
|
111
|
+
indices,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getOccupiedSlotSet(
|
|
116
|
+
elementsById: Map<string, PanelElement>,
|
|
117
|
+
gridId: string,
|
|
118
|
+
selfId: string,
|
|
119
|
+
layerId?: string
|
|
120
|
+
) {
|
|
121
|
+
const occupied = new Set<number>();
|
|
122
|
+
const grid = elementsById.get(gridId);
|
|
123
|
+
if (!grid || grid.materialType !== "grid") return occupied;
|
|
124
|
+
const { rows, cols } = getGridSlotLayout(grid);
|
|
125
|
+
for (const el of elementsById.values()) {
|
|
126
|
+
if (el.id === selfId) continue;
|
|
127
|
+
if (layerId && el.layerId !== layerId) continue;
|
|
128
|
+
if (el.parentGridId !== gridId) continue;
|
|
129
|
+
if (el.gridSlotIndex === undefined) continue;
|
|
130
|
+
const span = getGridSpanCells(
|
|
131
|
+
rows,
|
|
132
|
+
cols,
|
|
133
|
+
el.gridSlotIndex,
|
|
134
|
+
el.gridColSpan ?? 1,
|
|
135
|
+
el.gridRowSpan ?? 1
|
|
136
|
+
);
|
|
137
|
+
span.indices.forEach((idx) => occupied.add(idx));
|
|
138
|
+
}
|
|
139
|
+
return occupied;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isDescendantGrid(
|
|
143
|
+
elementsById: Map<string, PanelElement>,
|
|
144
|
+
possibleDescendantGridId: string,
|
|
145
|
+
ancestorGridId: string
|
|
146
|
+
) {
|
|
147
|
+
let current = elementsById.get(possibleDescendantGridId);
|
|
148
|
+
const visited = new Set<string>();
|
|
149
|
+
while (current?.parentGridId) {
|
|
150
|
+
if (visited.has(current.id)) return false;
|
|
151
|
+
visited.add(current.id);
|
|
152
|
+
if (current.parentGridId === ancestorGridId) return true;
|
|
153
|
+
current = elementsById.get(current.parentGridId);
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function MoveableLayer({
|
|
159
|
+
zoomX,
|
|
160
|
+
zoomY,
|
|
161
|
+
canvasContainer,
|
|
162
|
+
dragContainer,
|
|
163
|
+
selectedTargets,
|
|
164
|
+
elementsById,
|
|
165
|
+
updateElement,
|
|
166
|
+
refreshToken,
|
|
167
|
+
viewportScroll,
|
|
168
|
+
}: MoveableLayerProps) {
|
|
169
|
+
const moveableRef = useRef<any>(null);
|
|
170
|
+
const targets = useMemo(() => {
|
|
171
|
+
const validTargets = selectedTargets.filter(
|
|
172
|
+
(target) =>
|
|
173
|
+
!!target &&
|
|
174
|
+
target.isConnected &&
|
|
175
|
+
!!target.closest("[data-panel-canvas]")
|
|
176
|
+
);
|
|
177
|
+
return validTargets.length ? validTargets : null;
|
|
178
|
+
}, [selectedTargets]);
|
|
179
|
+
|
|
180
|
+
const getId = useCallback((target: HTMLElement) => {
|
|
181
|
+
const el = target.closest<HTMLElement>(".rv-selectable");
|
|
182
|
+
return el?.dataset.elementId ?? null;
|
|
183
|
+
}, []);
|
|
184
|
+
const selectedIds = useMemo(
|
|
185
|
+
() =>
|
|
186
|
+
(targets ?? [])
|
|
187
|
+
.map((target) => getId(target))
|
|
188
|
+
.filter((id): id is string => !!id),
|
|
189
|
+
[getId, targets]
|
|
190
|
+
);
|
|
191
|
+
const lockedSelected = useMemo(
|
|
192
|
+
() => selectedIds.filter((id) => elementsById.get(id)?.locked),
|
|
193
|
+
[elementsById, selectedIds]
|
|
194
|
+
);
|
|
195
|
+
const hasLockedSelected = lockedSelected.length > 0;
|
|
196
|
+
const movableTargets = useMemo(() => {
|
|
197
|
+
if (!targets) return null;
|
|
198
|
+
const unlocked = targets.filter((target) => {
|
|
199
|
+
const id = getId(target);
|
|
200
|
+
if (!id) return false;
|
|
201
|
+
return !elementsById.get(id)?.locked;
|
|
202
|
+
});
|
|
203
|
+
return unlocked.length ? unlocked : null;
|
|
204
|
+
}, [elementsById, getId, targets]);
|
|
205
|
+
const movableIds = useMemo(
|
|
206
|
+
() =>
|
|
207
|
+
(movableTargets ?? [])
|
|
208
|
+
.map((target) => getId(target))
|
|
209
|
+
.filter((id): id is string => !!id),
|
|
210
|
+
[getId, movableTargets]
|
|
211
|
+
);
|
|
212
|
+
const moveableRenderKey = useMemo(() => movableIds.join(","), [movableIds]);
|
|
213
|
+
const canvasScale = useMemo(
|
|
214
|
+
() => uniformViewportZoom({ x: zoomX, y: zoomY }),
|
|
215
|
+
[zoomX, zoomY]
|
|
216
|
+
);
|
|
217
|
+
/** 控制框在 scale 画布内渲染,用倒数保持屏幕像素尺寸 */
|
|
218
|
+
const moveableControlZoom = useMemo(
|
|
219
|
+
() => 1 / Math.max(0.0001, canvasScale),
|
|
220
|
+
[canvasScale]
|
|
221
|
+
);
|
|
222
|
+
const pointerRoot = dragContainer ?? canvasContainer;
|
|
223
|
+
const resolveSingleEventId = useCallback(
|
|
224
|
+
(target: HTMLElement | undefined | null) => {
|
|
225
|
+
const id = target ? getId(target) : null;
|
|
226
|
+
if (id) return id;
|
|
227
|
+
// 过滤锁定后只剩一个可移动节点时,Moveable 某些事件 target 可能不是节点本身
|
|
228
|
+
if (movableIds.length === 1) return movableIds[0] ?? null;
|
|
229
|
+
return null;
|
|
230
|
+
},
|
|
231
|
+
[getId, movableIds]
|
|
232
|
+
);
|
|
233
|
+
const toCanvasDeltaX = useCallback(
|
|
234
|
+
(value: number) => value / Math.max(0.0001, canvasScale),
|
|
235
|
+
[canvasScale]
|
|
236
|
+
);
|
|
237
|
+
const toCanvasDeltaY = useCallback(
|
|
238
|
+
(value: number) => value / Math.max(0.0001, canvasScale),
|
|
239
|
+
[canvasScale]
|
|
240
|
+
);
|
|
241
|
+
const readClientPoint = useCallback((eventLike: any) => {
|
|
242
|
+
const x =
|
|
243
|
+
typeof eventLike?.clientX === "number"
|
|
244
|
+
? eventLike.clientX
|
|
245
|
+
: typeof eventLike?.inputEvent?.clientX === "number"
|
|
246
|
+
? eventLike.inputEvent.clientX
|
|
247
|
+
: null;
|
|
248
|
+
const y =
|
|
249
|
+
typeof eventLike?.clientY === "number"
|
|
250
|
+
? eventLike.clientY
|
|
251
|
+
: typeof eventLike?.inputEvent?.clientY === "number"
|
|
252
|
+
? eventLike.inputEvent.clientY
|
|
253
|
+
: null;
|
|
254
|
+
if (x === null || y === null) return null;
|
|
255
|
+
return { x, y };
|
|
256
|
+
}, []);
|
|
257
|
+
const readTargetCanvasSize = useCallback(
|
|
258
|
+
(target: HTMLElement | null | undefined, fallback: { width: number; height: number }) => {
|
|
259
|
+
const rect = target?.getBoundingClientRect?.();
|
|
260
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
261
|
+
return {
|
|
262
|
+
width: Math.max(1, toCanvasDeltaX(rect.width)),
|
|
263
|
+
height: Math.max(1, toCanvasDeltaY(rect.height)),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return { width: Math.max(1, fallback.width), height: Math.max(1, fallback.height) };
|
|
267
|
+
},
|
|
268
|
+
[toCanvasDeltaX, toCanvasDeltaY]
|
|
269
|
+
);
|
|
270
|
+
const [lockBadgeScreen, setLockBadgeScreen] = useState<{ x: number; y: number } | null>(
|
|
271
|
+
null
|
|
272
|
+
);
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
const id = lockedSelected[0];
|
|
275
|
+
if (!id || !canvasContainer) {
|
|
276
|
+
setLockBadgeScreen(null);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const target = selectedTargets.find(
|
|
280
|
+
(node) => node.closest<HTMLElement>(".rv-selectable")?.dataset.elementId === id
|
|
281
|
+
);
|
|
282
|
+
if (!target) {
|
|
283
|
+
setLockBadgeScreen(null);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const sync = () => {
|
|
287
|
+
const rect = target.getBoundingClientRect();
|
|
288
|
+
const rootRect = canvasContainer.getBoundingClientRect();
|
|
289
|
+
setLockBadgeScreen({
|
|
290
|
+
x: rect.left - rootRect.left,
|
|
291
|
+
y: rect.top - rootRect.top,
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
sync();
|
|
295
|
+
const ro = new ResizeObserver(sync);
|
|
296
|
+
ro.observe(target);
|
|
297
|
+
ro.observe(canvasContainer);
|
|
298
|
+
canvasContainer.addEventListener("scroll", sync, { passive: true });
|
|
299
|
+
return () => {
|
|
300
|
+
ro.disconnect();
|
|
301
|
+
canvasContainer.removeEventListener("scroll", sync);
|
|
302
|
+
};
|
|
303
|
+
}, [canvasContainer, lockedSelected, selectedTargets, viewportScroll, zoomX, zoomY]);
|
|
304
|
+
|
|
305
|
+
const updateRectNextFrame = useCallback(() => {
|
|
306
|
+
// InfiniteViewer 用 transform 平移,需等视口与节点布局提交后再 updateRect
|
|
307
|
+
requestAnimationFrame(() => {
|
|
308
|
+
requestAnimationFrame(() => {
|
|
309
|
+
moveableRef.current?.updateRect?.();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}, []);
|
|
313
|
+
const getGridDirectChildren = useCallback(
|
|
314
|
+
(gridId: string) => {
|
|
315
|
+
const result: PanelElement[] = [];
|
|
316
|
+
for (const el of elementsById.values()) {
|
|
317
|
+
if (el.parentGridId !== gridId) continue;
|
|
318
|
+
result.push(el);
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
},
|
|
322
|
+
[elementsById]
|
|
323
|
+
);
|
|
324
|
+
const getSnapPatch = useCallback(
|
|
325
|
+
(movingId: string, x: number, y: number, width: number, height: number): Partial<PanelElement> => {
|
|
326
|
+
const moving = elementsById.get(movingId);
|
|
327
|
+
if (!moving) return {};
|
|
328
|
+
const centerX = x + width / 2;
|
|
329
|
+
const centerY = y + height / 2;
|
|
330
|
+
let nearest:
|
|
331
|
+
| {
|
|
332
|
+
gridId: string;
|
|
333
|
+
slotIndex: number;
|
|
334
|
+
slotX: number;
|
|
335
|
+
slotY: number;
|
|
336
|
+
width: number;
|
|
337
|
+
height: number;
|
|
338
|
+
colSpan: number;
|
|
339
|
+
rowSpan: number;
|
|
340
|
+
distance: number;
|
|
341
|
+
}
|
|
342
|
+
| null = null;
|
|
343
|
+
for (const [id, el] of elementsById.entries()) {
|
|
344
|
+
if (id === movingId || el.materialType !== "grid") continue;
|
|
345
|
+
if (el.layerId !== moving.layerId) continue;
|
|
346
|
+
if (
|
|
347
|
+
moving.materialType === "grid" &&
|
|
348
|
+
isDescendantGrid(elementsById, id, movingId)
|
|
349
|
+
) {
|
|
350
|
+
// 防止把父 grid 吸附到自己的子孙 grid,形成循环父子关系
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const { slots, cellWidth, cellHeight, rows, cols, gap } = getGridSlotLayout(el);
|
|
354
|
+
const occupiedSlots = getOccupiedSlotSet(elementsById, el.id, movingId, moving.layerId);
|
|
355
|
+
const threshold = Math.max(8, el.gridSnapThreshold ?? 36);
|
|
356
|
+
// 跨格优先采用显式 span,同时根据当前节点尺寸推断期望 span,避免“视觉占两格但逻辑仍是一格”
|
|
357
|
+
const inferredColSpan = inferSpanBySize(width, cellWidth, gap, cols);
|
|
358
|
+
const inferredRowSpan = inferSpanBySize(height, cellHeight, gap, rows);
|
|
359
|
+
// span 应该由当前尺寸主导,允许从大格回退到小格
|
|
360
|
+
const movingColSpan = Math.max(1, inferredColSpan);
|
|
361
|
+
const movingRowSpan = Math.max(1, inferredRowSpan);
|
|
362
|
+
const insideGridBounds =
|
|
363
|
+
centerX >= el.x &&
|
|
364
|
+
centerX <= el.x + Math.max(1, el.width) &&
|
|
365
|
+
centerY >= el.y &&
|
|
366
|
+
centerY <= el.y + Math.max(1, el.height);
|
|
367
|
+
for (const slot of slots) {
|
|
368
|
+
const spanCells = getGridSpanCells(
|
|
369
|
+
rows,
|
|
370
|
+
cols,
|
|
371
|
+
slot.index,
|
|
372
|
+
movingColSpan,
|
|
373
|
+
movingRowSpan
|
|
374
|
+
);
|
|
375
|
+
// 只接受能完整容纳目标 span 的槽位,避免“想占满但被起点裁剪”。
|
|
376
|
+
if (spanCells.colSpan !== movingColSpan || spanCells.rowSpan !== movingRowSpan) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const blocked = spanCells.indices.some((idx) => occupiedSlots.has(idx));
|
|
380
|
+
if (blocked) continue;
|
|
381
|
+
const distance = Math.hypot(slot.centerX - centerX, slot.centerY - centerY);
|
|
382
|
+
// 当节点中心已经进入网格区域时,允许吸附到任意空槽位(不受阈值限制)
|
|
383
|
+
if (!insideGridBounds && distance > threshold) continue;
|
|
384
|
+
if (!nearest || distance < nearest.distance) {
|
|
385
|
+
const spanWidth = spanCells.colSpan * cellWidth + (spanCells.colSpan - 1) * gap;
|
|
386
|
+
const spanHeight = spanCells.rowSpan * cellHeight + (spanCells.rowSpan - 1) * gap;
|
|
387
|
+
nearest = {
|
|
388
|
+
gridId: el.id,
|
|
389
|
+
slotIndex: spanCells.safeIndex,
|
|
390
|
+
slotX: slot.x,
|
|
391
|
+
slotY: slot.y,
|
|
392
|
+
width: spanWidth,
|
|
393
|
+
height: spanHeight,
|
|
394
|
+
colSpan: spanCells.colSpan,
|
|
395
|
+
rowSpan: spanCells.rowSpan,
|
|
396
|
+
distance,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!nearest) {
|
|
402
|
+
if (moving.parentGridId || moving.gridSlotIndex !== undefined) {
|
|
403
|
+
return { parentGridId: undefined, gridSlotIndex: undefined };
|
|
404
|
+
}
|
|
405
|
+
return {};
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
x: nearest.slotX,
|
|
409
|
+
y: nearest.slotY,
|
|
410
|
+
parentGridId: nearest.gridId,
|
|
411
|
+
gridSlotIndex: nearest.slotIndex,
|
|
412
|
+
gridColSpan: nearest.colSpan,
|
|
413
|
+
gridRowSpan: nearest.rowSpan,
|
|
414
|
+
width: nearest.width,
|
|
415
|
+
height: nearest.height,
|
|
416
|
+
};
|
|
417
|
+
},
|
|
418
|
+
[elementsById]
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
if (!targets) return;
|
|
423
|
+
updateRectNextFrame();
|
|
424
|
+
}, [targets, updateRectNextFrame]);
|
|
425
|
+
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (!targets) return;
|
|
428
|
+
updateRectNextFrame();
|
|
429
|
+
}, [refreshToken, targets, updateRectNextFrame]);
|
|
430
|
+
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
if (!targets) return;
|
|
433
|
+
updateRectNextFrame();
|
|
434
|
+
}, [targets, updateRectNextFrame, viewportScroll?.left, viewportScroll?.top]);
|
|
435
|
+
|
|
436
|
+
useEffect(() => {
|
|
437
|
+
if (!targets) return;
|
|
438
|
+
updateRectNextFrame();
|
|
439
|
+
}, [targets, updateRectNextFrame, zoomX, zoomY]);
|
|
440
|
+
|
|
441
|
+
if (!canvasContainer) return null;
|
|
442
|
+
|
|
443
|
+
const lockBadgeOnly =
|
|
444
|
+
lockBadgeScreen && hasLockedSelected && !movableTargets ? (
|
|
445
|
+
<div
|
|
446
|
+
className="pointer-events-none absolute z-[80] inline-flex select-none items-center gap-1 rounded border border-border bg-background/95 px-1.5 py-0.5 text-[11px] text-foreground shadow-sm"
|
|
447
|
+
style={{ left: lockBadgeScreen.x + 6, top: lockBadgeScreen.y - 22 }}
|
|
448
|
+
title="节点已锁定"
|
|
449
|
+
>
|
|
450
|
+
<LockGlyph />
|
|
451
|
+
<span>已锁定</span>
|
|
452
|
+
</div>
|
|
453
|
+
) : null;
|
|
454
|
+
|
|
455
|
+
if (!targets) {
|
|
456
|
+
return lockBadgeOnly ? createPortal(lockBadgeOnly, canvasContainer) : null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!movableTargets) {
|
|
460
|
+
return lockBadgeOnly ? createPortal(lockBadgeOnly, canvasContainer) : null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const layer = (
|
|
464
|
+
<>
|
|
465
|
+
<Moveable
|
|
466
|
+
key={moveableRenderKey}
|
|
467
|
+
ref={moveableRef}
|
|
468
|
+
// Moveable 支持单个 HTMLElement 或 HTMLElement[]
|
|
469
|
+
target={movableTargets as unknown as HTMLElement[]}
|
|
470
|
+
rootContainer={canvasContainer}
|
|
471
|
+
container={canvasContainer}
|
|
472
|
+
dragContainer={pointerRoot ?? canvasContainer}
|
|
473
|
+
className="rv-moveable-layer"
|
|
474
|
+
zoom={moveableControlZoom}
|
|
475
|
+
// 允许在控制框内部区域拖动(不必必须点到某个节点本体)
|
|
476
|
+
dragArea
|
|
477
|
+
draggable
|
|
478
|
+
resizable
|
|
479
|
+
rotatable
|
|
480
|
+
origin={false}
|
|
481
|
+
throttleDrag={0}
|
|
482
|
+
throttleResize={0}
|
|
483
|
+
throttleRotate={0}
|
|
484
|
+
onDragStart={(e: any) => {
|
|
485
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
486
|
+
if (!id) return;
|
|
487
|
+
const data = elementsById.get(id);
|
|
488
|
+
if (!data) return;
|
|
489
|
+
e.set([0, 0]);
|
|
490
|
+
e.datas.__startX = data.x;
|
|
491
|
+
e.datas.__startY = data.y;
|
|
492
|
+
const point = readClientPoint(e);
|
|
493
|
+
e.datas.__startClientX = point?.x ?? null;
|
|
494
|
+
e.datas.__startClientY = point?.y ?? null;
|
|
495
|
+
}}
|
|
496
|
+
onDrag={(e: any) => {
|
|
497
|
+
if (!e?.target?.style) return;
|
|
498
|
+
if (e.datas.__startX === undefined || e.datas.__startY === undefined) return;
|
|
499
|
+
const sx = e.datas.__startX ?? 0;
|
|
500
|
+
const sy = e.datas.__startY ?? 0;
|
|
501
|
+
const point = readClientPoint(e);
|
|
502
|
+
const hasClient =
|
|
503
|
+
typeof e.datas.__startClientX === "number" &&
|
|
504
|
+
typeof e.datas.__startClientY === "number" &&
|
|
505
|
+
!!point;
|
|
506
|
+
const tx = hasClient
|
|
507
|
+
? toCanvasDeltaX(point.x - e.datas.__startClientX)
|
|
508
|
+
: toCanvasDeltaX(e.beforeTranslate?.[0] ?? 0);
|
|
509
|
+
const ty = hasClient
|
|
510
|
+
? toCanvasDeltaY(point.y - e.datas.__startClientY)
|
|
511
|
+
: toCanvasDeltaY(e.beforeTranslate?.[1] ?? 0);
|
|
512
|
+
e.target.style.left = `${sx + tx}px`;
|
|
513
|
+
e.target.style.top = `${sy + ty}px`;
|
|
514
|
+
}}
|
|
515
|
+
onDragGroupStart={(e: any) => {
|
|
516
|
+
e.events.forEach((ev: any) => {
|
|
517
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
518
|
+
if (!id) return;
|
|
519
|
+
const data = elementsById.get(id);
|
|
520
|
+
if (!data) return;
|
|
521
|
+
ev.set([0, 0]);
|
|
522
|
+
ev.datas.__startX = data.x;
|
|
523
|
+
ev.datas.__startY = data.y;
|
|
524
|
+
const point = readClientPoint(ev) ?? readClientPoint(e);
|
|
525
|
+
ev.datas.__startClientX = point?.x ?? null;
|
|
526
|
+
ev.datas.__startClientY = point?.y ?? null;
|
|
527
|
+
});
|
|
528
|
+
}}
|
|
529
|
+
onDragGroup={(e: any) => {
|
|
530
|
+
e.events.forEach((ev: any) => {
|
|
531
|
+
if (!ev?.target?.style) return;
|
|
532
|
+
const sx = ev.datas.__startX ?? 0;
|
|
533
|
+
const sy = ev.datas.__startY ?? 0;
|
|
534
|
+
const point = readClientPoint(ev) ?? readClientPoint(e);
|
|
535
|
+
const hasClient =
|
|
536
|
+
typeof ev.datas.__startClientX === "number" &&
|
|
537
|
+
typeof ev.datas.__startClientY === "number" &&
|
|
538
|
+
!!point;
|
|
539
|
+
const tx = hasClient
|
|
540
|
+
? toCanvasDeltaX(point.x - ev.datas.__startClientX)
|
|
541
|
+
: toCanvasDeltaX(ev.beforeTranslate?.[0] ?? 0);
|
|
542
|
+
const ty = hasClient
|
|
543
|
+
? toCanvasDeltaY(point.y - ev.datas.__startClientY)
|
|
544
|
+
: toCanvasDeltaY(ev.beforeTranslate?.[1] ?? 0);
|
|
545
|
+
ev.target.style.left = `${sx + tx}px`;
|
|
546
|
+
ev.target.style.top = `${sy + ty}px`;
|
|
547
|
+
});
|
|
548
|
+
}}
|
|
549
|
+
onResizeStart={(e: any) => {
|
|
550
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
551
|
+
if (!id) return;
|
|
552
|
+
const data = elementsById.get(id);
|
|
553
|
+
if (!data) return;
|
|
554
|
+
e.setOrigin(["%", "%"]);
|
|
555
|
+
e.dragStart?.set([0, 0]);
|
|
556
|
+
e.datas.__startX = data.x;
|
|
557
|
+
e.datas.__startY = data.y;
|
|
558
|
+
e.datas.__startClientX = e.inputEvent?.clientX ?? null;
|
|
559
|
+
e.datas.__startClientY = e.inputEvent?.clientY ?? null;
|
|
560
|
+
}}
|
|
561
|
+
onResize={(e: any) => {
|
|
562
|
+
if (!e?.target?.style) return;
|
|
563
|
+
if (e.datas.__startX === undefined || e.datas.__startY === undefined) return;
|
|
564
|
+
e.target.style.width = `${e.width}px`;
|
|
565
|
+
e.target.style.height = `${e.height}px`;
|
|
566
|
+
const sx = e.datas.__startX ?? 0;
|
|
567
|
+
const sy = e.datas.__startY ?? 0;
|
|
568
|
+
// resize 场景下位移应以 moveable 的 drag 偏移为准,避免 client 坐标换算导致漂移
|
|
569
|
+
const tx = toCanvasDeltaX(e.drag.beforeTranslate?.[0] ?? 0);
|
|
570
|
+
const ty = toCanvasDeltaY(e.drag.beforeTranslate?.[1] ?? 0);
|
|
571
|
+
e.target.style.left = `${sx + tx}px`;
|
|
572
|
+
e.target.style.top = `${sy + ty}px`;
|
|
573
|
+
notifyPreviewLayoutChanged();
|
|
574
|
+
}}
|
|
575
|
+
onResizeGroupStart={(e: any) => {
|
|
576
|
+
e.events.forEach((ev: any) => {
|
|
577
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
578
|
+
if (!id) return;
|
|
579
|
+
const data = elementsById.get(id);
|
|
580
|
+
if (!data) return;
|
|
581
|
+
ev.setOrigin(["%", "%"]);
|
|
582
|
+
ev.dragStart?.set([0, 0]);
|
|
583
|
+
ev.datas.__startX = data.x;
|
|
584
|
+
ev.datas.__startY = data.y;
|
|
585
|
+
const input = ev.inputEvent ?? e.inputEvent;
|
|
586
|
+
ev.datas.__startClientX = input?.clientX ?? null;
|
|
587
|
+
ev.datas.__startClientY = input?.clientY ?? null;
|
|
588
|
+
});
|
|
589
|
+
}}
|
|
590
|
+
onResizeGroup={(e: any) => {
|
|
591
|
+
e.events.forEach((ev: any) => {
|
|
592
|
+
if (!ev?.target?.style) return;
|
|
593
|
+
ev.target.style.width = `${ev.width}px`;
|
|
594
|
+
ev.target.style.height = `${ev.height}px`;
|
|
595
|
+
const sx = ev.datas.__startX ?? 0;
|
|
596
|
+
const sy = ev.datas.__startY ?? 0;
|
|
597
|
+
const tx = toCanvasDeltaX(ev.drag.beforeTranslate?.[0] ?? 0);
|
|
598
|
+
const ty = toCanvasDeltaY(ev.drag.beforeTranslate?.[1] ?? 0);
|
|
599
|
+
ev.target.style.left = `${sx + tx}px`;
|
|
600
|
+
ev.target.style.top = `${sy + ty}px`;
|
|
601
|
+
});
|
|
602
|
+
notifyPreviewLayoutChanged();
|
|
603
|
+
}}
|
|
604
|
+
onRotateStart={(e: any) => {
|
|
605
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
606
|
+
if (!id) return;
|
|
607
|
+
const data = elementsById.get(id);
|
|
608
|
+
if (!data) return;
|
|
609
|
+
e.set(data.rotate ?? 0);
|
|
610
|
+
}}
|
|
611
|
+
onRotate={(e: any) => {
|
|
612
|
+
if (!e?.target?.style) return;
|
|
613
|
+
e.target.style.transform = `rotate(${e.beforeRotate}deg)`;
|
|
614
|
+
}}
|
|
615
|
+
onRotateGroup={(e: any) => {
|
|
616
|
+
e.events.forEach((ev: any) => {
|
|
617
|
+
if (!ev?.target?.style) return;
|
|
618
|
+
ev.target.style.transform = `rotate(${ev.beforeRotate}deg)`;
|
|
619
|
+
});
|
|
620
|
+
}}
|
|
621
|
+
onDragEnd={(e: any) => {
|
|
622
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
623
|
+
if (!id) return;
|
|
624
|
+
const data = elementsById.get(id);
|
|
625
|
+
if (!data) return;
|
|
626
|
+
const sx = e.datas.__startX ?? data.x;
|
|
627
|
+
const sy = e.datas.__startY ?? data.y;
|
|
628
|
+
const point = readClientPoint(e);
|
|
629
|
+
const hasClient =
|
|
630
|
+
typeof e.datas.__startClientX === "number" &&
|
|
631
|
+
typeof e.datas.__startClientY === "number" &&
|
|
632
|
+
!!point;
|
|
633
|
+
const tx = hasClient
|
|
634
|
+
? toCanvasDeltaX(point.x - e.datas.__startClientX)
|
|
635
|
+
: toCanvasDeltaX(e.lastEvent?.beforeTranslate?.[0] ?? 0);
|
|
636
|
+
const ty = hasClient
|
|
637
|
+
? toCanvasDeltaY(point.y - e.datas.__startClientY)
|
|
638
|
+
: toCanvasDeltaY(e.lastEvent?.beforeTranslate?.[1] ?? 0);
|
|
639
|
+
const nextX = sx + tx;
|
|
640
|
+
const nextY = sy + ty;
|
|
641
|
+
const size = readTargetCanvasSize(e.target as HTMLElement | null, {
|
|
642
|
+
width: data.width,
|
|
643
|
+
height: data.height,
|
|
644
|
+
});
|
|
645
|
+
const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
646
|
+
updateElement(id, { x: nextX, y: nextY, ...patch });
|
|
647
|
+
if (data.materialType === "grid") {
|
|
648
|
+
const dx = nextX - data.x;
|
|
649
|
+
const dy = nextY - data.y;
|
|
650
|
+
if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
|
|
651
|
+
const children = getGridDirectChildren(id);
|
|
652
|
+
const batchId = `move-grid-children-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
653
|
+
children.forEach((child) => {
|
|
654
|
+
updateElement(
|
|
655
|
+
child.id,
|
|
656
|
+
{ x: child.x + dx, y: child.y + dy },
|
|
657
|
+
{ batchId, meta: { type: "node.group-drag" } }
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
updateRectNextFrame();
|
|
663
|
+
}}
|
|
664
|
+
onDragGroupEnd={(e: any) => {
|
|
665
|
+
const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
666
|
+
const selectedSet = new Set(
|
|
667
|
+
e.events
|
|
668
|
+
.map((ev: any) => (ev.target ? getId(ev.target) : null))
|
|
669
|
+
.filter((id: string | null): id is string => !!id)
|
|
670
|
+
);
|
|
671
|
+
const gridDeltaMap = new Map<string, { dx: number; dy: number }>();
|
|
672
|
+
e.events.forEach((ev: any) => {
|
|
673
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
674
|
+
if (!id) return;
|
|
675
|
+
const data = elementsById.get(id);
|
|
676
|
+
if (!data) return;
|
|
677
|
+
const sx = ev.datas.__startX ?? data.x;
|
|
678
|
+
const sy = ev.datas.__startY ?? data.y;
|
|
679
|
+
const point = readClientPoint(ev) ?? readClientPoint(e);
|
|
680
|
+
const hasClient =
|
|
681
|
+
typeof ev.datas.__startClientX === "number" &&
|
|
682
|
+
typeof ev.datas.__startClientY === "number" &&
|
|
683
|
+
!!point;
|
|
684
|
+
const tx = hasClient
|
|
685
|
+
? toCanvasDeltaX(point.x - ev.datas.__startClientX)
|
|
686
|
+
: toCanvasDeltaX(ev.lastEvent?.beforeTranslate?.[0] ?? 0);
|
|
687
|
+
const ty = hasClient
|
|
688
|
+
? toCanvasDeltaY(point.y - ev.datas.__startClientY)
|
|
689
|
+
: toCanvasDeltaY(ev.lastEvent?.beforeTranslate?.[1] ?? 0);
|
|
690
|
+
const nextX = sx + tx;
|
|
691
|
+
const nextY = sy + ty;
|
|
692
|
+
const size = readTargetCanvasSize(ev.target as HTMLElement | null, {
|
|
693
|
+
width: data.width,
|
|
694
|
+
height: data.height,
|
|
695
|
+
});
|
|
696
|
+
const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
697
|
+
updateElement(
|
|
698
|
+
id,
|
|
699
|
+
{ x: nextX, y: nextY, ...patch },
|
|
700
|
+
{ batchId, meta: { type: "node.group-drag" } }
|
|
701
|
+
);
|
|
702
|
+
if (data.materialType === "grid") {
|
|
703
|
+
const dx = nextX - data.x;
|
|
704
|
+
const dy = nextY - data.y;
|
|
705
|
+
if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
|
|
706
|
+
gridDeltaMap.set(id, { dx, dy });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
for (const [gridId, delta] of gridDeltaMap.entries()) {
|
|
711
|
+
const children = getGridDirectChildren(gridId);
|
|
712
|
+
children.forEach((child) => {
|
|
713
|
+
if (selectedSet.has(child.id)) return;
|
|
714
|
+
updateElement(
|
|
715
|
+
child.id,
|
|
716
|
+
{ x: child.x + delta.dx, y: child.y + delta.dy },
|
|
717
|
+
{ batchId, meta: { type: "node.group-drag" } }
|
|
718
|
+
);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
updateRectNextFrame();
|
|
722
|
+
}}
|
|
723
|
+
onResizeEnd={(e: any) => {
|
|
724
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
725
|
+
if (!id) return;
|
|
726
|
+
const data = elementsById.get(id);
|
|
727
|
+
if (!data) return;
|
|
728
|
+
const width = e.lastEvent?.width ?? data.width;
|
|
729
|
+
const height = e.lastEvent?.height ?? data.height;
|
|
730
|
+
const tx = toCanvasDeltaX(e.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
|
|
731
|
+
const ty = toCanvasDeltaY(e.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
|
|
732
|
+
const sx = e.datas.__startX ?? data.x;
|
|
733
|
+
const sy = e.datas.__startY ?? data.y;
|
|
734
|
+
const nextX = sx + tx;
|
|
735
|
+
const nextY = sy + ty;
|
|
736
|
+
const size = readTargetCanvasSize(e.target as HTMLElement | null, { width, height });
|
|
737
|
+
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
738
|
+
updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
|
|
739
|
+
updateRectNextFrame();
|
|
740
|
+
}}
|
|
741
|
+
onResizeGroupEnd={(e: any) => {
|
|
742
|
+
const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
743
|
+
e.events.forEach((ev: any) => {
|
|
744
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
745
|
+
if (!id) return;
|
|
746
|
+
const data = elementsById.get(id);
|
|
747
|
+
if (!data) return;
|
|
748
|
+
const width = ev.lastEvent?.width ?? data.width;
|
|
749
|
+
const height = ev.lastEvent?.height ?? data.height;
|
|
750
|
+
const tx = toCanvasDeltaX(ev.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
|
|
751
|
+
const ty = toCanvasDeltaY(ev.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
|
|
752
|
+
const sx = ev.datas.__startX ?? data.x;
|
|
753
|
+
const sy = ev.datas.__startY ?? data.y;
|
|
754
|
+
const nextX = sx + tx;
|
|
755
|
+
const nextY = sy + ty;
|
|
756
|
+
const size = readTargetCanvasSize(ev.target as HTMLElement | null, { width, height });
|
|
757
|
+
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
758
|
+
updateElement(
|
|
759
|
+
id,
|
|
760
|
+
{ width, height, x: nextX, y: nextY, ...snapPatch },
|
|
761
|
+
{ batchId, meta: { type: "node.group-resize" } }
|
|
762
|
+
);
|
|
763
|
+
});
|
|
764
|
+
updateRectNextFrame();
|
|
765
|
+
}}
|
|
766
|
+
onRotateEnd={(e: any) => {
|
|
767
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
768
|
+
if (!id) return;
|
|
769
|
+
const data = elementsById.get(id);
|
|
770
|
+
if (!data) return;
|
|
771
|
+
const rotate = e.lastEvent?.beforeRotate ?? data.rotate ?? 0;
|
|
772
|
+
updateElement(id, { rotate });
|
|
773
|
+
updateRectNextFrame();
|
|
774
|
+
}}
|
|
775
|
+
onRotateGroupEnd={(e: any) => {
|
|
776
|
+
const batchId = `rotate-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
777
|
+
e.events.forEach((ev: any) => {
|
|
778
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
779
|
+
if (!id) return;
|
|
780
|
+
const data = elementsById.get(id);
|
|
781
|
+
if (!data) return;
|
|
782
|
+
const rotate = ev.lastEvent?.beforeRotate ?? data.rotate ?? 0;
|
|
783
|
+
updateElement(id, { rotate }, { batchId, meta: { type: "node.group-rotate" } });
|
|
784
|
+
});
|
|
785
|
+
updateRectNextFrame();
|
|
786
|
+
}}
|
|
787
|
+
/>
|
|
788
|
+
{hasLockedSelected && lockBadgeScreen ? (
|
|
789
|
+
<div
|
|
790
|
+
className="pointer-events-none absolute z-[80] inline-flex select-none items-center gap-1 rounded border border-border bg-background/95 px-1.5 py-0.5 text-[11px] text-foreground shadow-sm"
|
|
791
|
+
style={{ left: lockBadgeScreen.x + 6, top: lockBadgeScreen.y - 22 }}
|
|
792
|
+
title="部分节点已锁定,已自动排除"
|
|
793
|
+
>
|
|
794
|
+
<LockGlyph />
|
|
795
|
+
<span>锁定节点已排除</span>
|
|
796
|
+
</div>
|
|
797
|
+
) : null}
|
|
798
|
+
</>
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
return createPortal(layer, canvasContainer);
|
|
802
|
+
}
|
|
803
|
+
|