@arronqzy/react-view 1.0.20 → 1.0.21

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/dist/styles.css CHANGED
@@ -616,6 +616,10 @@ video {
616
616
  inset: 0px;
617
617
  }
618
618
 
619
+ .inset-1\.5 {
620
+ inset: 0.375rem;
621
+ }
622
+
619
623
  .inset-2 {
620
624
  inset: 0.5rem;
621
625
  }
@@ -645,6 +649,10 @@ video {
645
649
  left: 0.125rem;
646
650
  }
647
651
 
652
+ .left-1 {
653
+ left: 0.25rem;
654
+ }
655
+
648
656
  .left-1\/2 {
649
657
  left: 50%;
650
658
  }
@@ -653,6 +661,10 @@ video {
653
661
  left: 0.5rem;
654
662
  }
655
663
 
664
+ .left-6 {
665
+ left: 1.5rem;
666
+ }
667
+
656
668
  .left-\[var\(--rv-ruler-size\)\] {
657
669
  left: var(--rv-ruler-size);
658
670
  }
@@ -677,10 +689,18 @@ video {
677
689
  top: 0.125rem;
678
690
  }
679
691
 
692
+ .top-1 {
693
+ top: 0.25rem;
694
+ }
695
+
680
696
  .top-2 {
681
697
  top: 0.5rem;
682
698
  }
683
699
 
700
+ .top-4 {
701
+ top: 1rem;
702
+ }
703
+
684
704
  .top-\[calc\(100\%\+6px\)\] {
685
705
  top: calc(100% + 6px);
686
706
  }
@@ -1513,6 +1533,10 @@ video {
1513
1533
  border-radius: 1rem;
1514
1534
  }
1515
1535
 
1536
+ .rounded-\[2px\] {
1537
+ border-radius: 2px;
1538
+ }
1539
+
1516
1540
  .rounded-full {
1517
1541
  border-radius: 9999px;
1518
1542
  }
@@ -1533,6 +1557,15 @@ video {
1533
1557
  border-radius: 0.75rem;
1534
1558
  }
1535
1559
 
1560
+ .rounded-l {
1561
+ border-top-left-radius: 0.25rem;
1562
+ border-bottom-left-radius: 0.25rem;
1563
+ }
1564
+
1565
+ .rounded-tl {
1566
+ border-top-left-radius: 0.25rem;
1567
+ }
1568
+
1536
1569
  .border {
1537
1570
  border-width: 1px;
1538
1571
  }
@@ -1890,10 +1923,22 @@ video {
1890
1923
  background-color: hsl(var(--primary) / 0.15);
1891
1924
  }
1892
1925
 
1926
+ .bg-primary\/30 {
1927
+ background-color: hsl(var(--primary) / 0.3);
1928
+ }
1929
+
1893
1930
  .bg-primary\/35 {
1894
1931
  background-color: hsl(var(--primary) / 0.35);
1895
1932
  }
1896
1933
 
