@fieldnotes/react 0.13.0 → 0.14.1
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/README.md +49 -12
- package/dist/index.cjs +114 -264
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -12
- package/dist/index.d.ts +15 -12
- package/dist/index.js +150 -300
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
forwardRef,
|
|
7
7
|
useImperativeHandle
|
|
8
8
|
} from "react";
|
|
9
|
-
import { Viewport } from "@fieldnotes/core";
|
|
9
|
+
import { Viewport, createDefaultTools } from "@fieldnotes/core";
|
|
10
10
|
|
|
11
11
|
// src/context.ts
|
|
12
12
|
import { createContext } from "react";
|
|
@@ -21,6 +21,9 @@ var FieldNotesCanvas = forwardRef(
|
|
|
21
21
|
defaultTool,
|
|
22
22
|
tool,
|
|
23
23
|
onToolChange,
|
|
24
|
+
initialData,
|
|
25
|
+
onChange,
|
|
26
|
+
changeDebounceMs = 250,
|
|
24
27
|
snapToGrid,
|
|
25
28
|
className,
|
|
26
29
|
style,
|
|
@@ -29,23 +32,65 @@ var FieldNotesCanvas = forwardRef(
|
|
|
29
32
|
}, ref) {
|
|
30
33
|
const containerRef = useRef(null);
|
|
31
34
|
const [viewport, setViewport] = useState(null);
|
|
35
|
+
const changeRef = useRef({ onChange, changeDebounceMs });
|
|
36
|
+
changeRef.current = { onChange, changeDebounceMs };
|
|
32
37
|
useImperativeHandle(ref, () => ({ viewport }), [viewport]);
|
|
33
38
|
useEffect(() => {
|
|
34
39
|
const el = containerRef.current;
|
|
35
40
|
if (!el) return;
|
|
36
41
|
const vp = new Viewport(el, options);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
const activate = (fallback) => {
|
|
43
|
+
const name = tool ?? defaultTool ?? fallback;
|
|
44
|
+
if (name) vp.setTool(name);
|
|
45
|
+
};
|
|
46
|
+
const deferDefaults = tools === void 0 && options?.tools === void 0;
|
|
47
|
+
for (const t of tools === "default" ? createDefaultTools() : tools ?? []) {
|
|
48
|
+
vp.toolManager.register(t);
|
|
41
49
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
vp.
|
|
50
|
+
if (!deferDefaults) activate(tools === "default" ? "select" : void 0);
|
|
51
|
+
try {
|
|
52
|
+
if (typeof initialData === "string") vp.loadJSON(initialData);
|
|
53
|
+
else if (initialData) vp.loadState(initialData);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
vp.destroy();
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
let timer;
|
|
59
|
+
const emit = () => {
|
|
60
|
+
timer = void 0;
|
|
61
|
+
changeRef.current.onChange?.(vp.exportState());
|
|
62
|
+
};
|
|
63
|
+
const unsubscribe = vp.onStateChange(() => {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
timer = setTimeout(emit, changeRef.current.changeDebounceMs);
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
onReady?.(vp);
|
|
69
|
+
if (deferDefaults) {
|
|
70
|
+
const registerDefaults = vp.toolManager.toolNames.length === 0;
|
|
71
|
+
if (registerDefaults) {
|
|
72
|
+
for (const t of createDefaultTools()) vp.toolManager.register(t);
|
|
73
|
+
}
|
|
74
|
+
activate(registerDefaults ? "select" : void 0);
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
unsubscribe();
|
|
79
|
+
vp.destroy();
|
|
80
|
+
throw error;
|
|
45
81
|
}
|
|
46
82
|
setViewport(vp);
|
|
47
|
-
onReady?.(vp);
|
|
48
83
|
return () => {
|
|
84
|
+
if (timer !== void 0) {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
try {
|
|
87
|
+
emit();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (typeof reportError === "function") reportError(error);
|
|
90
|
+
else console.error("[fieldnotes] onChange failed during unmount", error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
unsubscribe();
|
|
49
94
|
vp.destroy();
|
|
50
95
|
setViewport(null);
|
|
51
96
|
};
|
|
@@ -65,7 +110,7 @@ var FieldNotesCanvas = forwardRef(
|
|
|
65
110
|
viewport.setSnapToGrid(snapToGrid);
|
|
66
111
|
}, [viewport, snapToGrid]);
|
|
67
112
|
useEffect(() => {
|
|
68
|
-
if (!viewport || !tools) return;
|
|
113
|
+
if (!viewport || !Array.isArray(tools)) return;
|
|
69
114
|
for (const t of tools) {
|
|
70
115
|
if (!viewport.toolManager.getTool(t.name)) {
|
|
71
116
|
viewport.toolManager.register(t);
|
|
@@ -113,12 +158,11 @@ function CanvasElement({ position, size, children }) {
|
|
|
113
158
|
};
|
|
114
159
|
elementIdRef.current = register();
|
|
115
160
|
setPortalTarget(container);
|
|
116
|
-
let disposed = false;
|
|
117
161
|
const handleStoreReplacement = () => {
|
|
118
162
|
const clearedId = elementIdRef.current;
|
|
119
163
|
if (clearedId === null) return;
|
|
120
164
|
queueMicrotask(() => {
|
|
121
|
-
if (
|
|
165
|
+
if (elementIdRef.current !== clearedId) return;
|
|
122
166
|
if (viewport.store.getById(clearedId)?.type === "html") {
|
|
123
167
|
viewport.updateHtmlElement(clearedId, container);
|
|
124
168
|
} else {
|
|
@@ -130,7 +174,6 @@ function CanvasElement({ position, size, children }) {
|
|
|
130
174
|
const unsubscribeClear = viewport.store.on("clear", handleStoreReplacement);
|
|
131
175
|
const unsubscribeBatch = viewport.store.on("batch", handleStoreReplacement);
|
|
132
176
|
return () => {
|
|
133
|
-
disposed = true;
|
|
134
177
|
unsubscribeClear();
|
|
135
178
|
unsubscribeBatch();
|
|
136
179
|
if (elementIdRef.current) {
|
|
@@ -154,67 +197,42 @@ function CanvasElement({ position, size, children }) {
|
|
|
154
197
|
}
|
|
155
198
|
|
|
156
199
|
// src/hooks/use-active-tool.ts
|
|
157
|
-
import { useCallback, useSyncExternalStore } from "react";
|
|
200
|
+
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
201
|
+
import { createActiveToolSource } from "@fieldnotes/core";
|
|
158
202
|
function useActiveTool() {
|
|
159
203
|
const viewport = useViewport();
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
[viewport]
|
|
163
|
-
);
|
|
164
|
-
const getSnapshot = useCallback(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
|
|
165
|
-
const toolName = useSyncExternalStore(subscribe, getSnapshot);
|
|
204
|
+
const source = useMemo(() => createActiveToolSource(viewport), [viewport]);
|
|
205
|
+
const toolName = useSyncExternalStore(source.subscribe, source.getSnapshot);
|
|
166
206
|
const setTool = useCallback((name) => viewport.setTool(name), [viewport]);
|
|
167
207
|
return [toolName, setTool];
|
|
168
208
|
}
|
|
169
209
|
|
|
170
210
|
// src/hooks/use-camera.ts
|
|
171
|
-
import {
|
|
211
|
+
import { useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
212
|
+
import { createCameraSource } from "@fieldnotes/core";
|
|
172
213
|
function useCamera() {
|
|
173
214
|
const viewport = useViewport();
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
(onStoreChange) => viewport.camera.onChange(onStoreChange),
|
|
177
|
-
[viewport]
|
|
178
|
-
);
|
|
179
|
-
const getSnapshot = useCallback2(() => {
|
|
180
|
-
const { position, zoom } = viewport.camera;
|
|
181
|
-
const cached = cachedRef.current;
|
|
182
|
-
if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {
|
|
183
|
-
return cached;
|
|
184
|
-
}
|
|
185
|
-
const next = { x: position.x, y: position.y, zoom };
|
|
186
|
-
cachedRef.current = next;
|
|
187
|
-
return next;
|
|
188
|
-
}, [viewport]);
|
|
189
|
-
return useSyncExternalStore2(subscribe, getSnapshot);
|
|
215
|
+
const source = useMemo2(() => createCameraSource(viewport), [viewport]);
|
|
216
|
+
return useSyncExternalStore2(source.subscribe, source.getSnapshot);
|
|
190
217
|
}
|
|
191
218
|
|
|
192
219
|
// src/hooks/use-tool-options.ts
|
|
193
|
-
import { useCallback as
|
|
194
|
-
|
|
195
|
-
}
|
|
220
|
+
import { useCallback as useCallback2, useMemo as useMemo3, useRef as useRef3, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
221
|
+
import { createToolOptionsSource } from "@fieldnotes/core";
|
|
196
222
|
function useToolOptions(toolName) {
|
|
197
223
|
const viewport = useViewport();
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return tool.onOptionsChange(onStoreChange);
|
|
204
|
-
},
|
|
205
|
-
[viewport, toolName]
|
|
224
|
+
const nameRef = useRef3(toolName);
|
|
225
|
+
nameRef.current = toolName;
|
|
226
|
+
const source = useMemo3(
|
|
227
|
+
() => createToolOptionsSource(viewport, () => nameRef.current),
|
|
228
|
+
[viewport]
|
|
206
229
|
);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
cachedRef.current = next;
|
|
214
|
-
return next;
|
|
215
|
-
}, [viewport, toolName]);
|
|
216
|
-
const options = useSyncExternalStore3(subscribe, getSnapshot);
|
|
217
|
-
const setOptions = useCallback3(
|
|
230
|
+
const subscribe = useCallback2(
|
|
231
|
+
(onStoreChange) => source.subscribe(onStoreChange),
|
|
232
|
+
[source, toolName]
|
|
233
|
+
);
|
|
234
|
+
const options = useSyncExternalStore3(subscribe, source.getSnapshot);
|
|
235
|
+
const setOptions = useCallback2(
|
|
218
236
|
(partial) => {
|
|
219
237
|
const tool = viewport.toolManager.getTool(toolName);
|
|
220
238
|
tool?.setOptions?.(partial);
|
|
@@ -223,84 +241,47 @@ function useToolOptions(toolName) {
|
|
|
223
241
|
);
|
|
224
242
|
return [options, setOptions];
|
|
225
243
|
}
|
|
226
|
-
function shallowEqual(a, b) {
|
|
227
|
-
const keysA = Object.keys(a);
|
|
228
|
-
const keysB = Object.keys(b);
|
|
229
|
-
if (keysA.length !== keysB.length) return false;
|
|
230
|
-
for (const key of keysA) {
|
|
231
|
-
const valA = a[key];
|
|
232
|
-
const valB = b[key];
|
|
233
|
-
if (valA === valB) continue;
|
|
234
|
-
if (typeof valA === "object" && valA !== null && typeof valB === "object" && valB !== null && !Array.isArray(valA) && !Array.isArray(valB)) {
|
|
235
|
-
const objA = valA;
|
|
236
|
-
const objB = valB;
|
|
237
|
-
const innerKeys = Object.keys(objA);
|
|
238
|
-
if (innerKeys.length === Object.keys(objB).length && innerKeys.every((k) => objA[k] === objB[k])) {
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
244
|
|
|
247
245
|
// src/hooks/use-layers.ts
|
|
248
|
-
import { useCallback as
|
|
246
|
+
import { useCallback as useCallback3, useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore4 } from "react";
|
|
247
|
+
import { createLayersSource } from "@fieldnotes/core";
|
|
249
248
|
function useLayers() {
|
|
250
249
|
const viewport = useViewport();
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
[viewport]
|
|
255
|
-
);
|
|
256
|
-
const getSnapshot = useCallback4(() => {
|
|
257
|
-
const layers = viewport.layerManager.getLayers();
|
|
258
|
-
const activeLayerId = viewport.layerManager.activeLayerId;
|
|
259
|
-
const cached = cachedRef.current;
|
|
260
|
-
if (cached.activeLayerId === activeLayerId && cached.layers.length === layers.length && cached.layers.every((l, i) => {
|
|
261
|
-
const next2 = layers[i];
|
|
262
|
-
return next2 !== void 0 && l.id === next2.id && l.name === next2.name && l.visible === next2.visible && l.locked === next2.locked && l.order === next2.order && l.opacity === next2.opacity;
|
|
263
|
-
})) {
|
|
264
|
-
return cached;
|
|
265
|
-
}
|
|
266
|
-
const next = { layers, activeLayerId };
|
|
267
|
-
cachedRef.current = next;
|
|
268
|
-
return next;
|
|
269
|
-
}, [viewport]);
|
|
270
|
-
const snapshot = useSyncExternalStore4(subscribe, getSnapshot);
|
|
271
|
-
const createLayer = useCallback4(
|
|
250
|
+
const source = useMemo4(() => createLayersSource(viewport), [viewport]);
|
|
251
|
+
const snapshot = useSyncExternalStore4(source.subscribe, source.getSnapshot);
|
|
252
|
+
const createLayer = useCallback3(
|
|
272
253
|
(name) => viewport.layerManager.createLayer(name),
|
|
273
254
|
[viewport]
|
|
274
255
|
);
|
|
275
|
-
const removeLayer =
|
|
256
|
+
const removeLayer = useCallback3(
|
|
276
257
|
(id) => viewport.layerManager.removeLayer(id),
|
|
277
258
|
[viewport]
|
|
278
259
|
);
|
|
279
|
-
const renameLayer =
|
|
260
|
+
const renameLayer = useCallback3(
|
|
280
261
|
(id, name) => viewport.layerManager.renameLayer(id, name),
|
|
281
262
|
[viewport]
|
|
282
263
|
);
|
|
283
|
-
const reorderLayer =
|
|
264
|
+
const reorderLayer = useCallback3(
|
|
284
265
|
(id, newOrder) => viewport.layerManager.reorderLayer(id, newOrder),
|
|
285
266
|
[viewport]
|
|
286
267
|
);
|
|
287
|
-
const setVisible =
|
|
268
|
+
const setVisible = useCallback3(
|
|
288
269
|
(id, visible) => viewport.layerManager.setLayerVisible(id, visible),
|
|
289
270
|
[viewport]
|
|
290
271
|
);
|
|
291
|
-
const setLocked =
|
|
272
|
+
const setLocked = useCallback3(
|
|
292
273
|
(id, locked) => viewport.layerManager.setLayerLocked(id, locked),
|
|
293
274
|
[viewport]
|
|
294
275
|
);
|
|
295
|
-
const setOpacity =
|
|
276
|
+
const setOpacity = useCallback3(
|
|
296
277
|
(id, opacity) => viewport.layerManager.setLayerOpacity(id, opacity),
|
|
297
278
|
[viewport]
|
|
298
279
|
);
|
|
299
|
-
const setActiveLayer =
|
|
280
|
+
const setActiveLayer = useCallback3(
|
|
300
281
|
(id) => viewport.layerManager.setActiveLayer(id),
|
|
301
282
|
[viewport]
|
|
302
283
|
);
|
|
303
|
-
const moveElement =
|
|
284
|
+
const moveElement = useCallback3(
|
|
304
285
|
(elementId, layerId) => viewport.layerManager.moveElementToLayer(elementId, layerId),
|
|
305
286
|
[viewport]
|
|
306
287
|
);
|
|
@@ -320,28 +301,14 @@ function useLayers() {
|
|
|
320
301
|
}
|
|
321
302
|
|
|
322
303
|
// src/hooks/use-history.ts
|
|
323
|
-
import { useCallback as
|
|
304
|
+
import { useCallback as useCallback4, useMemo as useMemo5, useSyncExternalStore as useSyncExternalStore5 } from "react";
|
|
305
|
+
import { createHistorySource } from "@fieldnotes/core";
|
|
324
306
|
function useHistory() {
|
|
325
307
|
const viewport = useViewport();
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
);
|
|
331
|
-
const getSnapshot = useCallback5(() => {
|
|
332
|
-
const canUndo = viewport.history.canUndo;
|
|
333
|
-
const canRedo = viewport.history.canRedo;
|
|
334
|
-
const cached = cachedRef.current;
|
|
335
|
-
if (cached.canUndo === canUndo && cached.canRedo === canRedo) {
|
|
336
|
-
return cached;
|
|
337
|
-
}
|
|
338
|
-
const next = { canUndo, canRedo };
|
|
339
|
-
cachedRef.current = next;
|
|
340
|
-
return next;
|
|
341
|
-
}, [viewport]);
|
|
342
|
-
const snapshot = useSyncExternalStore5(subscribe, getSnapshot);
|
|
343
|
-
const undo = useCallback5(() => viewport.undo(), [viewport]);
|
|
344
|
-
const redo = useCallback5(() => viewport.redo(), [viewport]);
|
|
308
|
+
const source = useMemo5(() => createHistorySource(viewport), [viewport]);
|
|
309
|
+
const snapshot = useSyncExternalStore5(source.subscribe, source.getSnapshot);
|
|
310
|
+
const undo = useCallback4(() => viewport.undo(), [viewport]);
|
|
311
|
+
const redo = useCallback4(() => viewport.redo(), [viewport]);
|
|
345
312
|
return {
|
|
346
313
|
canUndo: snapshot.canUndo,
|
|
347
314
|
canRedo: snapshot.canRedo,
|
|
@@ -351,197 +318,80 @@ function useHistory() {
|
|
|
351
318
|
}
|
|
352
319
|
|
|
353
320
|
// src/hooks/use-elements.ts
|
|
354
|
-
import {
|
|
355
|
-
|
|
356
|
-
if (Object.is(a, b)) return true;
|
|
357
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
358
|
-
return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
|
|
359
|
-
}
|
|
360
|
-
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
|
|
361
|
-
const ka = Object.keys(a);
|
|
362
|
-
const kb = Object.keys(b);
|
|
363
|
-
return ka.length === kb.length && ka.every(
|
|
364
|
-
(k) => k in b && Object.is(a[k], b[k])
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
return false;
|
|
368
|
-
}
|
|
321
|
+
import { useMemo as useMemo6, useRef as useRef4, useSyncExternalStore as useSyncExternalStore6 } from "react";
|
|
322
|
+
import { createElementsSelectorSource, createElementsSource } from "@fieldnotes/core";
|
|
369
323
|
function useElements(arg, isEqual) {
|
|
370
324
|
const viewport = useViewport();
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
(
|
|
325
|
+
const latest = useRef4({ arg, isEqual });
|
|
326
|
+
latest.current = { arg, isEqual };
|
|
327
|
+
const elementsSource = useMemo6(
|
|
328
|
+
() => createElementsSource(viewport, () => {
|
|
329
|
+
const a = latest.current.arg;
|
|
330
|
+
return typeof a === "function" ? void 0 : a;
|
|
331
|
+
}),
|
|
375
332
|
[viewport]
|
|
376
333
|
);
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const next = arg ? viewport.store.getElementsByType(arg) : viewport.store.getAll();
|
|
389
|
-
const cached = cachedRef.current;
|
|
390
|
-
if (cached.length === next.length && cached.every((el, i) => el === next[i])) {
|
|
391
|
-
return cached;
|
|
392
|
-
}
|
|
393
|
-
cachedRef.current = next;
|
|
394
|
-
return next;
|
|
395
|
-
}, [viewport, arg, isEqual]);
|
|
396
|
-
return useSyncExternalStore6(subscribe, getSnapshot);
|
|
334
|
+
const selectorSource = useMemo6(
|
|
335
|
+
() => createElementsSelectorSource(
|
|
336
|
+
viewport,
|
|
337
|
+
// Only read while `arg` is a selector: the source is picked below by `typeof arg`.
|
|
338
|
+
() => latest.current.arg,
|
|
339
|
+
() => latest.current.isEqual
|
|
340
|
+
),
|
|
341
|
+
[viewport]
|
|
342
|
+
);
|
|
343
|
+
const source = typeof arg === "function" ? selectorSource : elementsSource;
|
|
344
|
+
return useSyncExternalStore6(source.subscribe, source.getSnapshot);
|
|
397
345
|
}
|
|
398
346
|
|
|
399
347
|
// src/hooks/use-selection.ts
|
|
400
|
-
import {
|
|
348
|
+
import { useMemo as useMemo7, useSyncExternalStore as useSyncExternalStore7 } from "react";
|
|
349
|
+
import { createSelectionSource } from "@fieldnotes/core";
|
|
401
350
|
function useSelection() {
|
|
402
351
|
const viewport = useViewport();
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
[viewport]
|
|
406
|
-
);
|
|
407
|
-
const getSnapshot = useCallback7(() => viewport.getSelectedIds(), [viewport]);
|
|
408
|
-
return useSyncExternalStore7(subscribe, getSnapshot, getSnapshot);
|
|
352
|
+
const source = useMemo7(() => createSelectionSource(viewport), [viewport]);
|
|
353
|
+
return useSyncExternalStore7(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
409
354
|
}
|
|
410
355
|
|
|
411
356
|
// src/hooks/use-selection-style.ts
|
|
412
|
-
import { useCallback as
|
|
413
|
-
|
|
414
|
-
// src/hooks/style-keys.ts
|
|
415
|
-
var STYLE_KEYS = [
|
|
416
|
-
"color",
|
|
417
|
-
"fillColor",
|
|
418
|
-
"strokeWidth",
|
|
419
|
-
"opacity",
|
|
420
|
-
"fontSize",
|
|
421
|
-
"strokeStyle"
|
|
422
|
-
];
|
|
423
|
-
|
|
424
|
-
// src/hooks/use-selection-style.ts
|
|
425
|
-
function styleEqual(a, b) {
|
|
426
|
-
if (a === b) return true;
|
|
427
|
-
if (a === null || b === null) return false;
|
|
428
|
-
return STYLE_KEYS.every((k) => a[k] === b[k]);
|
|
429
|
-
}
|
|
357
|
+
import { useCallback as useCallback5, useMemo as useMemo8, useSyncExternalStore as useSyncExternalStore8 } from "react";
|
|
358
|
+
import { createSelectionStyleSource } from "@fieldnotes/core";
|
|
430
359
|
function useSelectionStyle() {
|
|
431
360
|
const viewport = useViewport();
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
const offSel = viewport.onSelectionChange(onChange);
|
|
436
|
-
const offStore = viewport.store.onChange(onChange);
|
|
437
|
-
return () => {
|
|
438
|
-
offSel();
|
|
439
|
-
offStore();
|
|
440
|
-
};
|
|
441
|
-
},
|
|
442
|
-
[viewport]
|
|
443
|
-
);
|
|
444
|
-
const getSnapshot = useCallback8(() => {
|
|
445
|
-
const next = viewport.getSelectionStyle();
|
|
446
|
-
if (styleEqual(cacheRef.current, next)) return cacheRef.current;
|
|
447
|
-
cacheRef.current = next;
|
|
448
|
-
return next;
|
|
449
|
-
}, [viewport]);
|
|
450
|
-
const style = useSyncExternalStore8(subscribe, getSnapshot, getSnapshot);
|
|
451
|
-
const apply = useCallback8((s) => viewport.applyStyleToSelection(s), [viewport]);
|
|
361
|
+
const source = useMemo8(() => createSelectionStyleSource(viewport), [viewport]);
|
|
362
|
+
const style = useSyncExternalStore8(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
363
|
+
const apply = useCallback5((s) => viewport.applyStyleToSelection(s), [viewport]);
|
|
452
364
|
return [style, apply];
|
|
453
365
|
}
|
|
454
366
|
|
|
455
367
|
// src/hooks/use-selection-style-details.ts
|
|
456
|
-
import { useCallback as
|
|
457
|
-
|
|
458
|
-
return a.length === b.length && a.every((k, i) => k === b[i]);
|
|
459
|
-
}
|
|
460
|
-
function detailsEqual(a, b) {
|
|
461
|
-
if (a === b) return true;
|
|
462
|
-
if (a === null || b === null) return false;
|
|
463
|
-
return keysEqual(a.applicable, b.applicable) && keysEqual(a.mixed, b.mixed) && STYLE_KEYS.every((k) => a.common[k] === b.common[k]);
|
|
464
|
-
}
|
|
368
|
+
import { useCallback as useCallback6, useMemo as useMemo9, useSyncExternalStore as useSyncExternalStore9 } from "react";
|
|
369
|
+
import { createSelectionStyleDetailsSource } from "@fieldnotes/core";
|
|
465
370
|
function useSelectionStyleDetails() {
|
|
466
371
|
const viewport = useViewport();
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
const offSel = viewport.onSelectionChange(onChange);
|
|
471
|
-
const offStore = viewport.store.onChange(onChange);
|
|
472
|
-
return () => {
|
|
473
|
-
offSel();
|
|
474
|
-
offStore();
|
|
475
|
-
};
|
|
476
|
-
},
|
|
477
|
-
[viewport]
|
|
478
|
-
);
|
|
479
|
-
const getSnapshot = useCallback9(() => {
|
|
480
|
-
const next = viewport.getSelectionStyleDetails();
|
|
481
|
-
if (detailsEqual(cacheRef.current, next)) return cacheRef.current;
|
|
482
|
-
cacheRef.current = next;
|
|
483
|
-
return next;
|
|
484
|
-
}, [viewport]);
|
|
485
|
-
const details = useSyncExternalStore9(subscribe, getSnapshot, getSnapshot);
|
|
486
|
-
const apply = useCallback9((s) => viewport.applyStyleToSelection(s), [viewport]);
|
|
372
|
+
const source = useMemo9(() => createSelectionStyleDetailsSource(viewport), [viewport]);
|
|
373
|
+
const details = useSyncExternalStore9(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
374
|
+
const apply = useCallback6((s) => viewport.applyStyleToSelection(s), [viewport]);
|
|
487
375
|
return [details, apply];
|
|
488
376
|
}
|
|
489
377
|
|
|
490
378
|
// src/hooks/use-selection-ops.ts
|
|
491
|
-
import { useCallback as
|
|
492
|
-
|
|
493
|
-
return a.selectedIds === b.selectedIds && a.canGroup === b.canGroup && a.canUngroup === b.canUngroup && a.canAlign === b.canAlign && a.canDistribute === b.canDistribute && a.isLocked === b.isLocked;
|
|
494
|
-
}
|
|
379
|
+
import { useCallback as useCallback7, useMemo as useMemo10, useSyncExternalStore as useSyncExternalStore10 } from "react";
|
|
380
|
+
import { createSelectionOpsSource } from "@fieldnotes/core";
|
|
495
381
|
function useSelectionOps() {
|
|
496
382
|
const viewport = useViewport();
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
offStore();
|
|
505
|
-
};
|
|
506
|
-
},
|
|
507
|
-
[viewport]
|
|
508
|
-
);
|
|
509
|
-
const getSnapshot = useCallback10(() => {
|
|
510
|
-
const ids = viewport.getSelectedIds();
|
|
511
|
-
const n = ids.length;
|
|
512
|
-
let canUngroup = false;
|
|
513
|
-
let lockedCount = 0;
|
|
514
|
-
for (const id of ids) {
|
|
515
|
-
const el = viewport.store.getById(id);
|
|
516
|
-
if (!el) continue;
|
|
517
|
-
if (el.groupId !== void 0) canUngroup = true;
|
|
518
|
-
if (el.locked) lockedCount++;
|
|
519
|
-
}
|
|
520
|
-
const isLocked = n === 0 ? null : lockedCount === n ? true : lockedCount === 0 ? false : null;
|
|
521
|
-
const next = {
|
|
522
|
-
selectedIds: ids,
|
|
523
|
-
canGroup: n >= 2,
|
|
524
|
-
canUngroup,
|
|
525
|
-
canAlign: n >= 2,
|
|
526
|
-
canDistribute: n >= 3,
|
|
527
|
-
isLocked
|
|
528
|
-
};
|
|
529
|
-
const prev = cacheRef.current;
|
|
530
|
-
if (prev && snapshotEqual(prev, next)) return prev;
|
|
531
|
-
cacheRef.current = next;
|
|
532
|
-
return next;
|
|
533
|
-
}, [viewport]);
|
|
534
|
-
const snap = useSyncExternalStore10(subscribe, getSnapshot, getSnapshot);
|
|
535
|
-
const group = useCallback10(() => viewport.groupSelection(), [viewport]);
|
|
536
|
-
const ungroup = useCallback10(() => viewport.ungroupSelection(), [viewport]);
|
|
537
|
-
const toggleLock = useCallback10(() => viewport.toggleLockSelection(), [viewport]);
|
|
538
|
-
const align = useCallback10((edge) => viewport.alignSelection(edge), [viewport]);
|
|
539
|
-
const distribute = useCallback10(
|
|
383
|
+
const source = useMemo10(() => createSelectionOpsSource(viewport), [viewport]);
|
|
384
|
+
const snap = useSyncExternalStore10(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
385
|
+
const group = useCallback7(() => viewport.groupSelection(), [viewport]);
|
|
386
|
+
const ungroup = useCallback7(() => viewport.ungroupSelection(), [viewport]);
|
|
387
|
+
const toggleLock = useCallback7(() => viewport.toggleLockSelection(), [viewport]);
|
|
388
|
+
const align = useCallback7((edge) => viewport.alignSelection(edge), [viewport]);
|
|
389
|
+
const distribute = useCallback7(
|
|
540
390
|
(axis) => viewport.distributeSelection(axis),
|
|
541
391
|
[viewport]
|
|
542
392
|
);
|
|
543
|
-
const rotateCW =
|
|
544
|
-
const rotateCCW =
|
|
393
|
+
const rotateCW = useCallback7(() => viewport.rotateSelection("cw"), [viewport]);
|
|
394
|
+
const rotateCCW = useCallback7(() => viewport.rotateSelection("ccw"), [viewport]);
|
|
545
395
|
return {
|
|
546
396
|
selectedIds: snap.selectedIds,
|
|
547
397
|
selectedCount: snap.selectedIds.length,
|
|
@@ -561,16 +411,16 @@ function useSelectionOps() {
|
|
|
561
411
|
}
|
|
562
412
|
|
|
563
413
|
// src/hooks/use-element-rects.ts
|
|
564
|
-
import { useCallback as
|
|
414
|
+
import { useCallback as useCallback8, useEffect as useEffect3, useRef as useRef5, useSyncExternalStore as useSyncExternalStore11 } from "react";
|
|
565
415
|
import { ElementRectTracker, computeElementRects, elementRectsEqual } from "@fieldnotes/core";
|
|
566
416
|
function useElementRects(match) {
|
|
567
417
|
const viewport = useViewport();
|
|
568
|
-
const matchRef =
|
|
418
|
+
const matchRef = useRef5(match);
|
|
569
419
|
matchRef.current = match;
|
|
570
|
-
const stableMatch =
|
|
571
|
-
const cacheRef =
|
|
572
|
-
const trackerRef =
|
|
573
|
-
const subscribe =
|
|
420
|
+
const stableMatch = useCallback8((el) => matchRef.current(el), []);
|
|
421
|
+
const cacheRef = useRef5(null);
|
|
422
|
+
const trackerRef = useRef5(null);
|
|
423
|
+
const subscribe = useCallback8(
|
|
574
424
|
(onStoreChange) => {
|
|
575
425
|
const tracker = new ElementRectTracker(viewport, { match: stableMatch });
|
|
576
426
|
trackerRef.current = tracker;
|
|
@@ -592,7 +442,7 @@ function useElementRects(match) {
|
|
|
592
442
|
},
|
|
593
443
|
[viewport, stableMatch]
|
|
594
444
|
);
|
|
595
|
-
const getSnapshot =
|
|
445
|
+
const getSnapshot = useCallback8(() => {
|
|
596
446
|
if (cacheRef.current === null) {
|
|
597
447
|
cacheRef.current = computeElementRects(viewport.store, stableMatch);
|
|
598
448
|
}
|
|
@@ -606,7 +456,7 @@ function useElementRects(match) {
|
|
|
606
456
|
}
|
|
607
457
|
|
|
608
458
|
// src/minimap.tsx
|
|
609
|
-
import { useEffect as useEffect4, useRef as
|
|
459
|
+
import { useEffect as useEffect4, useRef as useRef6, useState as useState3 } from "react";
|
|
610
460
|
import { MinimapController } from "@fieldnotes/core";
|
|
611
461
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
612
462
|
var toggleStyle = {
|
|
@@ -634,7 +484,7 @@ function Minimap({
|
|
|
634
484
|
}) {
|
|
635
485
|
const viewport = useViewport();
|
|
636
486
|
const [collapsed, setCollapsed] = useState3(defaultCollapsed);
|
|
637
|
-
const canvasRef =
|
|
487
|
+
const canvasRef = useRef6(null);
|
|
638
488
|
useEffect4(() => {
|
|
639
489
|
if (collapsed) return;
|
|
640
490
|
const canvas = canvasRef.current;
|