@arronqzy/vue-view 0.1.9 → 0.1.11

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 (29) hide show
  1. package/README.md +15 -1
  2. package/package.json +8 -8
  3. package/src/index.ts +26 -0
  4. package/src/panel/VueViewOnlinePreview.vue +68 -26
  5. package/src/panel/VueViewPanel.vue +15 -10
  6. package/src/panel/components/ElementsLayer.vue +4 -2
  7. package/src/panel/components/MaterialPreview.vue +8 -0
  8. package/src/panel/components/MaterialSidebar.vue +15 -18
  9. package/src/panel/components/MaterialSidebarTreeNode.vue +4 -2
  10. package/src/panel/components/PanelConfigSidebar.vue +17 -1
  11. package/src/panel/components/WorkspaceConfigSidebar.vue +1 -0
  12. package/src/panel/components/config/PanelConfigMultiSelect.vue +30 -1
  13. package/src/panel/components/config/PanelConfigViewportSection.vue +114 -0
  14. package/src/panel/components/elementsLayerNodes.ts +75 -17
  15. package/src/panel/components/materialSidebarData.ts +2 -0
  16. package/src/panel/hooks/usePanelElements.ts +10 -4
  17. package/src/panel/hooks/useWorkspaceProjects.ts +144 -34
  18. package/src/panel/library/event-subscription.ts +60 -0
  19. package/src/panel/library/parse-workspace-data.ts +142 -0
  20. package/src/panel/library/workspace-project-cache.ts +13 -4
  21. package/src/panel/library/workspace-project-db.ts +24 -13
  22. package/src/panel/library/workspace-project-sync.ts +17 -6
  23. package/src/panel/parseOnlinePreviewSearchParams.ts +3 -0
  24. package/src/panel/types.ts +4 -0
  25. package/src/panel/utils/outputScale.ts +15 -2
  26. package/src/panel/utils/panelElementDefaults.ts +10 -0
  27. package/src/panel/utils/panelElementNodes.ts +5 -3
  28. package/src/panel/utils/panelStateIO.ts +18 -0
  29. package/src/panel/utils/viewportPlacement.ts +73 -0
