@dxos/react-ui-canvas 0.9.1-staging.ee54ba693a → 0.11.0
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/lib/index.mjs +1312 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/types/src/components/CellGrid/input/pointer.d.ts +1 -1
- package/dist/types/src/components/CellGrid/input/pointer.d.ts.map +1 -1
- package/dist/types/src/components/CellGrid/input/wheel.d.ts +1 -1
- package/dist/types/src/components/CellGrid/input/wheel.d.ts.map +1 -1
- package/dist/types/src/components/CellGrid/state/atoms.d.ts +1 -1
- package/dist/types/src/components/CellGrid/state/atoms.d.ts.map +1 -1
- package/dist/types/src/hooks/useCanvasContext.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/svg-path.d.ts +4 -0
- package/dist/types/src/util/svg-path.d.ts.map +1 -0
- package/dist/types/src/util/svg.d.ts +0 -1
- package/dist/types/src/util/svg.d.ts.map +1 -1
- package/dist/types/src/util/svg.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -14
- package/src/components/CellGrid/CellGrid.tsx +2 -2
- package/src/components/CellGrid/headers/Ruler.tsx +1 -1
- package/src/components/CellGrid/input/pointer.ts +1 -1
- package/src/components/CellGrid/input/wheel.ts +1 -1
- package/src/components/CellGrid/state/atoms.ts +1 -1
- package/src/hooks/{useCanvasContext.tsx → useCanvasContext.ts} +3 -0
- package/src/util/index.ts +1 -0
- package/src/util/svg-path.ts +13 -0
- package/src/util/svg.stories.tsx +2 -1
- package/src/util/svg.tsx +1 -4
- package/dist/lib/browser/index.mjs +0 -1451
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/node-esm/index.mjs +0 -1453
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
|
@@ -0,0 +1,1312 @@
|
|
|
1
|
+
import { createContext, forwardRef, useContext, useEffect, useId, useImperativeHandle, useMemo, useReducer, useRef, useState } from "react";
|
|
2
|
+
import { useResizeDetector } from "react-resize-detector";
|
|
3
|
+
import { mx } from "@dxos/ui-theme";
|
|
4
|
+
import { easeSinOut, interpolate, interpolateObject, transition } from "d3";
|
|
5
|
+
import { applyToPoints, compose, identity, inverse, scale, translate } from "transformation-matrix";
|
|
6
|
+
import { raise } from "@dxos/debug";
|
|
7
|
+
import { bind, bindAll } from "bind-event-listener";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { RegistryContext } from "@effect-atom/atom-react";
|
|
10
|
+
import { Atom } from "@effect-atom/atom";
|
|
11
|
+
import { useForwardedRef } from "@dxos/react-ui";
|
|
12
|
+
import * as Schema from "effect/Schema";
|
|
13
|
+
//#region src/hooks/projection.tsx
|
|
14
|
+
var defaultOrigin = {
|
|
15
|
+
x: 0,
|
|
16
|
+
y: 0
|
|
17
|
+
};
|
|
18
|
+
var ProjectionMapper = class {
|
|
19
|
+
_bounds = {
|
|
20
|
+
width: 0,
|
|
21
|
+
height: 0
|
|
22
|
+
};
|
|
23
|
+
_scale = 1;
|
|
24
|
+
_offset = defaultOrigin;
|
|
25
|
+
_toScreen = identity();
|
|
26
|
+
_toModel = identity();
|
|
27
|
+
constructor(bounds, scale, offset) {
|
|
28
|
+
if (bounds && scale && offset) this.update(bounds, scale, offset);
|
|
29
|
+
}
|
|
30
|
+
update(bounds, scale$1, offset) {
|
|
31
|
+
this._bounds = bounds;
|
|
32
|
+
this._scale = scale$1;
|
|
33
|
+
this._offset = offset;
|
|
34
|
+
this._toScreen = compose(translate(this._offset.x, this._offset.y), scale(this._scale));
|
|
35
|
+
this._toModel = inverse(this._toScreen);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
get bounds() {
|
|
39
|
+
return this._bounds;
|
|
40
|
+
}
|
|
41
|
+
get scale() {
|
|
42
|
+
return this._scale;
|
|
43
|
+
}
|
|
44
|
+
get offset() {
|
|
45
|
+
return this._offset;
|
|
46
|
+
}
|
|
47
|
+
toScreen(points) {
|
|
48
|
+
return applyToPoints(this._toScreen, points);
|
|
49
|
+
}
|
|
50
|
+
toModel(points) {
|
|
51
|
+
return applyToPoints(this._toModel, points);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Maintain position while zooming.
|
|
56
|
+
*/
|
|
57
|
+
var getZoomTransform = ({ scale, offset, pos, newScale }) => {
|
|
58
|
+
return {
|
|
59
|
+
scale: newScale,
|
|
60
|
+
offset: {
|
|
61
|
+
x: pos.x - (pos.x - offset.x) * (newScale / scale),
|
|
62
|
+
y: pos.y - (pos.y - offset.y) * (newScale / scale)
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Zoom while keeping the specified position in place.
|
|
68
|
+
*/
|
|
69
|
+
var zoomInPlace = (setTransform, pos, offset, current, next, delay = 200) => {
|
|
70
|
+
const is = interpolate(current, next);
|
|
71
|
+
transition().ease(easeSinOut).duration(delay).tween("zoom", () => (t) => {
|
|
72
|
+
setTransform(getZoomTransform({
|
|
73
|
+
scale: current,
|
|
74
|
+
newScale: is(t),
|
|
75
|
+
offset,
|
|
76
|
+
pos
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
var noop = () => {};
|
|
81
|
+
/**
|
|
82
|
+
* Zoom to new scale and position.
|
|
83
|
+
*/
|
|
84
|
+
var zoomTo = (setTransform, current, next, delay = 200, cb = noop) => {
|
|
85
|
+
const is = interpolateObject({
|
|
86
|
+
scale: current.scale,
|
|
87
|
+
...current.offset
|
|
88
|
+
}, {
|
|
89
|
+
scale: next.scale,
|
|
90
|
+
...next.offset
|
|
91
|
+
});
|
|
92
|
+
transition().ease(easeSinOut).duration(delay).tween("zoom", () => (t) => {
|
|
93
|
+
const { scale, x, y } = is(t);
|
|
94
|
+
setTransform({
|
|
95
|
+
scale,
|
|
96
|
+
offset: {
|
|
97
|
+
x,
|
|
98
|
+
y
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}).on("end", cb);
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/hooks/useCanvasContext.ts
|
|
105
|
+
/**
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
var CanvasContext = createContext(null);
|
|
109
|
+
var useCanvasContext = () => {
|
|
110
|
+
return useContext(CanvasContext) ?? raise(/* @__PURE__ */ new Error("Missing CanvasContext"));
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/hooks/useDrag.tsx
|
|
114
|
+
/**
|
|
115
|
+
* Handle drag events to update the transform state (offset).
|
|
116
|
+
*/
|
|
117
|
+
var useDrag = (_options = {}) => {
|
|
118
|
+
const { root, setProjection } = useCanvasContext();
|
|
119
|
+
const state = useRef({
|
|
120
|
+
panning: false,
|
|
121
|
+
x: 0,
|
|
122
|
+
y: 0
|
|
123
|
+
});
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!root) return;
|
|
126
|
+
return bind(root, {
|
|
127
|
+
type: "pointerdown",
|
|
128
|
+
listener: (ev) => {
|
|
129
|
+
if (ev.button !== 0) return;
|
|
130
|
+
if (ev.defaultPrevented) return;
|
|
131
|
+
if (ev.target !== root || ev.shiftKey) return;
|
|
132
|
+
ev.preventDefault();
|
|
133
|
+
root.setPointerCapture(ev.pointerId);
|
|
134
|
+
state.current = {
|
|
135
|
+
panning: true,
|
|
136
|
+
x: ev.clientX,
|
|
137
|
+
y: ev.clientY
|
|
138
|
+
};
|
|
139
|
+
const moveUnbind = bind(root, {
|
|
140
|
+
type: "pointermove",
|
|
141
|
+
listener: (ev) => {
|
|
142
|
+
if (!state.current.panning) return;
|
|
143
|
+
const dx = ev.clientX - state.current.x;
|
|
144
|
+
const dy = ev.clientY - state.current.y;
|
|
145
|
+
state.current.x = ev.clientX;
|
|
146
|
+
state.current.y = ev.clientY;
|
|
147
|
+
setProjection((prev) => ({
|
|
148
|
+
...prev,
|
|
149
|
+
offset: {
|
|
150
|
+
x: prev.offset.x + dx,
|
|
151
|
+
y: prev.offset.y + dy
|
|
152
|
+
}
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const upUnbind = bind(root, {
|
|
157
|
+
type: "pointerup",
|
|
158
|
+
listener: (ev) => {
|
|
159
|
+
state.current.panning = false;
|
|
160
|
+
root.releasePointerCapture(ev.pointerId);
|
|
161
|
+
moveUnbind();
|
|
162
|
+
upUnbind();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}, [root]);
|
|
168
|
+
};
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/util/svg-path.ts
|
|
171
|
+
/** https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths */
|
|
172
|
+
var createPath = (points, join = false) => {
|
|
173
|
+
return [
|
|
174
|
+
"M",
|
|
175
|
+
points.map(({ x, y }) => `${x},${y}`).join(" L "),
|
|
176
|
+
join ? "Z" : ""
|
|
177
|
+
].join(" ");
|
|
178
|
+
};
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/util/svg.tsx
|
|
181
|
+
/**
|
|
182
|
+
* https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
|
|
183
|
+
* NOTE: Leave space around shape for line width.
|
|
184
|
+
*/
|
|
185
|
+
var Markers = ({ id = "dx-marker", classNames }) => {
|
|
186
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
187
|
+
/* @__PURE__ */ jsx(Arrow, {
|
|
188
|
+
id: `${id}-arrow-start`,
|
|
189
|
+
dir: "start",
|
|
190
|
+
classNames
|
|
191
|
+
}),
|
|
192
|
+
/* @__PURE__ */ jsx(Arrow, {
|
|
193
|
+
id: `${id}-arrow-end`,
|
|
194
|
+
dir: "end",
|
|
195
|
+
classNames
|
|
196
|
+
}),
|
|
197
|
+
/* @__PURE__ */ jsx(Arrow, {
|
|
198
|
+
id: `${id}-triangle-start`,
|
|
199
|
+
dir: "start",
|
|
200
|
+
closed: true,
|
|
201
|
+
classNames
|
|
202
|
+
}),
|
|
203
|
+
/* @__PURE__ */ jsx(Arrow, {
|
|
204
|
+
id: `${id}-triangle-end`,
|
|
205
|
+
dir: "end",
|
|
206
|
+
closed: true,
|
|
207
|
+
classNames
|
|
208
|
+
}),
|
|
209
|
+
/* @__PURE__ */ jsx(Marker, {
|
|
210
|
+
id: `${id}-circle`,
|
|
211
|
+
pos: {
|
|
212
|
+
x: 8,
|
|
213
|
+
y: 8
|
|
214
|
+
},
|
|
215
|
+
size: {
|
|
216
|
+
width: 16,
|
|
217
|
+
height: 16
|
|
218
|
+
},
|
|
219
|
+
children: /* @__PURE__ */ jsx("circle", {
|
|
220
|
+
cx: 8,
|
|
221
|
+
cy: 8,
|
|
222
|
+
r: 5,
|
|
223
|
+
stroke: "context-stroke",
|
|
224
|
+
className: mx(classNames)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
] });
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* https://www.w3.org/TR/SVG2/painting.html#Markers
|
|
231
|
+
*/
|
|
232
|
+
var Marker = ({ id, className, children, pos: { x: refX, y: refY }, size: { width: markerWidth, height: markerHeight }, fill, ...rest }) => /* @__PURE__ */ jsx("marker", {
|
|
233
|
+
id,
|
|
234
|
+
className,
|
|
235
|
+
refX,
|
|
236
|
+
refY,
|
|
237
|
+
markerWidth,
|
|
238
|
+
markerHeight,
|
|
239
|
+
markerUnits: "strokeWidth",
|
|
240
|
+
orient: "auto",
|
|
241
|
+
...rest,
|
|
242
|
+
children
|
|
243
|
+
});
|
|
244
|
+
var Arrow = ({ classNames, id, size = 16, dir = "end", closed = false }) => /* @__PURE__ */ jsx(Marker, {
|
|
245
|
+
id,
|
|
246
|
+
size: {
|
|
247
|
+
width: size,
|
|
248
|
+
height: size
|
|
249
|
+
},
|
|
250
|
+
pos: dir === "end" ? {
|
|
251
|
+
x: size,
|
|
252
|
+
y: size / 2
|
|
253
|
+
} : {
|
|
254
|
+
x: 0,
|
|
255
|
+
y: size / 2
|
|
256
|
+
},
|
|
257
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
258
|
+
fill: closed ? void 0 : "none",
|
|
259
|
+
stroke: "context-stroke",
|
|
260
|
+
className: mx(classNames),
|
|
261
|
+
d: createPath(dir === "end" ? [
|
|
262
|
+
{
|
|
263
|
+
x: 1,
|
|
264
|
+
y: 1
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
x: size,
|
|
268
|
+
y: size / 2
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
x: 1,
|
|
272
|
+
y: size - 1
|
|
273
|
+
}
|
|
274
|
+
] : [
|
|
275
|
+
{
|
|
276
|
+
x: size - 1,
|
|
277
|
+
y: 1
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
x: 0,
|
|
281
|
+
y: size / 2
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
x: size - 1,
|
|
285
|
+
y: size - 1
|
|
286
|
+
}
|
|
287
|
+
], closed)
|
|
288
|
+
})
|
|
289
|
+
});
|
|
290
|
+
var GridPattern = ({ classNames, id, size, offset }) => /* @__PURE__ */ jsx("pattern", {
|
|
291
|
+
id,
|
|
292
|
+
x: (size / 2 + offset.x) % size,
|
|
293
|
+
y: (size / 2 + offset.y) % size,
|
|
294
|
+
width: size,
|
|
295
|
+
height: size,
|
|
296
|
+
patternUnits: "userSpaceOnUse",
|
|
297
|
+
children: /* @__PURE__ */ jsxs("g", {
|
|
298
|
+
className: mx(classNames),
|
|
299
|
+
children: [/* @__PURE__ */ jsx("line", {
|
|
300
|
+
x1: 0,
|
|
301
|
+
y1: size / 2,
|
|
302
|
+
x2: size,
|
|
303
|
+
y2: size / 2
|
|
304
|
+
}), /* @__PURE__ */ jsx("line", {
|
|
305
|
+
x1: size / 2,
|
|
306
|
+
y1: 0,
|
|
307
|
+
x2: size / 2,
|
|
308
|
+
y2: size
|
|
309
|
+
})]
|
|
310
|
+
})
|
|
311
|
+
});
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/util/util.ts
|
|
314
|
+
var logged = false;
|
|
315
|
+
/**
|
|
316
|
+
* Get the relative point of the cursor.
|
|
317
|
+
* NOTE: ev.offset returns the position relative to the target.
|
|
318
|
+
*/
|
|
319
|
+
var getRelativePoint = (el, ev) => {
|
|
320
|
+
const rect = el.getBoundingClientRect();
|
|
321
|
+
return {
|
|
322
|
+
x: ev.clientX - rect.x,
|
|
323
|
+
y: ev.clientY - rect.top
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
*
|
|
328
|
+
*/
|
|
329
|
+
var testId = (id, inspect = false) => {
|
|
330
|
+
if (inspect) {
|
|
331
|
+
if (!logged) {
|
|
332
|
+
console.log("Open storybook in expanded window;\nthen run INSPECT()");
|
|
333
|
+
logged = true;
|
|
334
|
+
}
|
|
335
|
+
window.INSPECT = () => {
|
|
336
|
+
const el = document.querySelector(`[data-test-id="${id}"]`);
|
|
337
|
+
window.inspect(el);
|
|
338
|
+
console.log(el);
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return { [DATA_TEST_ID]: id };
|
|
342
|
+
};
|
|
343
|
+
var inspectElement = (el) => {
|
|
344
|
+
window.INSPECT = () => {
|
|
345
|
+
window.inspect(el);
|
|
346
|
+
window.element = el;
|
|
347
|
+
console.log("Open storybook in expanded window;\nthen run INSPECT()");
|
|
348
|
+
console.log(el);
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
var DATA_TEST_ID = "data-test-id";
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/hooks/useWheel.tsx
|
|
354
|
+
var defaultOptions = { zoom: true };
|
|
355
|
+
/**
|
|
356
|
+
* Handle wheel events to update the transform state (zoom and offset).
|
|
357
|
+
*/
|
|
358
|
+
var useWheel = (options = defaultOptions) => {
|
|
359
|
+
const { root, setProjection } = useCanvasContext();
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (!root) return;
|
|
362
|
+
return bindAll(root, [{
|
|
363
|
+
type: "wheel",
|
|
364
|
+
options: {
|
|
365
|
+
capture: true,
|
|
366
|
+
passive: false
|
|
367
|
+
},
|
|
368
|
+
listener: (ev) => {
|
|
369
|
+
const zooming = isWheelZooming(ev);
|
|
370
|
+
ev.preventDefault();
|
|
371
|
+
if (zooming && !options.zoom) return;
|
|
372
|
+
if (ev.ctrlKey) {
|
|
373
|
+
if (!root) return;
|
|
374
|
+
setProjection(({ scale, offset }) => {
|
|
375
|
+
const pos = getRelativePoint(root, ev);
|
|
376
|
+
return getZoomTransform({
|
|
377
|
+
scale,
|
|
378
|
+
offset,
|
|
379
|
+
newScale: scale * Math.exp(-ev.deltaY * .01),
|
|
380
|
+
pos
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
} else setProjection(({ scale, offset: { x, y } }) => {
|
|
384
|
+
return {
|
|
385
|
+
scale,
|
|
386
|
+
offset: {
|
|
387
|
+
x: x - ev.deltaX,
|
|
388
|
+
y: y - ev.deltaY
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}]);
|
|
394
|
+
}, [root]);
|
|
395
|
+
};
|
|
396
|
+
var isWheelZooming = (ev) => {
|
|
397
|
+
if (ev.ctrlKey || ev.metaKey) return Math.abs(ev.deltaY) > 0 || Math.abs(ev.deltaZ) > 0;
|
|
398
|
+
return false;
|
|
399
|
+
};
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/components/Canvas/Canvas.tsx
|
|
402
|
+
/**
|
|
403
|
+
* Root canvas component.
|
|
404
|
+
* Manages CSS projection.
|
|
405
|
+
*/
|
|
406
|
+
var Canvas = forwardRef(({ children, classNames, scale: scaleProp = 1, offset: offsetProp = defaultOrigin, ...props }, forwardedRef) => {
|
|
407
|
+
const { ref, width = 0, height = 0 } = useResizeDetector();
|
|
408
|
+
const [ready, setReady] = useState(false);
|
|
409
|
+
const [{ scale, offset }, setProjection] = useState({
|
|
410
|
+
scale: scaleProp,
|
|
411
|
+
offset: offsetProp
|
|
412
|
+
});
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
if (width && height && offset === defaultOrigin) setProjection({
|
|
415
|
+
scale,
|
|
416
|
+
offset: {
|
|
417
|
+
x: width / 2,
|
|
418
|
+
y: height / 2
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}, [
|
|
422
|
+
width,
|
|
423
|
+
height,
|
|
424
|
+
scale,
|
|
425
|
+
offset
|
|
426
|
+
]);
|
|
427
|
+
const projection = useMemo(() => new ProjectionMapper(), []);
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
projection.update({
|
|
430
|
+
width,
|
|
431
|
+
height
|
|
432
|
+
}, scale, offset);
|
|
433
|
+
if (offset !== defaultOrigin) setReady(true);
|
|
434
|
+
}, [
|
|
435
|
+
projection,
|
|
436
|
+
scale,
|
|
437
|
+
offset,
|
|
438
|
+
width,
|
|
439
|
+
height
|
|
440
|
+
]);
|
|
441
|
+
const styles = useMemo(() => {
|
|
442
|
+
return {
|
|
443
|
+
transform: `translate(${offset.x}px, ${offset.y}px) scale(${scale})`,
|
|
444
|
+
visibility: width && height ? "visible" : "hidden"
|
|
445
|
+
};
|
|
446
|
+
}, [scale, offset]);
|
|
447
|
+
useImperativeHandle(forwardedRef, () => {
|
|
448
|
+
return { setProjection: async (projection) => {
|
|
449
|
+
setProjection(projection);
|
|
450
|
+
} };
|
|
451
|
+
}, [ref]);
|
|
452
|
+
return /* @__PURE__ */ jsx(CanvasContext.Provider, {
|
|
453
|
+
value: {
|
|
454
|
+
root: ref.current,
|
|
455
|
+
ready,
|
|
456
|
+
width,
|
|
457
|
+
height,
|
|
458
|
+
scale,
|
|
459
|
+
offset,
|
|
460
|
+
styles,
|
|
461
|
+
projection,
|
|
462
|
+
setProjection
|
|
463
|
+
},
|
|
464
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
465
|
+
...props,
|
|
466
|
+
className: mx("absolute inset-0 overflow-hidden", classNames),
|
|
467
|
+
ref,
|
|
468
|
+
children: ready ? children : null
|
|
469
|
+
})
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/components/CellGrid/state/viewport.ts
|
|
474
|
+
var cellKey = (col, row) => `${col},${row}`;
|
|
475
|
+
var cellWidth = (viewport) => viewport.baseCellWidth * viewport.zoomX;
|
|
476
|
+
/**
|
|
477
|
+
* Convert a cell's world coordinates to screen-space pixel rectangle (relative to canvas origin).
|
|
478
|
+
*/
|
|
479
|
+
var worldToScreen = (viewport, headers, coord) => {
|
|
480
|
+
const w = cellWidth(viewport);
|
|
481
|
+
return {
|
|
482
|
+
x: headers.left + coord.col * w - viewport.scrollX,
|
|
483
|
+
y: headers.top + coord.row * viewport.cellHeight - viewport.scrollY,
|
|
484
|
+
w: (coord.length ?? 1) * w,
|
|
485
|
+
h: viewport.cellHeight
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* Convert screen-space pixels (relative to canvas origin) to fractional cell coordinates.
|
|
490
|
+
*/
|
|
491
|
+
var screenToWorld = (viewport, headers, point) => {
|
|
492
|
+
const w = cellWidth(viewport);
|
|
493
|
+
return {
|
|
494
|
+
col: (point.x - headers.left + viewport.scrollX) / w,
|
|
495
|
+
row: (point.y - headers.top + viewport.scrollY) / viewport.cellHeight
|
|
496
|
+
};
|
|
497
|
+
};
|
|
498
|
+
var hitTestCell = (viewport, headers, point) => {
|
|
499
|
+
if (point.x < headers.left || point.y < headers.top) return null;
|
|
500
|
+
const { col, row } = screenToWorld(viewport, headers, point);
|
|
501
|
+
if (col < 0 || row < 0) return null;
|
|
502
|
+
return {
|
|
503
|
+
col: Math.floor(col),
|
|
504
|
+
row: Math.floor(row)
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* Compute the inclusive range of cell coordinates intersecting the visible content rect.
|
|
509
|
+
*/
|
|
510
|
+
var visibleCellRange = (viewport, headers, size) => {
|
|
511
|
+
const w = cellWidth(viewport);
|
|
512
|
+
const innerW = Math.max(0, size.width - headers.left);
|
|
513
|
+
const innerH = Math.max(0, size.height - headers.top);
|
|
514
|
+
return {
|
|
515
|
+
minCol: Math.max(0, Math.floor(viewport.scrollX / w)),
|
|
516
|
+
maxCol: Math.floor((viewport.scrollX + innerW) / w),
|
|
517
|
+
minRow: Math.max(0, Math.floor(viewport.scrollY / viewport.cellHeight)),
|
|
518
|
+
maxRow: Math.floor((viewport.scrollY + innerH) / viewport.cellHeight)
|
|
519
|
+
};
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* Iterate sparse cell map, yielding only cells whose horizontal extent intersects visible cols and whose row is visible.
|
|
523
|
+
*/
|
|
524
|
+
var visibleCells = function* (cells, range) {
|
|
525
|
+
for (const cell of cells.values()) {
|
|
526
|
+
if (cell.row < range.minRow || cell.row > range.maxRow) continue;
|
|
527
|
+
const start = cell.col;
|
|
528
|
+
if (cell.col + cell.length - 1 < range.minCol || start > range.maxCol) continue;
|
|
529
|
+
yield cell;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/components/CellGrid/headers/Ruler.tsx
|
|
534
|
+
/**
|
|
535
|
+
* Frozen top ruler. Ticks reflect the current viewport scroll and zoom.
|
|
536
|
+
*/
|
|
537
|
+
var Ruler = ({ viewport, headers, width, majorEvery = 4, classNames }) => {
|
|
538
|
+
const safeMajorEvery = Math.max(1, Math.floor(majorEvery));
|
|
539
|
+
const ticks = useMemo(() => {
|
|
540
|
+
const w = cellWidth(viewport);
|
|
541
|
+
if (w < 1 || width <= headers.left) return [];
|
|
542
|
+
const innerWidth = width - headers.left;
|
|
543
|
+
const startCol = Math.floor(viewport.scrollX / w);
|
|
544
|
+
const endCol = Math.ceil((viewport.scrollX + innerWidth) / w);
|
|
545
|
+
const result = [];
|
|
546
|
+
for (let col = startCol; col <= endCol; col++) result.push({
|
|
547
|
+
col,
|
|
548
|
+
x: headers.left + col * w - viewport.scrollX,
|
|
549
|
+
major: col % safeMajorEvery === 0
|
|
550
|
+
});
|
|
551
|
+
return result;
|
|
552
|
+
}, [
|
|
553
|
+
viewport,
|
|
554
|
+
headers.left,
|
|
555
|
+
width,
|
|
556
|
+
safeMajorEvery
|
|
557
|
+
]);
|
|
558
|
+
return /* @__PURE__ */ jsx("div", {
|
|
559
|
+
className: mx("absolute top-0 left-0 right-0 border-b border-neutral-200 dark:border-neutral-700 bg-base-surface select-none overflow-hidden", classNames),
|
|
560
|
+
style: { height: headers.top },
|
|
561
|
+
children: ticks.map(({ col, x, major }) => /* @__PURE__ */ jsx("div", {
|
|
562
|
+
className: mx("absolute top-0 bottom-0 text-[10px] text-neutral-500 dark:text-neutral-400", major ? "border-l border-neutral-400 dark:border-neutral-500" : "border-l border-neutral-200 dark:border-neutral-700"),
|
|
563
|
+
style: { transform: `translateX(${x}px)` },
|
|
564
|
+
children: major ? /* @__PURE__ */ jsx("span", {
|
|
565
|
+
className: "absolute left-1 top-0",
|
|
566
|
+
children: col
|
|
567
|
+
}) : null
|
|
568
|
+
}, col))
|
|
569
|
+
});
|
|
570
|
+
};
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/components/CellGrid/headers/TrackHeader.tsx
|
|
573
|
+
/**
|
|
574
|
+
* Frozen left column listing row labels. Translates vertically in sync with viewport scroll.
|
|
575
|
+
*
|
|
576
|
+
* Row dividers and alternating shading intentionally MATCH the canvas — opaque borders
|
|
577
|
+
* and opaque alternating fills make the labels look out of phase with the cell area
|
|
578
|
+
* even when the y-positions align. We mirror the canvas's transparent-overlay model
|
|
579
|
+
* here so the frozen column reads as a direct continuation of the grid.
|
|
580
|
+
*/
|
|
581
|
+
var TrackHeader = ({ viewport, headers, rows, height, classNames }) => {
|
|
582
|
+
return /* @__PURE__ */ jsx("div", {
|
|
583
|
+
className: mx("absolute left-0 border-r border-neutral-200 dark:border-neutral-700 select-none overflow-hidden", classNames),
|
|
584
|
+
style: {
|
|
585
|
+
top: headers.top,
|
|
586
|
+
width: headers.left,
|
|
587
|
+
height: Math.max(0, height - headers.top)
|
|
588
|
+
},
|
|
589
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
590
|
+
style: { transform: `translateY(${-viewport.scrollY}px)` },
|
|
591
|
+
children: rows.map((row, index) => /* @__PURE__ */ jsx("div", {
|
|
592
|
+
className: "flex items-center px-2 text-xs text-neutral-700 dark:text-neutral-300",
|
|
593
|
+
style: {
|
|
594
|
+
height: viewport.cellHeight,
|
|
595
|
+
backgroundColor: index % 2 === 0 ? "transparent" : "rgba(128, 128, 128, 0.08)",
|
|
596
|
+
boxShadow: "inset 0 -1px 0 rgba(128, 128, 128, 0.25)"
|
|
597
|
+
},
|
|
598
|
+
children: row.label ?? row.id
|
|
599
|
+
}, row.id))
|
|
600
|
+
})
|
|
601
|
+
});
|
|
602
|
+
};
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region src/components/CellGrid/input/pointer.ts
|
|
605
|
+
/**
|
|
606
|
+
* Attach pointer handlers to an element. Returns an unsubscribe.
|
|
607
|
+
*/
|
|
608
|
+
var attachPointerHandlers = (element, { registry, atoms, headers, handlers }) => {
|
|
609
|
+
let drag = null;
|
|
610
|
+
const local = (event) => {
|
|
611
|
+
const rect = element.getBoundingClientRect();
|
|
612
|
+
return {
|
|
613
|
+
x: event.clientX - rect.left,
|
|
614
|
+
y: event.clientY - rect.top
|
|
615
|
+
};
|
|
616
|
+
};
|
|
617
|
+
const tryCapture = (pointerId) => {
|
|
618
|
+
try {
|
|
619
|
+
element.setPointerCapture(pointerId);
|
|
620
|
+
} catch {}
|
|
621
|
+
};
|
|
622
|
+
const onPointerDown = (event) => {
|
|
623
|
+
if (event.button === 1 || event.button === 0 && event.altKey) {
|
|
624
|
+
drag = {
|
|
625
|
+
kind: "pan",
|
|
626
|
+
lastX: event.clientX,
|
|
627
|
+
lastY: event.clientY
|
|
628
|
+
};
|
|
629
|
+
tryCapture(event.pointerId);
|
|
630
|
+
event.preventDefault();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (event.button !== 0) return;
|
|
634
|
+
const coord = hitTestCell(registry.get(atoms.viewport), headers, local(event));
|
|
635
|
+
if (!coord) return;
|
|
636
|
+
const tool = registry.get(atoms.tool);
|
|
637
|
+
tryCapture(event.pointerId);
|
|
638
|
+
switch (tool) {
|
|
639
|
+
case "toggle":
|
|
640
|
+
case "resize": {
|
|
641
|
+
const cells = registry.get(atoms.cells);
|
|
642
|
+
const key = cellKey(coord.col, coord.row);
|
|
643
|
+
const mode = cells.has(key) ? "unset" : "set";
|
|
644
|
+
handlers.onCellToggle?.(coord, mode);
|
|
645
|
+
drag = {
|
|
646
|
+
kind: "toggle",
|
|
647
|
+
mode,
|
|
648
|
+
touched: /* @__PURE__ */ new Set([key])
|
|
649
|
+
};
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case "edit":
|
|
653
|
+
drag = {
|
|
654
|
+
kind: "draw",
|
|
655
|
+
startCoord: coord,
|
|
656
|
+
endCoord: coord
|
|
657
|
+
};
|
|
658
|
+
handlers.onDrawUpdate?.(coord, coord);
|
|
659
|
+
break;
|
|
660
|
+
case "delete": {
|
|
661
|
+
const key = cellKey(coord.col, coord.row);
|
|
662
|
+
handlers.onCellToggle?.(coord, "unset");
|
|
663
|
+
drag = {
|
|
664
|
+
kind: "toggle",
|
|
665
|
+
mode: "unset",
|
|
666
|
+
touched: /* @__PURE__ */ new Set([key])
|
|
667
|
+
};
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
case "select":
|
|
671
|
+
drag = {
|
|
672
|
+
kind: "select",
|
|
673
|
+
origin: coord
|
|
674
|
+
};
|
|
675
|
+
registry.set(atoms.selection, { range: {
|
|
676
|
+
col0: coord.col,
|
|
677
|
+
row0: coord.row,
|
|
678
|
+
col1: coord.col,
|
|
679
|
+
row1: coord.row
|
|
680
|
+
} });
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
const onPointerMove = (event) => {
|
|
685
|
+
if (!drag) return;
|
|
686
|
+
if (drag.kind === "pan") {
|
|
687
|
+
const dx = event.clientX - drag.lastX;
|
|
688
|
+
const dy = event.clientY - drag.lastY;
|
|
689
|
+
drag.lastX = event.clientX;
|
|
690
|
+
drag.lastY = event.clientY;
|
|
691
|
+
registry.update(atoms.viewport, (current) => ({
|
|
692
|
+
...current,
|
|
693
|
+
scrollX: Math.max(0, current.scrollX - dx),
|
|
694
|
+
scrollY: Math.max(0, current.scrollY - dy)
|
|
695
|
+
}));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const coord = hitTestCell(registry.get(atoms.viewport), headers, local(event));
|
|
699
|
+
if (!coord) return;
|
|
700
|
+
if (drag.kind === "toggle") {
|
|
701
|
+
const key = cellKey(coord.col, coord.row);
|
|
702
|
+
if (!drag.touched.has(key)) {
|
|
703
|
+
drag.touched.add(key);
|
|
704
|
+
handlers.onCellToggle?.(coord, drag.mode);
|
|
705
|
+
}
|
|
706
|
+
} else if (drag.kind === "draw") {
|
|
707
|
+
const constrainedCol = coord.col;
|
|
708
|
+
if (constrainedCol !== drag.endCoord.col) {
|
|
709
|
+
drag.endCoord = {
|
|
710
|
+
col: constrainedCol,
|
|
711
|
+
row: drag.startCoord.row
|
|
712
|
+
};
|
|
713
|
+
handlers.onDrawUpdate?.(drag.startCoord, drag.endCoord);
|
|
714
|
+
}
|
|
715
|
+
} else if (drag.kind === "select") registry.set(atoms.selection, { range: {
|
|
716
|
+
col0: drag.origin.col,
|
|
717
|
+
row0: drag.origin.row,
|
|
718
|
+
col1: coord.col,
|
|
719
|
+
row1: coord.row
|
|
720
|
+
} });
|
|
721
|
+
};
|
|
722
|
+
const releaseCapture = (event) => {
|
|
723
|
+
if (element.hasPointerCapture(event.pointerId)) element.releasePointerCapture(event.pointerId);
|
|
724
|
+
};
|
|
725
|
+
const onPointerUp = (event) => {
|
|
726
|
+
if (!drag) return;
|
|
727
|
+
if (drag.kind === "draw") handlers.onDrawCommit?.(drag.startCoord, drag.endCoord);
|
|
728
|
+
else if (drag.kind === "select") {
|
|
729
|
+
const range = registry.get(atoms.selection).range;
|
|
730
|
+
if (range) handlers.onSelectionCommit?.(range);
|
|
731
|
+
}
|
|
732
|
+
drag = null;
|
|
733
|
+
releaseCapture(event);
|
|
734
|
+
};
|
|
735
|
+
const onPointerCancel = (event) => {
|
|
736
|
+
drag = null;
|
|
737
|
+
releaseCapture(event);
|
|
738
|
+
};
|
|
739
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
740
|
+
element.addEventListener("pointermove", onPointerMove);
|
|
741
|
+
element.addEventListener("pointerup", onPointerUp);
|
|
742
|
+
element.addEventListener("pointercancel", onPointerCancel);
|
|
743
|
+
return () => {
|
|
744
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
745
|
+
element.removeEventListener("pointermove", onPointerMove);
|
|
746
|
+
element.removeEventListener("pointerup", onPointerUp);
|
|
747
|
+
element.removeEventListener("pointercancel", onPointerCancel);
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
/**
|
|
751
|
+
* Utility for consumers: toggle, set, or unset membership of a cell in the cells atom.
|
|
752
|
+
*/
|
|
753
|
+
var toggleCell = (registry, atoms, coord, factory, mode = "toggle") => {
|
|
754
|
+
registry.update(atoms.cells, (current) => {
|
|
755
|
+
const next = new Map(current);
|
|
756
|
+
const key = cellKey(coord.col, coord.row);
|
|
757
|
+
const exists = next.has(key);
|
|
758
|
+
if (mode === "set" || mode === "toggle" && !exists) next.set(key, factory(coord));
|
|
759
|
+
else if (mode === "unset" || mode === "toggle" && exists) next.delete(key);
|
|
760
|
+
return next;
|
|
761
|
+
});
|
|
762
|
+
};
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/components/CellGrid/input/wheel.ts
|
|
765
|
+
var MIN_ZOOM = .25;
|
|
766
|
+
var MAX_ZOOM = 8;
|
|
767
|
+
/**
|
|
768
|
+
* Attach wheel handlers. Vertical wheel scrolls y; shift+wheel scrolls x;
|
|
769
|
+
* cmd/ctrl+wheel zooms x around the cursor.
|
|
770
|
+
*/
|
|
771
|
+
var attachWheelHandlers = (element, { registry, atoms, headers }) => {
|
|
772
|
+
const onWheel = (event) => {
|
|
773
|
+
if (event.ctrlKey || event.metaKey) {
|
|
774
|
+
event.preventDefault();
|
|
775
|
+
const rect = element.getBoundingClientRect();
|
|
776
|
+
const x = event.clientX - rect.left;
|
|
777
|
+
const factor = Math.exp(-event.deltaY / 200);
|
|
778
|
+
registry.update(atoms.viewport, (current) => {
|
|
779
|
+
const nextZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, current.zoomX * factor));
|
|
780
|
+
if (nextZoom === current.zoomX) return current;
|
|
781
|
+
const w = cellWidth(current);
|
|
782
|
+
const worldX = (x - headers.left + current.scrollX) / w;
|
|
783
|
+
const nextW = current.baseCellWidth * nextZoom;
|
|
784
|
+
const nextScrollX = Math.max(0, worldX * nextW - (x - headers.left));
|
|
785
|
+
return {
|
|
786
|
+
...current,
|
|
787
|
+
zoomX: nextZoom,
|
|
788
|
+
scrollX: nextScrollX
|
|
789
|
+
};
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const dx = event.shiftKey ? event.deltaY : event.deltaX;
|
|
794
|
+
const dy = event.shiftKey ? 0 : event.deltaY;
|
|
795
|
+
const current = registry.get(atoms.viewport);
|
|
796
|
+
const nextScrollX = Math.max(0, current.scrollX + dx);
|
|
797
|
+
const nextScrollY = Math.max(0, current.scrollY + dy);
|
|
798
|
+
if (nextScrollX === current.scrollX && nextScrollY === current.scrollY) return;
|
|
799
|
+
event.preventDefault();
|
|
800
|
+
registry.set(atoms.viewport, {
|
|
801
|
+
...current,
|
|
802
|
+
scrollX: nextScrollX,
|
|
803
|
+
scrollY: nextScrollY
|
|
804
|
+
});
|
|
805
|
+
};
|
|
806
|
+
element.addEventListener("wheel", onWheel, { passive: false });
|
|
807
|
+
return () => element.removeEventListener("wheel", onWheel);
|
|
808
|
+
};
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/components/CellGrid/render/overlay-layer.ts
|
|
811
|
+
var drawOverlay = ({ ctx, size, viewport, headers, selection, playhead, style }) => {
|
|
812
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
813
|
+
ctx.save();
|
|
814
|
+
ctx.beginPath();
|
|
815
|
+
ctx.rect(headers.left, headers.top, size.width - headers.left, size.height - headers.top);
|
|
816
|
+
ctx.clip();
|
|
817
|
+
if (selection.range) {
|
|
818
|
+
const { col0, row0, col1, row1 } = selection.range;
|
|
819
|
+
const minCol = Math.min(col0, col1);
|
|
820
|
+
const maxCol = Math.max(col0, col1);
|
|
821
|
+
const minRow = Math.min(row0, row1);
|
|
822
|
+
const maxRow = Math.max(row0, row1);
|
|
823
|
+
const tl = worldToScreen(viewport, headers, {
|
|
824
|
+
col: minCol,
|
|
825
|
+
row: minRow
|
|
826
|
+
});
|
|
827
|
+
const br = worldToScreen(viewport, headers, {
|
|
828
|
+
col: maxCol + 1,
|
|
829
|
+
row: maxRow + 1
|
|
830
|
+
});
|
|
831
|
+
ctx.fillStyle = style.selectionFill;
|
|
832
|
+
ctx.fillRect(tl.x, tl.y, br.x - tl.x, br.y - tl.y);
|
|
833
|
+
ctx.strokeStyle = style.selectionStroke;
|
|
834
|
+
ctx.setLineDash([4, 3]);
|
|
835
|
+
ctx.lineWidth = 1;
|
|
836
|
+
ctx.strokeRect(tl.x + .5, tl.y + .5, br.x - tl.x - 1, br.y - tl.y - 1);
|
|
837
|
+
ctx.setLineDash([]);
|
|
838
|
+
}
|
|
839
|
+
if (playhead !== null) {
|
|
840
|
+
const w = cellWidth(viewport);
|
|
841
|
+
const x = headers.left + playhead * w - viewport.scrollX;
|
|
842
|
+
if (x >= headers.left && x <= size.width) {
|
|
843
|
+
ctx.strokeStyle = style.playhead;
|
|
844
|
+
ctx.lineWidth = 2;
|
|
845
|
+
ctx.beginPath();
|
|
846
|
+
ctx.moveTo(x, headers.top);
|
|
847
|
+
ctx.lineTo(x, size.height);
|
|
848
|
+
ctx.stroke();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
ctx.restore();
|
|
852
|
+
};
|
|
853
|
+
//#endregion
|
|
854
|
+
//#region src/components/CellGrid/render/static-layer.ts
|
|
855
|
+
/**
|
|
856
|
+
* Paint the static layer: background, gridlines, alternating row bands, and cells.
|
|
857
|
+
* Pure with respect to its inputs (writes only to the supplied ctx).
|
|
858
|
+
*/
|
|
859
|
+
var drawCells = ({ ctx, size, viewport, headers, rows, cells, renderCell, style }) => {
|
|
860
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
861
|
+
if (style.background) {
|
|
862
|
+
ctx.fillStyle = style.background;
|
|
863
|
+
ctx.fillRect(0, 0, size.width, size.height);
|
|
864
|
+
}
|
|
865
|
+
const range = visibleCellRange(viewport, headers, size);
|
|
866
|
+
const w = cellWidth(viewport);
|
|
867
|
+
const h = viewport.cellHeight;
|
|
868
|
+
if (style.rowBand) {
|
|
869
|
+
ctx.fillStyle = style.rowBand;
|
|
870
|
+
for (let row = range.minRow; row <= Math.min(range.maxRow, rows.length - 1); row++) {
|
|
871
|
+
if (row % 2 === 0) continue;
|
|
872
|
+
const y = headers.top + row * h - viewport.scrollY;
|
|
873
|
+
ctx.fillRect(headers.left, y, size.width - headers.left, h);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
ctx.strokeStyle = style.gridLine;
|
|
877
|
+
ctx.lineWidth = 1;
|
|
878
|
+
ctx.beginPath();
|
|
879
|
+
for (let col = range.minCol; col <= range.maxCol + 1; col++) {
|
|
880
|
+
const x = Math.floor(headers.left + col * w - viewport.scrollX) + .5;
|
|
881
|
+
if (x < headers.left) continue;
|
|
882
|
+
ctx.moveTo(x, headers.top);
|
|
883
|
+
ctx.lineTo(x, size.height);
|
|
884
|
+
}
|
|
885
|
+
for (let row = range.minRow; row <= Math.min(range.maxRow + 1, rows.length); row++) {
|
|
886
|
+
const y = Math.floor(headers.top + row * h - viewport.scrollY) + .5;
|
|
887
|
+
if (y < headers.top) continue;
|
|
888
|
+
ctx.moveTo(headers.left, y);
|
|
889
|
+
ctx.lineTo(size.width, y);
|
|
890
|
+
}
|
|
891
|
+
ctx.stroke();
|
|
892
|
+
ctx.save();
|
|
893
|
+
ctx.beginPath();
|
|
894
|
+
ctx.rect(headers.left, headers.top, size.width - headers.left, size.height - headers.top);
|
|
895
|
+
ctx.clip();
|
|
896
|
+
for (const cell of visibleCells(cells, range)) {
|
|
897
|
+
if (cell.row >= rows.length) continue;
|
|
898
|
+
renderCell({
|
|
899
|
+
ctx,
|
|
900
|
+
...worldToScreen(viewport, headers, cell),
|
|
901
|
+
cell
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
ctx.restore();
|
|
905
|
+
};
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/components/CellGrid/CellGrid.tsx
|
|
908
|
+
var defaultHeaders = {
|
|
909
|
+
left: 80,
|
|
910
|
+
top: 24
|
|
911
|
+
};
|
|
912
|
+
var defaultStaticStyle = {
|
|
913
|
+
gridLine: "rgba(128,128,128,0.25)",
|
|
914
|
+
rowBand: "rgba(128,128,128,0.06)"
|
|
915
|
+
};
|
|
916
|
+
var defaultOverlayStyle = {
|
|
917
|
+
playhead: "rgb(220, 38, 38)",
|
|
918
|
+
selectionFill: "rgba(59, 130, 246, 0.15)",
|
|
919
|
+
selectionStroke: "rgb(59, 130, 246)"
|
|
920
|
+
};
|
|
921
|
+
var setupCanvas = (canvas, width, height) => {
|
|
922
|
+
const dpr = window.devicePixelRatio || 1;
|
|
923
|
+
canvas.width = Math.max(1, Math.floor(width * dpr));
|
|
924
|
+
canvas.height = Math.max(1, Math.floor(height * dpr));
|
|
925
|
+
canvas.style.width = `${width}px`;
|
|
926
|
+
canvas.style.height = `${height}px`;
|
|
927
|
+
const ctx = canvas.getContext("2d");
|
|
928
|
+
if (!ctx) return null;
|
|
929
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
930
|
+
return ctx;
|
|
931
|
+
};
|
|
932
|
+
/**
|
|
933
|
+
* Canvas-based 2D grid where cells contain pluggable shapes. Suitable for music sequencers,
|
|
934
|
+
* time-series data viz, and similar workloads at ~1k visible cells per viewport.
|
|
935
|
+
*/
|
|
936
|
+
var CellGrid = ({ atoms, rows, renderCell, headers: headersProp, staticStyle: staticStyleProp, overlayStyle: overlayStyleProp, classNames, onCellToggle, onSelectionCommit, onDrawUpdate, onDrawCommit }) => {
|
|
937
|
+
const registry = useContext(RegistryContext);
|
|
938
|
+
const headers = useMemo(() => {
|
|
939
|
+
if (headersProp === false) return {
|
|
940
|
+
left: 0,
|
|
941
|
+
top: 0
|
|
942
|
+
};
|
|
943
|
+
return {
|
|
944
|
+
...defaultHeaders,
|
|
945
|
+
...headersProp ?? {}
|
|
946
|
+
};
|
|
947
|
+
}, [headersProp]);
|
|
948
|
+
const staticStyle = useMemo(() => ({
|
|
949
|
+
...defaultStaticStyle,
|
|
950
|
+
...staticStyleProp ?? {}
|
|
951
|
+
}), [staticStyleProp]);
|
|
952
|
+
const overlayStyle = useMemo(() => ({
|
|
953
|
+
...defaultOverlayStyle,
|
|
954
|
+
...overlayStyleProp ?? {}
|
|
955
|
+
}), [overlayStyleProp]);
|
|
956
|
+
const { ref: containerRef, width = 0, height = 0 } = useResizeDetector();
|
|
957
|
+
const staticCanvasRef = useRef(null);
|
|
958
|
+
const overlayCanvasRef = useRef(null);
|
|
959
|
+
const overlayInputRef = useRef(null);
|
|
960
|
+
const [staticCtx, setStaticCtx] = useState(null);
|
|
961
|
+
const [overlayCtx, setOverlayCtx] = useState(null);
|
|
962
|
+
const [viewportState, setViewportState] = useState(() => registry.get(atoms.viewport));
|
|
963
|
+
useEffect(() => {
|
|
964
|
+
if (!width || !height) return;
|
|
965
|
+
if (staticCanvasRef.current) {
|
|
966
|
+
const ctx = setupCanvas(staticCanvasRef.current, width, height);
|
|
967
|
+
setStaticCtx(ctx);
|
|
968
|
+
}
|
|
969
|
+
if (overlayCanvasRef.current) {
|
|
970
|
+
const ctx = setupCanvas(overlayCanvasRef.current, width, height);
|
|
971
|
+
setOverlayCtx(ctx);
|
|
972
|
+
}
|
|
973
|
+
}, [width, height]);
|
|
974
|
+
useEffect(() => registry.subscribe(atoms.viewport, (next) => setViewportState(next)), [registry, atoms.viewport]);
|
|
975
|
+
useEffect(() => {
|
|
976
|
+
if (!staticCtx || !width || !height) return;
|
|
977
|
+
let raf = null;
|
|
978
|
+
const schedule = () => {
|
|
979
|
+
if (raf !== null) return;
|
|
980
|
+
raf = requestAnimationFrame(() => {
|
|
981
|
+
raf = null;
|
|
982
|
+
drawCells({
|
|
983
|
+
ctx: staticCtx,
|
|
984
|
+
size: {
|
|
985
|
+
width,
|
|
986
|
+
height
|
|
987
|
+
},
|
|
988
|
+
viewport: registry.get(atoms.viewport),
|
|
989
|
+
headers,
|
|
990
|
+
rows,
|
|
991
|
+
cells: registry.get(atoms.cells),
|
|
992
|
+
renderCell,
|
|
993
|
+
style: staticStyle
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
};
|
|
997
|
+
schedule();
|
|
998
|
+
const unsubCells = registry.subscribe(atoms.cells, schedule);
|
|
999
|
+
const unsubViewport = registry.subscribe(atoms.viewport, schedule);
|
|
1000
|
+
return () => {
|
|
1001
|
+
if (raf !== null) cancelAnimationFrame(raf);
|
|
1002
|
+
unsubCells();
|
|
1003
|
+
unsubViewport();
|
|
1004
|
+
};
|
|
1005
|
+
}, [
|
|
1006
|
+
staticCtx,
|
|
1007
|
+
width,
|
|
1008
|
+
height,
|
|
1009
|
+
registry,
|
|
1010
|
+
atoms.cells,
|
|
1011
|
+
atoms.viewport,
|
|
1012
|
+
headers,
|
|
1013
|
+
rows,
|
|
1014
|
+
renderCell,
|
|
1015
|
+
staticStyle
|
|
1016
|
+
]);
|
|
1017
|
+
useEffect(() => {
|
|
1018
|
+
if (!overlayCtx || !width || !height) return;
|
|
1019
|
+
let raf = null;
|
|
1020
|
+
let stopped = false;
|
|
1021
|
+
const paint = () => {
|
|
1022
|
+
drawOverlay({
|
|
1023
|
+
ctx: overlayCtx,
|
|
1024
|
+
size: {
|
|
1025
|
+
width,
|
|
1026
|
+
height
|
|
1027
|
+
},
|
|
1028
|
+
viewport: registry.get(atoms.viewport),
|
|
1029
|
+
headers,
|
|
1030
|
+
selection: registry.get(atoms.selection),
|
|
1031
|
+
playhead: registry.get(atoms.playhead),
|
|
1032
|
+
style: overlayStyle
|
|
1033
|
+
});
|
|
1034
|
+
};
|
|
1035
|
+
const isAnimating = () => registry.get(atoms.playhead) !== null;
|
|
1036
|
+
const loop = () => {
|
|
1037
|
+
if (stopped) return;
|
|
1038
|
+
paint();
|
|
1039
|
+
if (isAnimating()) raf = requestAnimationFrame(loop);
|
|
1040
|
+
else raf = null;
|
|
1041
|
+
};
|
|
1042
|
+
const kick = () => {
|
|
1043
|
+
paint();
|
|
1044
|
+
if (raf === null && isAnimating()) raf = requestAnimationFrame(loop);
|
|
1045
|
+
};
|
|
1046
|
+
kick();
|
|
1047
|
+
const unsubSelection = registry.subscribe(atoms.selection, () => paint());
|
|
1048
|
+
const unsubPlayhead = registry.subscribe(atoms.playhead, kick);
|
|
1049
|
+
const unsubViewport = registry.subscribe(atoms.viewport, () => paint());
|
|
1050
|
+
return () => {
|
|
1051
|
+
stopped = true;
|
|
1052
|
+
if (raf !== null) cancelAnimationFrame(raf);
|
|
1053
|
+
unsubSelection();
|
|
1054
|
+
unsubPlayhead();
|
|
1055
|
+
unsubViewport();
|
|
1056
|
+
};
|
|
1057
|
+
}, [
|
|
1058
|
+
overlayCtx,
|
|
1059
|
+
width,
|
|
1060
|
+
height,
|
|
1061
|
+
registry,
|
|
1062
|
+
atoms.selection,
|
|
1063
|
+
atoms.playhead,
|
|
1064
|
+
atoms.viewport,
|
|
1065
|
+
headers,
|
|
1066
|
+
overlayStyle
|
|
1067
|
+
]);
|
|
1068
|
+
const callbacksRef = useRef({
|
|
1069
|
+
onCellToggle,
|
|
1070
|
+
onSelectionCommit,
|
|
1071
|
+
onDrawUpdate,
|
|
1072
|
+
onDrawCommit
|
|
1073
|
+
});
|
|
1074
|
+
callbacksRef.current = {
|
|
1075
|
+
onCellToggle,
|
|
1076
|
+
onSelectionCommit,
|
|
1077
|
+
onDrawUpdate,
|
|
1078
|
+
onDrawCommit
|
|
1079
|
+
};
|
|
1080
|
+
useEffect(() => {
|
|
1081
|
+
const element = overlayInputRef.current;
|
|
1082
|
+
if (!element) return;
|
|
1083
|
+
const detachPointer = attachPointerHandlers(element, {
|
|
1084
|
+
registry,
|
|
1085
|
+
atoms,
|
|
1086
|
+
headers,
|
|
1087
|
+
handlers: {
|
|
1088
|
+
onCellToggle: (coord, mode) => callbacksRef.current.onCellToggle?.(coord, mode),
|
|
1089
|
+
onSelectionCommit: (range) => callbacksRef.current.onSelectionCommit?.(range),
|
|
1090
|
+
onDrawUpdate: (start, end) => callbacksRef.current.onDrawUpdate?.(start, end),
|
|
1091
|
+
onDrawCommit: (start, end) => callbacksRef.current.onDrawCommit?.(start, end)
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
const detachWheel = attachWheelHandlers(element, {
|
|
1095
|
+
registry,
|
|
1096
|
+
atoms,
|
|
1097
|
+
headers
|
|
1098
|
+
});
|
|
1099
|
+
return () => {
|
|
1100
|
+
detachPointer();
|
|
1101
|
+
detachWheel();
|
|
1102
|
+
};
|
|
1103
|
+
}, [
|
|
1104
|
+
registry,
|
|
1105
|
+
atoms,
|
|
1106
|
+
headers
|
|
1107
|
+
]);
|
|
1108
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1109
|
+
ref: containerRef,
|
|
1110
|
+
className: mx("relative w-full h-full overflow-hidden dx-base-surface", classNames),
|
|
1111
|
+
children: [
|
|
1112
|
+
/* @__PURE__ */ jsx("canvas", {
|
|
1113
|
+
ref: staticCanvasRef,
|
|
1114
|
+
className: "absolute inset-0 pointer-events-none",
|
|
1115
|
+
style: {
|
|
1116
|
+
top: -1,
|
|
1117
|
+
left: -1
|
|
1118
|
+
}
|
|
1119
|
+
}),
|
|
1120
|
+
/* @__PURE__ */ jsx("canvas", {
|
|
1121
|
+
ref: overlayCanvasRef,
|
|
1122
|
+
className: "absolute inset-0 pointer-events-none",
|
|
1123
|
+
style: {
|
|
1124
|
+
top: -1,
|
|
1125
|
+
left: -1
|
|
1126
|
+
}
|
|
1127
|
+
}),
|
|
1128
|
+
/* @__PURE__ */ jsx("div", {
|
|
1129
|
+
ref: overlayInputRef,
|
|
1130
|
+
className: "absolute inset-0 touch-none",
|
|
1131
|
+
style: {
|
|
1132
|
+
paddingLeft: headers.left,
|
|
1133
|
+
paddingTop: headers.top
|
|
1134
|
+
}
|
|
1135
|
+
}),
|
|
1136
|
+
headers.top > 0 && /* @__PURE__ */ jsx(Ruler, {
|
|
1137
|
+
viewport: viewportState,
|
|
1138
|
+
headers,
|
|
1139
|
+
width
|
|
1140
|
+
}),
|
|
1141
|
+
headers.left > 0 && /* @__PURE__ */ jsx(TrackHeader, {
|
|
1142
|
+
viewport: viewportState,
|
|
1143
|
+
headers,
|
|
1144
|
+
rows,
|
|
1145
|
+
height
|
|
1146
|
+
}),
|
|
1147
|
+
headers.top > 0 && headers.left > 0 && /* @__PURE__ */ jsx("div", {
|
|
1148
|
+
className: "absolute top-0 left-0 border-b border-r border-neutral-200 dark:border-neutral-700 bg-base-surface",
|
|
1149
|
+
style: {
|
|
1150
|
+
width: headers.left,
|
|
1151
|
+
height: headers.top
|
|
1152
|
+
}
|
|
1153
|
+
})
|
|
1154
|
+
]
|
|
1155
|
+
});
|
|
1156
|
+
};
|
|
1157
|
+
//#endregion
|
|
1158
|
+
//#region src/components/CellGrid/state/atoms.ts
|
|
1159
|
+
var defaultViewport = (options = {}) => ({
|
|
1160
|
+
scrollX: 0,
|
|
1161
|
+
scrollY: 0,
|
|
1162
|
+
baseCellWidth: options.cellWidth ?? 24,
|
|
1163
|
+
cellHeight: options.cellHeight ?? 24,
|
|
1164
|
+
zoomX: 1
|
|
1165
|
+
});
|
|
1166
|
+
/**
|
|
1167
|
+
* Create a fresh set of atoms backing a CellGrid instance. Consumers may pass any of these as props
|
|
1168
|
+
* or substitute their own (e.g., a cells atom backed by ECHO).
|
|
1169
|
+
*
|
|
1170
|
+
* Atoms are marked keepAlive so they preserve state across transient unsubscribe windows (e.g. React
|
|
1171
|
+
* Strict Mode mount-unmount-remount). Consumers are responsible for the atoms' lifetime.
|
|
1172
|
+
*/
|
|
1173
|
+
var createCellGridAtoms = (options = {}) => ({
|
|
1174
|
+
cells: Atom.keepAlive(Atom.make(/* @__PURE__ */ new Map())),
|
|
1175
|
+
viewport: Atom.keepAlive(Atom.make(defaultViewport(options))),
|
|
1176
|
+
selection: Atom.keepAlive(Atom.make({ range: null })),
|
|
1177
|
+
playhead: Atom.keepAlive(Atom.make(null)),
|
|
1178
|
+
tool: Atom.keepAlive(Atom.make("toggle"))
|
|
1179
|
+
});
|
|
1180
|
+
//#endregion
|
|
1181
|
+
//#region src/components/FPS.tsx
|
|
1182
|
+
var SEC = 1e3;
|
|
1183
|
+
var FPS = ({ classNames, width = 60, height = 30, bar = "bg-cyan-500" }) => {
|
|
1184
|
+
const [{ fps, max, len }, dispatch] = useReducer((state) => {
|
|
1185
|
+
const currentTime = Date.now();
|
|
1186
|
+
if (currentTime > state.prevTime + SEC) {
|
|
1187
|
+
const nextFPS = [...new Array(Math.floor((currentTime - state.prevTime - SEC) / SEC)).fill(0), Math.max(1, Math.round(state.frames * SEC / (currentTime - state.prevTime)))];
|
|
1188
|
+
return {
|
|
1189
|
+
max: Math.max(state.max, ...nextFPS),
|
|
1190
|
+
len: Math.min(state.len + nextFPS.length, width),
|
|
1191
|
+
fps: [...state.fps, ...nextFPS].slice(-width),
|
|
1192
|
+
frames: 1,
|
|
1193
|
+
prevTime: currentTime
|
|
1194
|
+
};
|
|
1195
|
+
} else return {
|
|
1196
|
+
...state,
|
|
1197
|
+
frames: state.frames + 1
|
|
1198
|
+
};
|
|
1199
|
+
}, {
|
|
1200
|
+
max: 0,
|
|
1201
|
+
len: 0,
|
|
1202
|
+
fps: [],
|
|
1203
|
+
frames: 0,
|
|
1204
|
+
prevTime: Date.now()
|
|
1205
|
+
});
|
|
1206
|
+
const requestRef = useRef(null);
|
|
1207
|
+
const tick = () => {
|
|
1208
|
+
dispatch();
|
|
1209
|
+
requestRef.current = requestAnimationFrame(tick);
|
|
1210
|
+
};
|
|
1211
|
+
useEffect(() => {
|
|
1212
|
+
requestRef.current = requestAnimationFrame(tick);
|
|
1213
|
+
return () => {
|
|
1214
|
+
if (requestRef.current) cancelAnimationFrame(requestRef.current);
|
|
1215
|
+
};
|
|
1216
|
+
}, []);
|
|
1217
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1218
|
+
style: { width: width + 6 },
|
|
1219
|
+
className: mx("relative flex flex-col p-0.5", "bg-base-surface text-xs text-subdued font-thin pointer-events-none border border-separator", classNames),
|
|
1220
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [fps[len - 1], " FPS"] }), /* @__PURE__ */ jsx("div", {
|
|
1221
|
+
className: "w-full relative",
|
|
1222
|
+
style: { height },
|
|
1223
|
+
children: fps.map((frame, i) => /* @__PURE__ */ jsx("div", {
|
|
1224
|
+
className: bar,
|
|
1225
|
+
style: {
|
|
1226
|
+
position: "absolute",
|
|
1227
|
+
bottom: 0,
|
|
1228
|
+
right: `${len - 1 - i}px`,
|
|
1229
|
+
height: `${height * frame / max}px`,
|
|
1230
|
+
width: 1
|
|
1231
|
+
}
|
|
1232
|
+
}, `fps-${i}`))
|
|
1233
|
+
})]
|
|
1234
|
+
});
|
|
1235
|
+
};
|
|
1236
|
+
//#endregion
|
|
1237
|
+
//#region src/components/Grid/Grid.tsx
|
|
1238
|
+
var gridRatios = [
|
|
1239
|
+
1 / 4,
|
|
1240
|
+
1,
|
|
1241
|
+
4,
|
|
1242
|
+
16
|
|
1243
|
+
];
|
|
1244
|
+
var defaultGridSize = 16;
|
|
1245
|
+
var defaultOffset = {
|
|
1246
|
+
x: 0,
|
|
1247
|
+
y: 0
|
|
1248
|
+
};
|
|
1249
|
+
var createId = (parent, grid) => `dx-canvas-grid-${parent}-${grid}`;
|
|
1250
|
+
var Grid = (props) => {
|
|
1251
|
+
const { scale, offset } = useCanvasContext();
|
|
1252
|
+
return /* @__PURE__ */ jsx(GridComponent, {
|
|
1253
|
+
...props,
|
|
1254
|
+
scale,
|
|
1255
|
+
offset
|
|
1256
|
+
});
|
|
1257
|
+
};
|
|
1258
|
+
var GridComponent = forwardRef(({ size: gridSize = defaultGridSize, scale = 1, offset = defaultOffset, showAxes = true, classNames }, forwardedRef) => {
|
|
1259
|
+
const svgRef = useForwardedRef(forwardedRef);
|
|
1260
|
+
const { width = 0, height = 0 } = svgRef.current?.getBoundingClientRect() ?? {};
|
|
1261
|
+
const instanceId = useId();
|
|
1262
|
+
const grids = useMemo(() => gridRatios.map((ratio) => ({
|
|
1263
|
+
id: ratio,
|
|
1264
|
+
size: ratio * gridSize * scale
|
|
1265
|
+
})).filter(({ size }) => size >= gridSize && size <= 128), [gridSize, scale]);
|
|
1266
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
1267
|
+
...testId("dx-canvas-grid"),
|
|
1268
|
+
ref: svgRef,
|
|
1269
|
+
className: mx("dx-fullscreen w-full h-full pointer-events-none touch-none select-none", "stroke-neutral-500", classNames),
|
|
1270
|
+
children: [
|
|
1271
|
+
/* @__PURE__ */ jsx("defs", { children: grids.map(({ id, size }) => /* @__PURE__ */ jsx(GridPattern, {
|
|
1272
|
+
id: createId(instanceId, id),
|
|
1273
|
+
offset,
|
|
1274
|
+
size
|
|
1275
|
+
}, id)) }),
|
|
1276
|
+
showAxes && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("line", {
|
|
1277
|
+
x1: 0,
|
|
1278
|
+
y1: offset.y,
|
|
1279
|
+
x2: width,
|
|
1280
|
+
y2: offset.y,
|
|
1281
|
+
className: "stroke-neutral-500 opacity-40"
|
|
1282
|
+
}), /* @__PURE__ */ jsx("line", {
|
|
1283
|
+
x1: offset.x,
|
|
1284
|
+
y1: 0,
|
|
1285
|
+
x2: offset.x,
|
|
1286
|
+
y2: height,
|
|
1287
|
+
className: "stroke-neutral-500 opacity-40"
|
|
1288
|
+
})] }),
|
|
1289
|
+
/* @__PURE__ */ jsx("g", { children: grids.map(({ id }, i) => /* @__PURE__ */ jsx("rect", {
|
|
1290
|
+
opacity: .1 + i * .05,
|
|
1291
|
+
fill: `url(#${createId(instanceId, id)})`,
|
|
1292
|
+
width: "100%",
|
|
1293
|
+
height: "100%"
|
|
1294
|
+
}, id)) })
|
|
1295
|
+
]
|
|
1296
|
+
});
|
|
1297
|
+
});
|
|
1298
|
+
//#endregion
|
|
1299
|
+
//#region src/types.ts
|
|
1300
|
+
var Point = Schema.Struct({
|
|
1301
|
+
x: Schema.Number,
|
|
1302
|
+
y: Schema.Number
|
|
1303
|
+
});
|
|
1304
|
+
var Dimension = Schema.Struct({
|
|
1305
|
+
width: Schema.Number,
|
|
1306
|
+
height: Schema.Number
|
|
1307
|
+
});
|
|
1308
|
+
var Rect = Schema.extend(Point, Dimension);
|
|
1309
|
+
//#endregion
|
|
1310
|
+
export { Arrow, Canvas, CanvasContext, CellGrid, DATA_TEST_ID, Dimension, FPS, Grid, GridComponent, GridPattern, Marker, Markers, Point, ProjectionMapper, Rect, Ruler, TrackHeader, attachPointerHandlers, attachWheelHandlers, cellKey, cellWidth, createCellGridAtoms, createPath, defaultOrigin, defaultViewport, drawCells, drawOverlay, getRelativePoint, getZoomTransform, hitTestCell, inspectElement, screenToWorld, testId, toggleCell, useCanvasContext, useDrag, useWheel, visibleCellRange, visibleCells, worldToScreen, zoomInPlace, zoomTo };
|
|
1311
|
+
|
|
1312
|
+
//# sourceMappingURL=index.mjs.map
|