@arronqzy/vue-view 0.1.17 → 0.1.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-view",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,13 +23,13 @@
23
23
  "selecto": "^1.26.3",
24
24
  "uuid": "^13.0.0",
25
25
  "vue": "^3.5.13",
26
- "@arronqzy/blueprint-dsl": "1.0.14",
26
+ "@arronqzy/rx-store": "1.0.8",
27
27
  "@arronqzy/i18n": "0.1.12",
28
28
  "@arronqzy/view-scene3d": "0.1.5",
29
+ "@arronqzy/vue-blueprint": "0.1.13",
29
30
  "@arronqzy/view-table": "0.1.8",
30
- "@arronqzy/rx-store": "1.0.8",
31
- "@arronqzy/vue-blueprint": "0.1.12",
32
- "@arronqzy/vue-rx-store": "0.1.6"
31
+ "@arronqzy/vue-rx-store": "0.1.6",
32
+ "@arronqzy/blueprint-dsl": "1.0.14"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "vue": "^3.4.0"
@@ -409,11 +409,25 @@ const workspaceProjects = useWorkspaceProjects({
409
409
  nameSpace: () => props.nameSpace,
410
410
  });
411
411
 
412
+ const blueprintLifecycleToken = computed(() => {
413
+ const doc = blueprintGraph.value.document;
414
+ const nodes = doc.nodes;
415
+ const edges = doc.edges;
416
+ let sig = `${nodes.length}:${edges.length}`;
417
+ for (const node of nodes) {
418
+ sig += `|${node.id}:${resolveRunnableNodeType(node)}:${node.lifecyclePhase ?? ""}`;
419
+ }
420
+ for (const edge of edges) {
421
+ sig += `>${edge.id}:${edge.source}:${edge.target}`;
422
+ }
423
+ return sig;
424
+ });
425
+
412
426
  const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
413
427
  graph: blueprintGraph,
414
428
  active: blueprintOpen,
415
429
  bootKey: computed(() => workspaceProjects.activeProjectId.value ?? undefined),
416
- onUpdated: computed(() => `${activeLayerId.value}|${historyCursor.value}`),
430
+ onUpdated: computed(() => `${activeLayerId.value}|${blueprintLifecycleToken.value}`),
417
431
  resolveLibraryBlueprint,
418
432
  libraryNameById: blueprintLibraryNameById,
419
433
  rootLibraryBlueprintId: activeBlueprintLibraryId,
@@ -185,6 +185,7 @@ function hasViewEvent(el: PanelElement) {
185
185
  <div
186
186
  v-for="el in sortedElements"
187
187
  :key="el.id"
188
+ v-memo="[el, selectedIds.includes(el.id), previewMode, layerLocked, hasViewEvent(el), previewLayoutKey]"
188
189
  :class="[
189
190
  'absolute select-none',
190
191
  previewMode ? '' : 'rv-selectable',
@@ -202,6 +203,8 @@ function hasViewEvent(el: PanelElement) {
202
203
  transformOrigin: 'center center',
203
204
  boxSizing: 'border-box',
204
205
  overflow: el.materialType === 'viewport' ? 'hidden' : undefined,
206
+ contentVisibility: 'auto',
207
+ containIntrinsicSize: `${Math.max(1, el.width)}px ${Math.max(1, el.height)}px`,
205
208
  ...getNodeVisualStyle(el),
206
209
  }"
207
210
  @mousedown="onSelect(el.id, $event)"
@@ -11,7 +11,7 @@ import {
11
11
  import Moveable from "moveable";
12
12
  import type { PanelElement } from "../types";
13
13
  import { uniformViewportZoom } from "../viewportZoom";
14
- import { notifyPreviewLayoutChanged } from "../utils/panelStateIO";
14
+ import { setCanvasInteractionBusy } from "../utils/canvas-interaction-busy";
15
15
 
16
16
  const { t, locale } = useI18n();
17
17
 
@@ -444,6 +444,7 @@ function createMoveable() {
444
444
  const point = readClientPoint(e);
445
445
  e.datas.__startClientX = point?.x ?? null;
446
446
  e.datas.__startClientY = point?.y ?? null;
447
+ setCanvasInteractionBusy(true);
447
448
  });
448
449
 
449
450
  moveable.on("drag", (e: any) => {
@@ -479,6 +480,7 @@ function createMoveable() {
479
480
  ev.datas.__startClientX = point?.x ?? null;
480
481
  ev.datas.__startClientY = point?.y ?? null;
481
482
  });
483
+ setCanvasInteractionBusy(true);
482
484
  });
