@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.
Files changed (75) hide show
  1. package/README.md +50 -0
  2. package/package.json +49 -0
  3. package/src/env.d.ts +62 -0
  4. package/src/index.ts +4 -0
  5. package/src/panel/VueViewOnlinePreview.vue +276 -0
  6. package/src/panel/VueViewPanel.vue +871 -0
  7. package/src/panel/components/ConfigHintIcon.vue +34 -0
  8. package/src/panel/components/ElementsLayer.vue +165 -0
  9. package/src/panel/components/MaterialPreview.vue +135 -0
  10. package/src/panel/components/MaterialSidebar.vue +526 -0
  11. package/src/panel/components/MaterialSidebarTreeNode.vue +305 -0
  12. package/src/panel/components/MoveableLayer.vue +859 -0
  13. package/src/panel/components/PanelCanvas.vue +630 -0
  14. package/src/panel/components/PanelConfigSidebar.vue +397 -0
  15. package/src/panel/components/PanelRulers.vue +177 -0
  16. package/src/panel/components/SelectLayer.vue +115 -0
  17. package/src/panel/components/ViewElementScopePanel.vue +76 -0
  18. package/src/panel/components/WorkspaceConfigSidebar.vue +147 -0
  19. package/src/panel/components/WorkspaceProjectNav.vue +192 -0
  20. package/src/panel/components/WorkspaceStageSplit.vue +258 -0
  21. package/src/panel/components/config/ConfigColorField.vue +52 -0
  22. package/src/panel/components/config/ConfigFieldGroup.vue +20 -0
  23. package/src/panel/components/config/ConfigSection.vue +50 -0
  24. package/src/panel/components/config/PanelConfigAudioSection.vue +256 -0
  25. package/src/panel/components/config/PanelConfigChartSection.vue +650 -0
  26. package/src/panel/components/config/PanelConfigGeometrySection.vue +209 -0
  27. package/src/panel/components/config/PanelConfigGridChildSpan.vue +68 -0
  28. package/src/panel/components/config/PanelConfigGridSection.vue +103 -0
  29. package/src/panel/components/config/PanelConfigImageSection.vue +136 -0
  30. package/src/panel/components/config/PanelConfigMultiSelect.vue +434 -0
  31. package/src/panel/components/config/PanelConfigNodeInfo.vue +165 -0
  32. package/src/panel/components/config/PanelConfigReferenceSection.vue +77 -0
  33. package/src/panel/components/config/PanelConfigStyleSections.vue +208 -0
  34. package/src/panel/components/config/PanelConfigTextSection.vue +195 -0
  35. package/src/panel/components/config/PanelConfigVideoSection.vue +107 -0
  36. package/src/panel/components/config/shared.ts +74 -0
  37. package/src/panel/components/elementsLayerNodes.ts +830 -0
  38. package/src/panel/components/materialSidebarData.ts +85 -0
  39. package/src/panel/components/scope-config/ScopeConfigProvider.vue +153 -0
  40. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +234 -0
  41. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +192 -0
  42. package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +42 -0
  43. package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +20 -0
  44. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +63 -0
  45. package/src/panel/components/scope-config/scopeConfigContext.ts +17 -0
  46. package/src/panel/components/scope-config/useScopeConfig.ts +11 -0
  47. package/src/panel/constants/messages.ts +34 -0
  48. package/src/panel/constants/zIndex.ts +6 -0
  49. package/src/panel/hooks/usePanelElements.ts +1075 -0
  50. package/src/panel/hooks/useRafThrottledScroll.ts +25 -0
  51. package/src/panel/hooks/useWorkspaceProjects.ts +240 -0
  52. package/src/panel/lib/panel-ruler-canvas.ts +139 -0
  53. package/src/panel/library/workspace-project-cache.ts +23 -0
  54. package/src/panel/library/workspace-project-db.ts +111 -0
  55. package/src/panel/library/workspace-project-sync.ts +41 -0
  56. package/src/panel/library/workspace-snapshot.ts +30 -0
  57. package/src/panel/parseOnlinePreviewSearchParams.ts +13 -0
  58. package/src/panel/scope/view-scope-store.ts +82 -0
  59. package/src/panel/types.ts +127 -0
  60. package/src/panel/utils/chartOptionBuilder.ts +327 -0
  61. package/src/panel/utils/gridPlacement.ts +189 -0
  62. package/src/panel/utils/mappingLayerOps.ts +142 -0
  63. package/src/panel/utils/panelElementDefaults.ts +161 -0
  64. package/src/panel/utils/panelElementNodes.ts +35 -0
  65. package/src/panel/utils/panelStateIO.ts +124 -0
  66. package/src/panel/utils/scope-autocomplete.ts +114 -0
  67. package/src/panel/utils/scope-field-labels.ts +46 -0
  68. package/src/panel/utils/scope-template-chart.ts +92 -0
  69. package/src/panel/utils/scope-template-preview.ts +124 -0
  70. package/src/panel/utils/scope-template-spread.ts +229 -0
  71. package/src/panel/utils/scope-template-warnings.ts +243 -0
  72. package/src/panel/utils/scope-template.ts +97 -0
  73. package/src/panel/utils/updateElementDraft.ts +221 -0
  74. package/src/panel/viewportZoom.ts +26 -0
  75. package/src/tailwind.css +43 -0