@@ -0,0 +1,60 @@
1
+ import type { WorkspaceProjectRecord } from "./workspace-project-db";
2
+
3
+ export const AbuilderEvents = {
4
+ /** 添加工作区 */
5
+ workspaceAdd: "workspace:add",
6
+ /** 同步工作区 */
7
+ workspaceSync: "workspace:sync",
8
+ } as const;
9
+
10
+ export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEvents];
11
+
12
+ /** 工作区完整数据(面板 + 蓝图),可用于外部持久化与再次加载 */
13
+ export type WorkspaceData = WorkspaceProjectRecord;
14
+
15
+ export type WorkspaceAddEventPayload = WorkspaceData;
16
+ export type WorkspaceSyncEventPayload = WorkspaceData;
17
+
18
+ export type AbuilderEventPayloadMap = {
19
+ [AbuilderEvents.workspaceAdd]: WorkspaceAddEventPayload;
20
+ [AbuilderEvents.workspaceSync]: WorkspaceSyncEventPayload;
21
+ };
22
+
23
+ type EventCallback = (payload: unknown) => void;
24
+
25
+ const listeners = new Map<string, Set<EventCallback>>();
26
+
27
+ export function emitAbuilderEvent<T extends AbuilderEventName>(
28
+ eventName: T,
29
+ payload: AbuilderEventPayloadMap[T]
30
+ ): void {
31
+ const set = listeners.get(eventName);
32
+ if (!set) return;
33
+ for (const callback of set) {
34
+ try {
35
+ callback(payload);
36
+ } catch (error) {
37
+ console.error(`[abuilder] event handler error (${eventName}):`, error);
38
+ }
39
+ }
40
+ }
41
+
42
+ export function addEventSubscription<T extends AbuilderEventName>(
43
+ eventName: T,
44
+ callback: (payload: AbuilderEventPayloadMap[T]) => void
45
+ ): { unsubscribe: () => void } {
46
+ let set = listeners.get(eventName);
47
+ if (!set) {
48
+ set = new Set();
49
+ listeners.set(eventName, set);
50
+ }
51
+ set.add(callback as EventCallback);
52
+ return {
53
+ unsubscribe: () => {
54
+ set.delete(callback as EventCallback);
55
+ if (set.size === 0) {
56
+ listeners.delete(eventName);
57
+ }
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,142 @@
1
+ import {
2
+ BlueprintGraph,
3
+ parseBlueprintData,
4
+ validateBlueprintData,
5
+ type BlueprintDocument,
6
+ type BlueprintMetaDraft,
7
+ type ParseCheckResult,
8
+ } from "@arronqzy/vue-blueprint";
9
+ import type { State } from "@arronqzy/rx-store";
10
+ import { createEmptyPanelState, normalizeImportedPanelState } from "../utils/panelStateIO";
11
+ import {
12
+ createWorkspaceProjectId,
13
+ type WorkspaceProjectRecord,
14
+ } from "./workspace-project-db";
15
+
16
+ export type { ParseCheckResult };
17
+
18
+ export type WorkspaceParseCheckResult = ParseCheckResult & {
19
+ view: ParseCheckResult;
20
+ blueprint: ParseCheckResult;
21
+ };
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
25
+ }
26
+
27
+ function extractPanelState(data: unknown): unknown {
28
+ if (isRecord(data) && data.panelState !== undefined) return data.panelState;
29
+ return data;
30
+ }
31
+
32
+ function readBlueprintMeta(raw: unknown): BlueprintMetaDraft {
33
+ if (!isRecord(raw)) return { name: "", remark: "" };
34
+ return {
35
+ name: typeof raw.name === "string" ? raw.name : "",
36
+ remark: typeof raw.remark === "string" ? raw.remark : "",
37
+ };
38
+ }
39
+
40
+ /**
41
+ * 校验数据能否被视图解析(工作区 `panelState` 或裸面板状态均可)。
42
+ */
43
+ export function parseViewData(
44
+ data: unknown
45
+ ): ParseCheckResult & { value?: State } {
46
+ if (data == null) {
47
+ return { ok: false, error: "empty" };
48
+ }
49
+ const normalized = normalizeImportedPanelState(extractPanelState(data));
50
+ if (!normalized) {
51
+ return { ok: false, error: "invalid-view-data" };
52
+ }
53
+ return { ok: true, value: normalized };
54
+ }
55
+
56
+ export function validateViewData(data: unknown): ParseCheckResult {
57
+ const parsed = parseViewData(data);
58
+ return parsed.ok ? { ok: true } : { ok: false, error: parsed.error };
59
+ }
60
+
61
+ export { parseBlueprintData, validateBlueprintData };
62
+
63
+ /**
64
+ * 构造一份可交给 `initialWorkspace` 的空工作区。
65
+ */
66
+ export function createEmptyWorkspace(
67
+ init?: Partial<WorkspaceProjectRecord>
68
+ ): WorkspaceProjectRecord {
69
+ const now = Date.now();
70
+ return {
71
+ id: init?.id || createWorkspaceProjectId(),
72
+ name: init?.name ?? "",
73
+ createdAt: init?.createdAt ?? now,
74
+ updatedAt: init?.updatedAt ?? now,
75
+ panelState: init?.panelState ?? createEmptyPanelState(),
76
+ blueprintDocument: init?.blueprintDocument ?? BlueprintGraph.empty().document,
77
+ blueprintMeta: init?.blueprintMeta ?? { name: "", remark: "" },
78
+ productName: init?.productName ?? "",
79
+ titleIconDataUrl: init?.titleIconDataUrl,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * 校验完整工作区数据:视图 `panelState` 与蓝图 `blueprintDocument` 都要能解析。
85
+ */
86
+ export function validateWorkspaceData(data: unknown): WorkspaceParseCheckResult {
87
+ if (data == null) {
88
+ const empty = { ok: false, error: "empty" } satisfies ParseCheckResult;
89
+ return { ok: false, error: "empty", view: empty, blueprint: empty };
90
+ }
91
+ if (!isRecord(data)) {
92
+ const invalid = { ok: false, error: "invalid-workspace-data" } satisfies ParseCheckResult;
93
+ return {
94
+ ok: false,
95
+ error: "invalid-workspace-data",
96
+ view: invalid,
97
+ blueprint: invalid,
98
+ };
99
+ }
100
+ const view = validateViewData(data);
101
+ const blueprint = validateBlueprintData(data);
102
+ const ok = view.ok && blueprint.ok;
103
+ return {
104
+ ok,
105
+ error: ok ? undefined : "invalid-workspace-data",
106
+ view,
107
+ blueprint,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * 解析并规范化工作区。成功时 `value` 可直接作为 `initialWorkspace`。
113
+ */
114
+ export function parseWorkspaceData(
115
+ data: unknown
116
+ ): WorkspaceParseCheckResult & { value?: WorkspaceProjectRecord } {
117
+ const check = validateWorkspaceData(data);
118
+ if (!check.ok || !isRecord(data)) {
119
+ return check;
120
+ }
121
+ const view = parseViewData(data);
122
+ const blueprint = parseBlueprintData(data);
123
+ if (!view.value || !blueprint.value) {
124
+ return check;
125
+ }
126
+ const now = Date.now();
127
+ return {
128
+ ...check,
129
+ value: {
130
+ id: typeof data.id === "string" && data.id ? data.id : createWorkspaceProjectId(),
131
+ name: typeof data.name === "string" ? data.name : "",
132
+ createdAt: typeof data.createdAt === "number" ? data.createdAt : now,
133
+ updatedAt: typeof data.updatedAt === "number" ? data.updatedAt : now,
134
+ panelState: view.value,
135
+ blueprintDocument: blueprint.value as BlueprintDocument,
136
+ blueprintMeta: readBlueprintMeta(data.blueprintMeta),
137
+ productName: typeof data.productName === "string" ? data.productName : "",
138
+ titleIconDataUrl:
139
+ typeof data.titleIconDataUrl === "string" ? data.titleIconDataUrl : undefined,
140
+ },
141
+ };
142
+ }
@@ -1,11 +1,19 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
1
2
  import type { WorkspaceProjectRecord } from "./workspace-project-db";
2
3
 
3
4
  const CACHE_PREFIX = "arronqzy-workspace-preview:";
4
5
 
5
- export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void {
6
+ function previewCacheKey(projectId: string, nameSpace?: string | null): string {
7
+ return appStorageKey(CACHE_PREFIX + projectId, nameSpace);
8
+ }
9
+
10
+ export function writeWorkspacePreviewCache(
11
+ record: WorkspaceProjectRecord,
12
+ nameSpace?: string | null
13
+ ): void {
6
14
  try {
7
15
  localStorage.setItem(
8
- CACHE_PREFIX + record.id,
16
+ previewCacheKey(record.id, nameSpace),
9
17
  JSON.stringify({ id: record.id, updatedAt: record.updatedAt })
10
18
  );
11
19
  } catch {
@@ -14,10 +22,11 @@ export function writeWorkspacePreviewCache(record: WorkspaceProjectRecord): void
14
22
  }
15
23
 
16
24
  export function readWorkspacePreviewCache(
17
- projectId: string
25
+ projectId: string,
26
+ nameSpace?: string | null
18
27
  ): WorkspaceProjectRecord | null {
19
28
  try {
20
- const raw = localStorage.getItem(CACHE_PREFIX + projectId);
29
+ const raw = localStorage.getItem(previewCacheKey(projectId, nameSpace));
21
30
  if (!raw) return null;
22
31
  const parsed = JSON.parse(raw) as Partial<WorkspaceProjectRecord> | null;
23
32
  if (!parsed || typeof parsed !== "object" || !parsed.panelState) return null;
@@ -1,5 +1,6 @@
1
1
  import type { BlueprintDocument, BlueprintMetaDraft } from "@arronqzy/vue-blueprint";
2
2
  import type { State } from "@arronqzy/rx-store";
3
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
3
4
 
4
5
  const DB_NAME = "arronqzy-workspace-projects";
5
6
  const DB_VERSION = 1;
@@ -24,9 +25,9 @@ export type WorkspaceProjectRecord = {
24
25
  titleIconDataUrl?: string;
25
26
  };
26
27
 
27
- function openDb(): Promise<IDBDatabase> {
28
+ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
28
29
  return new Promise((resolve, reject) => {
29
- const request = indexedDB.open(DB_NAME, DB_VERSION);
30
+ const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
30
31
  request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
31
32
  request.onupgradeneeded = () => {
32
33
  const db = request.result;
@@ -41,9 +42,10 @@ function openDb(): Promise<IDBDatabase> {
41
42
 
42
43
  function runTransaction<T>(
43
44
  mode: IDBTransactionMode,
44
- runner: (store: IDBObjectStore) => IDBRequest<T>
45
+ runner: (store: IDBObjectStore) => IDBRequest<T>,
46
+ nameSpace?: string | null
45
47
  ): Promise<T> {
46
- return openDb().then(
48
+ return openDb(nameSpace).then(
47
49
  (db) =>
48
50
  new Promise<T>((resolve, reject) => {
49
51
  const tx = db.transaction(STORE_NAME, mode);
@@ -76,8 +78,10 @@ export function createWorkspaceProjectId(): string {
76
78
  return `ws-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
77
79
  }
78
80
 
79
- export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[]> {
80
- const db = await openDb();
81
+ export async function listWorkspaceProjects(
82
+ nameSpace?: string | null
83
+ ): Promise<WorkspaceProjectListItem[]> {
84
+ const db = await openDb(nameSpace);
81
85
  return new Promise((resolve, reject) => {
82
86
  const tx = db.transaction(STORE_NAME, "readonly");
83
87
  const store = tx.objectStore(STORE_NAME);
@@ -109,21 +113,28 @@ export async function listWorkspaceProjects(): Promise<WorkspaceProjectListItem[
109
113
  }
110
114
 
111
115
  export async function getWorkspaceProject(
112
- id: string
116
+ id: string,
117
+ nameSpace?: string | null
113
118
  ): Promise<WorkspaceProjectRecord | null> {
114
- const record = await runTransaction<WorkspaceProjectRecord | undefined>("readonly", (store) =>
115
- store.get(id)
119
+ const record = await runTransaction<WorkspaceProjectRecord | undefined>(
120
+ "readonly",
121
+ (store) => store.get(id),
122
+ nameSpace
116
123
  );
117
124
  return record ?? null;
118
125
  }
119
126
 
120
127
  export async function putWorkspaceProject(
121
- record: WorkspaceProjectRecord
128
+ record: WorkspaceProjectRecord,
129
+ nameSpace?: string | null
122
130
  ): Promise<WorkspaceProjectRecord> {
123
- await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record));
131
+ await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record), nameSpace);
124
132
  return record;
125
133
  }
126
134
 
127
- export async function deleteWorkspaceProject(id: string): Promise<void> {
128
- await runTransaction<undefined>("readwrite", (store) => store.delete(id));
135
+ export async function deleteWorkspaceProject(
136
+ id: string,
137
+ nameSpace?: string | null
138
+ ): Promise<void> {
139
+ await runTransaction<undefined>("readwrite", (store) => store.delete(id), nameSpace);
129
140
  }
@@ -1,3 +1,5 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
2
+
1
3
  const CHANNEL_PREFIX = "arronqzy-workspace-project";
2
4
 
3
5
  export type WorkspaceProjectSyncMessage = {
@@ -6,16 +8,22 @@ export type WorkspaceProjectSyncMessage = {
6
8
  updatedAt: number;
7
9
  };
8
10
 
9
- export function getWorkspaceProjectChannelName(projectId: string): string {
10
- return `${CHANNEL_PREFIX}:${projectId}`;
11
+ export function getWorkspaceProjectChannelName(
12
+ projectId: string,
13
+ nameSpace?: string | null
14
+ ): string {
15
+ return appStorageKey(`${CHANNEL_PREFIX}:${projectId}`, nameSpace);
11
16
  }
12
17
 
13
18
  export function broadcastWorkspaceProjectUpdate(
14
19
  projectId: string,
15
- updatedAt: number
20
+ updatedAt: number,
21
+ nameSpace?: string | null
16
22
  ): void {
17
23
  if (typeof BroadcastChannel === "undefined") return;
18
- const channel = new BroadcastChannel(getWorkspaceProjectChannelName(projectId));
24
+ const channel = new BroadcastChannel(
25
+ getWorkspaceProjectChannelName(projectId, nameSpace)
26
+ );
19
27
  channel.postMessage({
20
28
  type: "updated",
21
29
  projectId,
@@ -26,12 +34,15 @@ export function broadcastWorkspaceProjectUpdate(
26
34
 
27
35
  export function subscribeWorkspaceProjectUpdates(
28
36
  projectId: string,
29
- onUpdate: (message: WorkspaceProjectSyncMessage) => void
37
+ onUpdate: (message: WorkspaceProjectSyncMessage) => void,
38
+ nameSpace?: string | null
30
39
  ): () => void {
31
40
  if (typeof BroadcastChannel === "undefined") {
32
41
  return () => {};
33
42
  }
34
- const channel = new BroadcastChannel(getWorkspaceProjectChannelName(projectId));
43
+ const channel = new BroadcastChannel(
44
+ getWorkspaceProjectChannelName(projectId, nameSpace)
45
+ );
35
46
  channel.onmessage = (event: MessageEvent<WorkspaceProjectSyncMessage>) => {
36
47
  const data = event.data;
37
48
  if (!data || data.type !== "updated" || data.projectId !== projectId) return;
@@ -1,13 +1,16 @@
1
1
  export function parseOnlinePreviewSearchParams(search: string): {
2
2
  projectId: string;
3
3
  previewInstanceId?: string;
4
+ nameSpace?: string;
4
5
  } | null {
5
6
  const params = new URLSearchParams(search);
6
7
  if (params.get("preview") !== "online") return null;
7
8
  const projectId = params.get("projectId") ?? params.get("id");
8
9
  if (!projectId) return null;
10
+ const nameSpace = (params.get("ns") ?? params.get("nameSpace") ?? "").trim();
9
11
  return {
10
12
  projectId,
11
13
  previewInstanceId: params.get("pid") ?? undefined,
14
+ nameSpace: nameSpace || undefined,
12
15
  };
13
16
  }
@@ -54,6 +54,8 @@ export type PanelElementStyle = {
54
54
 
55
55
  export type ReferenceCopyMode = "shallow" | "deep";
56
56
 
57
+ export type ViewportOverflowMode = "scroll-x" | "scroll-y" | "scroll" | "clip";
58
+
57
59
  export type { PanelTableConfig } from "@arronqzy/view-table";
58
60
  import type { PanelTableConfig } from "@arronqzy/view-table";
59
61
  import type { Scene3dConfig } from "@arronqzy/view-scene3d";
@@ -84,6 +86,8 @@ export type PanelElement = {
84
86
  gridPadding?: number;
85
87
  gridSnapThreshold?: number;
86
88
  parentGridId?: string;
89
+ /** 视窗节点:只显示自身尺寸内的引用图层内容,超出部分滚动或裁剪 */
90
+ viewportOverflow?: ViewportOverflowMode;
87
91
  gridSlotIndex?: number;
88
92
  gridColSpan?: number;
89
93
  gridRowSpan?: number;
@@ -1,14 +1,27 @@
1
1
  /** 产物预览是否缩放内部内容(字体等)。默认 false:块仍按视口撑满,内容保持原尺寸以免失真。 */
2
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
3
+
2
4
  export const OUTPUT_SCALE_STORAGE_KEY = "panel:outputScale";
3
5
 
4
- export function readOutputScale(): boolean {
6
+ export function readOutputScale(nameSpace?: string | null): boolean {
5
7
  try {
6
- return localStorage.getItem(OUTPUT_SCALE_STORAGE_KEY) === "1";
8
+ return localStorage.getItem(appStorageKey(OUTPUT_SCALE_STORAGE_KEY, nameSpace)) === "1";
7
9
  } catch {
8
10
  return false;
9
11
  }
10
12
  }
11
13
 
14
+ export function writeOutputScale(enabled: boolean, nameSpace?: string | null) {
15
+ try {
16
+ localStorage.setItem(
17
+ appStorageKey(OUTPUT_SCALE_STORAGE_KEY, nameSpace),
18
+ enabled ? "1" : "0"
19
+ );
20
+ } catch {
21
+ // ignore storage errors
22
+ }
23
+ }
24
+
12
25
  export type PreviewFillScale = {
13
26
  vw: number;
14
27
  vh: number;
@@ -15,6 +15,7 @@ const DEFAULT_NODE_NAME_KEYS: Record<string, string> = {
15
15
  funnel: "panel.defaults.funnel",
16
16
  text: "panel.defaults.text",
17
17
  grid: "panel.defaults.grid",
18
+ viewport: "panel.defaults.viewport",
18
19
  image: "panel.defaults.image",
19
20
  video: "panel.defaults.video",
20
21
  audio: "panel.defaults.audio",
@@ -71,6 +72,8 @@ export function getDefaultSizeByMaterial(materialType: string) {
71
72
  return { width: 180, height: 120 };
72
73
  case "grid":
73
74
  return { width: 320, height: 220 };
75
+ case "viewport":
76
+ return { width: 360, height: 240 };
74
77
  case "image":
75
78
  return { width: 220, height: 140 };
76
79
  case "video":
@@ -145,6 +148,13 @@ export function getDefaultGridConfig(materialType: string) {
145
148
  } as const;
146
149
  }
147
150
 
151
+ export function getDefaultViewportConfig(materialType: string) {
152
+ if (materialType !== "viewport") return {};
153
+ return {
154
+ viewportOverflow: "clip" as const,
155
+ };
156
+ }
157
+
148
158
  export function getDefaultChartConfig(
149
159
  materialType: string,
150
160
  t: TranslateFn = tForLocale("zh-CN")
@@ -26,9 +26,11 @@ export function buildDeepReferenceSnapshot(
26
26
  const layerNodes = allElements.filter((el) => el.layerId === layerId);
27
27
  return layerNodes.map((el) => {
28
28
  const cloned = clonePanelElement(el);
29
- if (cloned.materialType === "reference" && cloned.refLayerId) {
30
- cloned.refCopyMode = "deep";
31
- cloned.refSnapshot = buildDeepReferenceSnapshot(allElements, cloned.refLayerId, nextVisited);
29
+ if (cloned.materialType === "reference" || cloned.materialType === "viewport") {
30
+ if (cloned.refLayerId) {
31
+ cloned.refCopyMode = "deep";
32
+ cloned.refSnapshot = buildDeepReferenceSnapshot(allElements, cloned.refLayerId, nextVisited);
33
+ }
32
34
  }
33
35
  return cloned;
34
36
  });
@@ -26,6 +26,24 @@ export function normalizeImportedPanelState(state: unknown): State | null {
26
26
  return next;
27
27
  }
28
28
 
29
+ export function createEmptyPanelState(): State {
30
+ return {
31
+ root: {
32
+ id: "root",
33
+ type: "root",
34
+ props: {},
35
+ children: [],
36
+ },
37
+ selectedIds: [],
38
+ variables: {
39
+ counter: 0,
40
+ isOpen: false,
41
+ layers: [DEFAULT_LAYER],
42
+ activeLayerId: DEFAULT_LAYER_ID,
43
+ },
44
+ };
45
+ }
46
+
29
47
  export function parseAllPanelElements(state: State): PanelElement[] {
30
48
  return (state.root.children ?? [])
31
49
  .filter((node) => {
@@ -0,0 +1,73 @@
1
+ export const VIEWPORT_OVERFLOW_MODES = [
2
+ "scroll-x",
3
+ "scroll-y",
4
+ "scroll",
5
+ "clip",
6
+ ] as const;
7
+
8
+ export type ViewportOverflowMode = (typeof VIEWPORT_OVERFLOW_MODES)[number];
9
+
10
+ export const DEFAULT_VIEWPORT_OVERFLOW: ViewportOverflowMode = "clip";
11
+
12
+ export function isViewportOverflowMode(
13
+ value: unknown
14
+ ): value is ViewportOverflowMode {
15
+ return (
16
+ value === "scroll-x" ||
17
+ value === "scroll-y" ||
18
+ value === "scroll" ||
19
+ value === "clip"
20
+ );
21
+ }
22
+
23
+ export function normalizeViewportOverflow(
24
+ value: unknown
25
+ ): ViewportOverflowMode {
26
+ return isViewportOverflowMode(value) ? value : DEFAULT_VIEWPORT_OVERFLOW;
27
+ }
28
+
29
+ export function viewportOverflowStyle(mode: ViewportOverflowMode): {
30
+ overflowX: "hidden" | "auto";
31
+ overflowY: "hidden" | "auto";
32
+ } {
33
+ switch (mode) {
34
+ case "scroll-x":
35
+ return { overflowX: "auto", overflowY: "hidden" };
36
+ case "scroll-y":
37
+ return { overflowX: "hidden", overflowY: "auto" };
38
+ case "scroll":
39
+ return { overflowX: "auto", overflowY: "auto" };
40
+ default:
41
+ return { overflowX: "hidden", overflowY: "hidden" };
42
+ }
43
+ }
44
+
45
+ export function isViewportNode(
46
+ el: { materialType?: string } | undefined
47
+ ): boolean {
48
+ return el?.materialType === "viewport";
49
+ }
50
+
51
+ /** 引用节点与视窗节点都通过选择图层来预览内容 */
52
+ export function isLayerPreviewNode(
53
+ el: { materialType?: string } | undefined
54
+ ): boolean {
55
+ return el?.materialType === "reference" || el?.materialType === "viewport";
56
+ }
57
+
58
+ /**
59
+ * 视窗按自身宽高开窗;预览里节点会被整体拉满视口,
60
+ * 内部引用内容必须用同一套缩放,滚动比例才和编辑态一致。
61
+ */
62
+ export function viewportWindowContentScale(
63
+ current: { id: string; width: number; height: number },
64
+ originals: ReadonlyArray<{ id: string; width: number; height: number }>
65
+ ): { scaleX: number; scaleY: number } {
66
+ const original = originals.find((n) => n.id === current.id);
67
+ const ow = Math.max(1, original?.width ?? current.width);
68
+ const oh = Math.max(1, original?.height ?? current.height);
69
+ return {
70
+ scaleX: Math.max(1, current.width) / ow,
71
+ scaleY: Math.max(1, current.height) / oh,
72
+ };
73
+ }