@arronqzy/vue-view 0.1.0
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/README.md +50 -0
- package/package.json +49 -0
- package/src/env.d.ts +62 -0
- package/src/index.ts +4 -0
- package/src/panel/VueViewOnlinePreview.vue +276 -0
- package/src/panel/VueViewPanel.vue +871 -0
- package/src/panel/components/ConfigHintIcon.vue +34 -0
- package/src/panel/components/ElementsLayer.vue +165 -0
- package/src/panel/components/MaterialPreview.vue +135 -0
- package/src/panel/components/MaterialSidebar.vue +526 -0
- package/src/panel/components/MaterialSidebarTreeNode.vue +305 -0
- package/src/panel/components/MoveableLayer.vue +859 -0
- package/src/panel/components/PanelCanvas.vue +630 -0
- package/src/panel/components/PanelConfigSidebar.vue +397 -0
- package/src/panel/components/PanelRulers.vue +177 -0
- package/src/panel/components/SelectLayer.vue +115 -0
- package/src/panel/components/ViewElementScopePanel.vue +76 -0
- package/src/panel/components/WorkspaceConfigSidebar.vue +147 -0
- package/src/panel/components/WorkspaceProjectNav.vue +192 -0
- package/src/panel/components/WorkspaceStageSplit.vue +258 -0
- package/src/panel/components/config/ConfigColorField.vue +52 -0
- package/src/panel/components/config/ConfigFieldGroup.vue +20 -0
- package/src/panel/components/config/ConfigSection.vue +50 -0
- package/src/panel/components/config/PanelConfigAudioSection.vue +256 -0
- package/src/panel/components/config/PanelConfigChartSection.vue +650 -0
- package/src/panel/components/config/PanelConfigGeometrySection.vue +209 -0
- package/src/panel/components/config/PanelConfigGridChildSpan.vue +68 -0
- package/src/panel/components/config/PanelConfigGridSection.vue +103 -0
- package/src/panel/components/config/PanelConfigImageSection.vue +136 -0
- package/src/panel/components/config/PanelConfigMultiSelect.vue +434 -0
- package/src/panel/components/config/PanelConfigNodeInfo.vue +165 -0
- package/src/panel/components/config/PanelConfigReferenceSection.vue +77 -0
- package/src/panel/components/config/PanelConfigStyleSections.vue +208 -0
- package/src/panel/components/config/PanelConfigTextSection.vue +195 -0
- package/src/panel/components/config/PanelConfigVideoSection.vue +107 -0
- package/src/panel/components/config/shared.ts +74 -0
- package/src/panel/components/elementsLayerNodes.ts +830 -0
- package/src/panel/components/materialSidebarData.ts +85 -0
- package/src/panel/components/scope-config/ScopeConfigProvider.vue +153 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +234 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +192 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +42 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +20 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +63 -0
- package/src/panel/components/scope-config/scopeConfigContext.ts +17 -0
- package/src/panel/components/scope-config/useScopeConfig.ts +11 -0
- package/src/panel/constants/messages.ts +34 -0
- package/src/panel/constants/zIndex.ts +6 -0
- package/src/panel/hooks/usePanelElements.ts +1075 -0
- package/src/panel/hooks/useRafThrottledScroll.ts +25 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +240 -0
- package/src/panel/lib/panel-ruler-canvas.ts +139 -0
- package/src/panel/library/workspace-project-cache.ts +23 -0
- package/src/panel/library/workspace-project-db.ts +111 -0
- package/src/panel/library/workspace-project-sync.ts +41 -0
- package/src/panel/library/workspace-snapshot.ts +30 -0
- package/src/panel/parseOnlinePreviewSearchParams.ts +13 -0
- package/src/panel/scope/view-scope-store.ts +82 -0
- package/src/panel/types.ts +127 -0
- package/src/panel/utils/chartOptionBuilder.ts +327 -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 +46 -0
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +124 -0
- package/src/panel/utils/scope-template-spread.ts +229 -0
- package/src/panel/utils/scope-template-warnings.ts +243 -0
- package/src/panel/utils/scope-template.ts +97 -0
- package/src/panel/utils/updateElementDraft.ts +221 -0
- package/src/panel/viewportZoom.ts +26 -0
- package/src/tailwind.css +43 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { PanelElement } from "../types";
|
|
2
|
+
|
|
3
|
+
export function getGridSlotLayout(grid: PanelElement) {
|
|
4
|
+
const rows = Math.max(1, Math.floor(grid.gridRows ?? 2));
|
|
5
|
+
const cols = Math.max(1, Math.floor(grid.gridCols ?? 3));
|
|
6
|
+
const gap = Math.max(0, grid.gridGap ?? 8);
|
|
7
|
+
const padding = Math.max(0, grid.gridPadding ?? 10);
|
|
8
|
+
const innerWidth = Math.max(1, grid.width - padding * 2);
|
|
9
|
+
const innerHeight = Math.max(1, grid.height - padding * 2);
|
|
10
|
+
const cellWidth = Math.max(1, (innerWidth - gap * (cols - 1)) / cols);
|
|
11
|
+
const cellHeight = Math.max(1, (innerHeight - gap * (rows - 1)) / rows);
|
|
12
|
+
const slots: Array<{ index: number; x: number; y: number; width: number; height: number }> = [];
|
|
13
|
+
for (let r = 0; r < rows; r++) {
|
|
14
|
+
for (let c = 0; c < cols; c++) {
|
|
15
|
+
const x = grid.x + padding + c * (cellWidth + gap);
|
|
16
|
+
const y = grid.y + padding + r * (cellHeight + gap);
|
|
17
|
+
slots.push({
|
|
18
|
+
index: r * cols + c,
|
|
19
|
+
x,
|
|
20
|
+
y,
|
|
21
|
+
width: cellWidth,
|
|
22
|
+
height: cellHeight,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { slots, rows, cols, cellWidth, cellHeight, gap };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getGridChildSpanRect(grid: PanelElement, slotIndex: number, colSpan: number, rowSpan: number) {
|
|
30
|
+
const { rows, cols, cellWidth, cellHeight, gap, slots } = getGridSlotLayout(grid);
|
|
31
|
+
const total = rows * cols;
|
|
32
|
+
const safeIndex = Math.max(0, Math.min(total - 1, Math.floor(slotIndex || 0)));
|
|
33
|
+
const baseRow = Math.floor(safeIndex / cols);
|
|
34
|
+
const baseCol = safeIndex % cols;
|
|
35
|
+
const safeColSpan = Math.max(1, Math.min(cols - baseCol, Math.floor(colSpan || 1)));
|
|
36
|
+
const safeRowSpan = Math.max(1, Math.min(rows - baseRow, Math.floor(rowSpan || 1)));
|
|
37
|
+
const baseSlot = slots[safeIndex];
|
|
38
|
+
const width = safeColSpan * cellWidth + (safeColSpan - 1) * gap;
|
|
39
|
+
const height = safeRowSpan * cellHeight + (safeRowSpan - 1) * gap;
|
|
40
|
+
return {
|
|
41
|
+
index: safeIndex,
|
|
42
|
+
x: baseSlot?.x ?? grid.x,
|
|
43
|
+
y: baseSlot?.y ?? grid.y,
|
|
44
|
+
width: Math.max(1, width),
|
|
45
|
+
height: Math.max(1, height),
|
|
46
|
+
colSpan: safeColSpan,
|
|
47
|
+
rowSpan: safeRowSpan,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function inferSpanBySize(size: number, cellSize: number, gap: number, maxSpan: number) {
|
|
52
|
+
const safeMax = Math.max(1, Math.floor(maxSpan));
|
|
53
|
+
const unit = Math.max(1, cellSize + gap);
|
|
54
|
+
const ratio = (Math.max(1, size) + gap) / unit;
|
|
55
|
+
const whole = Math.floor(ratio);
|
|
56
|
+
const fraction = ratio - whole;
|
|
57
|
+
const promoted = fraction >= 0.72 ? whole + 1 : Math.max(1, whole);
|
|
58
|
+
return Math.max(1, Math.min(safeMax, promoted));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getGridSpanCellsForSnap(
|
|
62
|
+
rows: number,
|
|
63
|
+
cols: number,
|
|
64
|
+
slotIndex: number,
|
|
65
|
+
colSpan: number,
|
|
66
|
+
rowSpan: number
|
|
67
|
+
) {
|
|
68
|
+
const total = rows * cols;
|
|
69
|
+
const safeIndex = Math.max(0, Math.min(total - 1, Math.floor(slotIndex || 0)));
|
|
70
|
+
const startRow = Math.floor(safeIndex / cols);
|
|
71
|
+
const startCol = safeIndex % cols;
|
|
72
|
+
const safeColSpan = Math.max(1, Math.min(cols - startCol, Math.floor(colSpan || 1)));
|
|
73
|
+
const safeRowSpan = Math.max(1, Math.min(rows - startRow, Math.floor(rowSpan || 1)));
|
|
74
|
+
const indices: number[] = [];
|
|
75
|
+
for (let r = startRow; r < startRow + safeRowSpan; r++) {
|
|
76
|
+
for (let c = startCol; c < startCol + safeColSpan; c++) {
|
|
77
|
+
indices.push(r * cols + c);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
safeIndex,
|
|
82
|
+
startRow,
|
|
83
|
+
startCol,
|
|
84
|
+
colSpan: safeColSpan,
|
|
85
|
+
rowSpan: safeRowSpan,
|
|
86
|
+
indices,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getOccupiedSlotsForGrid(
|
|
91
|
+
elementsById: Map<string, PanelElement>,
|
|
92
|
+
gridId: string,
|
|
93
|
+
excludeId: string | undefined,
|
|
94
|
+
layerId: string
|
|
95
|
+
) {
|
|
96
|
+
const occupied = new Set<number>();
|
|
97
|
+
const grid = elementsById.get(gridId);
|
|
98
|
+
if (!grid || grid.materialType !== "grid") return occupied;
|
|
99
|
+
const { rows, cols } = getGridSlotLayout(grid);
|
|
100
|
+
for (const el of elementsById.values()) {
|
|
101
|
+
if (excludeId && el.id === excludeId) continue;
|
|
102
|
+
if (el.layerId !== layerId) continue;
|
|
103
|
+
if (el.parentGridId !== gridId) continue;
|
|
104
|
+
if (el.gridSlotIndex === undefined) continue;
|
|
105
|
+
const span = getGridSpanCellsForSnap(rows, cols, el.gridSlotIndex, el.gridColSpan ?? 1, el.gridRowSpan ?? 1);
|
|
106
|
+
span.indices.forEach((idx) => occupied.add(idx));
|
|
107
|
+
}
|
|
108
|
+
return occupied;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 新建节点落点吸附到指定图层上的网格(与 Moveable 吸附规则对齐) */
|
|
112
|
+
export function computeSnapPatchForNewElementOnLayer(
|
|
113
|
+
elementsById: Map<string, PanelElement>,
|
|
114
|
+
layerId: string,
|
|
115
|
+
x: number,
|
|
116
|
+
y: number,
|
|
117
|
+
width: number,
|
|
118
|
+
height: number,
|
|
119
|
+
excludeElementId?: string
|
|
120
|
+
): Partial<PanelElement> {
|
|
121
|
+
const centerX = x + width / 2;
|
|
122
|
+
const centerY = y + height / 2;
|
|
123
|
+
let nearest: {
|
|
124
|
+
gridId: string;
|
|
125
|
+
slotIndex: number;
|
|
126
|
+
slotX: number;
|
|
127
|
+
slotY: number;
|
|
128
|
+
width: number;
|
|
129
|
+
height: number;
|
|
130
|
+
colSpan: number;
|
|
131
|
+
rowSpan: number;
|
|
132
|
+
distance: number;
|
|
133
|
+
} | null = null;
|
|
134
|
+
|
|
135
|
+
for (const [, el] of elementsById.entries()) {
|
|
136
|
+
if (el.materialType !== "grid" || el.layerId !== layerId) continue;
|
|
137
|
+
const layout = getGridSlotLayout(el);
|
|
138
|
+
const { slots, cellWidth, cellHeight, rows, cols, gap } = layout;
|
|
139
|
+
const occupiedSlots = getOccupiedSlotsForGrid(elementsById, el.id, excludeElementId, layerId);
|
|
140
|
+
const threshold = Math.max(8, el.gridSnapThreshold ?? 36);
|
|
141
|
+
const inferredColSpan = inferSpanBySize(width, cellWidth, gap, cols);
|
|
142
|
+
const inferredRowSpan = inferSpanBySize(height, cellHeight, gap, rows);
|
|
143
|
+
const movingColSpan = Math.max(1, inferredColSpan);
|
|
144
|
+
const movingRowSpan = Math.max(1, inferredRowSpan);
|
|
145
|
+
const insideGridBounds =
|
|
146
|
+
centerX >= el.x &&
|
|
147
|
+
centerX <= el.x + Math.max(1, el.width) &&
|
|
148
|
+
centerY >= el.y &&
|
|
149
|
+
centerY <= el.y + Math.max(1, el.height);
|
|
150
|
+
|
|
151
|
+
for (const slot of slots) {
|
|
152
|
+
const spanCells = getGridSpanCellsForSnap(rows, cols, slot.index, movingColSpan, movingRowSpan);
|
|
153
|
+
if (spanCells.colSpan !== movingColSpan || spanCells.rowSpan !== movingRowSpan) continue;
|
|
154
|
+
const blocked = spanCells.indices.some((idx) => occupiedSlots.has(idx));
|
|
155
|
+
if (blocked) continue;
|
|
156
|
+
const slotCenterX = slot.x + cellWidth / 2;
|
|
157
|
+
const slotCenterY = slot.y + cellHeight / 2;
|
|
158
|
+
const distance = Math.hypot(slotCenterX - centerX, slotCenterY - centerY);
|
|
159
|
+
if (!insideGridBounds && distance > threshold) continue;
|
|
160
|
+
if (!nearest || distance < nearest.distance) {
|
|
161
|
+
const spanWidth = spanCells.colSpan * cellWidth + (spanCells.colSpan - 1) * gap;
|
|
162
|
+
const spanHeight = spanCells.rowSpan * cellHeight + (spanCells.rowSpan - 1) * gap;
|
|
163
|
+
nearest = {
|
|
164
|
+
gridId: el.id,
|
|
165
|
+
slotIndex: spanCells.safeIndex,
|
|
166
|
+
slotX: slot.x,
|
|
167
|
+
slotY: slot.y,
|
|
168
|
+
width: spanWidth,
|
|
169
|
+
height: spanHeight,
|
|
170
|
+
colSpan: spanCells.colSpan,
|
|
171
|
+
rowSpan: spanCells.rowSpan,
|
|
172
|
+
distance,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!nearest) return {};
|
|
179
|
+
return {
|
|
180
|
+
x: nearest.slotX,
|
|
181
|
+
y: nearest.slotY,
|
|
182
|
+
parentGridId: nearest.gridId,
|
|
183
|
+
gridSlotIndex: nearest.slotIndex,
|
|
184
|
+
gridColSpan: nearest.colSpan,
|
|
185
|
+
gridRowSpan: nearest.rowSpan,
|
|
186
|
+
width: nearest.width,
|
|
187
|
+
height: nearest.height,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Node, State } from "@arronqzy/rx-store";
|
|
2
|
+
import type { PanelElement, PanelLayer } from "../types";
|
|
3
|
+
import { DEFAULT_LAYER_ID, DEFAULT_LAYER } from "./panelElementDefaults";
|
|
4
|
+
import { isPanelElementNode } from "./panelElementNodes";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 沿 mappingSourceNodeId 追溯到「源画布节点」id。
|
|
8
|
+
* 多层映射 / 中间克隆指向时仍与同源簇对齐;用于批量同步与节点树一致。
|
|
9
|
+
*/
|
|
10
|
+
export function canonicalMappingFamilyRootId(
|
|
11
|
+
elementById: Map<string, PanelElement>,
|
|
12
|
+
nodeId: string
|
|
13
|
+
): string {
|
|
14
|
+
const visited = new Set<string>();
|
|
15
|
+
let cur = nodeId;
|
|
16
|
+
for (;;) {
|
|
17
|
+
if (visited.has(cur)) return cur;
|
|
18
|
+
visited.add(cur);
|
|
19
|
+
const el = elementById.get(cur);
|
|
20
|
+
const next = el?.mappingSourceNodeId;
|
|
21
|
+
if (!next || next === cur) return cur;
|
|
22
|
+
cur = next;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 映射图层克隆节点:基准图层上的节点 id → 该映射图层上与之源一致的节点 id */
|
|
27
|
+
export function findCloneIdForSourceNodeOnMappingLayer(
|
|
28
|
+
sourceElementId: string,
|
|
29
|
+
mappingLayerId: string,
|
|
30
|
+
elementsById: Map<string, PanelElement>
|
|
31
|
+
): string | undefined {
|
|
32
|
+
const root = canonicalMappingFamilyRootId(elementsById, sourceElementId);
|
|
33
|
+
for (const el of elementsById.values()) {
|
|
34
|
+
if (el.layerId !== mappingLayerId) continue;
|
|
35
|
+
if (!el.mappingSourceNodeId) continue;
|
|
36
|
+
if (canonicalMappingFamilyRootId(elementsById, el.mappingSourceNodeId) !== root) continue;
|
|
37
|
+
return el.id;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 克隆网格 id → 逻辑上的「源图层网格」同源 id(跨多层映射);非法/缺失父节点时返回 undefined */
|
|
43
|
+
export function logicalGridParentIdFromConcrete(
|
|
44
|
+
concreteParentGridId: string | undefined,
|
|
45
|
+
elementById: Map<string, PanelElement>
|
|
46
|
+
): string | undefined {
|
|
47
|
+
if (!concreteParentGridId) return undefined;
|
|
48
|
+
const g = elementById.get(concreteParentGridId);
|
|
49
|
+
if (!g || g.materialType !== "grid") return undefined;
|
|
50
|
+
return canonicalMappingFamilyRootId(elementById, g.id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 逻辑源网格同源 id → 在指定图层上的那张网格实例 id */
|
|
54
|
+
export function concreteGridParentIdForLayer(
|
|
55
|
+
logicalSourceGridId: string | undefined,
|
|
56
|
+
targetLayerId: string,
|
|
57
|
+
elements: Iterable<PanelElement>
|
|
58
|
+
): string | undefined {
|
|
59
|
+
if (!logicalSourceGridId) return undefined;
|
|
60
|
+
const byId = new Map<string, PanelElement>();
|
|
61
|
+
for (const el of elements) byId.set(el.id, el);
|
|
62
|
+
for (const el of elements) {
|
|
63
|
+
if (el.layerId !== targetLayerId || el.materialType !== "grid") continue;
|
|
64
|
+
if (canonicalMappingFamilyRootId(byId, el.id) === logicalSourceGridId) return el.id;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 打开映射图层时:把选中节点在同图层内的网格子树一并纳入,避免出现「父网格+兄弟节点已克隆、中间嵌套网格丢失」的断层结构 */
|
|
70
|
+
export function expandMappingSeedsWithGridDescendants(nodeById: Map<string, PanelElement>, seedIds: string[]): string[] {
|
|
71
|
+
const expanded = new Set<string>();
|
|
72
|
+
const queue: string[] = [];
|
|
73
|
+
for (const id of seedIds) {
|
|
74
|
+
if (!id || !nodeById.has(id)) continue;
|
|
75
|
+
expanded.add(id);
|
|
76
|
+
queue.push(id);
|
|
77
|
+
}
|
|
78
|
+
while (queue.length > 0) {
|
|
79
|
+
const id = queue.shift()!;
|
|
80
|
+
const anchor = nodeById.get(id);
|
|
81
|
+
const layerId = anchor?.layerId;
|
|
82
|
+
for (const el of nodeById.values()) {
|
|
83
|
+
if (el.parentGridId !== id) continue;
|
|
84
|
+
if (layerId !== undefined && el.layerId !== layerId) continue;
|
|
85
|
+
if (expanded.has(el.id)) continue;
|
|
86
|
+
expanded.add(el.id);
|
|
87
|
+
queue.push(el.id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [...expanded];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function removeMappingLayersBySourceIds(
|
|
94
|
+
draft: State,
|
|
95
|
+
sourceIds: Set<string>,
|
|
96
|
+
fallbackActiveLayerId: string = DEFAULT_LAYER_ID
|
|
97
|
+
) {
|
|
98
|
+
if (sourceIds.size === 0) return;
|
|
99
|
+
const nodes = draft.root.children ?? [];
|
|
100
|
+
const mappingLayerIds = new Set<string>();
|
|
101
|
+
nodes.forEach((n) => {
|
|
102
|
+
if (!isPanelElementNode(n) || !n.props) return;
|
|
103
|
+
const props = n.props as PanelElement;
|
|
104
|
+
if (!props.mappingSourceNodeId) return;
|
|
105
|
+
if (!sourceIds.has(props.mappingSourceNodeId)) return;
|
|
106
|
+
mappingLayerIds.add(props.layerId);
|
|
107
|
+
});
|
|
108
|
+
if (mappingLayerIds.size === 0) return;
|
|
109
|
+
removeLayersByIds(draft, mappingLayerIds, fallbackActiveLayerId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function removeLayersByIds(draft: State, layerIds: Set<string>, fallbackActiveLayerId: string = DEFAULT_LAYER_ID) {
|
|
113
|
+
if (layerIds.size === 0) return;
|
|
114
|
+
const nodes = draft.root.children ?? [];
|
|
115
|
+
|
|
116
|
+
draft.root.children = nodes.filter((n) => {
|
|
117
|
+
if (!isPanelElementNode(n) || !n.props) return true;
|
|
118
|
+
const props = n.props as PanelElement;
|
|
119
|
+
return !layerIds.has(props.layerId);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
draft.variables = draft.variables ?? {};
|
|
123
|
+
const layers = (draft.variables.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
|
|
124
|
+
draft.variables.layers = layers.filter((layer) => !layerIds.has(layer.id));
|
|
125
|
+
const activeLayerId = (draft.variables.activeLayerId as string | undefined) ?? fallbackActiveLayerId;
|
|
126
|
+
if (layerIds.has(activeLayerId)) {
|
|
127
|
+
draft.variables.activeLayerId =
|
|
128
|
+
(draft.variables.layers[0] as PanelLayer | undefined)?.id ?? fallbackActiveLayerId;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getMaxZIndexByLayer(nodes: Node[], layerId: string): number {
|
|
133
|
+
let maxZ = 0;
|
|
134
|
+
nodes.forEach((n) => {
|
|
135
|
+
if (!isPanelElementNode(n) || !n.props) return;
|
|
136
|
+
const props = n.props as PanelElement;
|
|
137
|
+
if (props.layerId !== layerId) return;
|
|
138
|
+
const z = typeof props.zIndex === "number" ? props.zIndex : 1;
|
|
139
|
+
if (z > maxZ) maxZ = z;
|
|
140
|
+
});
|
|
141
|
+
return maxZ;
|
|
142
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { PanelChartConfig, PanelLayer } from "../types";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_NODE_NAME_MAP: Record<string, string> = {
|
|
4
|
+
bar: "柱状图",
|
|
5
|
+
line: "折线图",
|
|
6
|
+
pie: "饼图",
|
|
7
|
+
area: "面积图",
|
|
8
|
+
scatter: "散点图",
|
|
9
|
+
radar: "雷达图",
|
|
10
|
+
gauge: "仪表盘",
|
|
11
|
+
funnel: "漏斗图",
|
|
12
|
+
text: "文本",
|
|
13
|
+
grid: "网格布局",
|
|
14
|
+
image: "图片",
|
|
15
|
+
video: "视频",
|
|
16
|
+
audio: "音频",
|
|
17
|
+
reference: "引用组件",
|
|
18
|
+
geometry: "几何",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_LAYER_ID = "layer-1";
|
|
22
|
+
|
|
23
|
+
export function getDefaultNodeName(materialType: string): string {
|
|
24
|
+
return DEFAULT_NODE_NAME_MAP[materialType] ?? materialType;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_LAYER: PanelLayer = {
|
|
28
|
+
id: DEFAULT_LAYER_ID,
|
|
29
|
+
name: "图层1",
|
|
30
|
+
locked: false,
|
|
31
|
+
editable: false,
|
|
32
|
+
isPrimary: true,
|
|
33
|
+
isMapping: false,
|
|
34
|
+
mappingBaseLayerId: undefined,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function normalizePrimaryLayer(layers: PanelLayer[]): PanelLayer[] {
|
|
38
|
+
if (layers.length === 0) return [DEFAULT_LAYER];
|
|
39
|
+
const explicitPrimary = layers.find((layer) => layer.isPrimary);
|
|
40
|
+
const primaryId = explicitPrimary?.id ?? layers[0].id;
|
|
41
|
+
return layers.map((layer) => {
|
|
42
|
+
if (layer.id === primaryId) return { ...layer, isPrimary: true };
|
|
43
|
+
return { ...layer, isPrimary: false };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function randomId(prefix: string) {
|
|
48
|
+
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getDefaultSizeByMaterial(materialType: string) {
|
|
52
|
+
switch (materialType) {
|
|
53
|
+
case "text":
|
|
54
|
+
return { width: 180, height: 56 };
|
|
55
|
+
case "rect":
|
|
56
|
+
return { width: 180, height: 120 };
|
|
57
|
+
case "grid":
|
|
58
|
+
return { width: 320, height: 220 };
|
|
59
|
+
case "image":
|
|
60
|
+
return { width: 220, height: 140 };
|
|
61
|
+
case "video":
|
|
62
|
+
return { width: 260, height: 150 };
|
|
63
|
+
case "audio":
|
|
64
|
+
return { width: 260, height: 90 };
|
|
65
|
+
case "gauge":
|
|
66
|
+
return { width: 260, height: 180 };
|
|
67
|
+
case "reference":
|
|
68
|
+
return { width: 280, height: 180 };
|
|
69
|
+
case "geometry":
|
|
70
|
+
return { width: 220, height: 220 };
|
|
71
|
+
default:
|
|
72
|
+
return { width: 220, height: 130 };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getDefaultTextContent(materialType: string) {
|
|
77
|
+
if (materialType !== "text") return {};
|
|
78
|
+
return {
|
|
79
|
+
textHtml: "<p>双击输入文本</p>",
|
|
80
|
+
textAllowInput: true,
|
|
81
|
+
textFontSize: 14,
|
|
82
|
+
textFontWeight: "400",
|
|
83
|
+
textLineHeight: 1.6,
|
|
84
|
+
textAlign: "left",
|
|
85
|
+
} as const;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getDefaultGridConfig(materialType: string) {
|
|
89
|
+
if (materialType !== "grid") return {};
|
|
90
|
+
return {
|
|
91
|
+
gridRows: 2,
|
|
92
|
+
gridCols: 3,
|
|
93
|
+
gridGap: 8,
|
|
94
|
+
gridPadding: 10,
|
|
95
|
+
gridSnapThreshold: 36,
|
|
96
|
+
} as const;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getDefaultChartConfig(materialType: string): PanelChartConfig | undefined {
|
|
100
|
+
if (!["bar", "line", "pie", "area", "scatter", "radar", "gauge", "funnel"].includes(materialType))
|
|
101
|
+
return undefined;
|
|
102
|
+
const common = {
|
|
103
|
+
color: "#3b82f6",
|
|
104
|
+
renderMode: "canvas" as const,
|
|
105
|
+
labels: ["A", "B", "C", "D"],
|
|
106
|
+
values: [12, 18, 9, 24],
|
|
107
|
+
};
|
|
108
|
+
if (materialType === "bar") {
|
|
109
|
+
return {
|
|
110
|
+
title: "柱状图",
|
|
111
|
+
...common,
|
|
112
|
+
barWidth: 24,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (materialType === "line") {
|
|
116
|
+
return {
|
|
117
|
+
title: "折线图",
|
|
118
|
+
...common,
|
|
119
|
+
smooth: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (materialType === "area") {
|
|
123
|
+
return {
|
|
124
|
+
title: "面积图",
|
|
125
|
+
...common,
|
|
126
|
+
smooth: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (materialType === "scatter") {
|
|
130
|
+
return {
|
|
131
|
+
title: "散点图",
|
|
132
|
+
...common,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (materialType === "radar") {
|
|
136
|
+
return {
|
|
137
|
+
title: "雷达图",
|
|
138
|
+
...common,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (materialType === "gauge") {
|
|
142
|
+
return {
|
|
143
|
+
title: "仪表盘",
|
|
144
|
+
color: "#3b82f6",
|
|
145
|
+
renderMode: "canvas",
|
|
146
|
+
values: [68],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (materialType === "funnel") {
|
|
150
|
+
return {
|
|
151
|
+
title: "漏斗图",
|
|
152
|
+
...common,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
title: "饼图",
|
|
157
|
+
...common,
|
|
158
|
+
pieInnerRadius: 30,
|
|
159
|
+
pieOuterRadius: 65,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Node } from "@arronqzy/rx-store";
|
|
2
|
+
import type { PanelElement } from "../types";
|
|
3
|
+
|
|
4
|
+
export function isPanelElementNode(node: Node): boolean {
|
|
5
|
+
const props = node.props as Partial<PanelElement> | undefined;
|
|
6
|
+
return !!props && typeof props.id === "string" && typeof props.layerId === "string";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function clonePanelElement(el: PanelElement): PanelElement {
|
|
10
|
+
return {
|
|
11
|
+
...el,
|
|
12
|
+
chart: el.chart ? { ...el.chart, option: el.chart.option ? { ...el.chart.option } : undefined } : undefined,
|
|
13
|
+
style: el.style ? { ...el.style } : undefined,
|
|
14
|
+
refSnapshot: el.refSnapshot ? el.refSnapshot.map(clonePanelElement) : undefined,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildDeepReferenceSnapshot(
|
|
19
|
+
allElements: PanelElement[],
|
|
20
|
+
layerId: string,
|
|
21
|
+
visitedLayers = new Set<string>()
|
|
22
|
+
): PanelElement[] {
|
|
23
|
+
if (visitedLayers.has(layerId)) return [];
|
|
24
|
+
const nextVisited = new Set(visitedLayers);
|
|
25
|
+
nextVisited.add(layerId);
|
|
26
|
+
const layerNodes = allElements.filter((el) => el.layerId === layerId);
|
|
27
|
+
return layerNodes.map((el) => {
|
|
28
|
+
const cloned = clonePanelElement(el);
|
|
29
|
+
if (cloned.materialType === "reference" && cloned.refLayerId) {
|
|
30
|
+
cloned.refCopyMode = "deep";
|
|
31
|
+
cloned.refSnapshot = buildDeepReferenceSnapshot(allElements, cloned.refLayerId, nextVisited);
|
|
32
|
+
}
|
|
33
|
+
return cloned;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { State } from "@arronqzy/rx-store";
|
|
2
|
+
import type { PanelElement, PanelLayer } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_LAYER,
|
|
5
|
+
DEFAULT_LAYER_ID,
|
|
6
|
+
normalizePrimaryLayer,
|
|
7
|
+
} from "./panelElementDefaults";
|
|
8
|
+
|
|
9
|
+
export function normalizeImportedPanelState(state: unknown): State | null {
|
|
10
|
+
if (!state || typeof state !== "object") return null;
|
|
11
|
+
const raw = state as State;
|
|
12
|
+
if (!raw.root || typeof raw.root !== "object") return null;
|
|
13
|
+
if (!raw.root.id || !raw.root.type) return null;
|
|
14
|
+
|
|
15
|
+
const next = JSON.parse(JSON.stringify(raw)) as State;
|
|
16
|
+
next.root.children = Array.isArray(next.root.children) ? next.root.children : [];
|
|
17
|
+
next.variables = next.variables ?? {};
|
|
18
|
+
if (!Array.isArray(next.variables.layers)) {
|
|
19
|
+
next.variables.layers = [DEFAULT_LAYER];
|
|
20
|
+
}
|
|
21
|
+
next.variables.layers = normalizePrimaryLayer(next.variables.layers as PanelLayer[]);
|
|
22
|
+
if (typeof next.variables.activeLayerId !== "string") {
|
|
23
|
+
next.variables.activeLayerId =
|
|
24
|
+
(next.variables.layers[0] as PanelLayer | undefined)?.id ?? DEFAULT_LAYER_ID;
|
|
25
|
+
}
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseAllPanelElements(state: State): PanelElement[] {
|
|
30
|
+
return (state.root.children ?? [])
|
|
31
|
+
.filter((node) => {
|
|
32
|
+
const props = node.props as Partial<PanelElement> | undefined;
|
|
33
|
+
if (!props || typeof props.layerId !== "string") return false;
|
|
34
|
+
const id = props.id ?? node.id;
|
|
35
|
+
return typeof id === "string" && id.length > 0;
|
|
36
|
+
})
|
|
37
|
+
.map((node) => {
|
|
38
|
+
const props = node.props as PanelElement;
|
|
39
|
+
return {
|
|
40
|
+
...props,
|
|
41
|
+
id: props.id ?? node.id,
|
|
42
|
+
zIndex: typeof props.zIndex === "number" ? props.zIndex : 1,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parsePanelLayers(state: State): PanelLayer[] {
|
|
48
|
+
const raw = (state.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
|
|
49
|
+
return normalizePrimaryLayer(raw);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getActiveLayerId(state: State): string {
|
|
53
|
+
return (state.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolvePreviewLayerElements(
|
|
57
|
+
allElements: PanelElement[],
|
|
58
|
+
layers: PanelLayer[],
|
|
59
|
+
activeLayerId: string
|
|
60
|
+
): PanelElement[] {
|
|
61
|
+
const primary = layers.find((layer) => layer.isPrimary) ?? layers[0];
|
|
62
|
+
if (primary) {
|
|
63
|
+
const onPrimary = allElements.filter((el) => el.layerId === primary.id);
|
|
64
|
+
if (onPrimary.length > 0) return onPrimary;
|
|
65
|
+
}
|
|
66
|
+
const onActive = allElements.filter((el) => el.layerId === activeLayerId);
|
|
67
|
+
if (onActive.length > 0) return onActive;
|
|
68
|
+
const mappingLayerIds = new Set(
|
|
69
|
+
layers.filter((layer) => layer.isMapping).map((layer) => layer.id)
|
|
70
|
+
);
|
|
71
|
+
const nonMapping = allElements.filter((el) => !mappingLayerIds.has(el.layerId));
|
|
72
|
+
return nonMapping.length > 0 ? nonMapping : allElements;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type PanelSceneBounds = {
|
|
76
|
+
minX: number;
|
|
77
|
+
minY: number;
|
|
78
|
+
width: number;
|
|
79
|
+
height: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function computePanelSceneBounds(elements: PanelElement[]): PanelSceneBounds {
|
|
83
|
+
if (!elements.length) {
|
|
84
|
+
return { minX: 0, minY: 0, width: 1, height: 1 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let minX = Infinity;
|
|
88
|
+
let minY = Infinity;
|
|
89
|
+
let maxX = -Infinity;
|
|
90
|
+
let maxY = -Infinity;
|
|
91
|
+
|
|
92
|
+
for (const el of elements) {
|
|
93
|
+
const w = Math.max(1, el.width);
|
|
94
|
+
const h = Math.max(1, el.height);
|
|
95
|
+
const rad = ((el.rotate ?? 0) * Math.PI) / 180;
|
|
96
|
+
const absCos = Math.abs(Math.cos(rad));
|
|
97
|
+
const absSin = Math.abs(Math.sin(rad));
|
|
98
|
+
const bw = w * absCos + h * absSin;
|
|
99
|
+
const bh = w * absSin + h * absCos;
|
|
100
|
+
const cx = el.x + w / 2;
|
|
101
|
+
const cy = el.y + h / 2;
|
|
102
|
+
const left = cx - bw / 2;
|
|
103
|
+
const top = cy - bh / 2;
|
|
104
|
+
const right = cx + bw / 2;
|
|
105
|
+
const bottom = cy + bh / 2;
|
|
106
|
+
minX = Math.min(minX, left);
|
|
107
|
+
minY = Math.min(minY, top);
|
|
108
|
+
maxX = Math.max(maxX, right);
|
|
109
|
+
maxY = Math.max(maxY, bottom);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
minX,
|
|
114
|
+
minY,
|
|
115
|
+
width: Math.max(1, maxX - minX),
|
|
116
|
+
height: Math.max(1, maxY - minY),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const PREVIEW_LAYOUT_EVENT = "arronqzy-preview-layout";
|
|
121
|
+
|
|
122
|
+
export function notifyPreviewLayoutChanged() {
|
|
123
|
+
window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
|
|
124
|
+
}
|