@zoneflow/react 0.0.20 → 0.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/canvas/UniverseCanvas.d.ts +30 -2
- package/dist/canvas/UniverseCanvas.js +90 -4
- package/dist/controls/useCameraControls.d.ts +2 -0
- package/dist/controls/useCameraControls.js +4 -2
- package/dist/editor/UniverseEditorCanvas.d.ts +12 -2
- package/dist/editor/UniverseEditorCanvas.js +24 -4
- package/dist/editor/ZoneMoveEditorOverlay.d.ts +14 -0
- package/dist/editor/ZoneMoveEditorOverlay.js +26 -0
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UniverseLayoutModel, UniverseModel } from "@zoneflow/core";
|
|
1
|
+
import type { UniverseLayoutModel, UniverseModel, ZoneId } from "@zoneflow/core";
|
|
2
2
|
import { type BackgroundRenderer, type CameraState, type ComponentLayoutEngine, type DensityEngine, type DrawEngine, type GraphLayoutEngine, type GridOptions, type RendererFrame, type PathComponentRendererMap, type RendererDebugOptions, type RendererInteractionHandlers, type ResolvePathColor, type ResolveZoneColor, type ResolveZoneShape, type TextScaleLevel, type ViewportConfig, type VisibilityEngine, type ZoneComponentRendererMap, type ZoneflowTheme } from "@zoneflow/renderer-dom";
|
|
3
3
|
import { type ZoneMoveEditorConfig } from "../editor/ZoneMoveEditorOverlay";
|
|
4
4
|
import { type BackgroundComponent, type PathSlotComponentMap, type ZoneSlotComponentMap } from "../slots/slotComponents";
|
|
@@ -30,4 +30,32 @@ export type UniverseCanvasProps = {
|
|
|
30
30
|
onFrameChange?: (frame: RendererFrame | null) => void;
|
|
31
31
|
debug?: RendererDebugOptions;
|
|
32
32
|
};
|
|
33
|
-
export
|
|
33
|
+
export type UniverseCanvasFocusZoneOptions = {
|
|
34
|
+
/**
|
|
35
|
+
* 이동 후 zoom. 미지정 시 현재 zoom 을 유지합니다.
|
|
36
|
+
* 카메라 컨트롤과 동일하게 0.25 ~ 3 범위로 clamp 됩니다.
|
|
37
|
+
*/
|
|
38
|
+
zoom?: number;
|
|
39
|
+
};
|
|
40
|
+
export type UniverseCanvasHandle = {
|
|
41
|
+
/**
|
|
42
|
+
* 특정 zone 이 화면 중앙에 오도록 카메라를 이동합니다.
|
|
43
|
+
*
|
|
44
|
+
* - 아직 첫 프레임이 그려지지 않았거나 zone 을 찾지 못하면 `false` 를 반환합니다.
|
|
45
|
+
* - controlled camera (`cameraState`/`onCameraChange`) 모드에서도 동작합니다 —
|
|
46
|
+
* 계산된 카메라가 `onCameraChange` 로 전달됩니다.
|
|
47
|
+
*/
|
|
48
|
+
focusZone: (zoneId: ZoneId, options?: UniverseCanvasFocusZoneOptions) => boolean;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* zone 이 뷰포트 중앙에 오는 CameraState 를 계산합니다.
|
|
52
|
+
* ref 핸들 없이 controlled camera 를 직접 관리하는 쪽에서 사용할 수 있습니다.
|
|
53
|
+
*/
|
|
54
|
+
export declare function computeZoneFocusCamera(params: {
|
|
55
|
+
frame: RendererFrame | null;
|
|
56
|
+
zoneId: ZoneId;
|
|
57
|
+
viewportWidth: number;
|
|
58
|
+
viewportHeight: number;
|
|
59
|
+
zoom: number;
|
|
60
|
+
}): CameraState | null;
|
|
61
|
+
export declare const UniverseCanvas: import("react").ForwardRefExoticComponent<UniverseCanvasProps & import("react").RefAttributes<UniverseCanvasHandle>>;
|
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { screenPointToWorldPoint } from "@zoneflow/editor-dom";
|
|
4
4
|
import { createRenderer, } from "@zoneflow/renderer-dom";
|
|
5
|
-
import { useCameraControls } from "../controls/useCameraControls";
|
|
5
|
+
import { CAMERA_MAX_ZOOM, CAMERA_MIN_ZOOM, useCameraControls, } from "../controls/useCameraControls";
|
|
6
6
|
import { ZoneMoveEditorOverlay, } from "../editor/ZoneMoveEditorOverlay";
|
|
7
7
|
import { resolvePermissions } from "../editor/editorPermissions";
|
|
8
8
|
import { SlotPortals, } from "../slots/slotComponents";
|
|
9
|
+
/**
|
|
10
|
+
* zone 이 뷰포트 중앙에 오는 CameraState 를 계산합니다.
|
|
11
|
+
* ref 핸들 없이 controlled camera 를 직접 관리하는 쪽에서 사용할 수 있습니다.
|
|
12
|
+
*/
|
|
13
|
+
export function computeZoneFocusCamera(params) {
|
|
14
|
+
const { frame, zoneId, viewportWidth, viewportHeight, zoom } = params;
|
|
15
|
+
const rect = frame?.pipeline.graphLayout.zonesById[zoneId]?.rect;
|
|
16
|
+
if (!rect)
|
|
17
|
+
return null;
|
|
18
|
+
if (viewportWidth <= 0 || viewportHeight <= 0)
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
x: viewportWidth / 2 - (rect.x + rect.width / 2) * zoom,
|
|
22
|
+
y: viewportHeight / 2 - (rect.y + rect.height / 2) * zoom,
|
|
23
|
+
zoom,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
9
26
|
const DEFAULT_CAMERA = {
|
|
10
27
|
x: 0,
|
|
11
28
|
y: 0,
|
|
12
29
|
zoom: 1,
|
|
13
30
|
};
|
|
14
31
|
const noopRenderer = () => { };
|
|
15
|
-
|
|
32
|
+
const HOST_RESIZE_DEBOUNCE_MS = 150;
|
|
33
|
+
export const UniverseCanvas = forwardRef(function UniverseCanvas({ model, layoutModel, theme, textScale = "md", viewport, grid, graphLayoutEngine, densityEngine, visibilityEngine, componentLayoutEngine, drawEngine, zoneComponentRenderers, pathComponentRenderers, resolveZoneShape, resolveZoneColor, resolvePathColor, zoneComponents, pathComponents, backgroundRenderer, background, interactionHandlers, zoneMoveEditor, cameraState, onCameraChange, onFrameChange, debug, }, handleRef) {
|
|
16
34
|
const viewportRef = useRef(null);
|
|
17
35
|
const ref = useRef(null);
|
|
18
36
|
const rendererRef = useRef(createRenderer());
|
|
19
37
|
const [internalCamera, setInternalCamera] = useState(DEFAULT_CAMERA);
|
|
20
38
|
const [frame, setFrame] = useState(null);
|
|
39
|
+
const frameRef = useRef(null);
|
|
21
40
|
const [exclusionState, setExclusionState] = useState(undefined);
|
|
22
41
|
const [mounts, setMounts] = useState({
|
|
23
42
|
zones: [],
|
|
@@ -83,6 +102,24 @@ export function UniverseCanvas({ model, layoutModel, theme, textScale = "md", vi
|
|
|
83
102
|
camera,
|
|
84
103
|
setCamera,
|
|
85
104
|
});
|
|
105
|
+
const focusZone = useCallback((zoneId, options) => {
|
|
106
|
+
const viewportEl = viewportRef.current;
|
|
107
|
+
if (!viewportEl)
|
|
108
|
+
return false;
|
|
109
|
+
const zoom = Math.min(Math.max(options?.zoom ?? cameraRef.current.zoom, CAMERA_MIN_ZOOM), CAMERA_MAX_ZOOM);
|
|
110
|
+
const nextCamera = computeZoneFocusCamera({
|
|
111
|
+
frame: frameRef.current,
|
|
112
|
+
zoneId,
|
|
113
|
+
viewportWidth: viewportEl.clientWidth,
|
|
114
|
+
viewportHeight: viewportEl.clientHeight,
|
|
115
|
+
zoom,
|
|
116
|
+
});
|
|
117
|
+
if (!nextCamera)
|
|
118
|
+
return false;
|
|
119
|
+
setCamera(nextCamera);
|
|
120
|
+
return true;
|
|
121
|
+
}, [setCamera]);
|
|
122
|
+
useImperativeHandle(handleRef, () => ({ focusZone }), [focusZone]);
|
|
86
123
|
useEffect(() => {
|
|
87
124
|
if (!ref.current)
|
|
88
125
|
return;
|
|
@@ -91,6 +128,53 @@ export function UniverseCanvas({ model, layoutModel, theme, textScale = "md", vi
|
|
|
91
128
|
rendererRef.current.destroy();
|
|
92
129
|
};
|
|
93
130
|
}, []);
|
|
131
|
+
// 렌더러는 update 시점에 host.clientWidth/Height 로 world viewport 를 계산해
|
|
132
|
+
// 화면 밖 zone/path 노드를 컬링한다. props 가 전혀 안 바뀌는 정적 viewer 는
|
|
133
|
+
// update 가 다시 돌지 않아 mount 시점 측정값이 stale 해질 수 있으므로
|
|
134
|
+
// (초기 0×0 측정, 윈도우/컨테이너 리사이즈 등) host 크기 변화를 감지해 재렌더한다.
|
|
135
|
+
// update 한 번이 캔버스 전체 redraw 라 리사이즈 중 매 프레임 그리지 않고,
|
|
136
|
+
// 크기 변화가 멈춘 시점에 한 번만 반영한다 (debounce).
|
|
137
|
+
const [hostSizeVersion, setHostSizeVersion] = useState(0);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const el = ref.current;
|
|
140
|
+
if (!el)
|
|
141
|
+
return;
|
|
142
|
+
if (typeof ResizeObserver === "undefined")
|
|
143
|
+
return;
|
|
144
|
+
let lastWidth = el.clientWidth;
|
|
145
|
+
let lastHeight = el.clientHeight;
|
|
146
|
+
let debounceTimer = null;
|
|
147
|
+
const observer = new ResizeObserver(() => {
|
|
148
|
+
const nextWidth = el.clientWidth;
|
|
149
|
+
const nextHeight = el.clientHeight;
|
|
150
|
+
if (nextWidth === lastWidth && nextHeight === lastHeight)
|
|
151
|
+
return;
|
|
152
|
+
// 0×0 측정 상태에서는 아무것도 안 그려져 있으므로, 처음 실제 크기를
|
|
153
|
+
// 얻는 순간만큼은 디바운스 없이 즉시 그려 mount 직후 공백을 줄인다.
|
|
154
|
+
const wasEmpty = lastWidth === 0 || lastHeight === 0;
|
|
155
|
+
lastWidth = nextWidth;
|
|
156
|
+
lastHeight = nextHeight;
|
|
157
|
+
if (debounceTimer !== null) {
|
|
158
|
+
window.clearTimeout(debounceTimer);
|
|
159
|
+
debounceTimer = null;
|
|
160
|
+
}
|
|
161
|
+
if (wasEmpty) {
|
|
162
|
+
setHostSizeVersion((version) => version + 1);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
debounceTimer = window.setTimeout(() => {
|
|
166
|
+
debounceTimer = null;
|
|
167
|
+
setHostSizeVersion((version) => version + 1);
|
|
168
|
+
}, HOST_RESIZE_DEBOUNCE_MS);
|
|
169
|
+
});
|
|
170
|
+
observer.observe(el);
|
|
171
|
+
return () => {
|
|
172
|
+
observer.disconnect();
|
|
173
|
+
if (debounceTimer !== null) {
|
|
174
|
+
window.clearTimeout(debounceTimer);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}, []);
|
|
94
178
|
useEffect(() => {
|
|
95
179
|
if (zoneMoveEditor?.enabled)
|
|
96
180
|
return;
|
|
@@ -120,6 +204,7 @@ export function UniverseCanvas({ model, layoutModel, theme, textScale = "md", vi
|
|
|
120
204
|
exclusionState,
|
|
121
205
|
debug,
|
|
122
206
|
});
|
|
207
|
+
frameRef.current = frame ?? null;
|
|
123
208
|
setFrame(frame ?? null);
|
|
124
209
|
setMounts(frame?.mounts ?? {
|
|
125
210
|
zones: [],
|
|
@@ -155,6 +240,7 @@ export function UniverseCanvas({ model, layoutModel, theme, textScale = "md", vi
|
|
|
155
240
|
cameraState,
|
|
156
241
|
onCameraChange,
|
|
157
242
|
onFrameChange,
|
|
243
|
+
hostSizeVersion,
|
|
158
244
|
]);
|
|
159
245
|
return (_jsxs("div", { ref: viewportRef, onDragOver: (event) => {
|
|
160
246
|
if (!externalDropEnabled)
|
|
@@ -198,4 +284,4 @@ export function UniverseCanvas({ model, layoutModel, theme, textScale = "md", vi
|
|
|
198
284
|
inset: 0,
|
|
199
285
|
zIndex: 1,
|
|
200
286
|
} }), _jsx(SlotPortals, { mounts: mounts, zoneComponents: zoneComponents, pathComponents: pathComponents, background: background }), _jsx(ZoneMoveEditorOverlay, { model: model, layoutModel: layoutModel, camera: camera, frame: frame, zoneComponents: zoneComponents, pathComponents: pathComponents, editor: zoneMoveEditor, resolveZoneShape: resolveZoneShape, onExclusionStateChange: setExclusionState })] }));
|
|
201
|
-
}
|
|
287
|
+
});
|
|
@@ -5,5 +5,7 @@ type UseCameraControlsParams = {
|
|
|
5
5
|
camera: CameraState;
|
|
6
6
|
setCamera: React.Dispatch<React.SetStateAction<CameraState>>;
|
|
7
7
|
};
|
|
8
|
+
export declare const CAMERA_MIN_ZOOM = 0.25;
|
|
9
|
+
export declare const CAMERA_MAX_ZOOM = 3;
|
|
8
10
|
export declare function useCameraControls({ hostRef, camera, setCamera, }: UseCameraControlsParams): void;
|
|
9
11
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
const
|
|
3
|
-
const
|
|
2
|
+
export const CAMERA_MIN_ZOOM = 0.25;
|
|
3
|
+
export const CAMERA_MAX_ZOOM = 3;
|
|
4
|
+
const MIN_ZOOM = CAMERA_MIN_ZOOM;
|
|
5
|
+
const MAX_ZOOM = CAMERA_MAX_ZOOM;
|
|
4
6
|
const ZOOM_STEP = 1.1;
|
|
5
7
|
const TOUCH_PAN_MIN_POINTERS = 2;
|
|
6
8
|
function clamp(value, min, max) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ZoneId } from "@zoneflow/core";
|
|
2
|
+
import { type UniverseCanvasFocusZoneOptions, type UniverseCanvasProps } from "../canvas/UniverseCanvas";
|
|
2
3
|
import type { ZoneMoveEditorConfig } from "./ZoneMoveEditorOverlay";
|
|
3
4
|
import type { UniverseEditorController } from "./useUniverseEditor";
|
|
4
5
|
type ControlledZoneMoveEditorConfig = Omit<ZoneMoveEditorConfig, "enabled" | "gridSnap" | "onModelChange" | "onLayoutModelChange" | "onTransactionStart" | "onTransactionCommit" | "onTransactionCancel" | "history">;
|
|
@@ -6,5 +7,14 @@ export type UniverseEditorCanvasProps = Omit<UniverseCanvasProps, "model" | "lay
|
|
|
6
7
|
editor: UniverseEditorController;
|
|
7
8
|
editorConfig?: ControlledZoneMoveEditorConfig;
|
|
8
9
|
};
|
|
9
|
-
export
|
|
10
|
+
export type UniverseEditorCanvasHandle = {
|
|
11
|
+
/** 특정 zone 이 화면 중앙에 오도록 카메라를 이동. zone 을 못 찾으면 false. */
|
|
12
|
+
focusZone: (zoneId: ZoneId, options?: UniverseCanvasFocusZoneOptions) => boolean;
|
|
13
|
+
/** 모든 zone/path 가 화면에 들어오도록 카메라를 맞춤. */
|
|
14
|
+
fitToView: () => void;
|
|
15
|
+
};
|
|
16
|
+
export declare const UniverseEditorCanvas: import("react").ForwardRefExoticComponent<Omit<UniverseCanvasProps, "model" | "layoutModel" | "zoneMoveEditor" | "cameraState" | "onCameraChange" | "onFrameChange"> & {
|
|
17
|
+
editor: UniverseEditorController;
|
|
18
|
+
editorConfig?: ControlledZoneMoveEditorConfig;
|
|
19
|
+
} & import("react").RefAttributes<UniverseEditorCanvasHandle>>;
|
|
10
20
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { UniverseCanvas } from "../canvas/UniverseCanvas";
|
|
2
|
+
import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { computeZoneFocusCamera, UniverseCanvas, } from "../canvas/UniverseCanvas";
|
|
4
4
|
import { getGridToggleLabel, getGridSnapToggleLabel, getObjectSnapToggleLabel, getZoneflowEditorStrings, resolveEditorLocale, } from "./strings";
|
|
5
5
|
import { resolveEditorTheme } from "./theme";
|
|
6
6
|
const DEFAULT_CAMERA = {
|
|
@@ -69,7 +69,7 @@ function fitCameraToWorldRect(params) {
|
|
|
69
69
|
zoom,
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
export function UniverseEditorCanvas(props) {
|
|
72
|
+
export const UniverseEditorCanvas = forwardRef(function UniverseEditorCanvas(props, handleRef) {
|
|
73
73
|
const { editor, editorConfig, grid, ...canvasProps } = props;
|
|
74
74
|
const hostRef = useRef(null);
|
|
75
75
|
const frameRef = useRef(null);
|
|
@@ -129,6 +129,26 @@ export function UniverseEditorCanvas(props) {
|
|
|
129
129
|
viewportHeight: rect.height,
|
|
130
130
|
}));
|
|
131
131
|
}, []);
|
|
132
|
+
const focusZone = useCallback((zoneId, options) => {
|
|
133
|
+
const rect = hostRef.current?.getBoundingClientRect();
|
|
134
|
+
const frame = frameRef.current;
|
|
135
|
+
const zoneRect = frame?.pipeline.graphLayout.zonesById[zoneId]?.rect;
|
|
136
|
+
if (!rect || rect.width <= 0 || rect.height <= 0 || !zoneRect) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
setCamera((prev) => {
|
|
140
|
+
const zoom = clamp(options?.zoom ?? prev.zoom, MIN_ZOOM, MAX_ZOOM);
|
|
141
|
+
return (computeZoneFocusCamera({
|
|
142
|
+
frame,
|
|
143
|
+
zoneId,
|
|
144
|
+
viewportWidth: rect.width,
|
|
145
|
+
viewportHeight: rect.height,
|
|
146
|
+
zoom,
|
|
147
|
+
}) ?? prev);
|
|
148
|
+
});
|
|
149
|
+
return true;
|
|
150
|
+
}, []);
|
|
151
|
+
useImperativeHandle(handleRef, () => ({ focusZone, fitToView }), [focusZone, fitToView]);
|
|
132
152
|
const zoneMoveEditor = editor.isEditMode
|
|
133
153
|
? {
|
|
134
154
|
...editorConfig,
|
|
@@ -241,4 +261,4 @@ export function UniverseEditorCanvas(props) {
|
|
|
241
261
|
...viewerHudButtonStyle,
|
|
242
262
|
fontVariantNumeric: "tabular-nums",
|
|
243
263
|
}, children: [Math.round(camera.zoom * 100), "%"] })) : (_jsx("div", {})), editorConfig?.overlayControls?.showZoomControls !== false ? (_jsx("button", { type: "button", onClick: () => zoomAtCenter(ZOOM_STEP), style: viewerHudButtonStyle, children: "+" })) : (_jsx("div", {}))] })) : null] })) : null] }));
|
|
244
|
-
}
|
|
264
|
+
});
|
|
@@ -103,6 +103,20 @@ export type ZoneMoveEditorConfig = {
|
|
|
103
103
|
onPathLabelClick?: (event: PathLabelEventPayload) => void;
|
|
104
104
|
onPathLabelDoubleClick?: (event: PathLabelEventPayload) => void;
|
|
105
105
|
onPathLabelContextMenu?: (event: PathLabelEventPayload) => void;
|
|
106
|
+
/**
|
|
107
|
+
* zone 선택이 바뀔 때마다 호출됩니다.
|
|
108
|
+
*
|
|
109
|
+
* - 단일 클릭, shift/ctrl/cmd 토글, 마퀴(드래그 박스) 선택 모두 같은 콜백으로 옵니다.
|
|
110
|
+
* - 선택 해제(빈 캔버스 클릭, 편집 모드 종료, 선택된 zone 삭제 등) 시 빈 배열로 호출됩니다.
|
|
111
|
+
* - 선택 내용이 실제로 바뀔 때만 호출됩니다 (같은 zone 재클릭 시 재호출되지 않음).
|
|
112
|
+
* - zone 과 path 선택은 상호 배타라서, zone 선택 시 path 선택이 비워지며
|
|
113
|
+
* `onPathSelectionChange` 도 빈 배열로 함께 호출될 수 있습니다.
|
|
114
|
+
*/
|
|
115
|
+
onZoneSelectionChange?: (zoneIds: ZoneId[]) => void;
|
|
116
|
+
/**
|
|
117
|
+
* path 선택이 바뀔 때마다 호출됩니다. 규칙은 `onZoneSelectionChange` 와 동일합니다.
|
|
118
|
+
*/
|
|
119
|
+
onPathSelectionChange?: (pathIds: PathId[]) => void;
|
|
106
120
|
/**
|
|
107
121
|
* 외부에서 zone 간 path 연결 가능 여부를 검증하는 콜백.
|
|
108
122
|
*
|
|
@@ -176,6 +176,10 @@ function togglePathSelection(pathIds, pathId) {
|
|
|
176
176
|
? pathIds.filter((current) => current !== pathId)
|
|
177
177
|
: [...pathIds, pathId];
|
|
178
178
|
}
|
|
179
|
+
function areIdListsEqual(left, right) {
|
|
180
|
+
return (left.length === right.length &&
|
|
181
|
+
left.every((id, index) => id === right[index]));
|
|
182
|
+
}
|
|
179
183
|
function clamp(value, min, max) {
|
|
180
184
|
return Math.min(Math.max(value, min), max);
|
|
181
185
|
}
|
|
@@ -576,6 +580,8 @@ export function ZoneMoveEditorOverlay(props) {
|
|
|
576
580
|
const marqueeSelectionRef = useRef(null);
|
|
577
581
|
const selectedZoneIdsRef = useRef([]);
|
|
578
582
|
const selectedPathIdsRef = useRef([]);
|
|
583
|
+
const notifiedZoneSelectionRef = useRef([]);
|
|
584
|
+
const notifiedPathSelectionRef = useRef([]);
|
|
579
585
|
const longPressRef = useRef(null);
|
|
580
586
|
const longPressTimerRef = useRef(null);
|
|
581
587
|
const deleteUndoTimerRef = useRef(null);
|
|
@@ -601,6 +607,8 @@ export function ZoneMoveEditorOverlay(props) {
|
|
|
601
607
|
canConnectPath: editor?.canConnectPath,
|
|
602
608
|
onPathCreated: editor?.onPathCreated,
|
|
603
609
|
onPathDropOnEmptySpace: editor?.onPathDropOnEmptySpace,
|
|
610
|
+
onZoneSelectionChange: editor?.onZoneSelectionChange,
|
|
611
|
+
onPathSelectionChange: editor?.onPathSelectionChange,
|
|
604
612
|
resolveZoneShape,
|
|
605
613
|
onExclusionStateChange,
|
|
606
614
|
});
|
|
@@ -622,6 +630,8 @@ export function ZoneMoveEditorOverlay(props) {
|
|
|
622
630
|
canConnectPath: editor?.canConnectPath,
|
|
623
631
|
onPathCreated: editor?.onPathCreated,
|
|
624
632
|
onPathDropOnEmptySpace: editor?.onPathDropOnEmptySpace,
|
|
633
|
+
onZoneSelectionChange: editor?.onZoneSelectionChange,
|
|
634
|
+
onPathSelectionChange: editor?.onPathSelectionChange,
|
|
625
635
|
resolveZoneShape,
|
|
626
636
|
onExclusionStateChange,
|
|
627
637
|
};
|
|
@@ -640,6 +650,22 @@ export function ZoneMoveEditorOverlay(props) {
|
|
|
640
650
|
useEffect(() => {
|
|
641
651
|
selectedPathIdsRef.current = selectedPathIds;
|
|
642
652
|
}, [selectedPathIds]);
|
|
653
|
+
// state 배열의 identity 가 아니라 내용이 바뀔 때만 외부에 알린다 —
|
|
654
|
+
// 같은 선택을 유지한 채 새 배열로 set 되는 경로(모델 prune, 재클릭 등)가 많다.
|
|
655
|
+
useEffect(() => {
|
|
656
|
+
if (areIdListsEqual(notifiedZoneSelectionRef.current, selectedZoneIds)) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
notifiedZoneSelectionRef.current = selectedZoneIds;
|
|
660
|
+
latestRef.current.onZoneSelectionChange?.([...selectedZoneIds]);
|
|
661
|
+
}, [selectedZoneIds]);
|
|
662
|
+
useEffect(() => {
|
|
663
|
+
if (areIdListsEqual(notifiedPathSelectionRef.current, selectedPathIds)) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
notifiedPathSelectionRef.current = selectedPathIds;
|
|
667
|
+
latestRef.current.onPathSelectionChange?.([...selectedPathIds]);
|
|
668
|
+
}, [selectedPathIds]);
|
|
643
669
|
useEffect(() => {
|
|
644
670
|
if (typeof window === "undefined")
|
|
645
671
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zoneflow/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React renderer and editor components for Zoneflow.",
|
|
6
6
|
"type": "module",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"react-dom": "^18 || ^19"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@zoneflow/core": "0.0.
|
|
27
|
-
"@zoneflow/editor-dom": "0.0.
|
|
28
|
-
"@zoneflow/renderer-dom": "0.0.
|
|
26
|
+
"@zoneflow/core": "0.0.21",
|
|
27
|
+
"@zoneflow/editor-dom": "0.0.21",
|
|
28
|
+
"@zoneflow/renderer-dom": "0.0.21"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/react": "^19.2.14",
|