@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.
Files changed (112) hide show
  1. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
  2. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
  3. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
  4. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
  5. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
  6. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
  7. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
  8. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
  9. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
  10. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
  11. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
  12. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
  13. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
  14. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
  15. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
  16. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
  17. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
  18. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
  19. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
  20. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
  21. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
  22. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
  23. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
  24. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
  25. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
  26. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
  27. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
  28. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
  29. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
  30. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
  31. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
  32. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
  33. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
  34. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
  35. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
  36. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
  37. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
  38. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
  39. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
  40. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
  41. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
  42. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
  43. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
  44. package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
  45. package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
  46. package/.pnpm-cache/metadata/registry.npmjs.org/pnpm.json +1 -0
  47. package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
  48. package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
  49. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
  50. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
  51. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
  52. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
  53. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
  54. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
  55. package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
  56. package/.turbo/turbo-lint.log +4 -0
  57. package/dist/styles.css +2624 -0
  58. package/env.d.ts +11 -0
  59. package/operation-guide.md +275 -0
  60. package/package.json +43 -0
  61. package/postcss.config.cjs +7 -0
  62. package/readme.md +21 -0
  63. package/src/core/createAction.ts +5 -0
  64. package/src/core/createView.ts +28 -0
  65. package/src/core/instructions.tsx +57 -0
  66. package/src/core/nodePool.ts +127 -0
  67. package/src/core/signal.ts +130 -0
  68. package/src/core/template.tsx +25 -0
  69. package/src/index.ts +7 -0
  70. package/src/panel/ReactViewOnlinePreview.tsx +331 -0
  71. package/src/panel/ReactViewPanel.tsx +3221 -0
  72. package/src/panel/components/ElementsLayer.tsx +891 -0
  73. package/src/panel/components/MaterialSidebar.tsx +1142 -0
  74. package/src/panel/components/MoveableLayer.tsx +803 -0
  75. package/src/panel/components/PanelCanvas.tsx +660 -0
  76. package/src/panel/components/PanelConfigSidebar.tsx +3703 -0
  77. package/src/panel/components/PanelRulers.tsx +89 -0
  78. package/src/panel/components/SelectLayer.tsx +106 -0
  79. package/src/panel/components/ViewElementScopePanel.tsx +79 -0
  80. package/src/panel/components/WorkspaceConfigSidebar.tsx +158 -0
  81. package/src/panel/components/WorkspaceProjectNav.tsx +220 -0
  82. package/src/panel/components/WorkspaceStageSplit.tsx +179 -0
  83. package/src/panel/components/scope-config/ScopeConfigContext.tsx +170 -0
  84. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +250 -0
  85. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +59 -0
  86. package/src/panel/constants/messages.ts +34 -0
  87. package/src/panel/constants/zIndex.ts +6 -0
  88. package/src/panel/hooks/usePanelElements.ts +1108 -0
  89. package/src/panel/hooks/useWorkspaceProjects.ts +278 -0
  90. package/src/panel/index.tsx +3 -0
  91. package/src/panel/library/workspace-project-cache.ts +23 -0
  92. package/src/panel/library/workspace-project-db.ts +112 -0
  93. package/src/panel/library/workspace-project-sync.ts +41 -0
  94. package/src/panel/library/workspace-snapshot.ts +27 -0
  95. package/src/panel/scope/view-scope-store.ts +55 -0
  96. package/src/panel/types.ts +125 -0
  97. package/src/panel/utils/chartOptionBuilder.ts +316 -0
  98. package/src/panel/utils/gridPlacement.ts +189 -0
  99. package/src/panel/utils/mappingLayerOps.ts +142 -0
  100. package/src/panel/utils/panelElementDefaults.ts +161 -0
  101. package/src/panel/utils/panelElementNodes.ts +35 -0
  102. package/src/panel/utils/panelStateIO.ts +124 -0
  103. package/src/panel/utils/scope-autocomplete.ts +114 -0
  104. package/src/panel/utils/scope-field-labels.ts +45 -0
  105. package/src/panel/utils/scope-template-warnings.ts +135 -0
  106. package/src/panel/utils/scope-template.ts +75 -0
  107. package/src/panel/utils/updateElementDraft.ts +221 -0
  108. package/src/panel/viewportZoom.ts +26 -0
  109. package/src/tailwind.css +58 -0
  110. package/src/types/react-infinite-viewer.d.ts +39 -0
  111. package/tailwind.config.ts +7 -0
  112. package/tsconfig.json +14 -0