483
485
 
484
486
  moveable.on("dragGroup", (e: any) => {
@@ -513,6 +515,7 @@ function createMoveable() {
513
515
  e.datas.__startY = data.y;
514
516
  e.datas.__startClientX = e.inputEvent?.clientX ?? null;
515
517
  e.datas.__startClientY = e.inputEvent?.clientY ?? null;
518
+ setCanvasInteractionBusy(true);
516
519
  });
517
520
 
518
521
  moveable.on("resize", (e: any) => {
@@ -526,7 +529,6 @@ function createMoveable() {
526
529
  const ty = toCanvasDeltaY(e.drag.beforeTranslate?.[1] ?? 0);
527
530
  e.target.style.left = `${sx + tx}px`;
528
531
  e.target.style.top = `${sy + ty}px`;
529
- notifyPreviewLayoutChanged();
530
532
  });
531
533
 
532
534
  moveable.on("resizeGroupStart", (e: any) => {
@@ -543,6 +545,7 @@ function createMoveable() {
543
545
  ev.datas.__startClientX = input?.clientX ?? null;
544
546
  ev.datas.__startClientY = input?.clientY ?? null;
545
547
  });
548
+ setCanvasInteractionBusy(true);
546
549
  });
547
550
 
548
551
  moveable.on("resizeGroup", (e: any) => {
@@ -557,7 +560,6 @@ function createMoveable() {
557
560
  ev.target.style.left = `${sx + tx}px`;
558
561
  ev.target.style.top = `${sy + ty}px`;
559
562
  });
560
- notifyPreviewLayoutChanged();
561
563
  });
562
564
 
