@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,630 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ nextTick,
5
+ onMounted,
6
+ onUnmounted,
7
+ ref,
8
+ shallowRef,
9
+ useTemplateRef,
10
+ watch,
11
+ } from "vue";
12
+ import InfiniteViewer from "infinite-viewer";
13
+ import {
14
+ clampViewportZoom,
15
+ clampViewportZoomXY,
16
+ uniformViewportZoom,
17
+ type ViewportZoom,
18
+ } from "../viewportZoom";
19
+
20
+ const props = withDefaults(
21
+ defineProps<{
22
+ zoom: ViewportZoom;
23
+ contentSize?: { width: number; height: number } | null;
24
+ class?: string;
25
+ canvasRef?: (el: HTMLDivElement | null) => void;
26
+ scrollRef?: (el: HTMLDivElement | null) => void;
27
+ viewportSyncRef?: { current: (() => void) | null };
28
+ onDropMaterial?: (payload: { materialId: string; x: number; y: number }) => void;
29
+ onCanvasMouseDownCapture?: (e: MouseEvent) => void;
30
+ onCanvasClickCapture?: (e: MouseEvent) => void;
31
+ onCanvasContextMenuCapture?: (e: MouseEvent) => void;
32
+ }>(),
33
+ { contentSize: null }
34
+ );
35
+
36
+ const emit = defineEmits<{
37
+ zoomChange: [next: ViewportZoom];
38
+ scrollChange: [pos: { left: number; top: number }];
39
+ }>();
40
+
41
+ const viewerHostRef = useTemplateRef<HTMLDivElement>("viewerHost");
42
+ const viewportRef = useTemplateRef<HTMLDivElement>("viewport");
43
+ const canvasElRef = useTemplateRef<HTMLDivElement>("canvasEl");
44
+
45
+ const viewerRef = shallowRef<InfiniteViewer | null>(null);
46
+ const syncingZoomRef = ref(false);
47
+ const didAutoCenterRef = ref(false);
48
+ const isPanning = ref(false);
49
+ const lastScrollRef = ref({ left: 0, top: 0 });
50
+ const zoomRef = ref({ ...props.zoom });
51
+ const viewportSizeRef = ref({ width: 0, height: 0 });
52
+ const viewport = ref({ width: 0, height: 0 });
53
+ const worldCanvasSize = ref<{ width: number; height: number } | null>(null);
54
+
55
+ const resizeZoomRafRef = ref<number | null>(null);
56
+ const resizeLayoutAnchorRef = ref<{
57
+ viewportWidth: number;
58
+ viewportHeight: number;
59
+ zoomX: number;
60
+ zoomY: number;
61
+ scrollLeft: number;
62
+ scrollTop: number;
63
+ } | null>(null);
64
+ const layoutBaselineRef = ref({
65
+ viewportWidth: 0,
66
+ viewportHeight: 0,
67
+ zoomX: 1,
68
+ zoomY: 1,
69
+ });
70
+
71
+ const panRef = ref({
72
+ active: false,
73
+ moved: false,
74
+ startClientX: 0,
75
+ startClientY: 0,
76
+ startLeft: 0,
77
+ startTop: 0,
78
+ });
79
+ const panHandlersRef = ref<{
80
+ move: ((ev: MouseEvent) => void) | null;
81
+ up: ((ev: MouseEvent) => void) | null;
82
+ }>({ move: null, up: null });
83
+
84
+ function applyCanvasTransform(scale: number) {
85
+ const el = canvasElRef.value;
86
+ if (!el) return;
87
+ el.style.transform = `scale(${scale})`;
88
+ el.style.transformOrigin = "0 0";
89
+ el.dataset.canvasScale = String(scale);
90
+ }
91
+
92
+ function notifyViewportSync() {
93
+ props.viewportSyncRef?.current?.();
94
+ }
95
+
96
+ function syncViewportElement(el: HTMLDivElement | null) {
97
+ props.scrollRef?.(el);
98
+ }
99
+
100
+ function syncCanvasElement(el: HTMLDivElement | null) {
101
+ if (el) {
102
+ applyCanvasTransform(uniformViewportZoom(zoomRef.value));
103
+ }
104
+ props.canvasRef?.(el);
105
+ }
106
+
107
+ function clearWindowPanListeners() {
108
+ if (panHandlersRef.value.move) {
109
+ window.removeEventListener("mousemove", panHandlersRef.value.move, true);
110
+ panHandlersRef.value.move = null;
111
+ }
112
+ if (panHandlersRef.value.up) {
113
+ window.removeEventListener("mouseup", panHandlersRef.value.up, true);
114
+ panHandlersRef.value.up = null;
115
+ }
116
+ }
117
+
118
+ const resolvedContentSize = computed(() => {
119
+ if (props.contentSize) return props.contentSize;
120
+ if (worldCanvasSize.value) return worldCanvasSize.value;
121
+ return {
122
+ width: Math.max(0, viewport.value.width),
123
+ height: Math.max(0, viewport.value.height),
124
+ };
125
+ });
126
+
127
+ const canvasScale = computed(() => uniformViewportZoom(props.zoom));
128
+
129
+ const worldStyle = computed(() => ({
130
+ width: `${resolvedContentSize.value.width}px`,
131
+ height: `${resolvedContentSize.value.height}px`,
132
+ position: "relative" as const,
133
+ }));
134
+
135
+ const canvasStyle = computed(() => ({
136
+ width: `${resolvedContentSize.value.width}px`,
137
+ height: `${resolvedContentSize.value.height}px`,
138
+ minWidth: "100%",
139
+ minHeight: "100%",
140
+ position: "relative" as const,
141
+ transformOrigin: "0 0",
142
+ }));
143
+
144
+ function applyViewportResizeZoom(w: number, h: number) {
145
+ if (w < 8 || h < 8) return;
146
+ if (syncingZoomRef.value) return;
147
+
148
+ const viewer = viewerRef.value;
149
+ if (!viewer) return;
150
+
151
+ const baseline = layoutBaselineRef.value;
152
+ if (baseline.viewportWidth < 8 || baseline.viewportHeight < 8) {
153
+ layoutBaselineRef.value = {
154
+ viewportWidth: w,
155
+ viewportHeight: h,
156
+ zoomX: zoomRef.value.x,
157
+ zoomY: zoomRef.value.y,
158
+ };
159
+ resizeLayoutAnchorRef.value = {
160
+ viewportWidth: w,
161
+ viewportHeight: h,
162
+ zoomX: zoomRef.value.x,
163
+ zoomY: zoomRef.value.y,
164
+ scrollLeft: lastScrollRef.value.left,
165
+ scrollTop: lastScrollRef.value.top,
166
+ };
167
+ return;
168
+ }
169
+
170
+ if (
171
+ Math.abs(w - baseline.viewportWidth) < 2 &&
172
+ Math.abs(h - baseline.viewportHeight) < 2
173
+ ) {
174
+ return;
175
+ }
176
+
177
+ const factorX = w / baseline.viewportWidth;
178
+ const factorY = h / baseline.viewportHeight;
179
+ const factor = Math.sqrt(factorX * factorY);
180
+
181
+ const anchor = resizeLayoutAnchorRef.value ?? {
182
+ viewportWidth: w,
183
+ viewportHeight: h,
184
+ zoomX: zoomRef.value.x,
185
+ zoomY: zoomRef.value.y,
186
+ scrollLeft: lastScrollRef.value.left,
187
+ scrollTop: lastScrollRef.value.top,
188
+ };
189
+
190
+ const nextZoomValue = clampViewportZoom(baseline.zoomX * factor);
191
+ const worldX =
192
+ (anchor.scrollLeft + anchor.viewportWidth / 2) / Math.max(0.0001, anchor.zoomX);
193
+ const worldY =
194
+ (anchor.scrollTop + anchor.viewportHeight / 2) / Math.max(0.0001, anchor.zoomY);
195
+ const nextScrollLeft = worldX * nextZoomValue - w / 2;
196
+ const nextScrollTop = worldY * nextZoomValue - h / 2;
197
+ const nextZoom = { x: nextZoomValue, y: nextZoomValue };
198
+
199
+ syncingZoomRef.value = true;
200
+ zoomRef.value = nextZoom;
201
+ applyCanvasTransform(nextZoomValue);
202
+ viewer.scrollTo(nextScrollLeft, nextScrollTop);
203
+ resizeLayoutAnchorRef.value = {
204
+ viewportWidth: w,
205
+ viewportHeight: h,
206
+ zoomX: nextZoomValue,
207
+ zoomY: nextZoomValue,
208
+ scrollLeft: nextScrollLeft,
209
+ scrollTop: nextScrollTop,
210
+ };
211
+ layoutBaselineRef.value = {
212
+ viewportWidth: w,
213
+ viewportHeight: h,
214
+ zoomX: nextZoomValue,
215
+ zoomY: nextZoomValue,
216
+ };
217
+ emit("zoomChange", {
218
+ x: Number(nextZoomValue.toFixed(4)),
219
+ y: Number(nextZoomValue.toFixed(4)),
220
+ });
221
+ }
222
+
223
+ function hasMaterialPayload(types: ArrayLike<string> | null | undefined) {
224
+ if (!types) return false;
225
+ return Array.from(types).includes("application/x-arronqzy-material");
226
+ }
227
+
228
+ function commitDropFromPoint(
229
+ clientX: number,
230
+ clientY: number,
231
+ dataTransfer: DataTransfer | null
232
+ ) {
233
+ const raw = dataTransfer?.getData("application/x-arronqzy-material");
234
+ if (!raw) return;
235
+ try {
236
+ const material = JSON.parse(raw) as { id?: string };
237
+ if (!material?.id) return;
238
+ const viewportEl = viewerRef.value?.getContainer?.() as HTMLDivElement | undefined;
239
+ if (!viewportEl) return;
240
+ const viewportRect = viewportEl.getBoundingClientRect();
241
+ const localX = clientX - viewportRect.left;
242
+ const localY = clientY - viewportRect.top;
243
+ const isInsideViewport =
244
+ localX >= 0 &&
245
+ localY >= 0 &&
246
+ localX <= viewportRect.width &&
247
+ localY <= viewportRect.height;
248
+ if (!isInsideViewport) return;
249
+ const canvasEl = canvasElRef.value;
250
+ if (!canvasEl) return;
251
+ const canvasRect = canvasEl.getBoundingClientRect();
252
+ const x = (clientX - canvasRect.left) / Math.max(0.0001, zoomRef.value.x);
253
+ const y = (clientY - canvasRect.top) / Math.max(0.0001, zoomRef.value.y);
254
+ props.onDropMaterial?.({ materialId: material.id, x, y });
255
+ } catch {
256
+ // ignore invalid payload
257
+ }
258
+ }
259
+
260
+ function setupWheelZoom(el: HTMLDivElement) {
261
+ const onWheelZoom = (e: WheelEvent) => {
262
+ const viewer = viewerRef.value;
263
+ if (!viewer) return;
264
+ const target = e.target as HTMLElement | null;
265
+ if (
266
+ target?.closest("input, textarea, select, [contenteditable='true']") ||
267
+ target?.closest("[role='dialog']")
268
+ ) {
269
+ return;
270
+ }
271
+ e.preventDefault();
272
+ const rect = el.getBoundingClientRect();
273
+ const pointerX = e.clientX - rect.left;
274
+ const pointerY = e.clientY - rect.top;
275
+ const zx = zoomRef.value.x;
276
+ const zy = zoomRef.value.y;
277
+ const currentScrollLeft = Number(lastScrollRef.value.left ?? 0);
278
+ const currentScrollTop = Number(lastScrollRef.value.top ?? 0);
279
+
280
+ const direction = e.deltaY > 0 ? -1 : 1;
281
+ const zoomStep = direction > 0 ? 1.08 : 0.92;
282
+ const nextZoom = clampViewportZoomXY({
283
+ x: zx * zoomStep,
284
+ y: zy * zoomStep,
285
+ });
286
+ if (Math.abs(nextZoom.x - zx) < 0.0001 && Math.abs(nextZoom.y - zy) < 0.0001) {
287
+ return;
288
+ }
289
+
290
+ const worldX = (pointerX + currentScrollLeft) / Math.max(0.0001, zx);
291
+ const worldY = (pointerY + currentScrollTop) / Math.max(0.0001, zy);
292
+ const nextScrollLeft = worldX * nextZoom.x - pointerX;
293
+ const nextScrollTop = worldY * nextZoom.y - pointerY;
294
+ const nextScale = uniformViewportZoom(nextZoom);
295
+
296
+ syncingZoomRef.value = true;
297
+ zoomRef.value = nextZoom;
298
+ applyCanvasTransform(nextScale);
299
+ viewer.scrollTo(nextScrollLeft, nextScrollTop);
300
+ const { width: vw, height: vh } = viewportSizeRef.value;
301
+ resizeLayoutAnchorRef.value = {
302
+ viewportWidth: vw,
303
+ viewportHeight: vh,
304
+ zoomX: nextZoom.x,
305
+ zoomY: nextZoom.y,
306
+ scrollLeft: nextScrollLeft,
307
+ scrollTop: nextScrollTop,
308
+ };
309
+ layoutBaselineRef.value = {
310
+ viewportWidth: vw,
311
+ viewportHeight: vh,
312
+ zoomX: nextZoom.x,
313
+ zoomY: nextZoom.y,
314
+ };
315
+ emit("zoomChange", {
316
+ x: Number(nextZoom.x.toFixed(4)),
317
+ y: Number(nextZoom.y.toFixed(4)),
318
+ });
319
+ };
320
+
321
+ el.addEventListener("wheel", onWheelZoom, { passive: false, capture: true });
322
+ return () => el.removeEventListener("wheel", onWheelZoom, true);
323
+ }
324
+
325
+ function setupPanHandlers(el: HTMLDivElement) {
326
+ const onContextMenu = (e: MouseEvent) => {
327
+ if (panRef.value.moved) {
328
+ e.preventDefault();
329
+ e.stopPropagation();
330
+ (e as Event & { stopImmediatePropagation?: () => void }).stopImmediatePropagation?.();
331
+ panRef.value.moved = false;
332
+ }
333
+ };
334
+
335
+ const onMouseDown = (e: MouseEvent) => {
336
+ const isRightLike = e.button === 2 || (e.button === 0 && e.ctrlKey);
337
+ if (!isRightLike) return;
338
+ const viewer = viewerRef.value;
339
+ if (!viewer) return;
340
+
341
+ panRef.value.active = true;
342
+ panRef.value.moved = false;
343
+ panRef.value.startClientX = e.clientX;
344
+ panRef.value.startClientY = e.clientY;
345
+ panRef.value.startLeft = lastScrollRef.value.left;
346
+ panRef.value.startTop = lastScrollRef.value.top;
347
+
348
+ const move = (ev: MouseEvent) => {
349
+ if (!panRef.value.active) return;
350
+ const dx = ev.clientX - panRef.value.startClientX;
351
+ const dy = ev.clientY - panRef.value.startClientY;
352
+ const distance = Math.hypot(dx, dy);
353
+ if (!panRef.value.moved && distance > 10) {
354
+ panRef.value.moved = true;
355
+ isPanning.value = true;
356
+ }
357
+ if (!panRef.value.moved) return;
358
+ ev.preventDefault();
359
+ const nextLeft = panRef.value.startLeft - dx;
360
+ const nextTop = panRef.value.startTop - dy;
361
+ viewer.scrollTo(nextLeft, nextTop);
362
+ lastScrollRef.value = { left: nextLeft, top: nextTop };
363
+ emit("scrollChange", { left: nextLeft, top: nextTop });
364
+ notifyViewportSync();
365
+ };
366
+
367
+ const up = (ev: MouseEvent) => {
368
+ if (!panRef.value.active) return;
369
+ if (panRef.value.moved) {
370
+ ev.preventDefault();
371
+ }
372
+ panRef.value.active = false;
373
+ isPanning.value = false;
374
+ clearWindowPanListeners();
375
+ };
376
+
377
+ panHandlersRef.value.move = move;
378
+ panHandlersRef.value.up = up;
379
+ window.addEventListener("mousemove", move, true);
380
+ window.addEventListener("mouseup", up, true);
381
+ };
382
+
383
+ el.addEventListener("contextmenu", onContextMenu, true);
384
+ el.addEventListener("mousedown", onMouseDown, true);
385
+ return () => {
386
+ el.removeEventListener("contextmenu", onContextMenu, true);
387
+ el.removeEventListener("mousedown", onMouseDown, true);
388
+ };
389
+ }
390
+
391
+ function setupDropHandlers(el: HTMLDivElement) {
392
+ const onDragOver = (e: DragEvent) => {
393
+ if (!hasMaterialPayload(e.dataTransfer?.types)) return;
394
+ e.preventDefault();
395
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
396
+ };
397
+
398
+ const onDrop = (e: DragEvent) => {
399
+ if (!hasMaterialPayload(e.dataTransfer?.types)) return;
400
+ e.preventDefault();
401
+ commitDropFromPoint(e.clientX, e.clientY, e.dataTransfer ?? null);
402
+ };
403
+
404
+ el.addEventListener("dragover", onDragOver, true);
405
+ el.addEventListener("drop", onDrop, true);
406
+ return () => {
407
+ el.removeEventListener("dragover", onDragOver, true);
408
+ el.removeEventListener("drop", onDrop, true);
409
+ };
410
+ }
411
+
412
+ let cleanupWheel: (() => void) | null = null;
413
+ let cleanupPan: (() => void) | null = null;
414
+ let cleanupDrop: (() => void) | null = null;
415
+ let resizeObserver: ResizeObserver | null = null;
416
+
417
+ watch(
418
+ () => props.zoom,
419
+ (zoom) => {
420
+ zoomRef.value = zoom;
421
+ },
422
+ { immediate: true, deep: true }
423
+ );
424
+
425
+ watch(viewport, (v) => {
426
+ viewportSizeRef.value = v;
427
+ });
428
+
429
+ watch(
430
+ () => [props.contentSize, viewport.value.width, viewport.value.height] as const,
431
+ () => {
432
+ if (props.contentSize) return;
433
+ if (viewport.value.width < 8 || viewport.value.height < 8) return;
434
+ if (worldCanvasSize.value) return;
435
+ const size = { width: viewport.value.width, height: viewport.value.height };
436
+ layoutBaselineRef.value = {
437
+ viewportWidth: viewport.value.width,
438
+ viewportHeight: viewport.value.height,
439
+ zoomX: zoomRef.value.x,
440
+ zoomY: zoomRef.value.y,
441
+ };
442
+ resizeLayoutAnchorRef.value = {
443
+ viewportWidth: viewport.value.width,
444
+ viewportHeight: viewport.value.height,
445
+ zoomX: zoomRef.value.x,
446
+ zoomY: zoomRef.value.y,
447
+ scrollLeft: lastScrollRef.value.left,
448
+ scrollTop: lastScrollRef.value.top,
449
+ };
450
+ worldCanvasSize.value = size;
451
+ }
452
+ );
453
+
454
+ watch(canvasScale, () => {
455
+ const scale = syncingZoomRef.value
456
+ ? uniformViewportZoom(zoomRef.value)
457
+ : uniformViewportZoom(props.zoom);
458
+ applyCanvasTransform(scale);
459
+ });
460
+
461
+ watch(
462
+ () => [props.zoom.x, props.zoom.y] as const,
463
+ () => {
464
+ if (
465
+ Math.abs(props.zoom.x - zoomRef.value.x) < 0.0001 &&
466
+ Math.abs(props.zoom.y - zoomRef.value.y) < 0.0001
467
+ ) {
468
+ syncingZoomRef.value = false;
469
+ }
470
+ }
471
+ );
472
+
473
+ watch(
474
+ () => [viewport.value.width, viewport.value.height, props.contentSize] as const,
475
+ () => {
476
+ if (props.contentSize) return;
477
+ if (viewport.value.width < 8 || viewport.value.height < 8) return;
478
+
479
+ if (resizeZoomRafRef.value != null) {
480
+ cancelAnimationFrame(resizeZoomRafRef.value);
481
+ }
482
+ resizeZoomRafRef.value = requestAnimationFrame(() => {
483
+ resizeZoomRafRef.value = null;
484
+ applyViewportResizeZoom(viewport.value.width, viewport.value.height);
485
+ });
486
+ }
487
+ );
488
+
489
+ watch(canvasElRef, (el) => syncCanvasElement(el), { immediate: true });
490
+
491
+ onMounted(async () => {
492
+ const host = viewerHostRef.value;
493
+ const viewportEl = viewportRef.value;
494
+ if (!host || !viewportEl) return;
495
+
496
+ const viewer = new InfiniteViewer(host, viewportEl, {
497
+ margin: 0,
498
+ threshold: 0,
499
+ useMouseDrag: false,
500
+ displayVerticalScroll: false,
501
+ displayHorizontalScroll: false,
502
+ useWheelScroll: false,
503
+ preventWheelClick: true,
504
+ });
505
+ viewerRef.value = viewer;
506
+
507
+ viewer.on("scroll", () => {
508
+ const next = {
509
+ left: viewer.getScrollLeft(),
510
+ top: viewer.getScrollTop(),
511
+ };
512
+ lastScrollRef.value = next;
513
+ emit("scrollChange", next);
514
+ if (!syncingZoomRef.value) {
515
+ notifyViewportSync();
516
+ }
517
+ });
518
+
519
+ await nextTick();
520
+ const container = viewer.getContainer() as HTMLDivElement | null;
521
+ syncViewportElement(container);
522
+
523
+ const updateViewportSize = () => {
524
+ if (!container) return;
525
+ const width = Math.round(container.clientWidth);
526
+ const height = Math.round(container.clientHeight);
527
+ if (viewport.value.width !== width || viewport.value.height !== height) {
528
+ viewport.value = { width, height };
529
+ }
530
+ };
531
+ updateViewportSize();
532
+
533
+ if (container) {
534
+ if (typeof ResizeObserver !== "undefined") {
535
+ resizeObserver = new ResizeObserver(updateViewportSize);
536
+ resizeObserver.observe(container);
537
+ } else {
538
+ window.addEventListener("resize", updateViewportSize);
539
+ }
540
+
541
+ cleanupWheel = setupWheelZoom(container);
542
+ cleanupPan = setupPanHandlers(container);
543
+ cleanupDrop = setupDropHandlers(container);
544
+ }
545
+
546
+ requestAnimationFrame(() => {
547
+ if (!container || didAutoCenterRef.value) return;
548
+ const nodes = container.querySelectorAll<HTMLElement>(".rv-selectable");
549
+ if (!nodes.length) return;
550
+
551
+ let minX = Infinity;
552
+ let minY = Infinity;
553
+ let maxX = -Infinity;
554
+ let maxY = -Infinity;
555
+
556
+ nodes.forEach((node) => {
557
+ const left = node.offsetLeft;
558
+ const top = node.offsetTop;
559
+ const width = node.offsetWidth;
560
+ const height = node.offsetHeight;
561
+ minX = Math.min(minX, left);
562
+ minY = Math.min(minY, top);
563
+ maxX = Math.max(maxX, left + width);
564
+ maxY = Math.max(maxY, top + height);
565
+ });
566
+
567
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) return;
568
+ const boundsWidth = Math.max(1, maxX - minX);
569
+ const boundsHeight = Math.max(1, maxY - minY);
570
+
571
+ const targetLeft = minX + boundsWidth / 2 - container.clientWidth / 2;
572
+ const targetTop = minY + boundsHeight / 2 - container.clientHeight / 2;
573
+ viewer.scrollTo(Math.round(targetLeft), Math.round(targetTop));
574
+ didAutoCenterRef.value = true;
575
+ });
576
+ });
577
+
578
+ onUnmounted(() => {
579
+ clearWindowPanListeners();
580
+ cleanupWheel?.();
581
+ cleanupPan?.();
582
+ cleanupDrop?.();
583
+ resizeObserver?.disconnect();
584
+ if (resizeZoomRafRef.value != null) {
585
+ cancelAnimationFrame(resizeZoomRafRef.value);
586
+ }
587
+ viewerRef.value?.destroy();
588
+ viewerRef.value = null;
589
+ syncViewportElement(null);
590
+ syncCanvasElement(null);
591
+ });
592
+ </script>
593
+
594
+ <template>
595
+ <div
596
+ ref="viewerHost"
597
+ :class="[
598
+ 'relative h-full w-full overflow-hidden bg-background [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden',
599
+ isPanning ? 'cursor-grabbing select-none' : '',
600
+ props.class ?? '',
601
+ ]"
602
+ :style="{
603
+ backgroundImage:
604
+ 'linear-gradient(to right, hsl(var(--border) / 0.45) 1px, transparent 1px), linear-gradient(to bottom, hsl(var(--border) / 0.45) 1px, transparent 1px)',
605
+ backgroundSize: '20px 20px',
606
+ }"
607
+ >
608
+ <div ref="viewport">
609
+ <div :style="worldStyle">
610
+ <div
611
+ ref="canvasEl"
612
+ data-panel-canvas
613
+ :style="canvasStyle"
614
+ class="h-full w-full"
615
+ @mousedown.capture="props.onCanvasMouseDownCapture"
616
+ @click.capture="props.onCanvasClickCapture"
617
+ @contextmenu.capture="props.onCanvasContextMenuCapture"
618
+ >
619
+ <slot />
620
+ </div>
621
+ <div
622
+ v-if="$slots['viewport-overlay']"
623
+ class="rv-viewport-overlay pointer-events-none absolute inset-0 z-[70] overflow-visible [&_.moveable-control-box]:pointer-events-auto [&_.moveable-group]:pointer-events-auto [&_.moveable-area]:pointer-events-auto"
624
+ >
625
+ <slot name="viewport-overlay" />
626
+ </div>
627
+ </div>
628
+ </div>
629
+ </div>
630
+ </template>