@arronqzy/react-view 1.0.28 → 1.0.30
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 +7 -7
- package/src/panel/ReactViewPanel.tsx +52 -8
- package/src/panel/components/ElementsLayer.tsx +178 -106
- package/src/panel/components/MoveableLayer.tsx +40 -3
- package/src/panel/components/PanelConfigSidebar.tsx +32 -0
- package/src/panel/constants/messages.ts +2 -0
- package/src/panel/hooks/usePanelElements.ts +24 -13
- package/src/panel/hooks/useWorkspaceProjects.ts +84 -49
- package/src/panel/library/workspace-project-db.ts +163 -16
- package/src/panel/library/workspace-storage-error.ts +60 -0
- package/src/panel/utils/canvas-interaction-busy.ts +36 -0
- package/src/panel/utils/materializePanelElements.ts +97 -0
- package/src/panel/utils/panelStateIO.ts +22 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/react-view",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"files": [
|
|
5
5
|
"src",
|
|
6
6
|
"dist",
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
32
32
|
"@codemirror/view": "^6.36.2",
|
|
33
33
|
"@arronqzy/ui": "1.0.9",
|
|
34
|
-
"@arronqzy/react-blueprint": "1.0.16",
|
|
35
34
|
"@arronqzy/blueprint-dsl": "1.0.14",
|
|
36
|
-
"@arronqzy/i18n": "0.1.
|
|
35
|
+
"@arronqzy/i18n": "0.1.12",
|
|
36
|
+
"@arronqzy/react-blueprint": "1.0.17",
|
|
37
37
|
"@arronqzy/rx-store": "1.0.8",
|
|
38
38
|
"@arronqzy/view-scene3d": "0.1.5",
|
|
39
|
-
"@arronqzy/
|
|
40
|
-
"@arronqzy/
|
|
39
|
+
"@arronqzy/view-table": "0.1.8",
|
|
40
|
+
"@arronqzy/webllm-assistant": "0.1.16"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/react": "^19.0.1",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"eslint": "^8.57.0",
|
|
47
47
|
"typescript": "^5.5.4",
|
|
48
48
|
"@arronqzy/eslint-config": "0.0.0",
|
|
49
|
-
"@arronqzy/
|
|
50
|
-
"@arronqzy/
|
|
49
|
+
"@arronqzy/typescript-config": "1.0.0",
|
|
50
|
+
"@arronqzy/tailwind": "1.0.2"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
@@ -15,7 +15,14 @@ import { type ViewportZoom } from "./viewportZoom";
|
|
|
15
15
|
import { ElementsLayer } from "./components/ElementsLayer";
|
|
16
16
|
import { SelectLayer } from "./components/SelectLayer";
|
|
17
17
|
import { MoveableLayer } from "./components/MoveableLayer";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
isCanvasInteractionBusy,
|
|
20
|
+
subscribeCanvasInteractionBusy,
|
|
21
|
+
} from "./utils/canvas-interaction-busy";
|
|
22
|
+
import {
|
|
23
|
+
buildChartOption,
|
|
24
|
+
CHART_TYPES,
|
|
25
|
+
} from "./utils/chartOptionBuilder";
|
|
19
26
|
import { readOutputScale, writeOutputScale } from "./utils/outputScale";
|
|
20
27
|
import {
|
|
21
28
|
captureEditorPreviewSnapshot,
|
|
@@ -391,9 +398,11 @@ function ReactViewPanelInner({
|
|
|
391
398
|
if (!root) return;
|
|
392
399
|
let rafId = 0;
|
|
393
400
|
const schedule = () => {
|
|
401
|
+
if (isCanvasInteractionBusy()) return;
|
|
394
402
|
if (rafId) return;
|
|
395
403
|
rafId = window.requestAnimationFrame(() => {
|
|
396
404
|
rafId = 0;
|
|
405
|
+
if (isCanvasInteractionBusy()) return;
|
|
397
406
|
const candidates = root.querySelectorAll<HTMLElement>(
|
|
398
407
|
".truncate, .line-clamp-1, .line-clamp-2, .line-clamp-3"
|
|
399
408
|
);
|
|
@@ -429,11 +438,15 @@ function ReactViewPanelInner({
|
|
|
429
438
|
const resizeObserver = new ResizeObserver(schedule);
|
|
430
439
|
resizeObserver.observe(root);
|
|
431
440
|
window.addEventListener("resize", schedule);
|
|
441
|
+
const unsubscribeBusy = subscribeCanvasInteractionBusy(() => {
|
|
442
|
+
if (!isCanvasInteractionBusy()) schedule();
|
|
443
|
+
});
|
|
432
444
|
return () => {
|
|
433
445
|
if (rafId) window.cancelAnimationFrame(rafId);
|
|
434
446
|
mutationObserver.disconnect();
|
|
435
447
|
resizeObserver.disconnect();
|
|
436
448
|
window.removeEventListener("resize", schedule);
|
|
449
|
+
unsubscribeBusy();
|
|
437
450
|
};
|
|
438
451
|
}, []);
|
|
439
452
|
|
|
@@ -1048,11 +1061,27 @@ function ReactViewPanelInner({
|
|
|
1048
1061
|
return () => registerPreviewSnapshotProvider(null);
|
|
1049
1062
|
}, [activeLayerId, allElements, canvasEl, layers, nameSpace]);
|
|
1050
1063
|
|
|
1064
|
+
const blueprintDocument = blueprintGraph.document;
|
|
1065
|
+
const blueprintLifecycleToken = useMemo(() => {
|
|
1066
|
+
// Only blueprint / layer changes — panel history (drag/resize) must not re-run
|
|
1067
|
+
// the full `updated` lifecycle (cycle detect + runner).
|
|
1068
|
+
const nodes = blueprintDocument.nodes;
|
|
1069
|
+
const edges = blueprintDocument.edges;
|
|
1070
|
+
let sig = `${nodes.length}:${edges.length}`;
|
|
1071
|
+
for (const node of nodes) {
|
|
1072
|
+
sig += `|${node.id}:${resolveRunnableNodeType(node)}:${node.lifecyclePhase ?? ""}`;
|
|
1073
|
+
}
|
|
1074
|
+
for (const edge of edges) {
|
|
1075
|
+
sig += `>${edge.id}:${edge.source}:${edge.target}`;
|
|
1076
|
+
}
|
|
1077
|
+
return sig;
|
|
1078
|
+
}, [blueprintDocument]);
|
|
1079
|
+
|
|
1051
1080
|
const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
|
|
1052
1081
|
graph: blueprintGraph,
|
|
1053
1082
|
active: blueprintOpen,
|
|
1054
1083
|
bootKey: workspaceProjects.activeProjectId ?? undefined,
|
|
1055
|
-
onUpdated: `${activeLayerId}|${
|
|
1084
|
+
onUpdated: `${activeLayerId}|${blueprintLifecycleToken}`,
|
|
1056
1085
|
resolveLibraryBlueprint,
|
|
1057
1086
|
libraryNameById: blueprintLibraryNameById,
|
|
1058
1087
|
rootLibraryBlueprintId: activeBlueprintLibraryId,
|
|
@@ -1079,11 +1108,18 @@ function ReactViewPanelInner({
|
|
|
1079
1108
|
);
|
|
1080
1109
|
|
|
1081
1110
|
const handleWorkspaceCreateProject = useCallback(async () => {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1111
|
+
try {
|
|
1112
|
+
const result = await workspaceProjects.handleCreateProject();
|
|
1113
|
+
if (result?.name) {
|
|
1114
|
+
toast({ title: messages.workspaceCreated(result.name) });
|
|
1115
|
+
}
|
|
1116
|
+
return result;
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
toast({
|
|
1119
|
+
title: error instanceof Error ? error.message : t("panel.messages.createWorkspaceFailed"),
|
|
1120
|
+
});
|
|
1121
|
+
throw error;
|
|
1085
1122
|
}
|
|
1086
|
-
return result;
|
|
1087
1123
|
}, [workspaceProjects]);
|
|
1088
1124
|
|
|
1089
1125
|
const handleWorkspaceOpenProject = useCallback(
|
|
@@ -1121,8 +1157,16 @@ function ReactViewPanelInner({
|
|
|
1121
1157
|
|
|
1122
1158
|
const handleWorkspaceDeleteProject = useCallback(
|
|
1123
1159
|
async (id: string) => {
|
|
1124
|
-
|
|
1125
|
-
|
|
1160
|
+
try {
|
|
1161
|
+
await workspaceProjects.handleDeleteProject(id);
|
|
1162
|
+
toast({ title: t("panel.messages.workspaceDeleted") });
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
toast({
|
|
1165
|
+
title:
|
|
1166
|
+
error instanceof Error ? error.message : t("panel.messages.deleteWorkspaceFailed"),
|
|
1167
|
+
});
|
|
1168
|
+
throw error;
|
|
1169
|
+
}
|
|
1126
1170
|
},
|
|
1127
1171
|
[workspaceProjects]
|
|
1128
1172
|
);
|
|
@@ -587,7 +587,7 @@ function GeometryNodeContent({ element }: { element: PanelElement }) {
|
|
|
587
587
|
return <canvas ref={canvasRef} className="h-full w-full" />;
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
-
function ChartNodeContent({
|
|
590
|
+
const ChartNodeContent = React.memo(function ChartNodeContent({
|
|
591
591
|
element,
|
|
592
592
|
previewLayoutKey,
|
|
593
593
|
previewMode = false,
|
|
@@ -654,7 +654,7 @@ function ChartNodeContent({
|
|
|
654
654
|
}
|
|
655
655
|
/>
|
|
656
656
|
);
|
|
657
|
-
}
|
|
657
|
+
});
|
|
658
658
|
|
|
659
659
|
function getNodeVisualStyle(element: PanelElement): React.CSSProperties {
|
|
660
660
|
const style = element.style ?? {};
|
|
@@ -884,6 +884,7 @@ export const ElementsLayer = React.memo(function ElementsLayer({
|
|
|
884
884
|
},
|
|
885
885
|
[boundViewEventTypes, onViewUiEvent]
|
|
886
886
|
);
|
|
887
|
+
const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
887
888
|
const sortedElements = useMemo(() => {
|
|
888
889
|
const byId = new Map<string, PanelElement>();
|
|
889
890
|
for (const el of allElements) byId.set(el.id, el);
|
|
@@ -891,111 +892,182 @@ export const ElementsLayer = React.memo(function ElementsLayer({
|
|
|
891
892
|
}, [allElements, elements]);
|
|
892
893
|
return (
|
|
893
894
|
<>
|
|
894
|
-
{sortedElements.map((el) =>
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (target?.closest("[data-scene3d-orbit-active='true']")) {
|
|
912
|
-
e.stopPropagation();
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (e.button !== 0) return;
|
|
916
|
-
// 单击选中(与 Selecto 的框选互补)
|
|
917
|
-
if (e.shiftKey) {
|
|
918
|
-
onSelectIds(
|
|
919
|
-
isSelected
|
|
920
|
-
? selectedIds.filter((id) => id !== el.id)
|
|
921
|
-
: [...selectedIds, el.id]
|
|
922
|
-
);
|
|
923
|
-
} else {
|
|
924
|
-
onSelectIds([el.id]);
|
|
925
|
-
}
|
|
926
|
-
}}
|
|
927
|
-
onClick={(e) => emitViewUiEvent(el, "click", e)}
|
|
928
|
-
onDoubleClick={(e) => emitViewUiEvent(el, "dblclick", e)}
|
|
929
|
-
onContextMenu={(e) => emitViewUiEvent(el, "contextmenu", e)}
|
|
930
|
-
onMouseEnter={(e) => emitViewUiEvent(el, "mouseenter", e)}
|
|
931
|
-
onMouseLeave={(e) => emitViewUiEvent(el, "mouseleave", e)}
|
|
932
|
-
style={{
|
|
933
|
-
left: el.x,
|
|
934
|
-
top: el.y,
|
|
935
|
-
width: Math.max(1, el.width),
|
|
936
|
-
height: Math.max(1, el.height),
|
|
937
|
-
zIndex: el.zIndex ?? 1,
|
|
938
|
-
transform: `rotate(${el.rotate ?? 0}deg)`,
|
|
939
|
-
transformOrigin: "center center",
|
|
940
|
-
overflow: el.materialType === "viewport" ? "hidden" : undefined,
|
|
941
|
-
boxSizing: "border-box",
|
|
942
|
-
...getNodeVisualStyle(el),
|
|
943
|
-
}}
|
|
944
|
-
>
|
|
945
|
-
{CHART_TYPES.has(el.materialType ?? "") ? (
|
|
946
|
-
<ChartNodeContent
|
|
947
|
-
element={el}
|
|
948
|
-
previewLayoutKey={previewLayoutKey}
|
|
949
|
-
previewMode={previewMode}
|
|
950
|
-
/>
|
|
951
|
-
) : el.materialType === "reference" || el.materialType === "viewport" ? (
|
|
952
|
-
<ReferenceNodeContent element={el} allElements={allElements} />
|
|
953
|
-
) : el.materialType === "grid" ? (
|
|
954
|
-
<GridNodeContent
|
|
955
|
-
element={el}
|
|
956
|
-
allElements={allElements}
|
|
957
|
-
previewMode={previewMode}
|
|
958
|
-
/>
|
|
959
|
-
) : el.materialType === "text" ? (
|
|
960
|
-
<TextNodeContent
|
|
961
|
-
element={el}
|
|
962
|
-
editable={(el.textAllowInput ?? true) && !el.locked && !layerLocked}
|
|
963
|
-
onChange={(nextHtml) => {
|
|
964
|
-
updateElement(el.id, { textHtml: nextHtml });
|
|
965
|
-
}}
|
|
966
|
-
/>
|
|
967
|
-
) : el.materialType === "audio" ? (
|
|
968
|
-
<AudioNodeContent element={el} selected={isSelected} />
|
|
969
|
-
) : el.materialType === "video" ? (
|
|
970
|
-
<VideoNodeContent element={el} selected={isSelected} />
|
|
971
|
-
) : el.materialType === "geometry" ? (
|
|
972
|
-
<GeometryNodeContent element={el} />
|
|
973
|
-
) : el.materialType === "scene3d" ? (
|
|
974
|
-
<Scene3dNodeContent
|
|
975
|
-
config={el.scene3d}
|
|
976
|
-
previewMode={previewMode}
|
|
977
|
-
selected={isSelected}
|
|
978
|
-
updateConfig={(patch) =>
|
|
979
|
-
updateElement(el.id, {
|
|
980
|
-
scene3d: mergeScene3dConfig({ ...el.scene3d, ...patch }),
|
|
981
|
-
})
|
|
982
|
-
}
|
|
983
|
-
/>
|
|
984
|
-
) : el.materialType === "image" ? (
|
|
985
|
-
<ImageNodeContent element={el} />
|
|
986
|
-
) : el.materialType === "table" ? (
|
|
987
|
-
<TableNodeContent
|
|
988
|
-
element={el}
|
|
989
|
-
interactive={previewMode || Boolean(onTableCellAction)}
|
|
990
|
-
onCellAction={onTableCellAction}
|
|
991
|
-
/>
|
|
992
|
-
) : (
|
|
993
|
-
<EmptyNodePlaceholder element={el} />
|
|
994
|
-
)}
|
|
995
|
-
</div>
|
|
996
|
-
);
|
|
997
|
-
})}
|
|
895
|
+
{sortedElements.map((el) => (
|
|
896
|
+
<PanelElementNode
|
|
897
|
+
key={el.id}
|
|
898
|
+
el={el}
|
|
899
|
+
allElements={allElements}
|
|
900
|
+
isSelected={selectedIdSet.has(el.id)}
|
|
901
|
+
selectedIds={selectedIds}
|
|
902
|
+
onSelectIds={onSelectIds}
|
|
903
|
+
updateElement={updateElement}
|
|
904
|
+
layerLocked={layerLocked}
|
|
905
|
+
previewMode={previewMode}
|
|
906
|
+
previewLayoutKey={previewLayoutKey}
|
|
907
|
+
onTableCellAction={onTableCellAction}
|
|
908
|
+
hasViewEvent={Boolean(boundViewEventTypes?.get(el.id)?.size && onViewUiEvent)}
|
|
909
|
+
emitViewUiEvent={emitViewUiEvent}
|
|
910
|
+
/>
|
|
911
|
+
))}
|
|
998
912
|
</>
|
|
999
913
|
);
|
|
1000
914
|
});
|
|
1001
915
|
|
|
916
|
+
function needsSharedElementIndex(el: PanelElement): boolean {
|
|
917
|
+
const type = el.materialType ?? "";
|
|
918
|
+
return type === "reference" || type === "viewport" || type === "grid";
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
type PanelElementNodeProps = {
|
|
922
|
+
el: PanelElement;
|
|
923
|
+
allElements: PanelElement[];
|
|
924
|
+
isSelected: boolean;
|
|
925
|
+
selectedIds: string[];
|
|
926
|
+
onSelectIds: (ids: string[]) => void;
|
|
927
|
+
updateElement: ElementsLayerProps["updateElement"];
|
|
928
|
+
layerLocked: boolean;
|
|
929
|
+
previewMode: boolean;
|
|
930
|
+
previewLayoutKey?: number;
|
|
931
|
+
onTableCellAction?: TableCellActionHandler;
|
|
932
|
+
hasViewEvent: boolean;
|
|
933
|
+
emitViewUiEvent: (
|
|
934
|
+
el: PanelElement,
|
|
935
|
+
eventType: ViewEventType,
|
|
936
|
+
domEvent: React.MouseEvent | MouseEvent
|
|
937
|
+
) => void;
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const PanelElementNode = React.memo(function PanelElementNode({
|
|
941
|
+
el,
|
|
942
|
+
allElements,
|
|
943
|
+
isSelected,
|
|
944
|
+
selectedIds,
|
|
945
|
+
onSelectIds,
|
|
946
|
+
updateElement,
|
|
947
|
+
layerLocked,
|
|
948
|
+
previewMode,
|
|
949
|
+
previewLayoutKey,
|
|
950
|
+
onTableCellAction,
|
|
951
|
+
hasViewEvent,
|
|
952
|
+
emitViewUiEvent,
|
|
953
|
+
}: PanelElementNodeProps) {
|
|
954
|
+
return (
|
|
955
|
+
<div
|
|
956
|
+
className={[
|
|
957
|
+
"absolute select-none",
|
|
958
|
+
previewMode ? "" : "rv-selectable",
|
|
959
|
+
!previewMode && isSelected ? "ring-2 ring-blue-500/90 ring-offset-0" : "",
|
|
960
|
+
hasViewEvent ? "cursor-pointer" : "",
|
|
961
|
+
].join(" ")}
|
|
962
|
+
data-element-id={el.id}
|
|
963
|
+
onMouseDown={(e) => {
|
|
964
|
+
if (previewMode) return;
|
|
965
|
+
const target = e.target as HTMLElement | null;
|
|
966
|
+
if (target?.closest("[data-scene3d-orbit-active='true']")) {
|
|
967
|
+
e.stopPropagation();
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (e.button !== 0) return;
|
|
971
|
+
if (e.shiftKey) {
|
|
972
|
+
onSelectIds(
|
|
973
|
+
isSelected
|
|
974
|
+
? selectedIds.filter((id) => id !== el.id)
|
|
975
|
+
: [...selectedIds, el.id]
|
|
976
|
+
);
|
|
977
|
+
} else {
|
|
978
|
+
onSelectIds([el.id]);
|
|
979
|
+
}
|
|
980
|
+
}}
|
|
981
|
+
onClick={(e) => emitViewUiEvent(el, "click", e)}
|
|
982
|
+
onDoubleClick={(e) => emitViewUiEvent(el, "dblclick", e)}
|
|
983
|
+
onContextMenu={(e) => emitViewUiEvent(el, "contextmenu", e)}
|
|
984
|
+
onMouseEnter={(e) => emitViewUiEvent(el, "mouseenter", e)}
|
|
985
|
+
onMouseLeave={(e) => emitViewUiEvent(el, "mouseleave", e)}
|
|
986
|
+
style={{
|
|
987
|
+
left: el.x,
|
|
988
|
+
top: el.y,
|
|
989
|
+
width: Math.max(1, el.width),
|
|
990
|
+
height: Math.max(1, el.height),
|
|
991
|
+
zIndex: el.zIndex ?? 1,
|
|
992
|
+
transform: `rotate(${el.rotate ?? 0}deg)`,
|
|
993
|
+
transformOrigin: "center center",
|
|
994
|
+
overflow: el.materialType === "viewport" ? "hidden" : undefined,
|
|
995
|
+
boxSizing: "border-box",
|
|
996
|
+
contentVisibility: "auto",
|
|
997
|
+
containIntrinsicSize: `${Math.max(1, el.width)}px ${Math.max(1, el.height)}px`,
|
|
998
|
+
...getNodeVisualStyle(el),
|
|
999
|
+
}}
|
|
1000
|
+
>
|
|
1001
|
+
{CHART_TYPES.has(el.materialType ?? "") ? (
|
|
1002
|
+
<ChartNodeContent
|
|
1003
|
+
element={el}
|
|
1004
|
+
previewLayoutKey={previewLayoutKey}
|
|
1005
|
+
previewMode={previewMode}
|
|
1006
|
+
/>
|
|
1007
|
+
) : el.materialType === "reference" || el.materialType === "viewport" ? (
|
|
1008
|
+
<ReferenceNodeContent element={el} allElements={allElements} />
|
|
1009
|
+
) : el.materialType === "grid" ? (
|
|
1010
|
+
<GridNodeContent
|
|
1011
|
+
element={el}
|
|
1012
|
+
allElements={allElements}
|
|
1013
|
+
previewMode={previewMode}
|
|
1014
|
+
/>
|
|
1015
|
+
) : el.materialType === "text" ? (
|
|
1016
|
+
<TextNodeContent
|
|
1017
|
+
element={el}
|
|
1018
|
+
editable={(el.textAllowInput ?? true) && !el.locked && !layerLocked}
|
|
1019
|
+
onChange={(nextHtml) => {
|
|
1020
|
+
updateElement(el.id, { textHtml: nextHtml });
|
|
1021
|
+
}}
|
|
1022
|
+
/>
|
|
1023
|
+
) : el.materialType === "audio" ? (
|
|
1024
|
+
<AudioNodeContent element={el} selected={isSelected} />
|
|
1025
|
+
) : el.materialType === "video" ? (
|
|
1026
|
+
<VideoNodeContent element={el} selected={isSelected} />
|
|
1027
|
+
) : el.materialType === "geometry" ? (
|
|
1028
|
+
<GeometryNodeContent element={el} />
|
|
1029
|
+
) : el.materialType === "scene3d" ? (
|
|
1030
|
+
<Scene3dNodeContent
|
|
1031
|
+
config={el.scene3d}
|
|
1032
|
+
previewMode={previewMode}
|
|
1033
|
+
selected={isSelected}
|
|
1034
|
+
updateConfig={(patch) =>
|
|
1035
|
+
updateElement(el.id, {
|
|
1036
|
+
scene3d: mergeScene3dConfig({ ...el.scene3d, ...patch }),
|
|
1037
|
+
})
|
|
1038
|
+
}
|
|
1039
|
+
/>
|
|
1040
|
+
) : el.materialType === "image" ? (
|
|
1041
|
+
<ImageNodeContent element={el} />
|
|
1042
|
+
) : el.materialType === "table" ? (
|
|
1043
|
+
<TableNodeContent
|
|
1044
|
+
element={el}
|
|
1045
|
+
interactive={previewMode || Boolean(onTableCellAction)}
|
|
1046
|
+
onCellAction={onTableCellAction}
|
|
1047
|
+
/>
|
|
1048
|
+
) : (
|
|
1049
|
+
<EmptyNodePlaceholder element={el} />
|
|
1050
|
+
)}
|
|
1051
|
+
</div>
|
|
1052
|
+
);
|
|
1053
|
+
}, (prev, next) => {
|
|
1054
|
+
if (prev.el !== next.el) return false;
|
|
1055
|
+
if (prev.isSelected !== next.isSelected) return false;
|
|
1056
|
+
if (prev.previewMode !== next.previewMode) return false;
|
|
1057
|
+
if (prev.layerLocked !== next.layerLocked) return false;
|
|
1058
|
+
if (prev.previewLayoutKey !== next.previewLayoutKey) return false;
|
|
1059
|
+
if (prev.hasViewEvent !== next.hasViewEvent) return false;
|
|
1060
|
+
if (prev.updateElement !== next.updateElement) return false;
|
|
1061
|
+
if (prev.onSelectIds !== next.onSelectIds) return false;
|
|
1062
|
+
if (prev.onTableCellAction !== next.onTableCellAction) return false;
|
|
1063
|
+
if (prev.emitViewUiEvent !== next.emitViewUiEvent) return false;
|
|
1064
|
+
if (prev.selectedIds !== next.selectedIds && (prev.isSelected || next.isSelected)) {
|
|
1065
|
+
// shift-toggle path reads selectedIds; only matters while selected
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
if (needsSharedElementIndex(next.el) && prev.allElements !== next.allElements) {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
return true;
|
|
1072
|
+
});
|
|
1073
|
+
|
|
@@ -5,7 +5,7 @@ import Moveable from "react-moveable";
|
|
|
5
5
|
|
|
6
6
|
import type { PanelElement } from "../types";
|
|
7
7
|
import { uniformViewportZoom } from "../viewportZoom";
|
|
8
|
-
import {
|
|
8
|
+
import { setCanvasInteractionBusy } from "../utils/canvas-interaction-busy";
|
|
9
9
|
|
|
10
10
|
function LockGlyph({ className = "h-3.5 w-3.5" }: { className?: string }) {
|
|
11
11
|
return (
|
|
@@ -511,6 +511,7 @@ export function MoveableLayer({
|
|
|
511
511
|
const point = readClientPoint(e);
|
|
512
512
|
e.datas.__startClientX = point?.x ?? null;
|
|
513
513
|
e.datas.__startClientY = point?.y ?? null;
|
|
514
|
+
setCanvasInteractionBusy(true);
|
|
514
515
|
}}
|
|
515
516
|
onDrag={(e: any) => {
|
|
516
517
|
if (!e?.target?.style) return;
|
|
@@ -548,6 +549,7 @@ export function MoveableLayer({
|
|
|
548
549
|
ev.datas.__startClientX = point?.x ?? null;
|
|
549
550
|
ev.datas.__startClientY = point?.y ?? null;
|
|
550
551
|
});
|
|
552
|
+
setCanvasInteractionBusy(true);
|
|
551
553
|
}}
|
|
552
554
|
onDragGroup={(e: any) => {
|
|
553
555
|
e.events.forEach((ev: any) => {
|
|
@@ -580,6 +582,7 @@ export function MoveableLayer({
|
|
|
580
582
|
e.datas.__startY = data.y;
|
|
581
583
|
e.datas.__startClientX = e.inputEvent?.clientX ?? null;
|
|
582
584
|
e.datas.__startClientY = e.inputEvent?.clientY ?? null;
|
|
585
|
+
setCanvasInteractionBusy(true);
|
|
583
586
|
}}
|
|
584
587
|
onResize={(e: any) => {
|
|
585
588
|
if (!e?.target?.style) return;
|
|
@@ -593,7 +596,6 @@ export function MoveableLayer({
|
|
|
593
596
|
const ty = toCanvasDeltaY(e.drag.beforeTranslate?.[1] ?? 0);
|
|
594
597
|
e.target.style.left = `${sx + tx}px`;
|
|
595
598
|
e.target.style.top = `${sy + ty}px`;
|
|
596
|
-
notifyPreviewLayoutChanged();
|
|
597
599
|
}}
|
|
598
600
|
onResizeGroupStart={(e: any) => {
|
|
599
601
|
e.events.forEach((ev: any) => {
|
|
@@ -609,6 +611,7 @@ export function MoveableLayer({
|
|
|
609
611
|
ev.datas.__startClientX = input?.clientX ?? null;
|
|
610
612
|
ev.datas.__startClientY = input?.clientY ?? null;
|
|
611
613
|
});
|
|
614
|
+
setCanvasInteractionBusy(true);
|
|
612
615
|
}}
|
|
613
616
|
onResizeGroup={(e: any) => {
|
|
614
617
|
e.events.forEach((ev: any) => {
|
|
@@ -622,7 +625,6 @@ export function MoveableLayer({
|
|
|
622
625
|
ev.target.style.left = `${sx + tx}px`;
|
|
623
626
|
ev.target.style.top = `${sy + ty}px`;
|
|
624
627
|
});
|
|
625
|
-
notifyPreviewLayoutChanged();
|
|
626
628
|
}}
|
|
627
629
|
onRotateStart={(e: any) => {
|
|
628
630
|
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
@@ -630,11 +632,22 @@ export function MoveableLayer({
|
|
|
630
632
|
const data = elementsById.get(id);
|
|
631
633
|
if (!data) return;
|
|
632
634
|
e.set(data.rotate ?? 0);
|
|
635
|
+
setCanvasInteractionBusy(true);
|
|
633
636
|
}}
|
|
634
637
|
onRotate={(e: any) => {
|
|
635
638
|
if (!e?.target?.style) return;
|
|
636
639
|
e.target.style.transform = `rotate(${e.beforeRotate}deg)`;
|
|
637
640
|
}}
|
|
641
|
+
onRotateGroupStart={(e: any) => {
|
|
642
|
+
e.events.forEach((ev: any) => {
|
|
643
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
644
|
+
if (!id) return;
|
|
645
|
+
const data = elementsById.get(id);
|
|
646
|
+
if (!data) return;
|
|
647
|
+
ev.set(data.rotate ?? 0);
|
|
648
|
+
});
|
|
649
|
+
setCanvasInteractionBusy(true);
|
|
650
|
+
}}
|
|
638
651
|
onRotateGroup={(e: any) => {
|
|
639
652
|
e.events.forEach((ev: any) => {
|
|
640
653
|
if (!ev?.target?.style) return;
|
|
@@ -642,6 +655,7 @@ export function MoveableLayer({
|
|
|
642
655
|
});
|
|
643
656
|
}}
|
|
644
657
|
onDragEnd={(e: any) => {
|
|
658
|
+
try {
|
|
645
659
|
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
646
660
|
if (!id) return;
|
|
647
661
|
const data = elementsById.get(id);
|
|
@@ -683,8 +697,12 @@ export function MoveableLayer({
|
|
|
683
697
|
}
|
|
684
698
|
}
|
|
685
699
|
updateRectNextFrame();
|
|
700
|
+
} finally {
|
|
701
|
+
setCanvasInteractionBusy(false);
|
|
702
|
+
}
|
|
686
703
|
}}
|
|
687
704
|
onDragGroupEnd={(e: any) => {
|
|
705
|
+
try {
|
|
688
706
|
const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
689
707
|
const selectedSet = new Set(
|
|
690
708
|
e.events
|
|
@@ -742,8 +760,12 @@ export function MoveableLayer({
|
|
|
742
760
|
});
|
|
743
761
|
}
|
|
744
762
|
updateRectNextFrame();
|
|
763
|
+
} finally {
|
|
764
|
+
setCanvasInteractionBusy(false);
|
|
765
|
+
}
|
|
745
766
|
}}
|
|
746
767
|
onResizeEnd={(e: any) => {
|
|
768
|
+
try {
|
|
747
769
|
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
748
770
|
if (!id) return;
|
|
749
771
|
const data = elementsById.get(id);
|
|
@@ -760,8 +782,12 @@ export function MoveableLayer({
|
|
|
760
782
|
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
761
783
|
updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
|
|
762
784
|
updateRectNextFrame();
|
|
785
|
+
} finally {
|
|
786
|
+
setCanvasInteractionBusy(false);
|
|
787
|
+
}
|
|
763
788
|
}}
|
|
764
789
|
onResizeGroupEnd={(e: any) => {
|
|
790
|
+
try {
|
|
765
791
|
const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
766
792
|
e.events.forEach((ev: any) => {
|
|
767
793
|
const id = ev.target ? getId(ev.target) : null;
|
|
@@ -785,8 +811,12 @@ export function MoveableLayer({
|
|
|
785
811
|
);
|
|
786
812
|
});
|
|
787
813
|
updateRectNextFrame();
|
|
814
|
+
} finally {
|
|
815
|
+
setCanvasInteractionBusy(false);
|
|
816
|
+
}
|
|
788
817
|
}}
|
|
789
818
|
onRotateEnd={(e: any) => {
|
|
819
|
+
try {
|
|
790
820
|
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
791
821
|
if (!id) return;
|
|
792
822
|
const data = elementsById.get(id);
|
|
@@ -794,8 +824,12 @@ export function MoveableLayer({
|
|
|
794
824
|
const rotate = e.lastEvent?.beforeRotate ?? data.rotate ?? 0;
|
|
795
825
|
updateElement(id, { rotate });
|
|
796
826
|
updateRectNextFrame();
|
|
827
|
+
} finally {
|
|
828
|
+
setCanvasInteractionBusy(false);
|
|
829
|
+
}
|
|
797
830
|
}}
|
|
798
831
|
onRotateGroupEnd={(e: any) => {
|
|
832
|
+
try {
|
|
799
833
|
const batchId = `rotate-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
800
834
|
e.events.forEach((ev: any) => {
|
|
801
835
|
const id = ev.target ? getId(ev.target) : null;
|
|
@@ -806,6 +840,9 @@ export function MoveableLayer({
|
|
|
806
840
|
updateElement(id, { rotate }, { batchId, meta: { type: "node.group-rotate" } });
|
|
807
841
|
});
|
|
808
842
|
updateRectNextFrame();
|
|
843
|
+
} finally {
|
|
844
|
+
setCanvasInteractionBusy(false);
|
|
845
|
+
}
|
|
809
846
|
}}
|
|
810
847
|
/>
|
|
811
848
|
{hasLockedSelected && lockBadgeScreen ? (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
useEffect,
|
|
3
|
+
useLayoutEffect,
|
|
3
4
|
useMemo,
|
|
4
5
|
useRef,
|
|
5
6
|
useState,
|
|
@@ -146,6 +147,37 @@ export function PanelConfigSidebar({
|
|
|
146
147
|
return selectedElement ? [selectedElement] : [];
|
|
147
148
|
}, [selectedElement, selectedElements]);
|
|
148
149
|
const isMultiSelectMode = effectiveSelectedElements.length > 1;
|
|
150
|
+
const configScrollKey =
|
|
151
|
+
!isMultiSelectMode && selectedElement ? selectedElement.id : null;
|
|
152
|
+
const configScrollPosByIdRef = useRef(new Map<string, number>());
|
|
153
|
+
const activeConfigScrollKeyRef = useRef<string | null>(null);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const el = sidebarScrollRef.current;
|
|
157
|
+
if (!el) return;
|
|
158
|
+
const onScroll = () => {
|
|
159
|
+
const key = activeConfigScrollKeyRef.current;
|
|
160
|
+
if (!key) return;
|
|
161
|
+
configScrollPosByIdRef.current.set(key, el.scrollTop);
|
|
162
|
+
};
|
|
163
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
164
|
+
return () => el.removeEventListener("scroll", onScroll);
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
useLayoutEffect(() => {
|
|
168
|
+
const el = sidebarScrollRef.current;
|
|
169
|
+
const prevKey = activeConfigScrollKeyRef.current;
|
|
170
|
+
if (el && prevKey && prevKey !== configScrollKey) {
|
|
171
|
+
configScrollPosByIdRef.current.set(prevKey, el.scrollTop);
|
|
172
|
+
}
|
|
173
|
+
activeConfigScrollKeyRef.current = configScrollKey;
|
|
174
|
+
if (!el) return;
|
|
175
|
+
const top =
|
|
176
|
+
configScrollKey != null
|
|
177
|
+
? (configScrollPosByIdRef.current.get(configScrollKey) ?? 0)
|
|
178
|
+
: 0;
|
|
179
|
+
el.scrollTop = top;
|
|
180
|
+
}, [configScrollKey]);
|
|
149
181
|
|
|
150
182
|
useEffect(() => {
|
|
151
183
|
if (!selectedElement) return;
|
|
@@ -52,7 +52,9 @@ export function getPanelMessages(t: TranslateFn) {
|
|
|
52
52
|
workspaceLoaded: t("panel.messages.workspaceLoaded"),
|
|
53
53
|
workspaceLoadedDesc: t("panel.messages.workspaceLoadedDesc"),
|
|
54
54
|
openWorkspaceFailed: t("panel.messages.openWorkspaceFailed"),
|
|
55
|
+
createWorkspaceFailed: t("panel.messages.createWorkspaceFailed"),
|
|
55
56
|
syncFailed: t("panel.messages.syncFailed"),
|
|
57
|
+
deleteWorkspaceFailed: t("panel.messages.deleteWorkspaceFailed"),
|
|
56
58
|
workspaceDeleted: t("panel.messages.workspaceDeleted"),
|
|
57
59
|
openPreviewFailed: t("panel.messages.openPreviewFailed"),
|
|
58
60
|
clearedBlueprintLogs: (removed: number) =>
|