@@ -0,0 +1,1108 @@
1
+ import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react";
2
+ import { store } from "../../../../rx-store/src/store";
3
+ import type { Node, State } from "../../../../rx-store/src/types";
4
+ import type {
5
+ PanelActionResult,
6
+ PanelElement,
7
+ PanelHistoryItem,
8
+ PanelLayer,
9
+ ReferenceCopyMode,
10
+ } from "../types";
11
+ import {
12
+ DEFAULT_LAYER,
13
+ DEFAULT_LAYER_ID,
14
+ getDefaultChartConfig,
15
+ getDefaultGridConfig,
16
+ getDefaultNodeName,
17
+ getDefaultSizeByMaterial,
18
+ getDefaultTextContent,
19
+ normalizePrimaryLayer,
20
+ randomId,
21
+ } from "../utils/panelElementDefaults";
22
+ import {
23
+ buildDeepReferenceSnapshot,
24
+ clonePanelElement,
25
+ isPanelElementNode,
26
+ } from "../utils/panelElementNodes";
27
+ import {
28
+ computeSnapPatchForNewElementOnLayer,
29
+ getGridChildSpanRect,
30
+ getGridSlotLayout,
31
+ inferSpanBySize,
32
+ } from "../utils/gridPlacement";
33
+ import {
34
+ canonicalMappingFamilyRootId,
35
+ expandMappingSeedsWithGridDescendants,
36
+ findCloneIdForSourceNodeOnMappingLayer,
37
+ getMaxZIndexByLayer,
38
+ removeMappingLayersBySourceIds,
39
+ } from "../utils/mappingLayerOps";
40
+ import {
41
+ applyGridLayoutPatchAcrossMappingFamily,
42
+ applyMappingFamilySyncPatch,
43
+ } from "../utils/updateElementDraft";
44
+
45
+ export type { PanelActionResult, PanelHistoryItem, PanelLayer } from "../types";
46
+
47
+ export function usePanelElements() {
48
+ const state = useSyncExternalStore(
49
+ (cb) => {
50
+ const sub = store.select().subscribe(cb);
51
+ return () => sub.unsubscribe();
52
+ },
53
+ () => store.getState(),
54
+ () => store.getState()
55
+ ) as State;
56
+ const rawLayers = (state.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
57
+ const layers = useMemo(() => normalizePrimaryLayer(rawLayers), [rawLayers]);
58
+ const activeLayerId =
59
+ (state.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID;
60
+ const canUndo = store.getHistoryCursorIndex() > 0;
61
+ const canRedo = store.getHistoryCursorIndex() < store.getHistoryEntries().length - 1;
62
+ const historyCursor = store.getHistoryCursorIndex();
63
+
64
+ useEffect(() => {
65
+ const vars = store.getState().variables ?? {};
66
+ const hasLayers = Array.isArray(vars.layers);
67
+ const hasActive = typeof vars.activeLayerId === "string";
68
+ const hasNodes = (store.getState().root.children?.length ?? 0) > 0;
69
+ if (hasLayers && hasActive && hasNodes) return;
70
+
71
+ store.update((draft) => {
72
+ draft.variables = draft.variables ?? {};
73
+ draft.variables.layers = hasLayers
74
+ ? normalizePrimaryLayer((draft.variables.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER])
75
+ : [DEFAULT_LAYER];
76
+ draft.variables.activeLayerId = hasActive
77
+ ? draft.variables.activeLayerId
78
+ : DEFAULT_LAYER_ID;
79
+
80
+ if (!hasNodes) {
81
+ const seed: PanelElement[] = [
82
+ // {
83
+ // id: "el-1",
84
+ // layerId: DEFAULT_LAYER_ID,
85
+ // materialType: "bar",
86
+ // chart: getDefaultChartConfig("bar"),
87
+ // x: 80,
88
+ // y: 80,
89
+ // width: 160,
90
+ // height: 96,
91
+ // rotate: 0,
92
+ // },
93
+ // {
94
+ // id: "el-2",
95
+ // layerId: DEFAULT_LAYER_ID,
96
+ // materialType: "line",
97
+ // chart: getDefaultChartConfig("line"),
98
+ // x: 300,
99
+ // y: 140,
100
+ // width: 220,
101
+ // height: 120,
102
+ // rotate: 0,
103
+ // },
104
+ // {
105
+ // id: "el-3",
106
+ // layerId: DEFAULT_LAYER_ID,
107
+ // materialType: "rect",
108
+ // x: 180,
109
+ // y: 300,
110
+ // width: 140,
111
+ // height: 140,
112
+ // rotate: 0,
113
+ // },
114
+ ];
115
+ draft.root.children = seed.map(
116
+ (el): Node => ({
117
+ id: el.id,
118
+ type: el.materialType ?? "unknown",
119
+ props: el,
120
+ children: [],
121
+ })
122
+ );
123
+ }
124
+ });
125
+ }, []);
126
+
127
+ const allElements = useMemo(() => {
128
+ const nodes = state.root.children ?? [];
129
+ return nodes
130
+ .filter((n) => isPanelElementNode(n) && n.props)
131
+ .map((n) => {
132
+ const props = n.props as PanelElement;
133
+ return { ...props, zIndex: typeof props.zIndex === "number" ? props.zIndex : 1 };
134
+ });
135
+ }, [state.root.children]);
136
+
137
+ const elements = useMemo(
138
+ () => allElements.filter((el) => el.layerId === activeLayerId),
139
+ [activeLayerId, allElements]
140
+ );
141
+
142
+ const byId = useMemo(() => {
143
+ const map = new Map<string, PanelElement>();
144
+ for (const el of allElements) map.set(el.id, el);
145
+ return map;
146
+ }, [allElements]);
147
+ const layerById = useMemo(() => {
148
+ const map = new Map<string, PanelLayer>();
149
+ for (const layer of layers) map.set(layer.id, layer);
150
+ return map;
151
+ }, [layers]);
152
+
153
+ const updateElement = useCallback(
154
+ (
155
+ id: string,
156
+ patch: Partial<PanelElement>,
157
+ options?: { batchId?: string; meta?: Record<string, unknown> }
158
+ ) => {
159
+ const current = store.getState();
160
+ const list = (current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
161
+ const target = current.root.children?.find((n) => n.id === id);
162
+ const layerId = target?.props?.layerId as string | undefined;
163
+ const layer = list.find((l) => l.id === layerId);
164
+ if (layer?.locked) return;
165
+ const currentElement = (target?.props ?? {}) as PanelElement;
166
+ const elementByIdSnapshot = new Map<string, PanelElement>();
167
+ for (const n of current.root.children ?? []) {
168
+ if (!isPanelElementNode(n) || !n.props) continue;
169
+ const p = n.props as PanelElement;
170
+ elementByIdSnapshot.set(p.id, p);
171
+ }
172
+ const mappingFamilyRootId = canonicalMappingFamilyRootId(elementByIdSnapshot, id);
173
+ const parentGrid =
174
+ currentElement.parentGridId
175
+ ? (current.root.children ?? [])
176
+ .filter((n) => isPanelElementNode(n) && n.props)
177
+ .map((n) => n.props as PanelElement)
178
+ .find((el) => el.id === currentElement.parentGridId && el.materialType === "grid")
179
+ : undefined;
180
+ const detachFromGrid = "parentGridId" in patch && patch.parentGridId === undefined;
181
+ const hasResizePatch = "width" in patch || "height" in patch;
182
+ if (parentGrid && hasResizePatch && !detachFromGrid) {
183
+ const parentLayout = getGridSlotLayout(parentGrid);
184
+ const nextWidth =
185
+ typeof patch.width === "number" ? patch.width : currentElement.width;
186
+ const nextHeight =
187
+ typeof patch.height === "number" ? patch.height : currentElement.height;
188
+ const inferredColSpan = inferSpanBySize(
189
+ nextWidth,
190
+ parentLayout.cellWidth,
191
+ parentLayout.gap,
192
+ parentLayout.cols
193
+ );
194
+ const inferredRowSpan = inferSpanBySize(
195
+ nextHeight,
196
+ parentLayout.cellHeight,
197
+ parentLayout.gap,
198
+ parentLayout.rows
199
+ );
200
+ const total = parentLayout.rows * parentLayout.cols;
201
+ const baseSlot = Math.max(
202
+ 0,
203
+ Math.min(
204
+ total - 1,
205
+ Math.floor(
206
+ patch.gridSlotIndex !== undefined
207
+ ? patch.gridSlotIndex
208
+ : currentElement.gridSlotIndex ?? 0
209
+ )
210
+ )
211
+ );
212
+ const currentRow = Math.floor(baseSlot / parentLayout.cols);
213
+ const currentCol = baseSlot % parentLayout.cols;
214
+ const nextStartCol = Math.max(
215
+ 0,
216
+ Math.min(parentLayout.cols - inferredColSpan, currentCol)
217
+ );
218
+ const nextStartRow = Math.max(
219
+ 0,
220
+ Math.min(parentLayout.rows - inferredRowSpan, currentRow)
221
+ );
222
+ const nextSlotIndex = nextStartRow * parentLayout.cols + nextStartCol;
223
+ patch = {
224
+ ...patch,
225
+ gridSlotIndex: nextSlotIndex,
226
+ gridColSpan: inferredColSpan,
227
+ gridRowSpan: inferredRowSpan,
228
+ };
229
+ }
230
+ const hasGridChildSlotPatch =
231
+ "gridSlotIndex" in patch || "gridColSpan" in patch || "gridRowSpan" in patch;
232
+ if (currentElement.parentGridId && hasGridChildSlotPatch && !detachFromGrid) {
233
+ if (parentGrid) {
234
+ const nextSlotIndex =
235
+ patch.gridSlotIndex !== undefined
236
+ ? patch.gridSlotIndex
237
+ : currentElement.gridSlotIndex ?? 0;
238
+ const nextColSpan =
239
+ patch.gridColSpan !== undefined ? patch.gridColSpan : currentElement.gridColSpan ?? 1;
240
+ const nextRowSpan =
241
+ patch.gridRowSpan !== undefined ? patch.gridRowSpan : currentElement.gridRowSpan ?? 1;
242
+ const spanRect = getGridChildSpanRect(parentGrid, nextSlotIndex, nextColSpan, nextRowSpan);
243
+ patch = {
244
+ ...patch,
245
+ gridSlotIndex: spanRect.index,
246
+ gridColSpan: spanRect.colSpan,
247
+ gridRowSpan: spanRect.rowSpan,
248
+ x: spanRect.x,
249
+ y: spanRect.y,
250
+ width: spanRect.width,
251
+ height: spanRect.height,
252
+ };
253
+ }
254
+ }
255
+ const hasTransformPatch =
256
+ "x" in patch ||
257
+ "y" in patch ||
258
+ "width" in patch ||
259
+ "height" in patch ||
260
+ "rotate" in patch ||
261
+ "layerId" in patch;
262
+ if (currentElement.locked && hasTransformPatch) return;
263
+ const isGridNode = currentElement.materialType === "grid";
264
+ const hasGridLayoutPatch =
265
+ "gridRows" in patch ||
266
+ "gridCols" in patch ||
267
+ "gridGap" in patch ||
268
+ "gridPadding" in patch ||
269
+ "width" in patch ||
270
+ "height" in patch ||
271
+ "x" in patch ||
272
+ "y" in patch;
273
+ if (isGridNode && hasGridLayoutPatch) {
274
+ store.update(
275
+ (draft) => {
276
+ applyGridLayoutPatchAcrossMappingFamily(draft, mappingFamilyRootId, patch);
277
+ },
278
+ {
279
+ batchId: options?.batchId,
280
+ meta: { type: "node.update", id, ...(options?.meta ?? {}) },
281
+ }
282
+ );
283
+ return;
284
+ }
285
+
286
+ const syncPatch = { ...patch } as Partial<PanelElement>;
287
+ delete (syncPatch as any).id;
288
+ delete (syncPatch as any).layerId;
289
+ delete (syncPatch as any).mappingSourceNodeId;
290
+ delete (syncPatch as any).mappingSourceLayerId;
291
+ if (Object.keys(syncPatch).length > 0) {
292
+ store.update(
293
+ (draft) => {
294
+ applyMappingFamilySyncPatch(draft, mappingFamilyRootId, syncPatch, patch);
295
+ },
296
+ {
297
+ batchId: options?.batchId,
298
+ meta: { type: "node.update", id, ...(options?.meta ?? {}) },
299
+ }
300
+ );
301
+ return;
302
+ }
303
+
304
+ store.updateById(
305
+ id,
306
+ (node) => {
307
+ node.props = { ...(node.props ?? {}), ...patch };
308
+ },
309
+ {
310
+ batchId: options?.batchId,
311
+ meta: { type: "node.update", id, ...(options?.meta ?? {}) },
312
+ }
313
+ );
314
+ },
315
+ []
316
+ );
317
+
318
+ const deleteElement = useCallback((id: string) => {
319
+ const current = store.getState();
320
+ const target = current.root.children?.find((n) => isPanelElementNode(n) && n.id === id);
321
+ if (!target?.props) return;
322
+ const currentLayers =
323
+ (current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
324
+ const targetLayer = currentLayers.find((l) => l.id === target.props?.layerId);
325
+ if ((target.props as PanelElement).locked) return;
326
+ if (targetLayer?.locked) return;
327
+ store.update(
328
+ (draft) => {
329
+ const deletedSourceIds = new Set<string>();
330
+ const deleting = (draft.root.children ?? []).find(
331
+ (n) => isPanelElementNode(n) && n.id === id
332
+ );
333
+ if (deleting && isPanelElementNode(deleting) && deleting.props?.id) {
334
+ const props = deleting.props as PanelElement;
335
+ deletedSourceIds.add(props.mappingSourceNodeId ?? props.id);
336
+ }
337
+ const familyIds = new Set<string>();
338
+ (draft.root.children ?? []).forEach((n) => {
339
+ if (!isPanelElementNode(n) || !n.props) return;
340
+ const props = n.props as PanelElement;
341
+ const sourceId = props.mappingSourceNodeId ?? props.id;
342
+ if (deletedSourceIds.has(sourceId)) familyIds.add(props.id);
343
+ });
344
+ draft.root.children = (draft.root.children ?? []).filter(
345
+ (n) => !(isPanelElementNode(n) && familyIds.has(n.id))
346
+ );
347
+ // 映射图层只能通过图层栏删除或删除源节点等方式移除,不得在删除单个映射节点时整层删掉
348
+ },
349
+ { meta: { type: "node.delete", id } }
350
+ );
351
+ }, []);
352
+
353
+ const deleteElements = useCallback((ids: string[]) => {
354
+ const unlockedIds = ids.filter((id) => {
355
+ const el = byId.get(id);
356
+ if (!id || !el) return false;
357
+ if (el.locked) return false;
358
+ const layer = layerById.get(el.layerId);
359
+ return !layer?.locked;
360
+ });
361
+ const idSet = new Set(unlockedIds);
362
+ if (idSet.size === 0) return;
363
+ store.update(
364
+ (draft) => {
365
+ const deletedSourceIds = new Set<string>();
366
+ (draft.root.children ?? []).forEach((n) => {
367
+ if (!isPanelElementNode(n) || !n.props) return;
368
+ if (!idSet.has(n.id)) return;
369
+ const props = n.props as PanelElement;
370
+ deletedSourceIds.add(props.mappingSourceNodeId ?? props.id);
371
+ });
372
+ const familyIds = new Set<string>();
373
+ (draft.root.children ?? []).forEach((n) => {
374
+ if (!isPanelElementNode(n) || !n.props) return;
375
+ const props = n.props as PanelElement;
376
+ const sourceId = props.mappingSourceNodeId ?? props.id;
377
+ if (deletedSourceIds.has(sourceId)) familyIds.add(props.id);
378
+ });
379
+ draft.root.children = (draft.root.children ?? []).filter(
380
+ (n) => !(isPanelElementNode(n) && familyIds.has(n.id))
381
+ );
382
+ },
383
+ { meta: { type: "node.batch-delete", ids: Array.from(idSet) } }
384
+ );
385
+ }, [byId, layerById]);
386
+
387
+ const bringElementsToFront = useCallback((ids: string[]) => {
388
+ const unlocked = ids.filter((id) => {
389
+ const el = byId.get(id);
390
+ if (!id || !el || el.locked) return false;
391
+ const layer = layerById.get(el.layerId);
392
+ return !layer?.locked;
393
+ });
394
+ const idSet = new Set(unlocked);
395
+ if (idSet.size === 0) return;
396
+ store.update(
397
+ (draft) => {
398
+ const list = draft.root.children ?? [];
399
+ const selectedByLayer = new Map<string, PanelElement[]>();
400
+ list.forEach((n) => {
401
+ if (!isPanelElementNode(n) || !n.props || !idSet.has(n.id)) return;
402
+ const props = n.props as PanelElement;
403
+ const group = selectedByLayer.get(props.layerId) ?? [];
404
+ group.push({
405
+ ...props,
406
+ zIndex: typeof props.zIndex === "number" ? props.zIndex : 1,
407
+ });
408
+ selectedByLayer.set(props.layerId, group);
409
+ });
410
+ selectedByLayer.forEach((selected, layerId) => {
411
+ const maxZ = getMaxZIndexByLayer(list, layerId);
412
+ selected
413
+ .sort((a, b) => (a.zIndex ?? 1) - (b.zIndex ?? 1))
414
+ .forEach((node, offset) => {
415
+ const target = list.find((n) => n.id === node.id);
416
+ if (!target?.props) return;
417
+ target.props = { ...(target.props as PanelElement), zIndex: maxZ + offset + 1 };
418
+ });
419
+ });
420
+ },
421
+ { meta: { type: "node.z.front", ids: Array.from(idSet) } }
422
+ );
423
+ }, [byId, layerById]);
424
+
425
+ const sendElementsToBack = useCallback((ids: string[]) => {
426
+ const unlocked = ids.filter((id) => {
427
+ const el = byId.get(id);
428
+ if (!id || !el || el.locked) return false;
429
+ const layer = layerById.get(el.layerId);
430
+ return !layer?.locked;
431
+ });
432
+ const idSet = new Set(unlocked);
433
+ if (idSet.size === 0) return;
434
+ store.update(
435
+ (draft) => {
436
+ const list = draft.root.children ?? [];
437
+ const selectedByLayer = new Map<string, PanelElement[]>();
438
+ list.forEach((n) => {
439
+ if (!isPanelElementNode(n) || !n.props || !idSet.has(n.id)) return;
440
+ const props = n.props as PanelElement;
441
+ const group = selectedByLayer.get(props.layerId) ?? [];
442
+ group.push({
443
+ ...props,
444
+ zIndex: typeof props.zIndex === "number" ? props.zIndex : 1,
445
+ });
446
+ selectedByLayer.set(props.layerId, group);
447
+ });
448
+ selectedByLayer.forEach((selected, layerId) => {
449
+ let minZ = Number.POSITIVE_INFINITY;
450
+ list.forEach((n) => {
451
+ if (!isPanelElementNode(n) || !n.props) return;
452
+ const props = n.props as PanelElement;
453
+ if (props.layerId !== layerId) return;
454
+ const z = typeof props.zIndex === "number" ? props.zIndex : 1;
455
+ if (z < minZ) minZ = z;
456
+ });
457
+ if (!Number.isFinite(minZ)) minZ = 1;
458
+ const start = minZ - selected.length;
459
+ selected
460
+ .sort((a, b) => (a.zIndex ?? 1) - (b.zIndex ?? 1))
461
+ .forEach((node, offset) => {
462
+ const target = list.find((n) => n.id === node.id);
463
+ if (!target?.props) return;
464
+ target.props = { ...(target.props as PanelElement), zIndex: start + offset };
465
+ });
466
+ });
467
+ },
468
+ { meta: { type: "node.z.back", ids: Array.from(idSet) } }
469
+ );
470
+ }, [byId, layerById]);
471
+
472
+ const bringElementsForward = useCallback((ids: string[]) => {
473
+ const unlocked = ids.filter((id) => {
474
+ const el = byId.get(id);
475
+ if (!id || !el || el.locked) return false;
476
+ const layer = layerById.get(el.layerId);
477
+ return !layer?.locked;
478
+ });
479
+ const idSet = new Set(unlocked);
480
+ if (idSet.size === 0) return;
481
+ store.update(
482
+ (draft) => {
483
+ const list = draft.root.children ?? [];
484
+ list.forEach((n) => {
485
+ if (!isPanelElementNode(n) || !n.props || !idSet.has(n.id)) return;
486
+ const props = n.props as PanelElement;
487
+ const z = typeof props.zIndex === "number" ? props.zIndex : 1;
488
+ n.props = { ...props, zIndex: z + 1 };
489
+ });
490
+ },
491
+ { meta: { type: "node.z.up", ids: Array.from(idSet) } }
492
+ );
493
+ }, [byId, layerById]);
494
+
495
+ const sendElementsBackward = useCallback((ids: string[]) => {
496
+ const unlocked = ids.filter((id) => {
497
+ const el = byId.get(id);
498
+ if (!id || !el || el.locked) return false;
499
+ const layer = layerById.get(el.layerId);
500
+ return !layer?.locked;
501
+ });
502
+ const idSet = new Set(unlocked);
503
+ if (idSet.size === 0) return;
504
+ store.update(
505
+ (draft) => {
506
+ const list = draft.root.children ?? [];
507
+ list.forEach((n) => {
508
+ if (!isPanelElementNode(n) || !n.props || !idSet.has(n.id)) return;
509
+ const props = n.props as PanelElement;
510
+ const z = typeof props.zIndex === "number" ? props.zIndex : 1;
511
+ n.props = { ...props, zIndex: z - 1 };
512
+ });
513
+ },
514
+ { meta: { type: "node.z.down", ids: Array.from(idSet) } }
515
+ );
516
+ }, [byId, layerById]);
517
+
518
+ const duplicateElement = useCallback(
519
+ (
520
+ id: string,
521
+ options?: { referenceCopyMode?: ReferenceCopyMode; position?: { x: number; y: number } }
522
+ ) => {
523
+ const current = store.getState();
524
+ const node = current.root.children?.find((n) => isPanelElementNode(n) && n.id === id);
525
+ if (!node?.props) return;
526
+ const layerId = node.props.layerId as string | undefined;
527
+ const currentLayers =
528
+ (current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
529
+ const layer = currentLayers.find((l) => l.id === layerId);
530
+ if (layer?.locked) return;
531
+
532
+ const nextId = randomId("el");
533
+ const nextProps: PanelElement = {
534
+ ...(node.props as PanelElement),
535
+ id: nextId,
536
+ x: Math.round(options?.position?.x ?? ((node.props.x ?? 0) + 20)),
537
+ y: Math.round(options?.position?.y ?? ((node.props.y ?? 0) + 20)),
538
+ zIndex: 1,
539
+ };
540
+ const all = (current.root.children ?? [])
541
+ .filter((n) => isPanelElementNode(n) && n.props)
542
+ .map((n) => n.props as PanelElement);
543
+ const desiredMode = options?.referenceCopyMode;
544
+ if (nextProps.materialType === "reference" && desiredMode) {
545
+ if (desiredMode === "deep" && nextProps.refLayerId) {
546
+ nextProps.refCopyMode = "deep";
547
+ nextProps.refSnapshot = buildDeepReferenceSnapshot(all, nextProps.refLayerId);
548
+ } else {
549
+ nextProps.refCopyMode = "shallow";
550
+ nextProps.refSnapshot = undefined;
551
+ }
552
+ }
553
+ store.update(
554
+ (draft) => {
555
+ draft.root.children = draft.root.children ?? [];
556
+ draft.root.children.push({
557
+ id: nextId,
558
+ type: node.type,
559
+ props: nextProps,
560
+ children: [],
561
+ });
562
+ },
563
+ { meta: { type: "node.duplicate", sourceId: id, id: nextId } }
564
+ );
565
+ },
566
+ []
567
+ );
568
+
569
+ const addElementFromMaterial = useCallback((materialType: string, x: number, y: number) => {
570
+ const current = store.getState();
571
+ const currentLayers =
572
+ (current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
573
+ const currentLayerId =
574
+ (current.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID;
575
+ const layer = currentLayers.find((l) => l.id === currentLayerId);
576
+ if (!layer || layer.locked) return;
577
+
578
+ const size = getDefaultSizeByMaterial(materialType);
579
+ const id = randomId("el");
580
+ const next: PanelElement = {
581
+ id,
582
+ layerId: currentLayerId,
583
+ zIndex: 1,
584
+ materialType,
585
+ name: getDefaultNodeName(materialType),
586
+ ...getDefaultTextContent(materialType),
587
+ ...getDefaultGridConfig(materialType),
588
+ refCopyMode: materialType === "reference" ? "shallow" : undefined,
589
+ chart: getDefaultChartConfig(materialType),
590
+ geometryShape: materialType === "geometry" ? "rect" : undefined,
591
+ geometryColor: materialType === "geometry" ? "#3b82f6" : undefined,
592
+ geometryScript: undefined,
593
+ x: Math.round(x - size.width / 2),
594
+ y: Math.round(y - size.height / 2),
595
+ width: size.width,
596
+ height: size.height,
597
+ rotate: 0,
598
+ };
599
+ if (layer.isMapping && layer.mappingBaseLayerId) {
600
+ const fullById = new Map<string, PanelElement>();
601
+ for (const n of current.root.children ?? []) {
602
+ if (!isPanelElementNode(n) || !n.props) continue;
603
+ const p = n.props as PanelElement;
604
+ fullById.set(p.id, p);
605
+ }
606
+ // 映射图层里落点时:基准图层上的源节点也要写入 parentGridId/slot,否则节点树里源侧的 C 不会挂在网格 B 下
607
+ const snapPatch = computeSnapPatchForNewElementOnLayer(
608
+ fullById,
609
+ layer.mappingBaseLayerId,
610
+ next.x,
611
+ next.y,
612
+ next.width,
613
+ next.height,
614
+ undefined
615
+ );
616
+ const sourceId = randomId("el");
617
+ const sourceNode: PanelElement = {
618
+ ...next,
619
+ ...snapPatch,
620
+ id: sourceId,
621
+ layerId: layer.mappingBaseLayerId,
622
+ };
623
+ const siblingMappings = currentLayers.filter(
624
+ (l) => l.isMapping && l.mappingBaseLayerId === layer.mappingBaseLayerId && !l.locked
625
+ );
626
+ const clones = siblingMappings.map((mappingLayer) => {
627
+ const cloneId = randomId("el");
628
+ const mappedParentGridId =
629
+ sourceNode.parentGridId !== undefined
630
+ ? findCloneIdForSourceNodeOnMappingLayer(
631
+ sourceNode.parentGridId,
632
+ mappingLayer.id,
633
+ fullById
634
+ )
635
+ : undefined;
636
+ return {
637
+ id: cloneId,
638
+ type: materialType,
639
+ props: {
640
+ ...clonePanelElement(sourceNode),
641
+ id: cloneId,
642
+ layerId: mappingLayer.id,
643
+ mappingSourceNodeId: sourceId,
644
+ mappingSourceLayerId: layer.mappingBaseLayerId,
645
+ parentGridId:
646
+ sourceNode.parentGridId === undefined
647
+ ? undefined
648
+ : mappedParentGridId ?? sourceNode.parentGridId,
649
+ } as PanelElement,
650
+ children: [] as Node[],
651
+ };
652
+ });
653
+ store.update(
654
+ (draft) => {
655
+ draft.root.children = draft.root.children ?? [];
656
+ draft.root.children.push({
657
+ id: sourceId,
658
+ type: materialType,
659
+ props: sourceNode,
660
+ children: [],
661
+ });
662
+ clones.forEach((clone) => draft.root.children!.push(clone as unknown as Node));
663
+ },
664
+ { meta: { type: "node.add", materialType, id: sourceId } }
665
+ );
666
+ return;
667
+ }
668
+ store.update(
669
+ (draft) => {
670
+ draft.root.children = draft.root.children ?? [];
671
+ draft.root.children.push({
672
+ id,
673
+ type: materialType,
674
+ props: next,
675
+ children: [],
676
+ });
677
+ },
678
+ { meta: { type: "node.add", materialType, id } }
679
+ );
680
+ }, []);
681
+
682
+ const setReferenceCopyMode = useCallback(
683
+ (id: string, mode: ReferenceCopyMode) => {
684
+ const current = store.getState();
685
+ const all = (current.root.children ?? [])
686
+ .filter((n) => isPanelElementNode(n) && n.props)
687
+ .map((n) => n.props as PanelElement);
688
+ const target = all.find((el) => el.id === id);
689
+ if (!target || target.materialType !== "reference") return;
690
+ const currentLayers =
691
+ (current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
692
+ const layer = currentLayers.find((l) => l.id === target.layerId);
693
+ if (layer?.locked) return;
694
+ store.updateById(
695
+ id,
696
+ (node) => {
697
+ const props = (node.props ?? {}) as PanelElement;
698
+ if (mode === "deep" && props.refLayerId) {
699
+ props.refCopyMode = "deep";
700
+ props.refSnapshot = buildDeepReferenceSnapshot(all, props.refLayerId);
701
+ } else {
702
+ props.refCopyMode = "shallow";
703
+ props.refSnapshot = undefined;
704
+ }
705
+ node.props = props;
706
+ },
707
+ { meta: { type: "node.ref-copy-mode", id, mode } }
708
+ );
709
+ },
710
+ []
711
+ );
712
+
713
+ const setActiveLayer = useCallback((layerId: string) => {
714
+ store.update(
715
+ (draft) => {
716
+ draft.variables = draft.variables ?? {};
717
+ draft.variables.activeLayerId = layerId;
718
+ },
719
+ { meta: { type: "layer.activate", layerId } }
720
+ );
721
+ }, []);
722
+
723
+ const addLayer = useCallback(() => {
724
+ store.update(
725
+ (draft) => {
726
+ draft.variables = draft.variables ?? {};
727
+ const list = (draft.variables.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
728
+ const nextId = randomId("layer");
729
+ const next: PanelLayer = {
730
+ id: nextId,
731
+ name: `图层${list.length + 1}`,
732
+ locked: false,
733
+ editable: true,
734
+ isPrimary: false,
735
+ isMapping: false,
736
+ mappingBaseLayerId: undefined,
737
+ mergeSelected: false,
738
+ };
739
+ draft.variables.layers = [...list, next];
740
+ draft.variables.activeLayerId = nextId;
741
+ },
742
+ { meta: { type: "layer.add" } }
743
+ );
744
+ }, []);
745
+
746
+ const openElementsInNewLayer = useCallback((ids: string[]): PanelActionResult => {
747
+ const filtered = ids.filter(Boolean);
748
+ if (filtered.length === 0) return { ok: false, reason: "未选择节点" };
749
+ const current = store.getState();
750
+ const currentLayers =
751
+ (current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
752
+ const nodeById = new Map(
753
+ (current.root.children ?? [])
754
+ .filter((n) => isPanelElementNode(n) && n.props)
755
+ .map((n) => [n.id, n.props as PanelElement])
756
+ );
757
+ const expandedIds = expandMappingSeedsWithGridDescendants(nodeById, filtered);
758
+ const selected = expandedIds
759
+ .map((id) => nodeById.get(id))
760
+ .filter((el): el is PanelElement => !!el);
761
+ if (selected.length === 0) return { ok: false, reason: "未找到可迁移节点" };
762
+ const anchorEl = nodeById.get(filtered[0]!) ?? selected[0];
763
+ const hasLocked = selected.some((el) => {
764
+ if (el.locked) return true;
765
+ const layer = currentLayers.find((l) => l.id === el.layerId);
766
+ return !!layer?.locked;
767
+ });
768
+ if (hasLocked) return { ok: false, reason: "选中节点包含锁定内容,无法在新图层打开" };
769
+
770
+ const selectedSet = new Set(selected.map((el) => el.id));
771
+ const idMap = new Map<string, string>();
772
+ selected.forEach((el) => idMap.set(el.id, randomId("el")));
773
+ const nextLayerId = randomId("layer");
774
+ const nextLayer: PanelLayer = {
775
+ id: nextLayerId,
776
+ name: `映射图层${currentLayers.length + 1}`,
777
+ locked: false,
778
+ editable: true,
779
+ isPrimary: false,
780
+ isMapping: true,
781
+ mappingBaseLayerId:
782
+ anchorEl?.mappingSourceLayerId ??
783
+ anchorEl?.layerId ??
784
+ (current.variables?.activeLayerId as string | undefined) ??
785
+ DEFAULT_LAYER_ID,
786
+ mergeSelected: false,
787
+ };
788
+ store.update(
789
+ (draft) => {
790
+ draft.variables = draft.variables ?? {};
791
+ const layers = (draft.variables.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
792
+ draft.variables.layers = normalizePrimaryLayer([...layers, nextLayer]);
793
+ draft.variables.activeLayerId = nextLayerId;
794
+ const clones: Node[] = [];
795
+ const all = draft.root.children ?? [];
796
+ all.forEach((n) => {
797
+ if (!isPanelElementNode(n) || !n.props || !selectedSet.has(n.id)) return;
798
+ const props = n.props as PanelElement;
799
+ const nextId = idMap.get(props.id);
800
+ if (!nextId) return;
801
+ const remappedParentGridId = props.parentGridId
802
+ ? idMap.get(props.parentGridId) ?? props.parentGridId
803
+ : undefined;
804
+ const nextProps: PanelElement = {
805
+ ...clonePanelElement(props),
806
+ id: nextId,
807
+ layerId: nextLayerId,
808
+ parentGridId: remappedParentGridId,
809
+ mappingSourceNodeId: props.mappingSourceNodeId ?? props.id,
810
+ mappingSourceLayerId: props.mappingSourceLayerId ?? props.layerId,
811
+ };
812
+ clones.push({
813
+ id: nextId,
814
+ type: n.type,
815
+ props: nextProps,
816
+ children: [],
817
+ });
818
+ });
819
+ draft.root.children = [...all, ...clones];
820
+ },
821
+ {
822
+ meta: {
823
+ type: "layer.open-selected-mapping",
824
+ layerId: nextLayerId,
825
+ nodeCount: selectedSet.size,
826
+ },
827
+ }
828
+ );
829
+ return { ok: true };
830
+ }, []);
831
+
832
+ const renameLayer = useCallback((layerId: string, name: string) => {
833
+ store.update(
834
+ (draft) => {
835
+ const list = (draft.variables?.layers as PanelLayer[] | undefined) ?? [];
836
+ const target = list.find((l) => l.id === layerId);
837
+ if (!target || !target.editable) return;
838
+ target.name = name.trim() || target.name;
839
+ },
840
+ { meta: { type: "layer.rename", layerId } }
841
+ );
842
+ }, []);
843
+
844
+ const toggleLayerLock = useCallback((layerId: string) => {
845
+ store.update(
846
+ (draft) => {
847
+ const list = (draft.variables?.layers as PanelLayer[] | undefined) ?? [];
848
+ const target = list.find((l) => l.id === layerId);
849
+ if (!target || !target.editable) return;
850
+ target.locked = !target.locked;
851
+ },
852
+ { meta: { type: "layer.toggle-lock", layerId } }
853
+ );
854
+ }, []);
855
+
856
+ const deleteLayer = useCallback(
857
+ (
858
+ layerId: string,
859
+ options?: { mode?: "remove" | "move"; targetLayerId?: string }
860
+ ): PanelActionResult => {
861
+ const mode = options?.mode ?? "remove";
862
+ const targetLayerId = options?.targetLayerId;
863
+ const current = store.getState();
864
+ const list = (current.variables?.layers as PanelLayer[] | undefined) ?? [];
865
+ const target = list.find((l) => l.id === layerId);
866
+ if (!target) return { ok: false, reason: "图层不存在" };
867
+ if (!target.editable) return { ok: false, reason: "默认图层不可删除" };
868
+ if (target.locked) return { ok: false, reason: "锁定图层不可删除" };
869
+ if (mode === "move" && !targetLayerId) {
870
+ return { ok: false, reason: "请选择目标图层" };
871
+ }
872
+ const remainingLayers = list.filter((l) => l.id !== layerId);
873
+ const moveTarget = remainingLayers.find((l) => l.id === targetLayerId);
874
+ if (mode === "move" && !moveTarget) {
875
+ return { ok: false, reason: "目标图层不存在" };
876
+ }
877
+ const allElements = (current.root.children ?? [])
878
+ .filter((n) => isPanelElementNode(n) && n.props)
879
+ .map((n) => n.props as PanelElement);
880
+ const hasBlockingRef = allElements.some((el) => {
881
+ if (target.isMapping) return false;
882
+ const willBeDeleted = mode === "remove" && el.layerId === layerId;
883
+ if (willBeDeleted) return false;
884
+ if (el.materialType !== "reference") return false;
885
+ if (el.refLayerId !== layerId) return false;
886
+ return (el.refCopyMode ?? "shallow") !== "deep";
887
+ });
888
+ if (hasBlockingRef) {
889
+ return { ok: false, reason: "该图层仍被浅拷贝引用,请先删除引用节点或改为深拷贝" };
890
+ }
891
+
892
+ store.update(
893
+ (draft) => {
894
+ draft.variables!.layers = normalizePrimaryLayer(remainingLayers.map((l) => ({
895
+ ...l,
896
+ mergeSelected: false,
897
+ })));
898
+
899
+ if (mode === "move" && moveTarget) {
900
+ draft.root.children = (draft.root.children ?? []).map((n) => {
901
+ if (isPanelElementNode(n) && n.props?.layerId === layerId) {
902
+ n.props = { ...(n.props ?? {}), layerId: moveTarget.id };
903
+ }
904
+ return n;
905
+ });
906
+ } else {
907
+ const deletedSourceIds = new Set<string>();
908
+ (draft.root.children ?? []).forEach((n) => {
909
+ if (!isPanelElementNode(n) || !n.props) return;
910
+ if (n.props?.layerId !== layerId) return;
911
+ deletedSourceIds.add((n.props as PanelElement).id);
912
+ });
913
+ draft.root.children = (draft.root.children ?? []).filter(
914
+ (n) => !isPanelElementNode(n) || n.props?.layerId !== layerId
915
+ );
916
+ removeMappingLayersBySourceIds(draft, deletedSourceIds);
917
+ }
918
+
919
+ if (draft.variables!.activeLayerId === layerId) {
920
+ draft.variables!.activeLayerId =
921
+ moveTarget?.id ?? remainingLayers[0]?.id ?? DEFAULT_LAYER_ID;
922
+ }
923
+ },
924
+ { meta: { type: "layer.delete", layerId, mode } }
925
+ );
926
+ return { ok: true };
927
+ },
928
+ []
929
+ );
930
+
931
+ const toggleLayerMergeSelected = useCallback((layerId: string) => {
932
+ store.update(
933
+ (draft) => {
934
+ const list = (draft.variables?.layers as PanelLayer[] | undefined) ?? [];
935
+ const target = list.find((l) => l.id === layerId);
936
+ if (!target) return;
937
+ if (target.isMapping) return;
938
+ target.mergeSelected = !target.mergeSelected;
939
+ },
940
+ { meta: { type: "layer.toggle-merge", layerId } }
941
+ );
942
+ }, []);
943
+
944
+ const mergeSelectedLayers = useCallback((name?: string) => {
945
+ store.update(
946
+ (draft) => {
947
+ draft.variables = draft.variables ?? {};
948
+ const list = ((draft.variables.layers as PanelLayer[] | undefined) ?? []).slice();
949
+ const selected = list.filter((l) => l.mergeSelected && !l.isMapping);
950
+ if (selected.length < 2) return;
951
+
952
+ const nextId = randomId("layer");
953
+ const nextLayer: PanelLayer = {
954
+ id: nextId,
955
+ name: name?.trim() || `图层-${Math.random().toString(36).slice(2, 6)}`,
956
+ locked: false,
957
+ editable: true,
958
+ isPrimary: false,
959
+ isMapping: false,
960
+ mappingBaseLayerId: undefined,
961
+ mergeSelected: false,
962
+ };
963
+ const selectedSet = new Set(selected.map((l) => l.id));
964
+
965
+ draft.root.children = (draft.root.children ?? []).map((n) => {
966
+ if (isPanelElementNode(n) && selectedSet.has(n.props?.layerId)) {
967
+ n.props = { ...(n.props ?? {}), layerId: nextId };
968
+ }
969
+ return n;
970
+ });
971
+
972
+ draft.variables.layers = normalizePrimaryLayer([
973
+ ...list
974
+ .filter((l) => !selectedSet.has(l.id))
975
+ .map((l) => ({ ...l, mergeSelected: false })),
976
+ nextLayer,
977
+ ]);
978
+ draft.variables.activeLayerId = nextId;
979
+ },
980
+ { meta: { type: "layer.merge", name } }
981
+ );
982
+ }, []);
983
+
984
+ const setPrimaryLayer = useCallback((layerId: string) => {
985
+ store.update(
986
+ (draft) => {
987
+ const list = (draft.variables?.layers as PanelLayer[] | undefined) ?? [];
988
+ if (list.length === 0) return;
989
+ draft.variables!.layers = list.map((layer) => ({
990
+ ...layer,
991
+ isPrimary: layer.id === layerId,
992
+ }));
993
+ },
994
+ { meta: { type: "layer.set-primary", layerId } }
995
+ );
996
+ }, []);
997
+
998
+ const undo = useCallback(() => {
999
+ store.undo();
1000
+ }, []);
1001
+
1002
+ const redo = useCallback(() => {
1003
+ store.redo();
1004
+ }, []);
1005
+ const goToHistory = useCallback((index: number) => {
1006
+ store.goToHistory(index);
1007
+ }, []);
1008
+
1009
+ const history = useMemo<PanelHistoryItem[]>(() => {
1010
+ const entries = store.getHistoryEntries();
1011
+ const cursor = store.getHistoryCursorIndex();
1012
+ const labelByType: Record<string, string> = {
1013
+ initial: "初始化",
1014
+ "node.update": "更新节点",
1015
+ "node.group-drag": "批量移动节点",
1016
+ "node.group-resize": "批量缩放节点",
1017
+ "node.group-rotate": "批量旋转节点",
1018
+ "node.delete": "删除节点",
1019
+ "node.batch-delete": "批量删除节点",
1020
+ "node.z.front": "节点置顶",
1021
+ "node.z.back": "节点置底",
1022
+ "node.z.up": "节点上移一层",
1023
+ "node.z.down": "节点下移一层",
1024
+ "node.duplicate": "复制节点",
1025
+ "node.ref-copy-mode": "设置引用拷贝模式",
1026
+ "node.add": "添加节点",
1027
+ "layer.activate": "切换图层",
1028
+ "layer.add": "新增图层",
1029
+ "layer.rename": "重命名图层",
1030
+ "layer.toggle-lock": "切换图层锁定",
1031
+ "layer.open-selected-mapping": "映射图层打开选中节点",
1032
+ "layer.delete": "删除图层",
1033
+ "layer.toggle-merge": "勾选合并图层",
1034
+ "layer.merge": "合并图层",
1035
+ "layer.set-primary": "设置主图层",
1036
+ "panel.import": "导入面板",
1037
+ };
1038
+ return entries.map((entry, index) => {
1039
+ const type = entry.meta?.type as string | undefined;
1040
+ return {
1041
+ index,
1042
+ timestamp: entry.timestamp,
1043
+ label: (type && labelByType[type]) || "编辑",
1044
+ active: index === cursor,
1045
+ };
1046
+ });
1047
+ }, [state]);
1048
+
1049
+ const exportPanelData = useCallback(() => {
1050
+ const current = store.getState();
1051
+ return JSON.parse(JSON.stringify(current)) as State;
1052
+ }, []);
1053
+
1054
+ const importPanelData = useCallback((nextState: State) => {
1055
+ if (!nextState || typeof nextState !== "object") return false;
1056
+ if (!nextState.root || typeof nextState.root !== "object") return false;
1057
+ if (!nextState.root.id || !nextState.root.type) return false;
1058
+ if (!Array.isArray(nextState.root.children)) nextState.root.children = [];
1059
+ nextState.variables = nextState.variables ?? {};
1060
+ if (!Array.isArray(nextState.variables.layers)) {
1061
+ nextState.variables.layers = [DEFAULT_LAYER];
1062
+ }
1063
+ nextState.variables.layers = normalizePrimaryLayer(nextState.variables.layers as PanelLayer[]);
1064
+ if (typeof nextState.variables.activeLayerId !== "string") {
1065
+ nextState.variables.activeLayerId =
1066
+ (nextState.variables.layers[0] as PanelLayer | undefined)?.id ?? DEFAULT_LAYER_ID;
1067
+ }
1068
+ store.replaceState(nextState, { type: "panel.import" });
1069
+ return true;
1070
+ }, []);
1071
+
1072
+ return {
1073
+ allElements,
1074
+ elements,
1075
+ byId,
1076
+ layers,
1077
+ activeLayerId,
1078
+ updateElement,
1079
+ deleteElement,
1080
+ deleteElements,
1081
+ bringElementsToFront,
1082
+ sendElementsToBack,
1083
+ bringElementsForward,
1084
+ sendElementsBackward,
1085
+ duplicateElement,
1086
+ setReferenceCopyMode,
1087
+ addElementFromMaterial,
1088
+ setActiveLayer,
1089
+ addLayer,
1090
+ openElementsInNewLayer,
1091
+ renameLayer,
1092
+ toggleLayerLock,
1093
+ deleteLayer,
1094
+ toggleLayerMergeSelected,
1095
+ mergeSelectedLayers,
1096
+ setPrimaryLayer,
1097
+ undo,
1098
+ redo,
1099
+ goToHistory,
1100
+ canUndo,
1101
+ canRedo,
1102
+ historyCursor,
1103
+ history,
1104
+ exportPanelData,
1105
+ importPanelData,
1106
+ };
1107
+ }
1108
+