@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 +45 -0
- package/package.json +6 -6
- package/src/panel/ReactViewPanel.tsx +20 -18
- package/src/panel/ai/applyAssistantAction.ts +1 -0
- package/src/panel/components/ElementsLayer.tsx +67 -16
- package/src/panel/components/MaterialSidebar.tsx +18 -11
- package/src/panel/components/PanelConfigSidebar.tsx +139 -1
- package/src/panel/components/WorkspaceConfigSidebar.tsx +1 -0
- package/src/panel/hooks/usePanelElements.ts +10 -4
- package/src/panel/library/capture-preview-snapshot.ts +4 -2
- package/src/panel/types.ts +4 -0
- package/src/panel/utils/panelElementDefaults.ts +10 -0
- package/src/panel/utils/panelElementNodes.ts +5 -3
- package/src/panel/utils/viewportPlacement.ts +73 -0
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.
|
|
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/
|
|
38
|
-
"@arronqzy/view-scene3d": "0.1.2",
|
|
38
|
+
"@arronqzy/webllm-assistant": "0.1.10",
|
|
39
39
|
"@arronqzy/view-table": "0.1.5",
|
|
40
|
-
"@arronqzy/
|
|
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
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
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();
|
|
@@ -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
|
|
728
|
-
const
|
|
729
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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.
|
|
756
|
-
const top = layout.offsetY + (box.top - layout.minY) * layout.
|
|
757
|
-
const width = Math.max(12, node.width * layout.
|
|
758
|
-
const height = Math.max(10, node.height * layout.
|
|
759
|
-
const boxWidth = Math.max(12, (box.right - box.left) * layout.
|
|
760
|
-
const boxHeight = Math.max(10, (box.bottom - box.top) * layout.
|
|
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",
|
|
@@ -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 (
|
|
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
|
-
|
|
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]
|
|
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>(
|
package/src/panel/types.ts
CHANGED
|
@@ -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"
|
|
30
|
-
cloned.
|
|
31
|
-
|
|
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
|
+
}
|