@@ -0,0 +1,243 @@
1
+ import type { PanelElement } from "../types";
2
+ import { hasScopeTemplate } from "./scope-template";
3
+ import { resolveScopeFieldLabel } from "./scope-field-labels";
4
+ import {
5
+ SCOPE_SPREAD_TEMPLATE_RE,
6
+ buildSpreadInvalidExpressionWarningMessage,
7
+ buildSpreadNotArrayWarningMessage,
8
+ formatScopePathSegments,
9
+ isSpreadTemplateBrace,
10
+ parseScopePathSegments,
11
+ resolveSpreadScopePath,
12
+ } from "./scope-template-spread";
13
+
14
+ export type ScopeTemplateWarningKind =
15
+ | "missing-path"
16
+ | "spread-not-array"
17
+ | "spread-invalid-expression";
18
+
19
+ export type ScopeTemplateWarning = {
20
+ fieldId: string;
21
+ fieldLabel: string;
22
+ template: string;
23
+ expression: string;
24
+ missingPath: string;
25
+ message: string;
26
+ kind: ScopeTemplateWarningKind;
27
+ };
28
+
29
+ const SCOPE_PATH_RE = /scope(?:\?\.|\.)(\w+(?:\?\.\w+)*)/g;
30
+
31
+ const SKIP_FIELD_PREFIXES = new Set(["refSnapshot", "geometrySketchDataUrl"]);
32
+
33
+ function extractScopePropertyPaths(expression: string): string[] {
34
+ const paths = new Set<string>();
35
+ let match: RegExpExecArray | null;
36
+ const re = new RegExp(SCOPE_PATH_RE.source, "g");
37
+ while ((match = re.exec(expression)) !== null) {
38
+ paths.add(match[1].replace(/\?\./g, "."));
39
+ }
40
+ return [...paths];
41
+ }
42
+
43
+ function pathExistsInScope(scope: unknown, path: string): boolean {
44
+ if (scope === null || scope === undefined) return false;
45
+ const parts = path.split(".").filter(Boolean);
46
+ let current: unknown = scope;
47
+ for (const part of parts) {
48
+ if (current === null || typeof current !== "object" || Array.isArray(current)) {
49
+ return false;
50
+ }
51
+ if (!Object.prototype.hasOwnProperty.call(current, part)) {
52
+ return false;
53
+ }
54
+ current = (current as Record<string, unknown>)[part];
55
+ }
56
+ return true;
57
+ }
58
+
59
+ function analyzeSpreadTemplateValue(
60
+ fieldId: string,
61
+ value: string,
62
+ scope: unknown
63
+ ): ScopeTemplateWarning[] {
64
+ const warnings: ScopeTemplateWarning[] = [];
65
+ const fieldLabel = resolveScopeFieldLabel(fieldId);
66
+ const spreadRe = new RegExp(SCOPE_SPREAD_TEMPLATE_RE.source, "g");
67
+ let match: RegExpExecArray | null;
68
+
69
+ while ((match = spreadRe.exec(value)) !== null) {
70
+ const spreadTemplate = match[0];
71
+ const expression = match[1].trim();
72
+ const segments = parseScopePathSegments(expression);
73
+
74
+ if (segments === null || segments.length === 0) {
75
+ warnings.push({
76
+ fieldId,
77
+ fieldLabel,
78
+ template: value,
79
+ expression,
80
+ missingPath: "",
81
+ kind: "spread-invalid-expression",
82
+ message: buildSpreadInvalidExpressionWarningMessage(
83
+ fieldLabel,
84
+ spreadTemplate
85
+ ),
86
+ });
87
+ continue;
88
+ }
89
+
90
+ const resolved = resolveSpreadScopePath(expression, scope);
91
+ if (resolved.ok) continue;
92
+
93
+ if (resolved.issue === "invalid-expression") {
94
+ warnings.push({
95
+ fieldId,
96
+ fieldLabel,
97
+ template: value,
98
+ expression,
99
+ missingPath: "",
100
+ kind: "spread-invalid-expression",
101
+ message: buildSpreadInvalidExpressionWarningMessage(
102
+ fieldLabel,
103
+ spreadTemplate
104
+ ),
105
+ });
106
+ continue;
107
+ }
108
+
109
+ const arrayPath =
110
+ resolved.arrayPath ?? formatScopePathSegments(segments.slice(0, -1));
111
+
112
+ if (resolved.issue === "path-missing") {
113
+ if (!pathExistsInScope(scope, arrayPath)) {
114
+ warnings.push({
115
+ fieldId,
116
+ fieldLabel,
117
+ template: value,
118
+ expression,
119
+ missingPath: arrayPath,
120
+ kind: "missing-path",
121
+ message: `「${fieldLabel}」引用了 scope.${arrayPath},但当前 scope 数据中并没有「${arrayPath}」字段,建议检查一下。`,
122
+ });
123
+ }
124
+ continue;
125
+ }
126
+
127
+ if (resolved.issue === "not-array") {
128
+ warnings.push({
129
+ fieldId,
130
+ fieldLabel,
131
+ template: value,
132
+ expression,
133
+ missingPath: arrayPath,
134
+ kind: "spread-not-array",
135
+ message: buildSpreadNotArrayWarningMessage(
136
+ fieldLabel,
137
+ spreadTemplate,
138
+ expression,
139
+ arrayPath
140
+ ),
141
+ });
142
+ }
143
+ }
144
+
145
+ return warnings;
146
+ }
147
+
148
+ function analyzeTemplateValue(
149
+ fieldId: string,
150
+ value: string,
151
+ scope: unknown
152
+ ): ScopeTemplateWarning[] {
153
+ if (!hasScopeTemplate(value)) return [];
154
+
155
+ const warnings: ScopeTemplateWarning[] = [
156
+ ...analyzeSpreadTemplateValue(fieldId, value, scope),
157
+ ];
158
+ const fieldLabel = resolveScopeFieldLabel(fieldId);
159
+ const expressionRe = /\{([^}]+)\}/g;
160
+ let match: RegExpExecArray | null;
161
+
162
+ while ((match = expressionRe.exec(value)) !== null) {
163
+ if (isSpreadTemplateBrace(value, match.index)) continue;
164
+ const expression = match[1].trim();
165
+ const paths = extractScopePropertyPaths(expression);
166
+ for (const path of paths) {
167
+ if (pathExistsInScope(scope, path)) continue;
168
+ warnings.push({
169
+ fieldId,
170
+ fieldLabel,
171
+ template: value,
172
+ expression,
173
+ missingPath: path,
174
+ kind: "missing-path",
175
+ message: `「${fieldLabel}」引用了 scope.${path},但当前 scope 数据中并没有「${path}」字段,建议检查一下。`,
176
+ });
177
+ }
178
+ }
179
+
180
+ return warnings;
181
+ }
182
+
183
+ function shouldSkipField(fieldId: string): boolean {
184
+ return [...SKIP_FIELD_PREFIXES].some((prefix) => fieldId.startsWith(prefix));
185
+ }
186
+
187
+ function walkElementStrings(
188
+ value: unknown,
189
+ fieldId: string,
190
+ scope: unknown,
191
+ warnings: ScopeTemplateWarning[]
192
+ ) {
193
+ if (shouldSkipField(fieldId)) return;
194
+
195
+ if (typeof value === "string") {
196
+ warnings.push(...analyzeTemplateValue(fieldId, value, scope));
197
+ return;
198
+ }
199
+
200
+ if (Array.isArray(value)) {
201
+ value.forEach((item, index) => {
202
+ walkElementStrings(item, `${fieldId}.${index}`, scope, warnings);
203
+ });
204
+ return;
205
+ }
206
+
207
+ if (value && typeof value === "object") {
208
+ for (const [key, nested] of Object.entries(value)) {
209
+ const nextId = fieldId ? `${fieldId}.${key}` : key;
210
+ walkElementStrings(nested, nextId, scope, warnings);
211
+ }
212
+ }
213
+ }
214
+
215
+ export function collectElementScopeWarnings(
216
+ element: PanelElement,
217
+ scope: unknown
218
+ ): ScopeTemplateWarning[] {
219
+ if (scope === undefined) return [];
220
+
221
+ const warnings: ScopeTemplateWarning[] = [];
222
+ walkElementStrings(element, "", scope, warnings);
223
+
224
+ const seen = new Set<string>();
225
+ return warnings.filter((warning) => {
226
+ const key = `${warning.fieldId}|${warning.kind}|${warning.missingPath}|${warning.expression}`;
227
+ if (seen.has(key)) return false;
228
+ seen.add(key);
229
+ return true;
230
+ });
231
+ }
232
+
233
+ export function groupScopeWarningsByField(
234
+ warnings: ScopeTemplateWarning[]
235
+ ): Map<string, ScopeTemplateWarning[]> {
236
+ const map = new Map<string, ScopeTemplateWarning[]>();
237
+ for (const warning of warnings) {
238
+ const list = map.get(warning.fieldId) ?? [];
239
+ list.push(warning);
240
+ map.set(warning.fieldId, list);
241
+ }
242
+ return map;
243
+ }
@@ -0,0 +1,97 @@
1
+ import type { PanelElement } from "../types";
2
+ import { materializeChartLabelsFromScope } from "./scope-template-chart";
3
+ import {
4
+ SCOPE_SPREAD_TEMPLATE_RE,
5
+ evaluateSpreadScopeExpression,
6
+ } from "./scope-template-spread";
7
+
8
+ const SCOPE_TEMPLATE_RE = /\{[^}]+\}|\[\.\.\.\{[^}]+\}\]/;
9
+
10
+ export function hasScopeTemplate(value: string): boolean {
11
+ return SCOPE_TEMPLATE_RE.test(value);
12
+ }
13
+
14
+ export function evaluateScopeExpression(
15
+ expression: string,
16
+ scope: unknown
17
+ ): unknown {
18
+ const trimmed = expression.trim();
19
+ if (!trimmed) return undefined;
20
+ try {
21
+ const fn = new Function("scope", `"use strict"; return (${trimmed});`);
22
+ return fn(scope);
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ export function evaluateScopeTemplate(
29
+ template: string,
30
+ scope: unknown
31
+ ): string {
32
+ if (!hasScopeTemplate(template)) return template;
33
+
34
+ const withSpread = template.replace(
35
+ SCOPE_SPREAD_TEMPLATE_RE,
36
+ (_, rawExpr: string) => {
37
+ const result = evaluateSpreadScopeExpression(rawExpr, scope);
38
+ return result.ok ? result.value : "";
39
+ }
40
+ );
41
+
42
+ return withSpread.replace(/\{([^}]+)\}/g, (_, rawExpr: string) => {
43
+ const value = evaluateScopeExpression(rawExpr, scope);
44
+ if (value === null || value === undefined) return "";
45
+ if (typeof value === "string") return value;
46
+ if (typeof value === "number" || typeof value === "boolean") {
47
+ return String(value);
48
+ }
49
+ try {
50
+ return JSON.stringify(value);
51
+ } catch {
52
+ return String(value);
53
+ }
54
+ });
55
+ }
56
+
57
+ function resolveScopedValue<T>(value: T, scope: unknown): T {
58
+ if (typeof value === "string" && hasScopeTemplate(value)) {
59
+ return evaluateScopeTemplate(value, scope) as T;
60
+ }
61
+ if (Array.isArray(value)) {
62
+ return value.map((item) => resolveScopedValue(item, scope)) as T;
63
+ }
64
+ if (value && typeof value === "object") {
65
+ const out: Record<string, unknown> = {};
66
+ for (const [key, nested] of Object.entries(value)) {
67
+ out[key] = resolveScopedValue(nested, scope);
68
+ }
69
+ return out as T;
70
+ }
71
+ return value;
72
+ }
73
+
74
+ export function resolvePanelElementScope(
75
+ element: PanelElement,
76
+ scope: unknown | undefined
77
+ ): PanelElement {
78
+ if (scope === undefined) return element;
79
+ const resolved = resolveScopedValue(structuredClone(element), scope);
80
+ if (resolved.chart) {
81
+ const labels = materializeChartLabelsFromScope(element.chart, scope);
82
+ if (labels !== undefined) {
83
+ resolved.chart.labels = labels;
84
+ delete resolved.chart.labelsText;
85
+ }
86
+ }
87
+ return resolved;
88
+ }
89
+
90
+ export function formatViewElementScope(scope: unknown): string {
91
+ if (scope === undefined) return "";
92
+ try {
93
+ return JSON.stringify(scope, null, 2);
94
+ } catch {
95
+ return String(scope);
96
+ }
97
+ }
@@ -0,0 +1,221 @@
1
+ import type { Node, State } from "@arronqzy/rx-store";
2
+ import type { PanelElement } from "../types";
3
+ import { getGridChildSpanRect, getGridSlotLayout } from "./gridPlacement";
4
+ import {
5
+ canonicalMappingFamilyRootId,
6
+ concreteGridParentIdForLayer,
7
+ logicalGridParentIdFromConcrete,
8
+ } from "./mappingLayerOps";
9
+ import { isPanelElementNode } from "./panelElementNodes";
10
+
11
+ /** 防止同源合并把「映射层上的网格 id」泄露到其它图层,保证节点树 parentGridId 只指向本图层网格 */
12
+ function fixCrossLayerParentGrid(
13
+ merged: PanelElement,
14
+ propsBefore: PanelElement,
15
+ byId: Map<string, PanelElement>,
16
+ allFlat: PanelElement[]
17
+ ): void {
18
+ const layerId = propsBefore.layerId;
19
+ const pg = merged.parentGridId;
20
+ if (!pg) return;
21
+
22
+ const parentNode = byId.get(pg);
23
+ const validHere =
24
+ !!parentNode &&
25
+ parentNode.materialType === "grid" &&
26
+ parentNode.layerId === layerId;
27
+
28
+ if (validHere) return;
29
+
30
+ const logical =
31
+ logicalGridParentIdFromConcrete(pg, byId) ??
32
+ (propsBefore.parentGridId !== undefined
33
+ ? logicalGridParentIdFromConcrete(propsBefore.parentGridId, byId)
34
+ : undefined);
35
+
36
+ if (logical !== undefined) {
37
+ const cp = concreteGridParentIdForLayer(logical, layerId, allFlat);
38
+ const gridEl = cp ? byId.get(cp) : undefined;
39
+ if (gridEl?.materialType === "grid") {
40
+ merged.parentGridId = cp;
41
+ const spanRect = getGridChildSpanRect(
42
+ gridEl,
43
+ merged.gridSlotIndex ?? 0,
44
+ merged.gridColSpan ?? 1,
45
+ merged.gridRowSpan ?? 1
46
+ );
47
+ merged.gridSlotIndex = spanRect.index;
48
+ merged.gridColSpan = spanRect.colSpan;
49
+ merged.gridRowSpan = spanRect.rowSpan;
50
+ merged.x = spanRect.x;
51
+ merged.y = spanRect.y;
52
+ merged.width = spanRect.width;
53
+ merged.height = spanRect.height;
54
+ return;
55
+ }
56
+ }
57
+
58
+ merged.parentGridId = propsBefore.parentGridId;
59
+ merged.gridSlotIndex = propsBefore.gridSlotIndex;
60
+ merged.gridColSpan = propsBefore.gridColSpan;
61
+ merged.gridRowSpan = propsBefore.gridRowSpan;
62
+ merged.x = propsBefore.x;
63
+ merged.y = propsBefore.y;
64
+ merged.width = propsBefore.width;
65
+ merged.height = propsBefore.height;
66
+ }
67
+
68
+ /** 同源映射:外层网格布局改动时,同源簇内每张网格实例一并 reflow 其子节点几何 */
69
+ export function applyGridLayoutPatchAcrossMappingFamily(
70
+ draft: State,
71
+ logicalGridSourceId: string,
72
+ patch: Partial<PanelElement>
73
+ ): void {
74
+ const nodes = draft.root.children ?? [];
75
+ const byIdPre = new Map<string, PanelElement>();
76
+ nodes.forEach((n) => {
77
+ if (!isPanelElementNode(n) || !n.props) return;
78
+ const p = n.props as PanelElement;
79
+ byIdPre.set(p.id, p);
80
+ });
81
+ const gridTargets = nodes.filter((n) => {
82
+ if (!isPanelElementNode(n) || !n.props) return false;
83
+ const p = n.props as PanelElement;
84
+ if (p.materialType !== "grid") return false;
85
+ return canonicalMappingFamilyRootId(byIdPre, p.id) === logicalGridSourceId;
86
+ });
87
+ if (gridTargets.length === 0) return;
88
+ const byParent = new Map<string, PanelElement[]>();
89
+ nodes.forEach((n) => {
90
+ if (!isPanelElementNode(n) || !n.props) return;
91
+ const p = n.props as PanelElement;
92
+ if (!p.parentGridId) return;
93
+ const list = byParent.get(p.parentGridId) ?? [];
94
+ list.push(p);
95
+ byParent.set(p.parentGridId, list);
96
+ });
97
+ const byIdNode = new Map<string, Node>();
98
+ nodes.forEach((n) => {
99
+ if (!isPanelElementNode(n) || !n.props) return;
100
+ byIdNode.set((n.props as PanelElement).id, n);
101
+ });
102
+ const reflowGridDescendants = (grid: PanelElement) => {
103
+ const { slots } = getGridSlotLayout(grid);
104
+ if (slots.length === 0) return;
105
+ const directChildren = byParent.get(grid.id) ?? [];
106
+ directChildren.forEach((child) => {
107
+ const slotIndex =
108
+ child.gridSlotIndex !== undefined ? Math.max(0, Math.floor(child.gridSlotIndex)) : 0;
109
+ const spanRect = getGridChildSpanRect(
110
+ grid,
111
+ slotIndex % slots.length,
112
+ child.gridColSpan ?? 1,
113
+ child.gridRowSpan ?? 1
114
+ );
115
+ child.gridSlotIndex = spanRect.index;
116
+ child.gridColSpan = spanRect.colSpan;
117
+ child.gridRowSpan = spanRect.rowSpan;
118
+ child.x = spanRect.x;
119
+ child.y = spanRect.y;
120
+ child.width = spanRect.width;
121
+ child.height = spanRect.height;
122
+ const childNode = byIdNode.get(child.id);
123
+ if (childNode) childNode.props = child;
124
+ if (child.materialType === "grid") {
125
+ reflowGridDescendants(child);
126
+ }
127
+ });
128
+ };
129
+ gridTargets.forEach((targetNode) => {
130
+ if (!targetNode.props) return;
131
+ const nextGrid = {
132
+ ...(targetNode.props as PanelElement),
133
+ ...patch,
134
+ } as PanelElement;
135
+ targetNode.props = nextGrid;
136
+ reflowGridDescendants(nextGrid);
137
+ });
138
+ }
139
+
140
+ /** 同源节点簇在非网格独占分支下的批量合并(含跨图层 parentGridId remap) */
141
+ export function applyMappingFamilySyncPatch(
142
+ draft: State,
143
+ syncSourceId: string,
144
+ syncPatch: Partial<PanelElement>,
145
+ patch: Partial<PanelElement>
146
+ ): void {
147
+ const nodes = draft.root.children ?? [];
148
+ const byId = new Map<string, PanelElement>();
149
+ nodes.forEach((n) => {
150
+ if (!isPanelElementNode(n) || !n.props) return;
151
+ const p = n.props as PanelElement;
152
+ byId.set(p.id, p);
153
+ });
154
+ const allFlat = [...byId.values()];
155
+ nodes.forEach((n) => {
156
+ if (!isPanelElementNode(n) || !n.props) return;
157
+ const props = n.props as PanelElement;
158
+ const sameFamily = canonicalMappingFamilyRootId(byId, props.id) === syncSourceId;
159
+ if (!sameFamily) return;
160
+
161
+ const merged = { ...props, ...syncPatch } as PanelElement;
162
+
163
+ if ("parentGridId" in patch && patch.parentGridId === undefined) {
164
+ merged.parentGridId = undefined;
165
+ merged.gridSlotIndex = undefined;
166
+ n.props = merged;
167
+ byId.set(merged.id, merged);
168
+ return;
169
+ }
170
+
171
+ const logicalParent =
172
+ patch.parentGridId !== undefined
173
+ ? logicalGridParentIdFromConcrete(patch.parentGridId, byId)
174
+ : props.parentGridId !== undefined
175
+ ? logicalGridParentIdFromConcrete(props.parentGridId, byId)
176
+ : undefined;
177
+
178
+ const touchesGridPlacement =
179
+ ("parentGridId" in patch && patch.parentGridId !== undefined) ||
180
+ "gridSlotIndex" in patch ||
181
+ "gridColSpan" in patch ||
182
+ "gridRowSpan" in patch;
183
+
184
+ if (touchesGridPlacement && logicalParent !== undefined) {
185
+ const concreteParent = concreteGridParentIdForLayer(logicalParent, props.layerId, allFlat);
186
+ if (concreteParent) {
187
+ const parentEl = byId.get(concreteParent);
188
+ if (parentEl?.materialType === "grid") {
189
+ merged.parentGridId = concreteParent;
190
+ const spanRect = getGridChildSpanRect(
191
+ parentEl,
192
+ merged.gridSlotIndex ?? 0,
193
+ merged.gridColSpan ?? 1,
194
+ merged.gridRowSpan ?? 1
195
+ );
196
+ merged.gridSlotIndex = spanRect.index;
197
+ merged.gridColSpan = spanRect.colSpan;
198
+ merged.gridRowSpan = spanRect.rowSpan;
199
+ merged.x = spanRect.x;
200
+ merged.y = spanRect.y;
201
+ merged.width = spanRect.width;
202
+ merged.height = spanRect.height;
203
+ }
204
+ } else {
205
+ merged.parentGridId = props.parentGridId;
206
+ merged.gridSlotIndex = props.gridSlotIndex;
207
+ merged.gridColSpan = props.gridColSpan;
208
+ merged.gridRowSpan = props.gridRowSpan;
209
+ merged.x = props.x;
210
+ merged.y = props.y;
211
+ merged.width = props.width;
212
+ merged.height = props.height;
213
+ }
214
+ }
215
+
216
+ fixCrossLayerParentGrid(merged, props, byId, allFlat);
217
+
218
+ n.props = merged;
219
+ byId.set(merged.id, merged);
220
+ });
221
+ }
@@ -0,0 +1,26 @@
1
+ export type ViewportZoom = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+
6
+ export function uniformViewportZoom(zoom: ViewportZoom): number {
7
+ return Math.min(zoom.x, zoom.y);
8
+ }
9
+
10
+ export function clampViewportZoom(value: number): number {
11
+ return Math.min(4, Math.max(0.25, value));
12
+ }
13
+
14
+ export function clampViewportZoomXY(zoom: ViewportZoom): ViewportZoom {
15
+ return { x: clampViewportZoom(zoom.x), y: clampViewportZoom(zoom.y) };
16
+ }
17
+
18
+ export function scaleViewportZoom(
19
+ zoom: ViewportZoom,
20
+ factor: number
21
+ ): ViewportZoom {
22
+ return clampViewportZoomXY({
23
+ x: zoom.x * factor,
24
+ y: zoom.y * factor,
25
+ });
26
+ }
@@ -0,0 +1,43 @@
1
+ /* Moveable:仅角点控制柄固定屏幕像素;勿作用于 moveable-direction 边线 */
2
+ .rv-moveable-layer {
3
+ --moveable-color: #1990ff;
4
+ z-index: 9999 !important;
5
+ }
6
+
7
+ .rv-moveable-layer.moveable-group,
8
+ .rv-moveable-layer .moveable-group,
9
+ .rv-moveable-layer.moveable-control-box,
10
+ .rv-moveable-layer .moveable-control-box {
11
+ z-index: 1 !important;
12
+ }
13
+
14
+ .rv-moveable-layer .moveable-line {
15
+ --moveable-color: #1990ff;
16
+ background: var(--moveable-color) !important;
17
+ }
18
+
19
+ /* 组选/单边控制线:厚度由 moveable zoom 补偿,保持屏幕 1px */
20
+ .rv-moveable-layer .moveable-line.moveable-direction {
21
+ background: var(--moveable-color) !important;
22
+ }
23
+
24
+ .rv-moveable-layer .moveable-group .moveable-control-box,
25
+ .rv-moveable-layer.moveable-group .moveable-control-box {
26
+ z-index: 2 !important;
27
+ }
28
+
29
+ .rv-moveable-layer .moveable-around-control {
30
+ background: transparent !important;
31
+ }
32
+
33
+ .rv-moveable-layer .moveable-control {
34
+ width: 14px !important;
35
+ height: 14px !important;
36
+ margin-top: -7px !important;
37
+ margin-left: -7px !important;
38
+ border-radius: 50% !important;
39
+ box-sizing: border-box !important;
40
+ border: 2px solid hsl(var(--background)) !important;
41
+ background: #1990ff !important;
42
+ background: var(--moveable-color) !important;
43
+ }