563
565
  moveable.on("rotateStart", (e: any) => {
@@ -566,6 +568,7 @@ function createMoveable() {
566
568
  const data = props.elementsById.get(id);
567
569
  if (!data) return;
568
570
  e.set(data.rotate ?? 0);
571
+ setCanvasInteractionBusy(true);
569
572
  });
570
573
 
571
574
  moveable.on("rotate", (e: any) => {
@@ -573,6 +576,17 @@ function createMoveable() {
573
576
  e.target.style.transform = `rotate(${e.beforeRotate}deg)`;
574
577
  });
575
578
 
579
+ moveable.on("rotateGroupStart", (e: any) => {
580
+ e.events.forEach((ev: any) => {
581
+ const id = ev.target ? getId(ev.target) : null;
582
+ if (!id) return;
583
+ const data = props.elementsById.get(id);
584
+ if (!data) return;
585
+ ev.set(data.rotate ?? 0);
586
+ });
587
+ setCanvasInteractionBusy(true);
588
+ });
589
+
576
590
  moveable.on("rotateGroup", (e: any) => {
577
591
  e.events.forEach((ev: any) => {
578
592
  if (!ev?.target?.style) return;
@@ -581,175 +595,199 @@ function createMoveable() {
581
595
  });
582
596
 
583
597
  moveable.on("dragEnd", (e: any) => {
584
- const id = resolveSingleEventId(e.target as HTMLElement | null);
585
- if (!id) return;
586
- const data = props.elementsById.get(id);
587
- if (!data) return;
588
- const sx = e.datas.__startX ?? data.x;
589
- const sy = e.datas.__startY ?? data.y;
590
- const point = readClientPoint(e);
591
- const hasClient =
592
- typeof e.datas.__startClientX === "number" &&
593
- typeof e.datas.__startClientY === "number" &&
594
- !!point;
595
- const tx = hasClient
596
- ? toCanvasDeltaX(point.x - e.datas.__startClientX)
597
- : toCanvasDeltaX(e.lastEvent?.beforeTranslate?.[0] ?? 0);
598
- const ty = hasClient
599
- ? toCanvasDeltaY(point.y - e.datas.__startClientY)
600
- : toCanvasDeltaY(e.lastEvent?.beforeTranslate?.[1] ?? 0);
601
- const nextX = sx + tx;
602
- const nextY = sy + ty;
603
- const size = readTargetCanvasSize(e.target as HTMLElement | null, {
604
- width: data.width,
605
- height: data.height,
606
- });
607
- const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
608
- props.updateElement(id, { x: nextX, y: nextY, ...patch });
609
- if (data.materialType === "grid") {
610
- const dx = nextX - data.x;
611
- const dy = nextY - data.y;
612
- if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
613
- const children = getGridDirectChildren(id);
614
- const batchId = `move-grid-children-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
615
- children.forEach((child) => {
616
- props.updateElement(
617
- child.id,
618
- { x: child.x + dx, y: child.y + dy },
619
- { batchId, meta: { type: "node.group-drag" } }
620
- );
621
- });
622
- }
623
- }
624
- scheduleViewportSync();
625
- });
626
-
627
- moveable.on("dragGroupEnd", (e: any) => {
628
- const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
629
- const selectedSet = new Set(
630
- e.events
631
- .map((ev: any) => (ev.target ? getId(ev.target) : null))
632
- .filter((id: string | null): id is string => !!id)
633
- );
634
- const gridDeltaMap = new Map<string, { dx: number; dy: number }>();
635
- e.events.forEach((ev: any) => {
636
- const id = ev.target ? getId(ev.target) : null;
598
+ try {
599
+ const id = resolveSingleEventId(e.target as HTMLElement | null);
637
600
  if (!id) return;
638
601
  const data = props.elementsById.get(id);
639
602
  if (!data) return;
640
- const sx = ev.datas.__startX ?? data.x;
641
- const sy = ev.datas.__startY ?? data.y;
642
- const point = readClientPoint(ev) ?? readClientPoint(e);
603
+ const sx = e.datas.__startX ?? data.x;
604
+ const sy = e.datas.__startY ?? data.y;
605
+ const point = readClientPoint(e);
643
606
  const hasClient =
644
- typeof ev.datas.__startClientX === "number" &&
645
- typeof ev.datas.__startClientY === "number" &&
607
+ typeof e.datas.__startClientX === "number" &&
608
+ typeof e.datas.__startClientY === "number" &&
646
609
  !!point;
647
610
  const tx = hasClient
648
- ? toCanvasDeltaX(point.x - ev.datas.__startClientX)
649
- : toCanvasDeltaX(ev.lastEvent?.beforeTranslate?.[0] ?? 0);
611
+ ? toCanvasDeltaX(point.x - e.datas.__startClientX)
612
+ : toCanvasDeltaX(e.lastEvent?.beforeTranslate?.[0] ?? 0);
650
613
  const ty = hasClient
651
- ? toCanvasDeltaY(point.y - ev.datas.__startClientY)
652
- : toCanvasDeltaY(ev.lastEvent?.beforeTranslate?.[1] ?? 0);
614
+ ? toCanvasDeltaY(point.y - e.datas.__startClientY)
615
+ : toCanvasDeltaY(e.lastEvent?.beforeTranslate?.[1] ?? 0);
653
616
  const nextX = sx + tx;
654
617
  const nextY = sy + ty;
655
- const size = readTargetCanvasSize(ev.target as HTMLElement | null, {
618
+ const size = readTargetCanvasSize(e.target as HTMLElement | null, {
656
619
  width: data.width,
657
620
  height: data.height,
658
621
  });
659
622
  const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
660
- props.updateElement(
661
- id,
662
- { x: nextX, y: nextY, ...patch },
663
- { batchId, meta: { type: "node.group-drag" } }
664
- );
623
+ props.updateElement(id, { x: nextX, y: nextY, ...patch });
665
624
  if (data.materialType === "grid") {
666
625
  const dx = nextX - data.x;
667
626
  const dy = nextY - data.y;
668
627
  if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
669
- gridDeltaMap.set(id, { dx, dy });
628
+ const children = getGridDirectChildren(id);
629
+ const batchId = `move-grid-children-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
630
+ children.forEach((child) => {
631
+ props.updateElement(
632
+ child.id,
633
+ { x: child.x + dx, y: child.y + dy },
634
+ { batchId, meta: { type: "node.group-drag" } }
635
+ );
636
+ });
670
637
  }
671
638
  }
672
- });
673
- for (const [gridId, delta] of gridDeltaMap.entries()) {
674
- const children = getGridDirectChildren(gridId);
675
- children.forEach((child) => {
676
- if (selectedSet.has(child.id)) return;
639
+ scheduleViewportSync();
640
+ } finally {
641
+ setCanvasInteractionBusy(false);
642
+ }
643
+ });
644
+
645
+ moveable.on("dragGroupEnd", (e: any) => {
646
+ try {
647
+ const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
648
+ const selectedSet = new Set(
649
+ e.events
650
+ .map((ev: any) => (ev.target ? getId(ev.target) : null))
651
+ .filter((id: string | null): id is string => !!id)
652
+ );
653
+ const gridDeltaMap = new Map<string, { dx: number; dy: number }>();
654
+ e.events.forEach((ev: any) => {
655
+ const id = ev.target ? getId(ev.target) : null;
656
+ if (!id) return;
657
+ const data = props.elementsById.get(id);
658
+ if (!data) return;
659
+ const sx = ev.datas.__startX ?? data.x;
660
+ const sy = ev.datas.__startY ?? data.y;
661
+ const point = readClientPoint(ev) ?? readClientPoint(e);
662
+ const hasClient =
663
+ typeof ev.datas.__startClientX === "number" &&
664
+ typeof ev.datas.__startClientY === "number" &&
665
+ !!point;
666
+ const tx = hasClient
667
+ ? toCanvasDeltaX(point.x - ev.datas.__startClientX)
668
+ : toCanvasDeltaX(ev.lastEvent?.beforeTranslate?.[0] ?? 0);
669
+ const ty = hasClient
670
+ ? toCanvasDeltaY(point.y - ev.datas.__startClientY)
671
+ : toCanvasDeltaY(ev.lastEvent?.beforeTranslate?.[1] ?? 0);
672
+ const nextX = sx + tx;
673
+ const nextY = sy + ty;
674
+ const size = readTargetCanvasSize(ev.target as HTMLElement | null, {
675
+ width: data.width,
676
+ height: data.height,
677
+ });
678
+ const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
677
679
  props.updateElement(
678
- child.id,
679
- { x: child.x + delta.dx, y: child.y + delta.dy },
680
+ id,
681
+ { x: nextX, y: nextY, ...patch },
680
682
  { batchId, meta: { type: "node.group-drag" } }
681
683
  );
684
+ if (data.materialType === "grid") {
685
+ const dx = nextX - data.x;
686
+ const dy = nextY - data.y;
687
+ if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
688
+ gridDeltaMap.set(id, { dx, dy });
689
+ }
690
+ }
682
691
  });
692
+ for (const [gridId, delta] of gridDeltaMap.entries()) {
693
+ const children = getGridDirectChildren(gridId);
694
+ children.forEach((child) => {
695
+ if (selectedSet.has(child.id)) return;
696
+ props.updateElement(
697
+ child.id,
698
+ { x: child.x + delta.dx, y: child.y + delta.dy },
699
+ { batchId, meta: { type: "node.group-drag" } }
700
+ );
701
+ });
702
+ }
703
+ scheduleViewportSync();
704
+ } finally {
705
+ setCanvasInteractionBusy(false);
683
706
  }
684
- scheduleViewportSync();
685
707
  });
686
708
 
687
709
  moveable.on("resizeEnd", (e: any) => {
688
- const id = resolveSingleEventId(e.target as HTMLElement | null);
689
- if (!id) return;
690
- const data = props.elementsById.get(id);
691
- if (!data) return;
692
- const width = e.lastEvent?.width ?? data.width;
693
- const height = e.lastEvent?.height ?? data.height;
694
- const tx = toCanvasDeltaX(e.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
695
- const ty = toCanvasDeltaY(e.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
696
- const sx = e.datas.__startX ?? data.x;
697
- const sy = e.datas.__startY ?? data.y;
698
- const nextX = sx + tx;
699
- const nextY = sy + ty;
700
- const size = readTargetCanvasSize(e.target as HTMLElement | null, { width, height });
701
- const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
702
- props.updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
703
- scheduleViewportSync();
704
- });
705
-
706
- moveable.on("resizeGroupEnd", (e: any) => {
707
- const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
708
- e.events.forEach((ev: any) => {
709
- const id = ev.target ? getId(ev.target) : null;
710
+ try {
711
+ const id = resolveSingleEventId(e.target as HTMLElement | null);
710
712
  if (!id) return;
711
713
  const data = props.elementsById.get(id);
712
714
  if (!data) return;
713
- const width = ev.lastEvent?.width ?? data.width;
714
- const height = ev.lastEvent?.height ?? data.height;
715
- const tx = toCanvasDeltaX(ev.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
716
- const ty = toCanvasDeltaY(ev.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
717
- const sx = ev.datas.__startX ?? data.x;
718
- const sy = ev.datas.__startY ?? data.y;
715
+ const width = e.lastEvent?.width ?? data.width;
716
+ const height = e.lastEvent?.height ?? data.height;
717
+ const tx = toCanvasDeltaX(e.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
718
+ const ty = toCanvasDeltaY(e.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
719
+ const sx = e.datas.__startX ?? data.x;
720
+ const sy = e.datas.__startY ?? data.y;
719
721
  const nextX = sx + tx;
720
722
  const nextY = sy + ty;
721
- const size = readTargetCanvasSize(ev.target as HTMLElement | null, { width, height });
723
+ const size = readTargetCanvasSize(e.target as HTMLElement | null, { width, height });
722
724
  const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
723
- props.updateElement(
724
- id,
725
- { width, height, x: nextX, y: nextY, ...snapPatch },
726
- { batchId, meta: { type: "node.group-resize" } }
727
- );
728
- });
729
- scheduleViewportSync();
725
+ props.updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
726
+ scheduleViewportSync();
727
+ } finally {
728
+ setCanvasInteractionBusy(false);
729
+ }
730
730
  });
731
731
 
732
- moveable.on("rotateEnd", (e: any) => {
733
- const id = resolveSingleEventId(e.target as HTMLElement | null);
734
- if (!id) return;
735
- const data = props.elementsById.get(id);
736
- if (!data) return;
737
- const rotate = e.lastEvent?.beforeRotate ?? data.rotate ?? 0;
738
- props.updateElement(id, { rotate });
739
- scheduleViewportSync();
732
+ moveable.on("resizeGroupEnd", (e: any) => {
733
+ try {
734
+ const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
735
+ e.events.forEach((ev: any) => {
736
+ const id = ev.target ? getId(ev.target) : null;
737
+ if (!id) return;
738
+ const data = props.elementsById.get(id);
739
+ if (!data) return;
740
+ const width = ev.lastEvent?.width ?? data.width;
741
+ const height = ev.lastEvent?.height ?? data.height;
742
+ const tx = toCanvasDeltaX(ev.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
743
+ const ty = toCanvasDeltaY(ev.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
744
+ const sx = ev.datas.__startX ?? data.x;
745
+ const sy = ev.datas.__startY ?? data.y;
746
+ const nextX = sx + tx;
747
+ const nextY = sy + ty;
748
+ const size = readTargetCanvasSize(ev.target as HTMLElement | null, { width, height });
749
+ const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
750
+ props.updateElement(
751
+ id,
752
+ { width, height, x: nextX, y: nextY, ...snapPatch },
753
+ { batchId, meta: { type: "node.group-resize" } }
754
+ );
755
+ });
756
+ scheduleViewportSync();
757
+ } finally {
758
+ setCanvasInteractionBusy(false);
759
+ }
740
760
  });
741
761
 
742
- moveable.on("rotateGroupEnd", (e: any) => {
743
- const batchId = `rotate-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
744
- e.events.forEach((ev: any) => {
745
- const id = ev.target ? getId(ev.target) : null;
762
+ moveable.on("rotateEnd", (e: any) => {
763
+ try {
764
+ const id = resolveSingleEventId(e.target as HTMLElement | null);
746
765
  if (!id) return;
747
766
  const data = props.elementsById.get(id);
748
767
  if (!data) return;
749
- const rotate = ev.lastEvent?.beforeRotate ?? data.rotate ?? 0;
750
- props.updateElement(id, { rotate }, { batchId, meta: { type: "node.group-rotate" } });
751
- });
752
- scheduleViewportSync();
768
+ const rotate = e.lastEvent?.beforeRotate ?? data.rotate ?? 0;
769
+ props.updateElement(id, { rotate });
770
+ scheduleViewportSync();
771
+ } finally {
772
+ setCanvasInteractionBusy(false);
773
+ }
774
+ });
775
+
776
+ moveable.on("rotateGroupEnd", (e: any) => {
777
+ try {
778
+ const batchId = `rotate-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
779
+ e.events.forEach((ev: any) => {
780
+ const id = ev.target ? getId(ev.target) : null;
781
+ if (!id) return;
782
+ const data = props.elementsById.get(id);
783
+ if (!data) return;
784
+ const rotate = ev.lastEvent?.beforeRotate ?? data.rotate ?? 0;
785
+ props.updateElement(id, { rotate }, { batchId, meta: { type: "node.group-rotate" } });
786
+ });
787
+ scheduleViewportSync();
788
+ } finally {
789
+ setCanvasInteractionBusy(false);
790
+ }
753
791
  });
754
792
 
755
793
  moveableRef.value = moveable;
@@ -1,4 +1,4 @@
1
- import { computed, onMounted } from "vue";
1
+ import { computed, onMounted, shallowRef } from "vue";
2
2
  import { store, type Node, type State } from "@arronqzy/rx-store";
3
3
  import { useStoreRef } from "@arronqzy/vue-rx-store";
4
4
  import { useI18nOptional } from "@arronqzy/i18n/vue";
@@ -47,6 +47,10 @@ import {
47
47
  applyGridLayoutPatchAcrossMappingFamily,
48
48
  applyMappingFamilySyncPatch,
49
49
  } from "../utils/updateElementDraft";
50
+ import {
51
+ materializePanelElements,
52
+ stabilizeElementList,
53
+ } from "../utils/materializePanelElements";
50
54
 
51
55
  export type { PanelActionResult, PanelHistoryItem, PanelLayer } from "../types";
52
56
 
@@ -126,19 +130,24 @@ export function usePanelElements() {
126
130
  (stateRef.value.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID
127
131
  );
128
132
 
133
+ const allElementsCache = shallowRef<PanelElement[]>([]);
134
+ const elementsCache = shallowRef<PanelElement[]>([]);
135
+
129
136
  const allElements = computed(() => {
130
- const nodes = stateRef.value.root.children ?? [];
131
- return nodes
132
- .filter((n) => isPanelElementNode(n) && n.props)
133
- .map((n) => {
134
- const props = n.props as PanelElement;
135
- return { ...props, zIndex: typeof props.zIndex === "number" ? props.zIndex : 1 };
136
- });
137
+ const next = materializePanelElements(
138
+ stateRef.value.root.children,
139
+ allElementsCache.value
140
+ );
141
+ allElementsCache.value = next;
142
+ return next;
137
143
  });
138
144
 
139
- const elements = computed(() =>
140
- allElements.value.filter((el) => el.layerId === activeLayerId.value)
141
- );
145
+ const elements = computed(() => {
146
+ const next = allElements.value.filter((el) => el.layerId === activeLayerId.value);
147
+ const stable = stabilizeElementList(next, elementsCache.value);
148
+ elementsCache.value = stable;
149
+ return stable;
150
+ });
142
151
 
143
152
  const byId = computed(() => {
144
153
  const map = new Map<string, PanelElement>();
@@ -0,0 +1,26 @@
1
+ /**
2
+ * While Moveable is actively dragging/resizing/rotating, pause expensive
3
+ * canvas-wide work (e.g. ellipsis overflow measurement via MutationObserver).
4
+ */
5
+ let busyDepth = 0;
6
+ const listeners = new Set<() => void>();
7
+
8
+ export function isCanvasInteractionBusy(): boolean {
9
+ return busyDepth > 0;
10
+ }
11
+
12
+ export function setCanvasInteractionBusy(busy: boolean): void {
13
+ const prev = busyDepth > 0;
14
+ if (busy) busyDepth += 1;
15
+ else busyDepth = Math.max(0, busyDepth - 1);
16
+ const next = busyDepth > 0;
17
+ if (prev === next) return;
18
+ listeners.forEach((listener) => listener());
19
+ }
20
+
21
+ export function subscribeCanvasInteractionBusy(listener: () => void): () => void {
22
+ listeners.add(listener);
23
+ return () => {
24
+ listeners.delete(listener);
25
+ };
26
+ }
@@ -0,0 +1,96 @@
1
+ import type { PanelElement } from "../types";
2
+ import { isPanelElementNode } from "./panelElementNodes";
3
+ import type { Node } from "@arronqzy/rx-store";
4
+
5
+ /**
6
+ * Preserve Immer structural sharing: unchanged nodes keep the same props
7
+ * object reference so React.memo / Vue can skip re-rendering them.
8
+ */
9
+ export function materializePanelElements(
10
+ nodes: Node[] | undefined,
11
+ previous?: readonly PanelElement[] | null
12
+ ): PanelElement[] {
13
+ const list = nodes ?? [];
14
+ const prevById = previous?.length
15
+ ? new Map(previous.map((el) => [el.id, el] as const))
16
+ : null;
17
+ const next: PanelElement[] = [];
18
+
19
+ for (const n of list) {
20
+ if (!isPanelElementNode(n) || !n.props) continue;
21
+ const props = n.props as PanelElement;
22
+ const normalized: PanelElement =
23
+ typeof props.zIndex === "number" ? props : { ...props, zIndex: 1 };
24
+
25
+ const prev = prevById?.get(normalized.id);
26
+ if (prev && prev === normalized) {
27
+ next.push(prev);
28
+ continue;
29
+ }
30
+ if (
31
+ prev &&
32
+ prev.id === normalized.id &&
33
+ prev.x === normalized.x &&
34
+ prev.y === normalized.y &&
35
+ prev.width === normalized.width &&
36
+ prev.height === normalized.height &&
37
+ prev.rotate === normalized.rotate &&
38
+ prev.zIndex === normalized.zIndex &&
39
+ prev.layerId === normalized.layerId &&
40
+ prev.locked === normalized.locked &&
41
+ prev.materialType === normalized.materialType &&
42
+ prev.name === normalized.name &&
43
+ prev.chart === normalized.chart &&
44
+ prev.table === normalized.table &&
45
+ prev.style === normalized.style &&
46
+ prev.textHtml === normalized.textHtml &&
47
+ prev.textAllowInput === normalized.textAllowInput &&
48
+ prev.gridRows === normalized.gridRows &&
49
+ prev.gridCols === normalized.gridCols &&
50
+ prev.gridGap === normalized.gridGap &&
51
+ prev.gridPadding === normalized.gridPadding &&
52
+ prev.parentGridId === normalized.parentGridId &&
53
+ prev.gridSlotIndex === normalized.gridSlotIndex &&
54
+ prev.gridColSpan === normalized.gridColSpan &&
55
+ prev.gridRowSpan === normalized.gridRowSpan &&
56
+ prev.viewportOverflow === normalized.viewportOverflow &&
57
+ prev.scene3d === normalized.scene3d &&
58
+ prev.refLayerId === normalized.refLayerId &&
59
+ prev.refCopyMode === normalized.refCopyMode &&
60
+ prev.refSnapshot === normalized.refSnapshot &&
61
+ prev.audioSrc === normalized.audioSrc &&
62
+ prev.videoSrc === normalized.videoSrc &&
63
+ prev.geometryShape === normalized.geometryShape &&
64
+ prev.geometryColor === normalized.geometryColor &&
65
+ prev.geometrySketchDataUrl === normalized.geometrySketchDataUrl
66
+ ) {
67
+ next.push(prev);
68
+ continue;
69
+ }
70
+ next.push(normalized);
71
+ }
72
+
73
+ if (
74
+ previous &&
75
+ previous.length === next.length &&
76
+ previous.every((el, i) => el === next[i])
77
+ ) {
78
+ return previous as PanelElement[];
79
+ }
80
+ return next;
81
+ }
82
+
83
+ /** Keep previous array identity when item references are unchanged (order-sensitive). */
84
+ export function stabilizeElementList(
85
+ next: PanelElement[],
86
+ previous?: readonly PanelElement[] | null
87
+ ): PanelElement[] {
88
+ if (
89
+ previous &&
90
+ previous.length === next.length &&
91
+ previous.every((el, i) => el === next[i])
92
+ ) {
93
+ return previous as PanelElement[];
94
+ }
95
+ return next;
96
+ }
@@ -137,6 +137,26 @@ export function computePanelSceneBounds(elements: PanelElement[]): PanelSceneBou
137
137
 
138
138
  export const PREVIEW_LAYOUT_EVENT = "arronqzy-preview-layout";
139
139
 
140
- export function notifyPreviewLayoutChanged() {
141
- window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
140
+ let previewLayoutRaf = 0;
141
+
142
+ /**
143
+ * Notify chart/preview hosts that layout changed.
144
+ * Default path is rAF-coalesced (safe during Moveable resize).
145
+ * Pass `{ flush: true }` after resize ends so charts catch the final size.
146
+ */
147
+ export function notifyPreviewLayoutChanged(options?: { flush?: boolean }) {
148
+ if (typeof window === "undefined") return;
149
+ if (options?.flush) {
150
+ if (previewLayoutRaf) {
151
+ window.cancelAnimationFrame(previewLayoutRaf);
152
+ previewLayoutRaf = 0;
153
+ }
154
+ window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
155
+ return;
156
+ }
157
+ if (previewLayoutRaf) return;
158
+ previewLayoutRaf = window.requestAnimationFrame(() => {
159
+ previewLayoutRaf = 0;
160
+ window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
161
+ });
142
162
  }