@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/README.md
CHANGED
|
@@ -8,13 +8,42 @@ React bindings for the [Field Notes](https://github.com/IrakliDevelop/fieldnotes
|
|
|
8
8
|
npm install @fieldnotes/core @fieldnotes/react
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Requires React 18+.
|
|
11
|
+
Requires React 18+. Using Vue or Svelte? See [`@fieldnotes/vue`](../vue) and [`@fieldnotes/svelte`](../svelte).
|
|
12
12
|
|
|
13
13
|
## Quick Start
|
|
14
14
|
|
|
15
15
|
Full runnable example: `examples/react-app` — `pnpm --filter fieldnotes-react-example dev`
|
|
16
16
|
|
|
17
|
-
The canvas needs a container with a defined size — it fills whatever it's given
|
|
17
|
+
The canvas needs a container with a defined size — it fills whatever it's given. With no `tools`
|
|
18
|
+
prop and no tools registered in `onReady`, it registers the default tool bundle
|
|
19
|
+
(`createDefaultTools()`: hand, select, pencil, eraser, arrow, note, text, shape) with `select`
|
|
20
|
+
active, so a persisting board is one line:
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { FieldNotesCanvas } from '@fieldnotes/react';
|
|
24
|
+
import type { CanvasState } from '@fieldnotes/core';
|
|
25
|
+
|
|
26
|
+
const saved = localStorage.getItem('board') ?? undefined;
|
|
27
|
+
const save = (state: CanvasState) => localStorage.setItem('board', JSON.stringify(state));
|
|
28
|
+
|
|
29
|
+
export function App() {
|
|
30
|
+
return (
|
|
31
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
32
|
+
<FieldNotesCanvas
|
|
33
|
+
initialData={saved}
|
|
34
|
+
onChange={save}
|
|
35
|
+
style={{ width: '100%', height: '100%' }}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`initialData` takes a saved state object or JSON string. `onChange` receives `viewport.exportState()`,
|
|
43
|
+
trailing-debounced by `changeDebounceMs` (default 250); a pending call is flushed on unmount, and
|
|
44
|
+
the initial load is not reported as a change.
|
|
45
|
+
|
|
46
|
+
For your own toolbar, pass an explicit tool array and control the active tool:
|
|
18
47
|
|
|
19
48
|
```tsx
|
|
20
49
|
import { useState } from 'react';
|
|
@@ -80,14 +109,22 @@ function Toolbar({ tool, onSelect }: { tool: string; onSelect: (name: string) =>
|
|
|
80
109
|
|
|
81
110
|
Not all props are reactive — the canvas is stateful. Changing a mount-only prop after mount has no effect:
|
|
82
111
|
|
|
83
|
-
| Prop | Reactivity
|
|
84
|
-
| ---------------------------------- |
|
|
85
|
-
| `options` | Mount-only — the canvas is stateful; construct with the right options
|
|
86
|
-
| `tools` |
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
112
|
+
| Prop | Reactivity |
|
|
113
|
+
| ---------------------------------- | ---------------------------------------------------------------------------- |
|
|
114
|
+
| `options` | Mount-only — the canvas is stateful; construct with the right options |
|
|
115
|
+
| `tools` | Array: reactive, append-only. Omitted or `'default'`: mount-only (see below) |
|
|
116
|
+
| `initialData` | Mount-only — loaded once before `onReady` |
|
|
117
|
+
| `onChange` / `changeDebounceMs` | Reactive — the latest callback is used without resubscribing |
|
|
118
|
+
| `tool` / `onToolChange` | Reactive (controlled). Memoize the callback |
|
|
119
|
+
| `defaultTool` | Mount-only (uncontrolled initial tool) |
|
|
120
|
+
| `snapToGrid` (deprecated) | Reactive compatibility prop; prefer a domain constraint service |
|
|
121
|
+
| `className` / `style` / `children` | Reactive (plain React) |
|
|
122
|
+
|
|
123
|
+
`tools` as an array registers each tool once (tools cannot be unregistered); hoist it out of render.
|
|
124
|
+
`tools="default"` registers `createDefaultTools()` at mount, before `onReady` (so you can customise
|
|
125
|
+
them there). Omitted registers the same bundle after `onReady` only when neither `options.tools` is
|
|
126
|
+
set nor any tool was registered by then, then activates `tool`, `defaultTool` or `select`; hosts
|
|
127
|
+
that register tools in `onReady` get no defaults. `tools={[]}` registers nothing.
|
|
91
128
|
|
|
92
129
|
Runtime changes beyond these go through the viewport — access it via `useViewport()` or the ref. Grid snapping is domain behavior; VTT consumers should configure `GridController` and its constraint service rather than adopting the deprecated React prop. The background pattern is constructor-only (`options.background`).
|
|
93
130
|
|
|
@@ -352,11 +389,11 @@ export class StampTool implements Tool {
|
|
|
352
389
|
Register alongside the built-in tools:
|
|
353
390
|
|
|
354
391
|
```tsx
|
|
355
|
-
import {
|
|
392
|
+
import { createDefaultTools } from '@fieldnotes/core';
|
|
356
393
|
import { StampTool } from './StampTool';
|
|
357
394
|
|
|
358
395
|
// Hoisted — stable reference across renders
|
|
359
|
-
const TOOLS = [
|
|
396
|
+
const TOOLS = [...createDefaultTools(), new StampTool()];
|
|
360
397
|
|
|
361
398
|
function App() {
|
|
362
399
|
const [tool, setTool] = useState('select');
|
package/dist/index.cjs
CHANGED
|
@@ -56,6 +56,9 @@ var FieldNotesCanvas = (0, import_react2.forwardRef)(
|
|
|
56
56
|
defaultTool,
|
|
57
57
|
tool,
|
|
58
58
|
onToolChange,
|
|
59
|
+
initialData,
|
|
60
|
+
onChange,
|
|
61
|
+
changeDebounceMs = 250,
|
|
59
62
|
snapToGrid,
|
|
60
63
|
className,
|
|
61
64
|
style,
|
|
@@ -64,23 +67,65 @@ var FieldNotesCanvas = (0, import_react2.forwardRef)(
|
|
|
64
67
|
}, ref) {
|
|
65
68
|
const containerRef = (0, import_react2.useRef)(null);
|
|
66
69
|
const [viewport, setViewport] = (0, import_react2.useState)(null);
|
|
70
|
+
const changeRef = (0, import_react2.useRef)({ onChange, changeDebounceMs });
|
|
71
|
+
changeRef.current = { onChange, changeDebounceMs };
|
|
67
72
|
(0, import_react2.useImperativeHandle)(ref, () => ({ viewport }), [viewport]);
|
|
68
73
|
(0, import_react2.useEffect)(() => {
|
|
69
74
|
const el = containerRef.current;
|
|
70
75
|
if (!el) return;
|
|
71
76
|
const vp = new import_core.Viewport(el, options);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
const activate = (fallback) => {
|
|
78
|
+
const name = tool ?? defaultTool ?? fallback;
|
|
79
|
+
if (name) vp.setTool(name);
|
|
80
|
+
};
|
|
81
|
+
const deferDefaults = tools === void 0 && options?.tools === void 0;
|
|
82
|
+
for (const t of tools === "default" ? (0, import_core.createDefaultTools)() : tools ?? []) {
|
|
83
|
+
vp.toolManager.register(t);
|
|
76
84
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
vp.
|
|
85
|
+
if (!deferDefaults) activate(tools === "default" ? "select" : void 0);
|
|
86
|
+
try {
|
|
87
|
+
if (typeof initialData === "string") vp.loadJSON(initialData);
|
|
88
|
+
else if (initialData) vp.loadState(initialData);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
vp.destroy();
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
let timer;
|
|
94
|
+
const emit = () => {
|
|
95
|
+
timer = void 0;
|
|
96
|
+
changeRef.current.onChange?.(vp.exportState());
|
|
97
|
+
};
|
|
98
|
+
const unsubscribe = vp.onStateChange(() => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
timer = setTimeout(emit, changeRef.current.changeDebounceMs);
|
|
101
|
+
});
|
|
102
|
+
try {
|
|
103
|
+
onReady?.(vp);
|
|
104
|
+
if (deferDefaults) {
|
|
105
|
+
const registerDefaults = vp.toolManager.toolNames.length === 0;
|
|
106
|
+
if (registerDefaults) {
|
|
107
|
+
for (const t of (0, import_core.createDefaultTools)()) vp.toolManager.register(t);
|
|
108
|
+
}
|
|
109
|
+
activate(registerDefaults ? "select" : void 0);
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
unsubscribe();
|
|
114
|
+
vp.destroy();
|
|
115
|
+
throw error;
|
|
80
116
|
}
|
|
81
117
|
setViewport(vp);
|
|
82
|
-
onReady?.(vp);
|
|
83
118
|
return () => {
|
|
119
|
+
if (timer !== void 0) {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
try {
|
|
122
|
+
emit();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (typeof reportError === "function") reportError(error);
|
|
125
|
+
else console.error("[fieldnotes] onChange failed during unmount", error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
unsubscribe();
|
|
84
129
|
vp.destroy();
|
|
85
130
|
setViewport(null);
|
|
86
131
|
};
|
|
@@ -100,7 +145,7 @@ var FieldNotesCanvas = (0, import_react2.forwardRef)(
|
|
|
100
145
|
viewport.setSnapToGrid(snapToGrid);
|
|
101
146
|
}, [viewport, snapToGrid]);
|
|
102
147
|
(0, import_react2.useEffect)(() => {
|
|
103
|
-
if (!viewport || !tools) return;
|
|
148
|
+
if (!viewport || !Array.isArray(tools)) return;
|
|
104
149
|
for (const t of tools) {
|
|
105
150
|
if (!viewport.toolManager.getTool(t.name)) {
|
|
106
151
|
viewport.toolManager.register(t);
|
|
@@ -148,12 +193,11 @@ function CanvasElement({ position, size, children }) {
|
|
|
148
193
|
};
|
|
149
194
|
elementIdRef.current = register();
|
|
150
195
|
setPortalTarget(container);
|
|
151
|
-
let disposed = false;
|
|
152
196
|
const handleStoreReplacement = () => {
|
|
153
197
|
const clearedId = elementIdRef.current;
|
|
154
198
|
if (clearedId === null) return;
|
|
155
199
|
queueMicrotask(() => {
|
|
156
|
-
if (
|
|
200
|
+
if (elementIdRef.current !== clearedId) return;
|
|
157
201
|
if (viewport.store.getById(clearedId)?.type === "html") {
|
|
158
202
|
viewport.updateHtmlElement(clearedId, container);
|
|
159
203
|
} else {
|
|
@@ -165,7 +209,6 @@ function CanvasElement({ position, size, children }) {
|
|
|
165
209
|
const unsubscribeClear = viewport.store.on("clear", handleStoreReplacement);
|
|
166
210
|
const unsubscribeBatch = viewport.store.on("batch", handleStoreReplacement);
|
|
167
211
|
return () => {
|
|
168
|
-
disposed = true;
|
|
169
212
|
unsubscribeClear();
|
|
170
213
|
unsubscribeBatch();
|
|
171
214
|
if (elementIdRef.current) {
|
|
@@ -190,65 +233,40 @@ function CanvasElement({ position, size, children }) {
|
|
|
190
233
|
|
|
191
234
|
// src/hooks/use-active-tool.ts
|
|
192
235
|
var import_react5 = require("react");
|
|
236
|
+
var import_core2 = require("@fieldnotes/core");
|
|
193
237
|
function useActiveTool() {
|
|
194
238
|
const viewport = useViewport();
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
[viewport]
|
|
198
|
-
);
|
|
199
|
-
const getSnapshot = (0, import_react5.useCallback)(() => viewport.toolManager.activeTool?.name ?? "", [viewport]);
|
|
200
|
-
const toolName = (0, import_react5.useSyncExternalStore)(subscribe, getSnapshot);
|
|
239
|
+
const source = (0, import_react5.useMemo)(() => (0, import_core2.createActiveToolSource)(viewport), [viewport]);
|
|
240
|
+
const toolName = (0, import_react5.useSyncExternalStore)(source.subscribe, source.getSnapshot);
|
|
201
241
|
const setTool = (0, import_react5.useCallback)((name) => viewport.setTool(name), [viewport]);
|
|
202
242
|
return [toolName, setTool];
|
|
203
243
|
}
|
|
204
244
|
|
|
205
245
|
// src/hooks/use-camera.ts
|
|
206
246
|
var import_react6 = require("react");
|
|
247
|
+
var import_core3 = require("@fieldnotes/core");
|
|
207
248
|
function useCamera() {
|
|
208
249
|
const viewport = useViewport();
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
(onStoreChange) => viewport.camera.onChange(onStoreChange),
|
|
212
|
-
[viewport]
|
|
213
|
-
);
|
|
214
|
-
const getSnapshot = (0, import_react6.useCallback)(() => {
|
|
215
|
-
const { position, zoom } = viewport.camera;
|
|
216
|
-
const cached = cachedRef.current;
|
|
217
|
-
if (cached.x === position.x && cached.y === position.y && cached.zoom === zoom) {
|
|
218
|
-
return cached;
|
|
219
|
-
}
|
|
220
|
-
const next = { x: position.x, y: position.y, zoom };
|
|
221
|
-
cachedRef.current = next;
|
|
222
|
-
return next;
|
|
223
|
-
}, [viewport]);
|
|
224
|
-
return (0, import_react6.useSyncExternalStore)(subscribe, getSnapshot);
|
|
250
|
+
const source = (0, import_react6.useMemo)(() => (0, import_core3.createCameraSource)(viewport), [viewport]);
|
|
251
|
+
return (0, import_react6.useSyncExternalStore)(source.subscribe, source.getSnapshot);
|
|
225
252
|
}
|
|
226
253
|
|
|
227
254
|
// src/hooks/use-tool-options.ts
|
|
228
255
|
var import_react7 = require("react");
|
|
229
|
-
|
|
230
|
-
}
|
|
256
|
+
var import_core4 = require("@fieldnotes/core");
|
|
231
257
|
function useToolOptions(toolName) {
|
|
232
258
|
const viewport = useViewport();
|
|
233
|
-
const
|
|
259
|
+
const nameRef = (0, import_react7.useRef)(toolName);
|
|
260
|
+
nameRef.current = toolName;
|
|
261
|
+
const source = (0, import_react7.useMemo)(
|
|
262
|
+
() => (0, import_core4.createToolOptionsSource)(viewport, () => nameRef.current),
|
|
263
|
+
[viewport]
|
|
264
|
+
);
|
|
234
265
|
const subscribe = (0, import_react7.useCallback)(
|
|
235
|
-
(onStoreChange) =>
|
|
236
|
-
|
|
237
|
-
if (!tool?.onOptionsChange) return noop;
|
|
238
|
-
return tool.onOptionsChange(onStoreChange);
|
|
239
|
-
},
|
|
240
|
-
[viewport, toolName]
|
|
266
|
+
(onStoreChange) => source.subscribe(onStoreChange),
|
|
267
|
+
[source, toolName]
|
|
241
268
|
);
|
|
242
|
-
const
|
|
243
|
-
const tool = viewport.toolManager.getTool(toolName);
|
|
244
|
-
if (!tool?.getOptions) return null;
|
|
245
|
-
const next = tool.getOptions();
|
|
246
|
-
const cached = cachedRef.current;
|
|
247
|
-
if (cached && shallowEqual(cached, next)) return cached;
|
|
248
|
-
cachedRef.current = next;
|
|
249
|
-
return next;
|
|
250
|
-
}, [viewport, toolName]);
|
|
251
|
-
const options = (0, import_react7.useSyncExternalStore)(subscribe, getSnapshot);
|
|
269
|
+
const options = (0, import_react7.useSyncExternalStore)(subscribe, source.getSnapshot);
|
|
252
270
|
const setOptions = (0, import_react7.useCallback)(
|
|
253
271
|
(partial) => {
|
|
254
272
|
const tool = viewport.toolManager.getTool(toolName);
|
|
@@ -258,51 +276,14 @@ function useToolOptions(toolName) {
|
|
|
258
276
|
);
|
|
259
277
|
return [options, setOptions];
|
|
260
278
|
}
|
|
261
|
-
function shallowEqual(a, b) {
|
|
262
|
-
const keysA = Object.keys(a);
|
|
263
|
-
const keysB = Object.keys(b);
|
|
264
|
-
if (keysA.length !== keysB.length) return false;
|
|
265
|
-
for (const key of keysA) {
|
|
266
|
-
const valA = a[key];
|
|
267
|
-
const valB = b[key];
|
|
268
|
-
if (valA === valB) continue;
|
|
269
|
-
if (typeof valA === "object" && valA !== null && typeof valB === "object" && valB !== null && !Array.isArray(valA) && !Array.isArray(valB)) {
|
|
270
|
-
const objA = valA;
|
|
271
|
-
const objB = valB;
|
|
272
|
-
const innerKeys = Object.keys(objA);
|
|
273
|
-
if (innerKeys.length === Object.keys(objB).length && innerKeys.every((k) => objA[k] === objB[k])) {
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
279
|
|
|
282
280
|
// src/hooks/use-layers.ts
|
|
283
281
|
var import_react8 = require("react");
|
|
282
|
+
var import_core5 = require("@fieldnotes/core");
|
|
284
283
|
function useLayers() {
|
|
285
284
|
const viewport = useViewport();
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
(onStoreChange) => viewport.layerManager.on("change", onStoreChange),
|
|
289
|
-
[viewport]
|
|
290
|
-
);
|
|
291
|
-
const getSnapshot = (0, import_react8.useCallback)(() => {
|
|
292
|
-
const layers = viewport.layerManager.getLayers();
|
|
293
|
-
const activeLayerId = viewport.layerManager.activeLayerId;
|
|
294
|
-
const cached = cachedRef.current;
|
|
295
|
-
if (cached.activeLayerId === activeLayerId && cached.layers.length === layers.length && cached.layers.every((l, i) => {
|
|
296
|
-
const next2 = layers[i];
|
|
297
|
-
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;
|
|
298
|
-
})) {
|
|
299
|
-
return cached;
|
|
300
|
-
}
|
|
301
|
-
const next = { layers, activeLayerId };
|
|
302
|
-
cachedRef.current = next;
|
|
303
|
-
return next;
|
|
304
|
-
}, [viewport]);
|
|
305
|
-
const snapshot = (0, import_react8.useSyncExternalStore)(subscribe, getSnapshot);
|
|
285
|
+
const source = (0, import_react8.useMemo)(() => (0, import_core5.createLayersSource)(viewport), [viewport]);
|
|
286
|
+
const snapshot = (0, import_react8.useSyncExternalStore)(source.subscribe, source.getSnapshot);
|
|
306
287
|
const createLayer = (0, import_react8.useCallback)(
|
|
307
288
|
(name) => viewport.layerManager.createLayer(name),
|
|
308
289
|
[viewport]
|
|
@@ -356,25 +337,11 @@ function useLayers() {
|
|
|
356
337
|
|
|
357
338
|
// src/hooks/use-history.ts
|
|
358
339
|
var import_react9 = require("react");
|
|
340
|
+
var import_core6 = require("@fieldnotes/core");
|
|
359
341
|
function useHistory() {
|
|
360
342
|
const viewport = useViewport();
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
(onStoreChange) => viewport.history.onChange(onStoreChange),
|
|
364
|
-
[viewport]
|
|
365
|
-
);
|
|
366
|
-
const getSnapshot = (0, import_react9.useCallback)(() => {
|
|
367
|
-
const canUndo = viewport.history.canUndo;
|
|
368
|
-
const canRedo = viewport.history.canRedo;
|
|
369
|
-
const cached = cachedRef.current;
|
|
370
|
-
if (cached.canUndo === canUndo && cached.canRedo === canRedo) {
|
|
371
|
-
return cached;
|
|
372
|
-
}
|
|
373
|
-
const next = { canUndo, canRedo };
|
|
374
|
-
cachedRef.current = next;
|
|
375
|
-
return next;
|
|
376
|
-
}, [viewport]);
|
|
377
|
-
const snapshot = (0, import_react9.useSyncExternalStore)(subscribe, getSnapshot);
|
|
343
|
+
const source = (0, import_react9.useMemo)(() => (0, import_core6.createHistorySource)(viewport), [viewport]);
|
|
344
|
+
const snapshot = (0, import_react9.useSyncExternalStore)(source.subscribe, source.getSnapshot);
|
|
378
345
|
const undo = (0, import_react9.useCallback)(() => viewport.undo(), [viewport]);
|
|
379
346
|
const redo = (0, import_react9.useCallback)(() => viewport.redo(), [viewport]);
|
|
380
347
|
return {
|
|
@@ -387,186 +354,69 @@ function useHistory() {
|
|
|
387
354
|
|
|
388
355
|
// src/hooks/use-elements.ts
|
|
389
356
|
var import_react10 = require("react");
|
|
390
|
-
|
|
391
|
-
if (Object.is(a, b)) return true;
|
|
392
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
393
|
-
return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
|
|
394
|
-
}
|
|
395
|
-
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
|
|
396
|
-
const ka = Object.keys(a);
|
|
397
|
-
const kb = Object.keys(b);
|
|
398
|
-
return ka.length === kb.length && ka.every(
|
|
399
|
-
(k) => k in b && Object.is(a[k], b[k])
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
return false;
|
|
403
|
-
}
|
|
357
|
+
var import_core7 = require("@fieldnotes/core");
|
|
404
358
|
function useElements(arg, isEqual) {
|
|
405
359
|
const viewport = useViewport();
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
(
|
|
360
|
+
const latest = (0, import_react10.useRef)({ arg, isEqual });
|
|
361
|
+
latest.current = { arg, isEqual };
|
|
362
|
+
const elementsSource = (0, import_react10.useMemo)(
|
|
363
|
+
() => (0, import_core7.createElementsSource)(viewport, () => {
|
|
364
|
+
const a = latest.current.arg;
|
|
365
|
+
return typeof a === "function" ? void 0 : a;
|
|
366
|
+
}),
|
|
410
367
|
[viewport]
|
|
411
368
|
);
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const next = arg ? viewport.store.getElementsByType(arg) : viewport.store.getAll();
|
|
424
|
-
const cached = cachedRef.current;
|
|
425
|
-
if (cached.length === next.length && cached.every((el, i) => el === next[i])) {
|
|
426
|
-
return cached;
|
|
427
|
-
}
|
|
428
|
-
cachedRef.current = next;
|
|
429
|
-
return next;
|
|
430
|
-
}, [viewport, arg, isEqual]);
|
|
431
|
-
return (0, import_react10.useSyncExternalStore)(subscribe, getSnapshot);
|
|
369
|
+
const selectorSource = (0, import_react10.useMemo)(
|
|
370
|
+
() => (0, import_core7.createElementsSelectorSource)(
|
|
371
|
+
viewport,
|
|
372
|
+
// Only read while `arg` is a selector: the source is picked below by `typeof arg`.
|
|
373
|
+
() => latest.current.arg,
|
|
374
|
+
() => latest.current.isEqual
|
|
375
|
+
),
|
|
376
|
+
[viewport]
|
|
377
|
+
);
|
|
378
|
+
const source = typeof arg === "function" ? selectorSource : elementsSource;
|
|
379
|
+
return (0, import_react10.useSyncExternalStore)(source.subscribe, source.getSnapshot);
|
|
432
380
|
}
|
|
433
381
|
|
|
434
382
|
// src/hooks/use-selection.ts
|
|
435
383
|
var import_react11 = require("react");
|
|
384
|
+
var import_core8 = require("@fieldnotes/core");
|
|
436
385
|
function useSelection() {
|
|
437
386
|
const viewport = useViewport();
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
[viewport]
|
|
441
|
-
);
|
|
442
|
-
const getSnapshot = (0, import_react11.useCallback)(() => viewport.getSelectedIds(), [viewport]);
|
|
443
|
-
return (0, import_react11.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
387
|
+
const source = (0, import_react11.useMemo)(() => (0, import_core8.createSelectionSource)(viewport), [viewport]);
|
|
388
|
+
return (0, import_react11.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
444
389
|
}
|
|
445
390
|
|
|
446
391
|
// src/hooks/use-selection-style.ts
|
|
447
392
|
var import_react12 = require("react");
|
|
448
|
-
|
|
449
|
-
// src/hooks/style-keys.ts
|
|
450
|
-
var STYLE_KEYS = [
|
|
451
|
-
"color",
|
|
452
|
-
"fillColor",
|
|
453
|
-
"strokeWidth",
|
|
454
|
-
"opacity",
|
|
455
|
-
"fontSize",
|
|
456
|
-
"strokeStyle"
|
|
457
|
-
];
|
|
458
|
-
|
|
459
|
-
// src/hooks/use-selection-style.ts
|
|
460
|
-
function styleEqual(a, b) {
|
|
461
|
-
if (a === b) return true;
|
|
462
|
-
if (a === null || b === null) return false;
|
|
463
|
-
return STYLE_KEYS.every((k) => a[k] === b[k]);
|
|
464
|
-
}
|
|
393
|
+
var import_core9 = require("@fieldnotes/core");
|
|
465
394
|
function useSelectionStyle() {
|
|
466
395
|
const viewport = useViewport();
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
(onChange) => {
|
|
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 = (0, import_react12.useCallback)(() => {
|
|
480
|
-
const next = viewport.getSelectionStyle();
|
|
481
|
-
if (styleEqual(cacheRef.current, next)) return cacheRef.current;
|
|
482
|
-
cacheRef.current = next;
|
|
483
|
-
return next;
|
|
484
|
-
}, [viewport]);
|
|
485
|
-
const style = (0, import_react12.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
396
|
+
const source = (0, import_react12.useMemo)(() => (0, import_core9.createSelectionStyleSource)(viewport), [viewport]);
|
|
397
|
+
const style = (0, import_react12.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
486
398
|
const apply = (0, import_react12.useCallback)((s) => viewport.applyStyleToSelection(s), [viewport]);
|
|
487
399
|
return [style, apply];
|
|
488
400
|
}
|
|
489
401
|
|
|
490
402
|
// src/hooks/use-selection-style-details.ts
|
|
491
403
|
var import_react13 = require("react");
|
|
492
|
-
|
|
493
|
-
return a.length === b.length && a.every((k, i) => k === b[i]);
|
|
494
|
-
}
|
|
495
|
-
function detailsEqual(a, b) {
|
|
496
|
-
if (a === b) return true;
|
|
497
|
-
if (a === null || b === null) return false;
|
|
498
|
-
return keysEqual(a.applicable, b.applicable) && keysEqual(a.mixed, b.mixed) && STYLE_KEYS.every((k) => a.common[k] === b.common[k]);
|
|
499
|
-
}
|
|
404
|
+
var import_core10 = require("@fieldnotes/core");
|
|
500
405
|
function useSelectionStyleDetails() {
|
|
501
406
|
const viewport = useViewport();
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
(onChange) => {
|
|
505
|
-
const offSel = viewport.onSelectionChange(onChange);
|
|
506
|
-
const offStore = viewport.store.onChange(onChange);
|
|
507
|
-
return () => {
|
|
508
|
-
offSel();
|
|
509
|
-
offStore();
|
|
510
|
-
};
|
|
511
|
-
},
|
|
512
|
-
[viewport]
|
|
513
|
-
);
|
|
514
|
-
const getSnapshot = (0, import_react13.useCallback)(() => {
|
|
515
|
-
const next = viewport.getSelectionStyleDetails();
|
|
516
|
-
if (detailsEqual(cacheRef.current, next)) return cacheRef.current;
|
|
517
|
-
cacheRef.current = next;
|
|
518
|
-
return next;
|
|
519
|
-
}, [viewport]);
|
|
520
|
-
const details = (0, import_react13.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
407
|
+
const source = (0, import_react13.useMemo)(() => (0, import_core10.createSelectionStyleDetailsSource)(viewport), [viewport]);
|
|
408
|
+
const details = (0, import_react13.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
521
409
|
const apply = (0, import_react13.useCallback)((s) => viewport.applyStyleToSelection(s), [viewport]);
|
|
522
410
|
return [details, apply];
|
|
523
411
|
}
|
|
524
412
|
|
|
525
413
|
// src/hooks/use-selection-ops.ts
|
|
526
414
|
var import_react14 = require("react");
|
|
527
|
-
|
|
528
|
-
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;
|
|
529
|
-
}
|
|
415
|
+
var import_core11 = require("@fieldnotes/core");
|
|
530
416
|
function useSelectionOps() {
|
|
531
417
|
const viewport = useViewport();
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
(onChange) => {
|
|
535
|
-
const offSel = viewport.onSelectionChange(onChange);
|
|
536
|
-
const offStore = viewport.store.onChange(onChange);
|
|
537
|
-
return () => {
|
|
538
|
-
offSel();
|
|
539
|
-
offStore();
|
|
540
|
-
};
|
|
541
|
-
},
|
|
542
|
-
[viewport]
|
|
543
|
-
);
|
|
544
|
-
const getSnapshot = (0, import_react14.useCallback)(() => {
|
|
545
|
-
const ids = viewport.getSelectedIds();
|
|
546
|
-
const n = ids.length;
|
|
547
|
-
let canUngroup = false;
|
|
548
|
-
let lockedCount = 0;
|
|
549
|
-
for (const id of ids) {
|
|
550
|
-
const el = viewport.store.getById(id);
|
|
551
|
-
if (!el) continue;
|
|
552
|
-
if (el.groupId !== void 0) canUngroup = true;
|
|
553
|
-
if (el.locked) lockedCount++;
|
|
554
|
-
}
|
|
555
|
-
const isLocked = n === 0 ? null : lockedCount === n ? true : lockedCount === 0 ? false : null;
|
|
556
|
-
const next = {
|
|
557
|
-
selectedIds: ids,
|
|
558
|
-
canGroup: n >= 2,
|
|
559
|
-
canUngroup,
|
|
560
|
-
canAlign: n >= 2,
|
|
561
|
-
canDistribute: n >= 3,
|
|
562
|
-
isLocked
|
|
563
|
-
};
|
|
564
|
-
const prev = cacheRef.current;
|
|
565
|
-
if (prev && snapshotEqual(prev, next)) return prev;
|
|
566
|
-
cacheRef.current = next;
|
|
567
|
-
return next;
|
|
568
|
-
}, [viewport]);
|
|
569
|
-
const snap = (0, import_react14.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
418
|
+
const source = (0, import_react14.useMemo)(() => (0, import_core11.createSelectionOpsSource)(viewport), [viewport]);
|
|
419
|
+
const snap = (0, import_react14.useSyncExternalStore)(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
570
420
|
const group = (0, import_react14.useCallback)(() => viewport.groupSelection(), [viewport]);
|
|
571
421
|
const ungroup = (0, import_react14.useCallback)(() => viewport.ungroupSelection(), [viewport]);
|
|
572
422
|
const toggleLock = (0, import_react14.useCallback)(() => viewport.toggleLockSelection(), [viewport]);
|
|
@@ -597,7 +447,7 @@ function useSelectionOps() {
|
|
|
597
447
|
|
|
598
448
|
// src/hooks/use-element-rects.ts
|
|
599
449
|
var import_react15 = require("react");
|
|
600
|
-
var
|
|
450
|
+
var import_core12 = require("@fieldnotes/core");
|
|
601
451
|
function useElementRects(match) {
|
|
602
452
|
const viewport = useViewport();
|
|
603
453
|
const matchRef = (0, import_react15.useRef)(match);
|
|
@@ -607,11 +457,11 @@ function useElementRects(match) {
|
|
|
607
457
|
const trackerRef = (0, import_react15.useRef)(null);
|
|
608
458
|
const subscribe = (0, import_react15.useCallback)(
|
|
609
459
|
(onStoreChange) => {
|
|
610
|
-
const tracker = new
|
|
460
|
+
const tracker = new import_core12.ElementRectTracker(viewport, { match: stableMatch });
|
|
611
461
|
trackerRef.current = tracker;
|
|
612
462
|
const next = tracker.getRects();
|
|
613
463
|
const previous = cacheRef.current;
|
|
614
|
-
if (previous === null || !(0,
|
|
464
|
+
if (previous === null || !(0, import_core12.elementRectsEqual)(previous, next)) {
|
|
615
465
|
cacheRef.current = next;
|
|
616
466
|
onStoreChange();
|
|
617
467
|
}
|
|
@@ -629,7 +479,7 @@ function useElementRects(match) {
|
|
|
629
479
|
);
|
|
630
480
|
const getSnapshot = (0, import_react15.useCallback)(() => {
|
|
631
481
|
if (cacheRef.current === null) {
|
|
632
|
-
cacheRef.current = (0,
|
|
482
|
+
cacheRef.current = (0, import_core12.computeElementRects)(viewport.store, stableMatch);
|
|
633
483
|
}
|
|
634
484
|
return cacheRef.current;
|
|
635
485
|
}, [viewport, stableMatch]);
|
|
@@ -642,7 +492,7 @@ function useElementRects(match) {
|
|
|
642
492
|
|
|
643
493
|
// src/minimap.tsx
|
|
644
494
|
var import_react16 = require("react");
|
|
645
|
-
var
|
|
495
|
+
var import_core13 = require("@fieldnotes/core");
|
|
646
496
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
647
497
|
var toggleStyle = {
|
|
648
498
|
position: "absolute",
|
|
@@ -674,7 +524,7 @@ function Minimap({
|
|
|
674
524
|
if (collapsed) return;
|
|
675
525
|
const canvas = canvasRef.current;
|
|
676
526
|
if (!canvas) return;
|
|
677
|
-
const controller = new
|
|
527
|
+
const controller = new import_core13.MinimapController(viewport, canvas, {
|
|
678
528
|
width,
|
|
679
529
|
height,
|
|
680
530
|
background,
|