1934
+ .bg-primary\/40 {
1935
+ background-color: hsl(var(--primary) / 0.4);
1936
+ }
1937
+
1938
+ .bg-primary\/45 {
1939
+ background-color: hsl(var(--primary) / 0.45);
1940
+ }
1941
+
1897
1942
  .bg-primary\/5 {
1898
1943
  background-color: hsl(var(--primary) / 0.05);
1899
1944
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/react-view",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "files": [
5
5
  "src",
6
6
  "dist",
@@ -30,14 +30,14 @@
30
30
  "@codemirror/lang-json": "^6.0.1",
31
31
  "@codemirror/theme-one-dark": "^6.1.2",
32
32
  "@codemirror/view": "^6.36.2",
33
- "@arronqzy/i18n": "0.1.7",
34
- "@arronqzy/blueprint-dsl": "1.0.9",
35
33
  "@arronqzy/ui": "1.0.6",
34
+ "@arronqzy/blueprint-dsl": "1.0.10",
35
+ "@arronqzy/i18n": "0.1.8",
36
+ "@arronqzy/react-blueprint": "1.0.12",
36
37
  "@arronqzy/rx-store": "1.0.5",
37
- "@arronqzy/react-blueprint": "1.0.11",
38
- "@arronqzy/view-scene3d": "0.1.2",
38
+ "@arronqzy/webllm-assistant": "0.1.10",
39
39
  "@arronqzy/view-table": "0.1.5",
40
- "@arronqzy/webllm-assistant": "0.1.9"
40
+ "@arronqzy/view-scene3d": "0.1.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/react": "^19.0.1",
@@ -755,6 +755,7 @@ function ReactViewPanelInner({
755
755
  | "lifecyclePhase"
756
756
  | "fetchConfig"
757
757
  | "jsonConfig"
758
+ | "storageConfig"
758
759
  | "logicConfig"
759
760
  | "clockConfig"
760
761
  | "eventConfig"
@@ -1602,7 +1603,7 @@ function ReactViewPanelInner({
1602
1603
  if (targetLayer.isMapping) return null;
1603
1604
  const hasBlockingRef = allElements.some((el) => {
1604
1605
  if (el.layerId === layerId) return false;
1605
- if (el.materialType !== "reference") return false;
1606
+ if (el.materialType !== "reference" && el.materialType !== "viewport") return false;
1606
1607
  if (el.refLayerId !== layerId) return false;
1607
1608
  return (el.refCopyMode ?? "shallow") !== "deep";
1608
1609
  });
@@ -1875,6 +1876,13 @@ function ReactViewPanelInner({
1875
1876
  node.dataset.origWidth = String(parseFloat(node.style.width) || node.offsetWidth || 0);
1876
1877
  node.dataset.origHeight = String(parseFloat(node.style.height) || node.offsetHeight || 0);
1877
1878
  }
1879
+ function applyBoxScale(node, sx, sy) {
1880
+ rememberBox(node);
1881
+ node.style.left = (Number(node.dataset.origLeft) * sx) + "px";
1882
+ node.style.top = (Number(node.dataset.origTop) * sy) + "px";
1883
+ node.style.width = (Number(node.dataset.origWidth) * sx) + "px";
1884
+ node.style.height = (Number(node.dataset.origHeight) * sy) + "px";
1885
+ }
1878
1886
  function fitScene() {
1879
1887
  if (!scene) return;
1880
1888
  var vw = window.innerWidth || 1;
@@ -1885,29 +1893,23 @@ function ReactViewPanelInner({
1885
1893
  var scaleY = vh / sh;
1886
1894
  if (root) root.style.overflow = "hidden";
1887
1895
  var nodes = scene.querySelectorAll("[data-element-id]");
1896
+ var sx = outputScale ? 1 : scaleX;
1897
+ var sy = outputScale ? 1 : scaleY;
1888
1898
  if (outputScale) {
1889
1899
  scene.style.width = sw + "px";
1890
1900
  scene.style.height = sh + "px";
1891
1901
  scene.style.transform = "translate(0px,0px) scale(" + scaleX + "," + scaleY + ")";
1892
- nodes.forEach(function (node) {
1893
- rememberBox(node);
1894
- node.style.left = node.dataset.origLeft + "px";
1895
- node.style.top = node.dataset.origTop + "px";
1896
- node.style.width = node.dataset.origWidth + "px";
1897
- node.style.height = node.dataset.origHeight + "px";
1898
- });
1899
- return;
1902
+ } else {
1903
+ scene.style.width = vw + "px";
1904
+ scene.style.height = vh + "px";
1905
+ scene.style.transform = "none";
1900
1906
  }
1901
- // 块仍按视口撑满,但不对整棵内容做 transform,字体保持原尺寸
1902
- scene.style.width = vw + "px";
1903
- scene.style.height = vh + "px";
1904
- scene.style.transform = "none";
1905
1907
  nodes.forEach(function (node) {
1906
- rememberBox(node);
1907
- node.style.left = (Number(node.dataset.origLeft) * scaleX) + "px";
1908
- node.style.top = (Number(node.dataset.origTop) * scaleY) + "px";
1909
- node.style.width = (Number(node.dataset.origWidth) * scaleX) + "px";
1910
- node.style.height = (Number(node.dataset.origHeight) * scaleY) + "px";
1908
+ applyBoxScale(node, sx, sy);
1909
+ var inner = node.querySelectorAll("[data-viewport-sizer], [data-viewport-item]");
1910
+ inner.forEach(function (el) {
1911
+ applyBoxScale(el, sx, sy);
1912
+ });
1911
1913
  });
1912
1914
  }
1913
1915
  fitScene();
@@ -201,6 +201,7 @@ function applyBlueprintAddNode(
201
201
  clock: "clock",
202
202
  fetch: "fetch",
203
203
  json: "json",
204
+ storage: "storage",
204
205
  and: "and",
205
206
  event: "event",
206
207
  };
@@ -6,6 +6,12 @@ import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
6
6
  import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
7
7
  import { TableNodeContent, type TableCellActionHandler } from "./table/TableNodeContent";
8
8
  import { comparePanelElementsPaintOrder } from "../utils/gridPlacement";
9
+ import {
10
+ isViewportNode,
11
+ normalizeViewportOverflow,
12
+ viewportOverflowStyle,
13
+ viewportWindowContentScale,
14
+ } from "../utils/viewportPlacement";
9
15
  import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
10
16
  import { mergeScene3dConfig } from "@arronqzy/view-scene3d";
11
17
  import { Scene3dNodeContent } from "@arronqzy/view-scene3d/react";
@@ -724,18 +730,30 @@ function ReferenceNodeContent({
724
730
 
725
731
  const innerW = Math.max(1, element.width);
726
732
  const innerH = Math.max(1, element.height);
727
- const scale = Math.max(0.05, Math.min(innerW / sourceWidth, innerH / sourceHeight));
728
- const mappedW = sourceWidth * scale;
729
- const mappedH = sourceHeight * scale;
733
+ const windowFit = isViewportNode(element);
734
+ const windowScale = windowFit
735
+ ? viewportWindowContentScale(element, allElements)
736
+ : { scaleX: 1, scaleY: 1 };
737
+ const scaleX = windowFit
738
+ ? windowScale.scaleX
739
+ : Math.max(0.05, Math.min(innerW / sourceWidth, innerH / sourceHeight));
740
+ const scaleY = windowFit
741
+ ? windowScale.scaleY
742
+ : scaleX;
743
+ const mappedW = sourceWidth * scaleX;
744
+ const mappedH = sourceHeight * scaleY;
730
745
 
731
746
  return {
732
747
  minX,
733
748
  minY,
734
- scale,
735
- offsetX: (innerW - mappedW) / 2,
736
- offsetY: (innerH - mappedH) / 2,
749
+ scaleX,
750
+ scaleY,
751
+ sourceWidth: mappedW,
752
+ sourceHeight: mappedH,
753
+ offsetX: windowFit ? 0 : (innerW - mappedW) / 2,
754
+ offsetY: windowFit ? 0 : (innerH - mappedH) / 2,
737
755
  };
738
- }, [element.height, element.width, sourceNodes]);
756
+ }, [allElements, element, sourceNodes]);
739
757
 
740
758
  if (!layout || sourceNodes.length === 0) {
741
759
  const hintText = element.refLayerId ? t("panel.config.refLayerEmpty") : t("panel.config.selectRefLayer");
@@ -748,20 +766,50 @@ function ReferenceNodeContent({
748
766
  );
749
767
  }
750
768
 
769
+ const overflowCss = isViewportNode(element)
770
+ ? viewportOverflowStyle(normalizeViewportOverflow(element.viewportOverflow))
771
+ : { overflowX: "hidden" as const, overflowY: "hidden" as const };
772
+ const canScroll =
773
+ isViewportNode(element) &&
774
+ (overflowCss.overflowX === "auto" || overflowCss.overflowY === "auto");
775
+
751
776
  return (
752
- <div className="pointer-events-none relative h-full w-full overflow-hidden">
777
+ <div
778
+ className="relative h-full w-full"
779
+ style={{
780
+ overflowX: overflowCss.overflowX,
781
+ overflowY: overflowCss.overflowY,
782
+ pointerEvents: isViewportNode(element) ? "auto" : "none",
783
+ }}
784
+ onWheel={canScroll ? (e) => e.stopPropagation() : undefined}
785
+ >
786
+ <div
787
+ className="pointer-events-none relative"
788
+ data-viewport-sizer={isViewportNode(element) ? "true" : undefined}
789
+ style={
790
+ isViewportNode(element)
791
+ ? {
792
+ width: layout.sourceWidth,
793
+ height: layout.sourceHeight,
794
+ minWidth: "100%",
795
+ minHeight: "100%",
796
+ }
797
+ : { width: "100%", height: "100%" }
798
+ }
799
+ >
753
800
  {sourceNodes.map((node) => {
754
801
  const box = getNodeAABB(node);
755
- const left = layout.offsetX + (box.left - layout.minX) * layout.scale;
756
- const top = layout.offsetY + (box.top - layout.minY) * layout.scale;
757
- const width = Math.max(12, node.width * layout.scale);
758
- const height = Math.max(10, node.height * layout.scale);
759
- const boxWidth = Math.max(12, (box.right - box.left) * layout.scale);
760
- const boxHeight = Math.max(10, (box.bottom - box.top) * layout.scale);
802
+ const left = layout.offsetX + (box.left - layout.minX) * layout.scaleX;
803
+ const top = layout.offsetY + (box.top - layout.minY) * layout.scaleY;
804
+ const width = Math.max(12, node.width * layout.scaleX);
805
+ const height = Math.max(10, node.height * layout.scaleY);
806
+ const boxWidth = Math.max(12, (box.right - box.left) * layout.scaleX);
807
+ const boxHeight = Math.max(10, (box.bottom - box.top) * layout.scaleY);
761
808
  return (
762
809
  <div
763
810
  key={node.id}
764
811
  className="absolute overflow-visible"
812
+ data-viewport-item={isViewportNode(element) ? "true" : undefined}
765
813
  style={{
766
814
  left,
767
815
  top,
@@ -771,6 +819,7 @@ function ReferenceNodeContent({
771
819
  >
772
820
  <div
773
821
  className="absolute"
822
+ data-viewport-item={isViewportNode(element) ? "true" : undefined}
774
823
  style={{
775
824
  left: (boxWidth - width) / 2,
776
825
  top: (boxHeight - height) / 2,
@@ -783,7 +832,7 @@ function ReferenceNodeContent({
783
832
  >
784
833
  {CHART_TYPES.has(node.materialType ?? "") ? (
785
834
  <ChartNodeContent element={node} />
786
- ) : node.materialType === "reference" ? (
835
+ ) : node.materialType === "reference" || node.materialType === "viewport" ? (
787
836
  <ReferenceNodeContent
788
837
  element={node}
789
838
  allElements={allElements}
@@ -803,6 +852,7 @@ function ReferenceNodeContent({
803
852
  </div>
804
853
  );
805
854
  })}
855
+ </div>
806
856
  </div>
807
857
  );
808
858
  }
@@ -887,6 +937,7 @@ export const ElementsLayer = React.memo(function ElementsLayer({
887
937
  zIndex: el.zIndex ?? 1,
888
938
  transform: `rotate(${el.rotate ?? 0}deg)`,
889
939
  transformOrigin: "center center",
940
+ overflow: el.materialType === "viewport" ? "hidden" : undefined,
890
941
  boxSizing: "border-box",
891
942
  ...getNodeVisualStyle(el),
892
943
  }}
@@ -897,7 +948,7 @@ export const ElementsLayer = React.memo(function ElementsLayer({
897
948
  previewLayoutKey={previewLayoutKey}
898
949
  previewMode={previewMode}
899
950
  />
900
- ) : el.materialType === "reference" ? (
951
+ ) : el.materialType === "reference" || el.materialType === "viewport" ? (
901
952
  <ReferenceNodeContent element={el} allElements={allElements} />
902
953
  ) : el.materialType === "grid" ? (
903
954
  <GridNodeContent
@@ -63,6 +63,7 @@ const MATERIAL_LABEL_KEYS: Record<string, string> = {
63
63
  text: "panel.material.text",
64
64
  rect: "panel.material.rect",
65
65
  grid: "panel.material.grid",
66
+ viewport: "panel.material.viewport",
66
67
  image: "panel.material.image",
67
68
  video: "panel.material.video",
68
69
  audio: "panel.material.audio",
@@ -374,6 +375,18 @@ function MaterialPreview({ id }: { id: string }) {
374
375
  </div>
375
376
  );
376
377
  }
378
+ if (id === "viewport") {
379
+ return (
380
+ <div className={common}>
381
+ <div className="absolute inset-1.5 overflow-hidden rounded-sm border border-primary/70 bg-primary/10">
382
+ <div className="absolute left-1 top-1 h-6 w-8 rounded-[2px] bg-primary/45" />
383
+ <div className="absolute left-6 top-4 h-7 w-10 rounded-[2px] bg-primary/30" />
384
+ <div className="absolute bottom-0 right-0 h-1.5 w-6 rounded-tl bg-primary/55" />
385
+ <div className="absolute bottom-0 right-0 top-2 w-1.5 rounded-l bg-primary/40" />
386
+ </div>
387
+ </div>
388
+ );
389
+ }
377
390
 
378
391
  return <div className={common} />;
379
392
  }
@@ -403,6 +416,7 @@ function getDefaultCategories(t: (key: string) => string): MaterialCategory[] {
403
416
  { id: "geometry", title: t("panel.material.geometry") },
404
417
  { id: "scene3d", title: t("panel.material.scene3d") },
405
418
  { id: "grid", title: t("panel.material.grid") },
419
+ { id: "viewport", title: t("panel.material.viewport") },
406
420
  { id: "image", title: t("panel.material.image") },
407
421
  { id: "reference", title: t("panel.material.reference") },
408
422
  ],
@@ -568,9 +582,8 @@ export function MaterialSidebar({
568
582
  };
569
583
 
570
584
  const getNodeChildren = (node: PanelElement, sourceOverride?: PanelElement[]) => {
571
- const isRef = node.materialType === "reference";
585
+ const isRef = node.materialType === "reference" || node.materialType === "viewport";
572
586
  const isGrid = node.materialType === "grid";
573
- // 必须用节点自身所在图层查找子节点;外层传入的 layerId 在深嵌套/引用子树中会错位,导致子节点不显示在对应网格下
574
587
  const gridChildren = isGrid
575
588
  ? [...(childrenByGridByLayer.get(node.layerId)?.get(node.id) ?? [])].sort(
576
589
  compareGridTreeChildOrder
@@ -611,7 +624,7 @@ export function MaterialSidebar({
611
624
  ) => {
612
625
  const selected = selectedIds.includes(node.id);
613
626
  if (!nodeMatchesTreeSearch(node, visited, sourceOverride)) return null;
614
- const isRef = node.materialType === "reference";
627
+ const isRef = node.materialType === "reference" || node.materialType === "viewport";
615
628
  const refMode = node.refCopyMode ?? "shallow";
616
629
  const isDeepRef = isRef && refMode === "deep";
617
630
  const children = getNodeChildren(node, sourceOverride);
@@ -829,17 +842,11 @@ export function MaterialSidebar({
829
842
  };
830
843
 
831
844
  const hasRefInSubtree = (node: PanelElement, visited: Set<string>): boolean => {
832
- if (node.materialType === "reference") return true;
833
- if (node.materialType !== "reference") return false;
845
+ if (node.materialType === "reference" || node.materialType === "viewport") return true;
834
846
  if (visited.has(node.id)) return false;
835
847
  const nextVisited = new Set(visited);
836
848
  nextVisited.add(node.id);
837
- const children =
838
- node.refCopyMode === "deep"
839
- ? node.refSnapshot ?? []
840
- : node.refLayerId
841
- ? elementsByLayer.get(node.refLayerId) ?? []
842
- : [];
849
+ const children = getNodeChildren(node);
843
850
  return children.some((child) => hasRefInSubtree(child, nextVisited));
844
851
  };
845
852
 
@@ -28,6 +28,11 @@ import type {
28
28
  ReferenceCopyMode,
29
29
  } from "../types";
30
30
  import { buildChartOption, CHART_TYPES, getChartLabelsDisplayText, getChartValuesDisplayText } from "../utils/chartOptionBuilder";
31
+ import {
32
+ VIEWPORT_OVERFLOW_MODES,
33
+ normalizeViewportOverflow,
34
+ type ViewportOverflowMode,
35
+ } from "../utils/viewportPlacement";
31
36
  import type { PanelLayer } from "../types";
32
37
  import { getPanelMessages } from "../constants/messages";
33
38
  import { ViewElementScopePanel } from "./ViewElementScopePanel";
@@ -1863,7 +1868,7 @@ export function PanelConfigSidebar({
1863
1868
  </label>
1864
1869
  </div>
1865
1870
  ) : null}
1866
- {el.materialType === "reference" ? (
1871
+ {el.materialType === "reference" || el.materialType === "viewport" ? (
1867
1872
  <div className="grid grid-cols-2 gap-2">
1868
1873
  <label className="block space-y-1.5">
1869
1874
  <div>{t("panel.config.refLayer")}</div>
@@ -1895,6 +1900,34 @@ export function PanelConfigSidebar({
1895
1900
  </label>
1896
1901
  </div>
1897
1902
  ) : null}
1903
+ {el.materialType === "viewport" ? (
1904
+ <label className="block space-y-1.5">
1905
+ <div>{t("panel.config.viewportOverflow")}</div>
1906
+ <Select
1907
+ value={normalizeViewportOverflow(el.viewportOverflow)}
1908
+ onValueChange={(value) =>
1909
+ updateElement(el.id, {
1910
+ viewportOverflow: value as ViewportOverflowMode,
1911
+ })
1912
+ }
1913
+ >
1914
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1915
+ <SelectContent>
1916
+ {VIEWPORT_OVERFLOW_MODES.map((mode) => (
1917
+ <SelectItem key={mode} value={mode}>
1918
+ {mode === "scroll-x"
1919
+ ? t("panel.config.viewportOverflowScrollX")
1920
+ : mode === "scroll-y"
1921
+ ? t("panel.config.viewportOverflowScrollY")
1922
+ : mode === "scroll"
1923
+ ? t("panel.config.viewportOverflowScroll")
1924
+ : t("panel.config.viewportOverflowClip")}
1925
+ </SelectItem>
1926
+ ))}
1927
+ </SelectContent>
1928
+ </Select>
1929
+ </label>
1930
+ ) : null}
1898
1931
  </div>
1899
1932
  </fieldset>
1900
1933
  </CardContent>
@@ -3603,6 +3636,111 @@ export function PanelConfigSidebar({
3603
3636
  {t("panel.config.gridHint")}
3604
3637
  </>
3605
3638
  )
3639
+ ) : selectedElement.materialType === "viewport" ? (
3640
+ renderSection(
3641
+ "viewportConfig",
3642
+ t("panel.config.sectionViewport"),
3643
+ <>
3644
+ {renderFieldGroup(
3645
+ t("panel.config.groupRefSource"),
3646
+ <label className="block space-y-1">
3647
+ <div>{t("panel.config.refLayer")}</div>
3648
+ <Select
3649
+ value={selectedElement.refLayerId ?? "__none__"}
3650
+ onValueChange={(value) =>
3651
+ updateElement(selectedElement.id, {
3652
+ refLayerId: value === "__none__" ? undefined : value,
3653
+ })
3654
+ }
3655
+ >
3656
+ <SelectTrigger className="h-7">
3657
+ <SelectValue placeholder={t("panel.config.pleaseSelectLayer")} />
3658
+ </SelectTrigger>
3659
+ <SelectContent>
3660
+ <SelectItem value="__none__">{t("panel.config.noneNoRef")}</SelectItem>
3661
+ {layers
3662
+ .filter((l) => l.id !== selectedElement.layerId)
3663
+ .map((l) => (
3664
+ <SelectItem key={l.id} value={l.id}>
3665
+ {l.name}
3666
+ </SelectItem>
3667
+ ))}
3668
+ </SelectContent>
3669
+ </Select>
3670
+ </label>
3671
+ )}
3672
+ {renderFieldGroup(
3673
+ t("panel.config.groupCopyStrategy"),
3674
+ <>
3675
+ <label className="block space-y-1">
3676
+ <div>{t("panel.config.copyMode")}</div>
3677
+ <Select
3678
+ value={selectedElement.refCopyMode ?? "shallow"}
3679
+ onValueChange={(value) =>
3680
+ setReferenceCopyMode?.(
3681
+ selectedElement.id,
3682
+ value as ReferenceCopyMode
3683
+ )
3684
+ }
3685
+ >
3686
+ <SelectTrigger className="h-7">
3687
+ <SelectValue placeholder={t("panel.config.selectCopyMode")} />
3688
+ </SelectTrigger>
3689
+ <SelectContent>
3690
+ <SelectItem value="shallow">{t("panel.config.shallowFollow")}</SelectItem>
3691
+ <SelectItem value="deep">{t("panel.config.deepFreeze")}</SelectItem>
3692
+ </SelectContent>
3693
+ </Select>
3694
+ </label>
3695
+ </>,
3696
+ <>{t("panel.config.copyStrategyHint")}</>,
3697
+ { groupKey: "viewportCopyStrategy", defaultOpen: false }
3698
+ )}
3699
+ {renderFieldGroup(
3700
+ t("panel.config.groupViewportOverflow"),
3701
+ <label className="block space-y-1">
3702
+ <div>{t("panel.config.viewportOverflow")}</div>
3703
+ <Select
3704
+ value={normalizeViewportOverflow(selectedElement.viewportOverflow)}
3705
+ onValueChange={(value) =>
3706
+ updateElement(selectedElement.id, {
3707
+ viewportOverflow: value as ViewportOverflowMode,
3708
+ })
3709
+ }
3710
+ >
3711
+ <SelectTrigger className="h-7">
3712
+ <SelectValue placeholder={t("panel.config.viewportOverflow")} />
3713
+ </SelectTrigger>
3714
+ <SelectContent>
3715
+ {VIEWPORT_OVERFLOW_MODES.map((mode) => (
3716
+ <SelectItem key={mode} value={mode}>
3717
+ {mode === "scroll-x"
3718
+ ? t("panel.config.viewportOverflowScrollX")
3719
+ : mode === "scroll-y"
3720
+ ? t("panel.config.viewportOverflowScrollY")
3721
+ : mode === "scroll"
3722
+ ? t("panel.config.viewportOverflowScroll")
3723
+ : t("panel.config.viewportOverflowClip")}
3724
+ </SelectItem>
3725
+ ))}
3726
+ </SelectContent>
3727
+ </Select>
3728
+ </label>
3729
+ )}
3730
+ </>,
3731
+ true,
3732
+ [
3733
+ t("panel.config.searchKwViewport"),
3734
+ "viewport",
3735
+ t("panel.config.refLayer"),
3736
+ t("panel.config.viewportOverflow"),
3737
+ t("panel.config.viewportOverflowScrollX"),
3738
+ t("panel.config.viewportOverflowClip"),
3739
+ t("panel.material.shallowCopy"),
3740
+ t("panel.material.deepCopy"),
3741
+ ],
3742
+ <>{t("panel.config.viewportHint")}</>
3743
+ )
3606
3744
  ) : selectedElement.materialType === "reference" ? (
3607
3745
  renderSection(
3608
3746
  "reference",
@@ -62,6 +62,7 @@ export type WorkspaceConfigSidebarProps = Omit<
62
62
  | "lifecyclePhase"
63
63
  | "fetchConfig"
64
64
  | "jsonConfig"
65
+ | "storageConfig"
65
66
  | "logicConfig"
66
67
  | "clockConfig"
67
68
  | "eventConfig"
@@ -16,6 +16,7 @@ import {
16
16
  getDefaultChartConfig,
17
17
  getDefaultTableConfig,
18
18
  getDefaultGridConfig,
19
+ getDefaultViewportConfig,
19
20
  getDefaultLayer,
20
21
  getDefaultNodeName,
21
22
  getDefaultScene3dConfigForMaterial,
@@ -552,7 +553,10 @@ export function usePanelElements() {
552
553
  .filter((n) => isPanelElementNode(n) && n.props)
553
554
  .map((n) => n.props as PanelElement);
554
555
  const desiredMode = options?.referenceCopyMode;
555
- if (nextProps.materialType === "reference" && desiredMode) {
556
+ if (
557
+ (nextProps.materialType === "reference" || nextProps.materialType === "viewport") &&
558
+ desiredMode
559
+ ) {
556
560
  if (desiredMode === "deep" && nextProps.refLayerId) {
557
561
  nextProps.refCopyMode = "deep";
558
562
  nextProps.refSnapshot = buildDeepReferenceSnapshot(all, nextProps.refLayerId);
@@ -596,7 +600,9 @@ export function usePanelElements() {
596
600
  name: getDefaultNodeName(materialType, t),
597
601
  ...getDefaultTextContent(materialType, t),
598
602
  ...getDefaultGridConfig(materialType),
599
- refCopyMode: materialType === "reference" ? "shallow" : undefined,
603
+ ...getDefaultViewportConfig(materialType),
604
+ refCopyMode:
605
+ materialType === "reference" || materialType === "viewport" ? "shallow" : undefined,
600
606
  chart: getDefaultChartConfig(materialType, t),
601
607
  table: getDefaultTableConfig(materialType, t),
602
608
  geometryShape: materialType === "geometry" ? "rect" : undefined,
@@ -700,7 +706,7 @@ export function usePanelElements() {
700
706
  .filter((n) => isPanelElementNode(n) && n.props)
701
707
  .map((n) => n.props as PanelElement);
702
708
  const target = all.find((el) => el.id === id);
703
- if (!target || target.materialType !== "reference") return;
709
+ if (!target || (target.materialType !== "reference" && target.materialType !== "viewport")) return;
704
710
  const currentLayers =
705
711
  (current.variables?.layers as PanelLayer[] | undefined) ?? [DEFAULT_LAYER];
706
712
  const layer = currentLayers.find((l) => l.id === target.layerId);
@@ -895,7 +901,7 @@ export function usePanelElements() {
895
901
  if (target.isMapping) return false;
896
902
  const willBeDeleted = mode === "remove" && el.layerId === layerId;
897
903
  if (willBeDeleted) return false;
898
- if (el.materialType !== "reference") return false;
904
+ if (el.materialType !== "reference" && el.materialType !== "viewport") return false;
899
905
  if (el.refLayerId !== layerId) return false;
900
906
  return (el.refCopyMode ?? "shallow") !== "deep";
901
907
  });
@@ -620,7 +620,8 @@ async function paintPanelElementOntoCanvas(
620
620
  liveNode &&
621
621
  (el.materialType === "reference" ||
622
622
  el.materialType === "table" ||
623
- el.materialType === "grid")
623
+ el.materialType === "grid" ||
624
+ el.materialType === "viewport")
624
625
  ) {
625
626
  try {
626
627
  await drawNodeOntoCanvas(ctx, liveNode, 0, 0, w, h, 0);
@@ -681,7 +682,8 @@ export async function capturePreviewLayerSnapshot(
681
682
  ctx.fillStyle = options.backgroundColor ?? "#ffffff";
682
683
  ctx.fillRect(0, 0, width, height);
683
684
 
684
- const sorted = [...elements].sort((a, b) => (a.zIndex ?? 1) - (b.zIndex ?? 1));
685
+ const sorted = [...elements]
686
+ .sort((a, b) => (a.zIndex ?? 1) - (b.zIndex ?? 1));
685
687
  for (const el of sorted) {
686
688
  const liveNode =
687
689
  options.canvasRoot?.querySelector<HTMLElement>(
@@ -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
  import type { PanelTableConfig } from "@arronqzy/view-table";
58
60
  import type { Scene3dConfig } from "@arronqzy/view-scene3d";
59
61
 
@@ -83,6 +85,8 @@ export type PanelElement = {
83
85
  gridPadding?: number;
84
86
  gridSnapThreshold?: number;
85
87
  parentGridId?: string;
88
+ /** 视窗节点:只显示自身尺寸内的引用图层内容,超出部分滚动或裁剪 */
89
+ viewportOverflow?: ViewportOverflowMode;
86
90
  gridSlotIndex?: number;
87
91
  gridColSpan?: number;
88
92
  gridRowSpan?: number;
@@ -17,6 +17,7 @@ const DEFAULT_NODE_NAME_KEYS: Record<string, string> = {
17
17
  funnel: "panel.defaults.funnel",
18
18
  text: "panel.defaults.text",
19
19
  grid: "panel.defaults.grid",
20
+ viewport: "panel.defaults.viewport",
20
21
  image: "panel.defaults.image",
21
22
  video: "panel.defaults.video",
22
23
  audio: "panel.defaults.audio",
@@ -70,6 +71,8 @@ export function getDefaultSizeByMaterial(materialType: string) {
70
71
  return { width: 180, height: 120 };
71
72
  case "grid":
72
73
  return { width: 320, height: 220 };
74
+ case "viewport":
75
+ return { width: 360, height: 240 };
73
76
  case "image":
74
77
  return { width: 220, height: 140 };
75
78
  case "video":
@@ -141,6 +144,13 @@ export function getDefaultGridConfig(materialType: string) {
141
144
  } as const;
142
145
  }
143
146
 
147
+ export function getDefaultViewportConfig(materialType: string) {
148
+ if (materialType !== "viewport") return {};
149
+ return {
150
+ viewportOverflow: "clip" as const,
151
+ };
152
+ }
153
+
144
154
  export function getDefaultChartConfig(
145
155
  materialType: string,
146
156
  t: TranslateFn = tFallback
@@ -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
  });
@@ -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
+ }