@weasel-js/labkit 1.3.0 → 1.4.0-pre.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/_dts/CanvasStackContext-DtAn_3O-.d.ts +42 -0
- package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
- package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
- package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
- package/dist/_dts/frac-Cp3NivlC.d.ts +728 -0
- package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
- package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
- package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
- package/dist/_dts/{types-DCGEcMO_.d.ts → types-D6s4b7if.d.ts} +1 -1
- package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
- package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
- package/dist/canvas/index.d.ts +43 -25
- package/dist/canvas/index.js +5 -4
- package/dist/chrome/index.d.ts +9 -6
- package/dist/chrome/index.js +6 -6
- package/dist/chunk-2AYGEN57.js +32 -0
- package/dist/chunk-2AYGEN57.js.map +1 -0
- package/dist/{chunk-QRYPXSGP.js → chunk-3P5TTTA7.js} +16 -8
- package/dist/chunk-3P5TTTA7.js.map +1 -0
- package/dist/{chunk-BDJXIBRZ.js → chunk-3ZELRRBV.js} +44 -27
- package/dist/chunk-3ZELRRBV.js.map +1 -0
- package/dist/{chunk-LGKRIUSW.js → chunk-4EY67BKJ.js} +32 -7
- package/dist/chunk-4EY67BKJ.js.map +1 -0
- package/dist/{chunk-3XXPU73K.js → chunk-5TCEVMPI.js} +9 -7
- package/dist/chunk-5TCEVMPI.js.map +1 -0
- package/dist/{chunk-JDROYQM3.js → chunk-6JMU56CU.js} +130 -131
- package/dist/chunk-6JMU56CU.js.map +1 -0
- package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
- package/dist/chunk-CQLQPQ4P.js.map +1 -0
- package/dist/chunk-CTRKTLYZ.js +33 -0
- package/dist/chunk-CTRKTLYZ.js.map +1 -0
- package/dist/chunk-EQIB2PGC.js +458 -0
- package/dist/chunk-EQIB2PGC.js.map +1 -0
- package/dist/chunk-HEORBLVT.js +7128 -0
- package/dist/chunk-HEORBLVT.js.map +1 -0
- package/dist/{chunk-BMW4TDP5.js → chunk-JDDIZSLL.js} +3 -3
- package/dist/{chunk-BMW4TDP5.js.map → chunk-JDDIZSLL.js.map} +1 -1
- package/dist/chunk-LITE5YFB.js +669 -0
- package/dist/chunk-LITE5YFB.js.map +1 -0
- package/dist/{chunk-QCICIHEO.js → chunk-ROQDRXKW.js} +716 -311
- package/dist/chunk-ROQDRXKW.js.map +1 -0
- package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
- package/dist/chunk-XJ6N32QP.js.map +1 -0
- package/dist/{chunk-WTI26YTM.js → chunk-YQJNT4XQ.js} +40 -13
- package/dist/chunk-YQJNT4XQ.js.map +1 -0
- package/dist/controls/index.d.ts +4 -28
- package/dist/controls/index.js +3 -3
- package/dist/dragdrop/index.d.ts +9 -5
- package/dist/dragdrop/index.js +3 -3
- package/dist/index.d.ts +518 -190
- package/dist/index.js +1753 -75
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +187 -0
- package/dist/loupe/index.js +7 -0
- package/dist/loupe/index.js.map +1 -0
- package/dist/passthrough/weasel-canvas.d.ts +3 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +138 -91
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +5 -5
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/styles.css +229 -1
- package/dist/surface/index.d.ts +18 -2
- package/dist/surface/index.js +2 -2
- package/dist/ui/layers/index.js +2 -2
- package/dist/undo/index.d.ts +5 -4
- package/package.json +14 -7
- package/src/annotations/AnnotationOverlay.tsx +205 -0
- package/src/annotations/AnnotationTargets.tsx +37 -0
- package/src/annotations/Annotations.less +129 -0
- package/src/annotations/Annotations.meaning.test.tsx +111 -0
- package/src/annotations/Annotations.overlay.test.tsx +142 -0
- package/src/annotations/AnnotationsContext.ts +49 -0
- package/src/annotations/ExportMenu.test.tsx +86 -0
- package/src/annotations/ExportMenu.tsx +182 -0
- package/src/annotations/MarkList.tsx +75 -0
- package/src/annotations/capture.test.ts +140 -0
- package/src/annotations/capture.ts +237 -0
- package/src/annotations/drawOne.test.ts +56 -0
- package/src/annotations/drawOne.ts +47 -0
- package/src/annotations/frac.test.ts +59 -0
- package/src/annotations/frac.ts +66 -0
- package/src/annotations/history.test.ts +82 -0
- package/src/annotations/history.ts +118 -0
- package/src/annotations/index.ts +37 -0
- package/src/annotations/paint.test.ts +125 -0
- package/src/annotations/paint.ts +130 -0
- package/src/annotations/staleness.test.ts +56 -0
- package/src/annotations/staleness.ts +41 -0
- package/src/annotations/store.test.ts +175 -0
- package/src/annotations/store.ts +317 -0
- package/src/annotations/svgNodes.test.ts +77 -0
- package/src/annotations/svgNodes.ts +74 -0
- package/src/annotations/toolMap.test.ts +36 -0
- package/src/annotations/toolMap.ts +54 -0
- package/src/annotations/types.ts +222 -0
- package/src/annotations/view.test.ts +37 -0
- package/src/annotations/view.ts +40 -0
- package/src/canvas/AGENTS.md +55 -5
- package/src/canvas/CanvasStack.test.tsx +27 -1
- package/src/canvas/CanvasStack.tsx +29 -4
- package/src/canvas/CanvasStackContext.ts +23 -4
- package/src/canvas/camera.test.ts +78 -0
- package/src/canvas/camera.ts +62 -0
- package/src/canvas/canvasCoords.test.ts +26 -0
- package/src/canvas/canvasCoords.ts +20 -7
- package/src/canvas/index.ts +5 -1
- package/src/canvas/useLayerScheduler.ts +7 -3
- package/src/canvas/usePanZoom.test.ts +36 -5
- package/src/canvas/usePanZoom.ts +20 -15
- package/src/canvas/worldSpec.test.ts +63 -0
- package/src/canvas/worldSpec.ts +48 -0
- package/src/chrome/builtins.test.ts +31 -1
- package/src/chrome/builtins.tsx +63 -19
- package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
- package/src/chrome/regions/PaletteRegion.tsx +4 -0
- package/src/chrome/regions/SidebarRegion.tsx +37 -13
- package/src/chrome/regions/ToolbarRegion.tsx +2 -1
- package/src/chrome/regions/ViewportRegion.tsx +9 -1
- package/src/chrome/regions/regions.test.tsx +6 -1
- package/src/chrome/types.ts +20 -0
- package/src/controls/ControlPanel.test.tsx +38 -0
- package/src/controls/ControlPanel.tsx +50 -3
- package/src/dragdrop/DragDropRuntime.tsx +8 -2
- package/src/dragdrop/dragDrop.test.tsx +29 -2
- package/src/index.test.ts +22 -0
- package/src/index.ts +87 -2
- package/src/instrument/SineWave.smoke.test.tsx +1 -0
- package/src/instrument/types.ts +24 -1
- package/src/lab/Lab.surface.test.tsx +162 -0
- package/src/lab/Lab.tsx +102 -17
- package/src/lab/Lab.undock.test.tsx +85 -0
- package/src/lab/LabFullChrome.stories.tsx +20 -0
- package/src/lab/LabShell.less +11 -0
- package/src/lab/Workspace.less +73 -0
- package/src/lab/Workspace.surface.test.tsx +2 -0
- package/src/lab/Workspace.tsx +97 -8
- package/src/lab/index.ts +1 -1
- package/src/lab/panelHost.ts +37 -0
- package/src/loupe/AGENTS.md +49 -0
- package/src/loupe/CanvasLoupe.tsx +83 -0
- package/src/loupe/DomLoupe.tsx +62 -0
- package/src/loupe/Loupe.less +38 -0
- package/src/loupe/LoupeBubble.tsx +32 -0
- package/src/loupe/TrialLoupe.tsx +93 -0
- package/src/loupe/canvasLens.test.ts +205 -0
- package/src/loupe/canvasLens.ts +141 -0
- package/src/loupe/index.ts +19 -0
- package/src/loupe/types.test.ts +29 -0
- package/src/loupe/types.ts +90 -0
- package/src/loupe/useHostSize.ts +30 -0
- package/src/loupe/useLoupe.test.tsx +197 -0
- package/src/loupe/useLoupe.ts +189 -0
- package/src/passthrough/weasel-ui.ts +3 -0
- package/src/primitives/FloatingPanel.test.tsx +76 -1
- package/src/primitives/FloatingPanel.tsx +19 -3
- package/src/primitives/ScaleIndicator.test.tsx +7 -2
- package/src/primitives/Sidebar.less +18 -0
- package/src/primitives/Toolbar.less +8 -0
- package/src/primitives/Toolbar.tsx +5 -0
- package/src/primitives/useRovingTabIndex.test.ts +21 -0
- package/src/primitives/useRovingTabIndex.ts +26 -17
- package/src/state/document.test.ts +23 -0
- package/src/state/document.ts +14 -2
- package/src/state/index.ts +2 -0
- package/src/state/store.test.ts +24 -0
- package/src/state/store.ts +35 -1
- package/src/state/types.ts +11 -0
- package/src/state/undock.test.ts +40 -0
- package/src/state/undock.ts +39 -0
- package/src/styles.less +2 -0
- package/src/surface/SurfaceContext.ts +4 -0
- package/src/surface/index.ts +8 -3
- package/src/surface/useSurfaceTile.test.tsx +2 -0
- package/src/surface/useSurfaceTile.ts +7 -1
- package/src/surface/useTiledSurface.test.tsx +58 -0
- package/src/surface/useTiledSurface.ts +38 -3
- package/src/trial/Trial.annotations.persist.test.tsx +166 -0
- package/src/trial/Trial.annotations.test.tsx +121 -0
- package/src/trial/Trial.canvas.test.tsx +76 -1
- package/src/trial/Trial.less +19 -0
- package/src/trial/Trial.loupe.test.tsx +128 -0
- package/src/trial/Trial.stories.tsx +1 -0
- package/src/trial/Trial.test.tsx +12 -0
- package/src/trial/Trial.trialId.test.tsx +36 -0
- package/src/trial/Trial.tsx +215 -40
- package/src/trial/TrialChrome.tsx +35 -0
- package/src/trial/UndockedSections.tsx +65 -0
- package/src/trial/trialOps.test.ts +21 -0
- package/src/trial/trialOps.ts +5 -3
- package/dist/_dts/types-BttTIent.d.ts +0 -314
- package/dist/chunk-3XXPU73K.js.map +0 -1
- package/dist/chunk-4TMMVIDM.js.map +0 -1
- package/dist/chunk-6X5RBC6G.js +0 -366
- package/dist/chunk-6X5RBC6G.js.map +0 -1
- package/dist/chunk-BDJXIBRZ.js.map +0 -1
- package/dist/chunk-CPUJ3QXL.js.map +0 -1
- package/dist/chunk-JDROYQM3.js.map +0 -1
- package/dist/chunk-LGKRIUSW.js.map +0 -1
- package/dist/chunk-QCICIHEO.js.map +0 -1
- package/dist/chunk-QRYPXSGP.js.map +0 -1
- package/dist/chunk-RGT4EJIB.js +0 -6961
- package/dist/chunk-RGT4EJIB.js.map +0 -1
- package/dist/chunk-T66HU3HX.js +0 -17
- package/dist/chunk-T66HU3HX.js.map +0 -1
- package/dist/chunk-WTI26YTM.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,35 +1,1401 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { createEventBus, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
|
|
2
|
+
export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
|
|
3
|
+
import { useDragDrop, DragOverlay, Palette } from './chunk-5TCEVMPI.js';
|
|
4
|
+
import { useSurfaceOptional, useSurfaceCanvas, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-4EY67BKJ.js';
|
|
5
|
+
export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-4EY67BKJ.js';
|
|
4
6
|
import { useJob } from './chunk-LN6JDUGB.js';
|
|
5
7
|
export { useJob } from './chunk-LN6JDUGB.js';
|
|
6
|
-
import { suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, SidebarRegion, ViewportRegion, StatusRegion } from './chunk-
|
|
7
|
-
export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from './chunk-
|
|
8
|
-
import { JobProgress } from './chunk-
|
|
9
|
-
export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-
|
|
10
|
-
export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-
|
|
11
|
-
import { LabStoreContext, createLabStore, noneAdapter } from './chunk-
|
|
12
|
-
export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, labDocumentKey, localStorageAdapter, noneAdapter, quarantineKey, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-
|
|
13
|
-
import { fromConfigFields } from './chunk-
|
|
14
|
-
export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-
|
|
15
|
-
import { CanvasStack } from './chunk-
|
|
16
|
-
export { CanvasStack } from './chunk-
|
|
17
|
-
|
|
18
|
-
export {
|
|
19
|
-
import { LayerList } from './chunk-BMW4TDP5.js';
|
|
20
|
-
export { LayerList } from './chunk-BMW4TDP5.js';
|
|
8
|
+
import { ANNOTATION_WEASEL_TOOLS, annotationToolInfo, suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, SidebarRegion, ViewportRegion, StatusRegion, AnnotationsContext } from './chunk-LITE5YFB.js';
|
|
9
|
+
export { ANNOTATION_TOOLS, ANNOTATION_WEASEL_TOOLS, AnnotationsContext, ExportMenu, MarkList, PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, annotationToolInfo, builtinContributions, mergeContributions, suppressContributions, useAnnotations, useAnnotationsOptional } from './chunk-LITE5YFB.js';
|
|
10
|
+
import { JobProgress } from './chunk-3P5TTTA7.js';
|
|
11
|
+
export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-3P5TTTA7.js';
|
|
12
|
+
export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-6JMU56CU.js';
|
|
13
|
+
import { LabStoreContext, TrialIdProvider, createLabStore, panelKey, noneAdapter } from './chunk-XJ6N32QP.js';
|
|
14
|
+
export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-XJ6N32QP.js';
|
|
15
|
+
import { fromConfigFields } from './chunk-YQJNT4XQ.js';
|
|
16
|
+
export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-YQJNT4XQ.js';
|
|
17
|
+
import { CanvasStack } from './chunk-3ZELRRBV.js';
|
|
18
|
+
export { CanvasStack } from './chunk-3ZELRRBV.js';
|
|
19
|
+
import { LayerList } from './chunk-JDDIZSLL.js';
|
|
20
|
+
export { LayerList } from './chunk-JDDIZSLL.js';
|
|
21
21
|
import './chunk-SBXNQW4G.js';
|
|
22
|
-
import {
|
|
23
|
-
export {
|
|
24
|
-
import './chunk-
|
|
25
|
-
|
|
26
|
-
export {
|
|
27
|
-
import {
|
|
28
|
-
|
|
22
|
+
import { C, p, r2 } from './chunk-HEORBLVT.js';
|
|
23
|
+
export { k2 as CheckboxRow, O2 as ColorRow, m2 as CurveField, _3 as EffectCard, y2 as EffectCardList, c3 as LayerStack, j2 as NumberRow, b3 as PropertyGroup, S2 as PropertyList, x3 as PropertyPanel, w as PropertyRow, C2 as PropertySpan, M as SelectRow, E as SliderRow, g as Subpanel, A2 as TextRow, N as ToggleRow } from './chunk-HEORBLVT.js';
|
|
24
|
+
import { resolveLoupe, TrialLoupe } from './chunk-EQIB2PGC.js';
|
|
25
|
+
export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-EQIB2PGC.js';
|
|
26
|
+
export { centerOn, zoomAt } from './chunk-CTRKTLYZ.js';
|
|
27
|
+
import { applyCamera } from './chunk-2AYGEN57.js';
|
|
28
|
+
export { DEFAULT_FRAME, applyCamera, resolveFrame, screenToWorld, worldToScreen } from './chunk-2AYGEN57.js';
|
|
29
|
+
export { CanvasStackContext } from './chunk-CQLQPQ4P.js';
|
|
30
|
+
import { textCommand, linePath, markerDrawCommands, ellipsePath, rectPath, createScene, asNodeId as asNodeId$1, WeaselProvider, SceneCanvas, sceneFromJSON, PathBuilder, renderSceneToPixels, useActiveToolContext, resolveStrokeWidth, getMarker, getPaintKind, boundsOfPath, tileGeometry, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, SCRIPT_METRICS } from './chunk-ROQDRXKW.js';
|
|
31
|
+
import { createContext, useState, useRef, useEffect, useCallback, useContext, useMemo, Children, useLayoutEffect, useSyncExternalStore } from 'react';
|
|
32
|
+
import { createPortal } from 'react-dom';
|
|
33
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
29
34
|
import { useStore } from 'zustand/react';
|
|
30
35
|
import { Provider, StrategyRegistryProvider, DragProvider, Container, useDragHandle } from 'windease/react';
|
|
31
|
-
import { asNodeId, Store, createNode, gridStrategy } from 'windease';
|
|
36
|
+
import { asNodeId, Store, createNode, floatingStrategy, gridStrategy } from 'windease';
|
|
37
|
+
|
|
38
|
+
// ../svg/dist/index.js
|
|
39
|
+
var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
|
|
40
|
+
function multiply(a, b2) {
|
|
41
|
+
return [
|
|
42
|
+
a[0] * b2[0] + a[2] * b2[1],
|
|
43
|
+
a[1] * b2[0] + a[3] * b2[1],
|
|
44
|
+
a[0] * b2[2] + a[2] * b2[3],
|
|
45
|
+
a[1] * b2[2] + a[3] * b2[3],
|
|
46
|
+
a[0] * b2[4] + a[2] * b2[5] + a[4],
|
|
47
|
+
a[1] * b2[4] + a[3] * b2[5] + a[5]
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
function isIdentity(m2, eps = 1e-9) {
|
|
51
|
+
return Math.abs(m2[0] - 1) < eps && Math.abs(m2[1]) < eps && Math.abs(m2[2]) < eps && Math.abs(m2[3] - 1) < eps && Math.abs(m2[4]) < eps && Math.abs(m2[5]) < eps;
|
|
52
|
+
}
|
|
53
|
+
function formatMatrix(m2) {
|
|
54
|
+
if (isIdentity(m2)) return null;
|
|
55
|
+
return `matrix(${m2.map((n) => trimNumber(n)).join(" ")})`;
|
|
56
|
+
}
|
|
57
|
+
function trimNumber(n) {
|
|
58
|
+
if (!Number.isFinite(n)) return "0";
|
|
59
|
+
const r2 = Math.round(n * 1e6) / 1e6;
|
|
60
|
+
if (Number.isInteger(r2)) return String(r2);
|
|
61
|
+
return String(r2);
|
|
62
|
+
}
|
|
63
|
+
function patternSpecOf(paint) {
|
|
64
|
+
return "tile" in paint.pattern ? paint.pattern : null;
|
|
65
|
+
}
|
|
66
|
+
function patternXml(id, paint, onWarn) {
|
|
67
|
+
const spec = patternSpecOf(paint);
|
|
68
|
+
if (!spec) {
|
|
69
|
+
onWarn?.("pattern fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
const geometry = tileGeometry(spec);
|
|
73
|
+
const body = geometry.shapes.map(shapeXml).join("");
|
|
74
|
+
const origin = paint.origin ?? { x: 0, y: 0 };
|
|
75
|
+
const attrs = [
|
|
76
|
+
`id="${id}"`,
|
|
77
|
+
'patternUnits="userSpaceOnUse"',
|
|
78
|
+
`width="${trimNumber(geometry.size)}"`,
|
|
79
|
+
`height="${trimNumber(geometry.size)}"`
|
|
80
|
+
];
|
|
81
|
+
if (origin.x !== 0 || origin.y !== 0) {
|
|
82
|
+
attrs.push(`x="${trimNumber(origin.x)}"`, `y="${trimNumber(origin.y)}"`);
|
|
83
|
+
}
|
|
84
|
+
attrs.push(`data-weasel-tile="${escapeAttr(JSON.stringify(spec))}"`);
|
|
85
|
+
return `<pattern ${attrs.join(" ")}>${body}</pattern>`;
|
|
86
|
+
}
|
|
87
|
+
function shapeXml(shape) {
|
|
88
|
+
switch (shape.kind) {
|
|
89
|
+
case "line":
|
|
90
|
+
return `<line x1="${trimNumber(shape.x1)}" y1="${trimNumber(shape.y1)}" x2="${trimNumber(shape.x2)}" y2="${trimNumber(shape.y2)}" stroke="${shape.color}" stroke-width="${trimNumber(shape.width)}"/>`;
|
|
91
|
+
case "circle":
|
|
92
|
+
return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${shape.color}"/>`;
|
|
93
|
+
case "ellipse": {
|
|
94
|
+
const deg = shape.rotation * 180 / Math.PI;
|
|
95
|
+
const transform = deg === 0 ? "" : ` transform="rotate(${trimNumber(deg)} ${trimNumber(shape.cx)} ${trimNumber(shape.cy)})"`;
|
|
96
|
+
return `<ellipse cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" rx="${trimNumber(shape.rx)}" ry="${trimNumber(shape.ry)}" fill="${shape.color}"${transform}/>`;
|
|
97
|
+
}
|
|
98
|
+
case "rect":
|
|
99
|
+
return `<rect x="${trimNumber(shape.x)}" y="${trimNumber(shape.y)}" width="${trimNumber(shape.width)}" height="${trimNumber(shape.height)}" fill="${shape.color}"/>`;
|
|
100
|
+
default:
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function escapeAttr(s) {
|
|
105
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
106
|
+
}
|
|
107
|
+
function serializePathD(path) {
|
|
108
|
+
if (path.kind === "rect") {
|
|
109
|
+
const { x: x2, y: y2, width, height } = path;
|
|
110
|
+
return `M${trimNumber(x2)} ${trimNumber(y2)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
|
|
111
|
+
}
|
|
112
|
+
const cmds = path.commands;
|
|
113
|
+
const xs = path.coords;
|
|
114
|
+
const out = [];
|
|
115
|
+
let ci = 0;
|
|
116
|
+
for (let i = 0; i < cmds.length; i++) {
|
|
117
|
+
const c2 = cmds[i];
|
|
118
|
+
if (c2 === PATH_M) {
|
|
119
|
+
out.push(`M${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
|
|
120
|
+
ci += 2;
|
|
121
|
+
} else if (c2 === PATH_L) {
|
|
122
|
+
out.push(`L${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
|
|
123
|
+
ci += 2;
|
|
124
|
+
} else if (c2 === PATH_C) {
|
|
125
|
+
out.push(
|
|
126
|
+
`C${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])} ${trimNumber(xs[ci + 4])} ${trimNumber(xs[ci + 5])}`
|
|
127
|
+
);
|
|
128
|
+
ci += 6;
|
|
129
|
+
} else if (c2 === PATH_Q) {
|
|
130
|
+
out.push(
|
|
131
|
+
`Q${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])}`
|
|
132
|
+
);
|
|
133
|
+
ci += 4;
|
|
134
|
+
} else if (c2 === PATH_Z) {
|
|
135
|
+
out.push("Z");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out.join("");
|
|
139
|
+
}
|
|
140
|
+
var PaintServerRegistry = class {
|
|
141
|
+
byPaint = /* @__PURE__ */ new Map();
|
|
142
|
+
order = [];
|
|
143
|
+
counter = 0;
|
|
144
|
+
// Marker keys referenced by any stroke, in first-use order. The `<defs>`
|
|
145
|
+
// id is the marker key itself, not a minted counter id: `parseSvg` reads a
|
|
146
|
+
// `marker-end="url(#id)"` fragment back as the marker key directly (it
|
|
147
|
+
// never inspects the `<marker>` element), so a synthetic id would come
|
|
148
|
+
// back unresolvable and warn on round-trip.
|
|
149
|
+
markerKeys = [];
|
|
150
|
+
markerKeySet = /* @__PURE__ */ new Set();
|
|
151
|
+
register(paint) {
|
|
152
|
+
const existing = this.byPaint.get(paint);
|
|
153
|
+
if (existing) return existing;
|
|
154
|
+
const id = paint.fill === "pattern" ? `pat${this.counter++}` : `grad${this.counter++}`;
|
|
155
|
+
this.byPaint.set(paint, id);
|
|
156
|
+
this.order.push(paint);
|
|
157
|
+
return id;
|
|
158
|
+
}
|
|
159
|
+
/** The `<defs>` id for a marker key — the key itself, minting nothing.
|
|
160
|
+
* `undefined` when nothing is registered under `key`, so a caller emits no
|
|
161
|
+
* attribute at all rather than a `url(#…)` pointing at a def that will
|
|
162
|
+
* never be written. */
|
|
163
|
+
markerId(key) {
|
|
164
|
+
if (getMarker(key) === void 0) return void 0;
|
|
165
|
+
if (!this.markerKeySet.has(key)) {
|
|
166
|
+
this.markerKeySet.add(key);
|
|
167
|
+
this.markerKeys.push(key);
|
|
168
|
+
}
|
|
169
|
+
return key;
|
|
170
|
+
}
|
|
171
|
+
/** Emit `<defs>...</defs>` XML for every registered paint server and marker. */
|
|
172
|
+
toDefsXml(onWarn) {
|
|
173
|
+
if (this.order.length === 0 && this.markerKeys.length === 0) return "";
|
|
174
|
+
const parts = ["<defs>"];
|
|
175
|
+
for (const paint of this.order) {
|
|
176
|
+
parts.push(paintServerXml(this.byPaint.get(paint), paint, onWarn));
|
|
177
|
+
}
|
|
178
|
+
for (const key of this.markerKeys) {
|
|
179
|
+
const entry = getMarker(key);
|
|
180
|
+
if (!entry) continue;
|
|
181
|
+
parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry));
|
|
182
|
+
}
|
|
183
|
+
parts.push("</defs>");
|
|
184
|
+
return parts.join("");
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function defaultMarkerXml(id, entry) {
|
|
188
|
+
const path = entry.path({ size: 1, stroke: { paint: { fill: "solid", color: "#000" } } });
|
|
189
|
+
const d = serializePathD(path);
|
|
190
|
+
const fill = entry.fill === "none" ? "none" : "context-stroke";
|
|
191
|
+
const outline = entry.outline ? ` stroke="context-stroke" stroke-width="${entry.outline.width}" stroke-linecap="round" stroke-linejoin="round"` : "";
|
|
192
|
+
return `<marker id="${id}" markerUnits="strokeWidth" markerWidth="8" markerHeight="8" refX="0" refY="0" orient="auto" overflow="visible"><path d="${d}" fill="${fill}"${outline}/></marker>`;
|
|
193
|
+
}
|
|
194
|
+
function paintServerXml(id, paint, onWarn) {
|
|
195
|
+
if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
|
|
196
|
+
const builtin = gradientXml(id, paint);
|
|
197
|
+
if (builtin) return builtin;
|
|
198
|
+
const custom = getPaintKind(paint.fill)?.toSvg?.(id, paint);
|
|
199
|
+
if (custom) return custom;
|
|
200
|
+
onWarn?.(`${paint.fill} fill has no vector form \u2014 omitted from <defs>`);
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
function gradientUnitsAttr(units) {
|
|
204
|
+
return units === "bounds" ? "objectBoundingBox" : "userSpaceOnUse";
|
|
205
|
+
}
|
|
206
|
+
function gradientXml(id, paint) {
|
|
207
|
+
if (paint.fill === "linear-gradient") {
|
|
208
|
+
const stops = paint.stops.map(stopXml).join("");
|
|
209
|
+
return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}">${stops}</linearGradient>`;
|
|
210
|
+
}
|
|
211
|
+
if (paint.fill === "radial-gradient") {
|
|
212
|
+
const stops = paint.stops.map(stopXml).join("");
|
|
213
|
+
return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}">${stops}</radialGradient>`;
|
|
214
|
+
}
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
function stopXml(s) {
|
|
218
|
+
const m2 = /^#([0-9a-f]{6})([0-9a-f]{2})$/i.exec(s.color);
|
|
219
|
+
if (m2) {
|
|
220
|
+
const alpha = parseInt(m2[2], 16) / 255;
|
|
221
|
+
return `<stop offset="${trimNumber(s.offset)}" stop-color="#${m2[1].toLowerCase()}" stop-opacity="${trimNumber(alpha)}"/>`;
|
|
222
|
+
}
|
|
223
|
+
return `<stop offset="${trimNumber(s.offset)}" stop-color="${s.color}"/>`;
|
|
224
|
+
}
|
|
225
|
+
function serializeSvg(nodes, opts = {}) {
|
|
226
|
+
const registry = new PaintServerRegistry();
|
|
227
|
+
registerPaintServers(nodes, registry);
|
|
228
|
+
const bounds = opts.viewBox ?? computeBounds(nodes);
|
|
229
|
+
const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
|
|
230
|
+
const namespaces = opts.namespaces ?? {};
|
|
231
|
+
const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
|
|
232
|
+
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
233
|
+
rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
|
|
234
|
+
}
|
|
235
|
+
rootAttrs.push(`viewBox="${vb}"`);
|
|
236
|
+
if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
|
|
237
|
+
if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
|
|
238
|
+
if (opts.documentMeta) {
|
|
239
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
240
|
+
const bucket = opts.documentMeta[prefix];
|
|
241
|
+
if (!bucket?.attrs) continue;
|
|
242
|
+
for (const [name, value] of Object.entries(bucket.attrs)) {
|
|
243
|
+
rootAttrs.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
let docMetaXml = "";
|
|
248
|
+
if (opts.documentMeta) {
|
|
249
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
250
|
+
const bucket = opts.documentMeta[prefix];
|
|
251
|
+
if (!bucket?.elements) continue;
|
|
252
|
+
for (const [localName, list] of Object.entries(bucket.elements)) {
|
|
253
|
+
for (const el of list) docMetaXml += namespacedElementXml(prefix, localName, el);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const defsXml = registry.toDefsXml(opts.onWarn);
|
|
258
|
+
const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces)).join("");
|
|
259
|
+
const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
|
|
260
|
+
return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
|
|
261
|
+
}
|
|
262
|
+
function namespacedElementXml(prefix, localName, el) {
|
|
263
|
+
const attrs = [];
|
|
264
|
+
for (const [name, value] of Object.entries(el.attrs)) {
|
|
265
|
+
attrs.push(`${name}="${escapeAttr2(value)}"`);
|
|
266
|
+
}
|
|
267
|
+
const head = attrs.length > 0 ? `<${prefix}:${localName} ${attrs.join(" ")}>` : `<${prefix}:${localName}>`;
|
|
268
|
+
let body = "";
|
|
269
|
+
if (el.children) {
|
|
270
|
+
for (const [childName, list] of Object.entries(el.children)) {
|
|
271
|
+
for (const child of list) body += namespacedElementXml(prefix, childName, child);
|
|
272
|
+
}
|
|
273
|
+
} else if (el.text != null) {
|
|
274
|
+
body = escapeText(el.text);
|
|
275
|
+
}
|
|
276
|
+
return `${head}${body}</${prefix}:${localName}>`;
|
|
277
|
+
}
|
|
278
|
+
function metaAttrsXml(meta, namespaces) {
|
|
279
|
+
if (!meta) return "";
|
|
280
|
+
const parts = [];
|
|
281
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
282
|
+
const bucket = meta[prefix];
|
|
283
|
+
if (!bucket?.attrs) continue;
|
|
284
|
+
for (const [name, value] of Object.entries(bucket.attrs)) {
|
|
285
|
+
parts.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
289
|
+
}
|
|
290
|
+
function metaElementsXml(meta, namespaces) {
|
|
291
|
+
if (!meta) return "";
|
|
292
|
+
let out = "";
|
|
293
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
294
|
+
const bucket = meta[prefix];
|
|
295
|
+
if (!bucket?.elements) continue;
|
|
296
|
+
for (const [localName, list] of Object.entries(bucket.elements)) {
|
|
297
|
+
for (const el of list) out += namespacedElementXml(prefix, localName, el);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
function registerPaintServers(nodes, registry) {
|
|
303
|
+
for (const n of nodes) {
|
|
304
|
+
if (n.kind === "group") {
|
|
305
|
+
registerPaintServers(n.children, registry);
|
|
306
|
+
} else if (n.kind === "path") {
|
|
307
|
+
if (n.fill.kind === "gradient") registry.register(n.fill.paint);
|
|
308
|
+
if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
|
|
309
|
+
registerMarkers(n.stroke, registry);
|
|
310
|
+
} else if (n.kind === "text") {
|
|
311
|
+
registerTextPaint(n.fill, registry);
|
|
312
|
+
registerTextPaint(n.stroke?.paint, registry);
|
|
313
|
+
registerMarkers(n.stroke, registry);
|
|
314
|
+
for (const run of n.runs ?? []) {
|
|
315
|
+
registerTextPaint(run.fill, registry);
|
|
316
|
+
registerTextPaint(run.stroke?.paint, registry);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function registerTextPaint(paint, registry) {
|
|
322
|
+
if (paint && !("color" in paint)) registry.register(paint);
|
|
323
|
+
}
|
|
324
|
+
function markerKeyOfRef(ref) {
|
|
325
|
+
if (typeof ref === "string") return ref;
|
|
326
|
+
if (ref && typeof ref === "object" && "key" in ref) return String(ref.key);
|
|
327
|
+
return void 0;
|
|
328
|
+
}
|
|
329
|
+
function registerMarkers(stroke, registry) {
|
|
330
|
+
if (!stroke) return;
|
|
331
|
+
for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
|
|
332
|
+
const key = markerKeyOfRef(ref);
|
|
333
|
+
if (key) registry.markerId(key);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function nodeXml(node, registry, namespaces) {
|
|
337
|
+
if (node.kind === "group") return groupXml(node, registry, namespaces);
|
|
338
|
+
if (node.kind === "text") return textXml(node, registry, namespaces);
|
|
339
|
+
if (node.kind === "image") return imageXml(node, namespaces);
|
|
340
|
+
return pathXml(node, registry, namespaces);
|
|
341
|
+
}
|
|
342
|
+
function imageXml(node, namespaces) {
|
|
343
|
+
const attrs = [
|
|
344
|
+
`href="${escapeAttr2(node.href)}"`,
|
|
345
|
+
`x="${trimNumber(node.x)}"`,
|
|
346
|
+
`y="${trimNumber(node.y)}"`,
|
|
347
|
+
`width="${trimNumber(node.width)}"`,
|
|
348
|
+
`height="${trimNumber(node.height)}"`,
|
|
349
|
+
`preserveAspectRatio="none"`
|
|
350
|
+
];
|
|
351
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
352
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
353
|
+
}
|
|
354
|
+
if (node.rotation != null && node.rotation !== 0) {
|
|
355
|
+
const cx = node.x + node.width / 2;
|
|
356
|
+
const cy = node.y + node.height / 2;
|
|
357
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
358
|
+
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
359
|
+
}
|
|
360
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
361
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
362
|
+
if (metaEls) return `<image ${attrs.join(" ")}${metaAttrs}>${metaEls}</image>`;
|
|
363
|
+
return `<image ${attrs.join(" ")}${metaAttrs}/>`;
|
|
364
|
+
}
|
|
365
|
+
function groupXml(node, registry, namespaces) {
|
|
366
|
+
const attrs = [];
|
|
367
|
+
if (node.transform) {
|
|
368
|
+
const m2 = formatMatrix(node.transform);
|
|
369
|
+
if (m2) attrs.push(`transform="${m2}"`);
|
|
370
|
+
}
|
|
371
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
372
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
373
|
+
}
|
|
374
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
375
|
+
const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
|
|
376
|
+
const body = node.children.map((c2) => nodeXml(c2, registry, namespaces)).join("");
|
|
377
|
+
return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
|
|
378
|
+
}
|
|
379
|
+
function pathXml(node, registry, namespaces) {
|
|
380
|
+
const attrs = [`d="${serializePathD(node.path)}"`];
|
|
381
|
+
const fillAttrs = paintAttrs(node.fill, "fill", registry);
|
|
382
|
+
for (const a of fillAttrs) attrs.push(a);
|
|
383
|
+
if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
|
|
384
|
+
attrs.push(`fill-rule="evenodd"`);
|
|
385
|
+
}
|
|
386
|
+
if (node.stroke) {
|
|
387
|
+
const strokeAttrs = strokeAttrsFor(node.stroke, registry);
|
|
388
|
+
for (const a of strokeAttrs) attrs.push(a);
|
|
389
|
+
} else {
|
|
390
|
+
attrs.push('stroke="none"');
|
|
391
|
+
}
|
|
392
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
393
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
394
|
+
}
|
|
395
|
+
if (node.rotation != null && node.rotation !== 0) {
|
|
396
|
+
const b2 = pathBounds(node.path);
|
|
397
|
+
const cx = b2.minX + (b2.maxX - b2.minX) / 2;
|
|
398
|
+
const cy = b2.minY + (b2.maxY - b2.minY) / 2;
|
|
399
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
400
|
+
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
401
|
+
}
|
|
402
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
403
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
404
|
+
if (metaEls) {
|
|
405
|
+
return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
|
|
406
|
+
}
|
|
407
|
+
return `<path ${attrs.join(" ")}${metaAttrs}/>`;
|
|
408
|
+
}
|
|
409
|
+
function paintAttrs(paint, name, registry, includeOpacity = true) {
|
|
410
|
+
if (paint.kind === "none") return [`${name}="none"`];
|
|
411
|
+
if (paint.kind === "solid") {
|
|
412
|
+
const out = [`${name}="${paint.color}"`];
|
|
413
|
+
if (includeOpacity && paint.opacity != null && paint.opacity !== 1) {
|
|
414
|
+
out.push(`${name}-opacity="${trimNumber(paint.opacity)}"`);
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
const id = registry.register(paint.paint);
|
|
419
|
+
return [`${name}="url(#${id})"`];
|
|
420
|
+
}
|
|
421
|
+
function coreStrokeAttrs(stroke, registry) {
|
|
422
|
+
if (!stroke) return [];
|
|
423
|
+
const width = resolveStrokeWidth(stroke.width ?? 1, 1);
|
|
424
|
+
if (!(width > 0)) return [];
|
|
425
|
+
const attrs = [];
|
|
426
|
+
if ("color" in stroke.paint) {
|
|
427
|
+
attrs.push(`stroke="${stroke.paint.color}"`);
|
|
428
|
+
if (stroke.paint.opacity != null && stroke.paint.opacity !== 1) {
|
|
429
|
+
attrs.push(`stroke-opacity="${trimNumber(stroke.paint.opacity)}"`);
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
attrs.push(`stroke="url(#${registry.register(stroke.paint)})"`);
|
|
433
|
+
}
|
|
434
|
+
attrs.push(`stroke-width="${trimNumber(width)}"`);
|
|
435
|
+
if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
436
|
+
if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
437
|
+
if (stroke.dash && stroke.dash.length > 0) {
|
|
438
|
+
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
439
|
+
}
|
|
440
|
+
if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
441
|
+
for (const [field, attr] of [
|
|
442
|
+
["markerStart", "marker-start"],
|
|
443
|
+
["markerMid", "marker-mid"],
|
|
444
|
+
["markerEnd", "marker-end"]
|
|
445
|
+
]) {
|
|
446
|
+
const ref = stroke[field];
|
|
447
|
+
if (ref === void 0) continue;
|
|
448
|
+
const key = typeof ref === "string" ? ref : ref.key;
|
|
449
|
+
const id = registry.markerId(key);
|
|
450
|
+
if (id === void 0) continue;
|
|
451
|
+
attrs.push(`${attr}="url(#${id})"`);
|
|
452
|
+
}
|
|
453
|
+
return attrs;
|
|
454
|
+
}
|
|
455
|
+
function strokeAttrsFor(stroke, registry) {
|
|
456
|
+
const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
|
|
457
|
+
attrs.push(`stroke-width="${trimNumber(stroke.width)}"`);
|
|
458
|
+
const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
|
|
459
|
+
if (opacity != null && opacity !== 1) {
|
|
460
|
+
attrs.push(`stroke-opacity="${trimNumber(opacity)}"`);
|
|
461
|
+
}
|
|
462
|
+
if (stroke.cap) {
|
|
463
|
+
attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
464
|
+
}
|
|
465
|
+
if (stroke.join) {
|
|
466
|
+
attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
467
|
+
}
|
|
468
|
+
if (stroke.dash && stroke.dash.length > 0) {
|
|
469
|
+
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
470
|
+
}
|
|
471
|
+
if (stroke.miterLimit != null) {
|
|
472
|
+
attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
473
|
+
}
|
|
474
|
+
for (const [field, attr] of [
|
|
475
|
+
["markerStart", "marker-start"],
|
|
476
|
+
["markerMid", "marker-mid"],
|
|
477
|
+
["markerEnd", "marker-end"]
|
|
478
|
+
]) {
|
|
479
|
+
const ref = stroke[field];
|
|
480
|
+
if (ref === void 0) continue;
|
|
481
|
+
const id = registry.markerId(ref);
|
|
482
|
+
if (id === void 0) continue;
|
|
483
|
+
attrs.push(`${attr}="url(#${id})"`);
|
|
484
|
+
}
|
|
485
|
+
return attrs;
|
|
486
|
+
}
|
|
487
|
+
function computeBounds(nodes) {
|
|
488
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
489
|
+
const visit = (n, ctm) => {
|
|
490
|
+
if (n.kind === "group") {
|
|
491
|
+
const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
|
|
492
|
+
n.children.forEach((c2) => visit(c2, childCtm));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const b2 = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
|
|
496
|
+
const local = n.rotation ? rotateAboutCenter(ctm, b2, n.rotation) : ctm;
|
|
497
|
+
for (const [x2, y2] of corners(b2)) {
|
|
498
|
+
const px = local[0] * x2 + local[2] * y2 + local[4];
|
|
499
|
+
const py = local[1] * x2 + local[3] * y2 + local[5];
|
|
500
|
+
if (px < minX) minX = px;
|
|
501
|
+
if (py < minY) minY = py;
|
|
502
|
+
if (px > maxX) maxX = px;
|
|
503
|
+
if (py > maxY) maxY = py;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
|
|
507
|
+
if (!Number.isFinite(minX)) {
|
|
508
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
509
|
+
}
|
|
510
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
511
|
+
}
|
|
512
|
+
function corners(b2) {
|
|
513
|
+
return [[b2.minX, b2.minY], [b2.maxX, b2.minY], [b2.maxX, b2.maxY], [b2.minX, b2.maxY]];
|
|
514
|
+
}
|
|
515
|
+
function rotateAboutCenter(ctm, b2, angle) {
|
|
516
|
+
const cx = (b2.minX + b2.maxX) / 2;
|
|
517
|
+
const cy = (b2.minY + b2.maxY) / 2;
|
|
518
|
+
const c2 = Math.cos(angle);
|
|
519
|
+
const s = Math.sin(angle);
|
|
520
|
+
return multiply(ctm, [c2, s, -s, c2, cx - cx * c2 + cy * s, cy - cx * s - cy * c2]);
|
|
521
|
+
}
|
|
522
|
+
function textXml(node, registry, namespaces) {
|
|
523
|
+
const attrs = [
|
|
524
|
+
`x="${trimNumber(node.x)}"`,
|
|
525
|
+
`y="${trimNumber(node.y)}"`,
|
|
526
|
+
`dominant-baseline="text-before-edge"`,
|
|
527
|
+
// The runs model stores real line breaks; SVG's default whitespace
|
|
528
|
+
// handling would collapse them (and any indentation) away on re-import.
|
|
529
|
+
`xml:space="preserve"`,
|
|
530
|
+
`data-weasel-width="${trimNumber(node.width)}"`,
|
|
531
|
+
`data-weasel-height="${trimNumber(node.height)}"`
|
|
532
|
+
];
|
|
533
|
+
const style = node.style;
|
|
534
|
+
if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(style.fontSize)}"`);
|
|
535
|
+
if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
|
|
536
|
+
if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
|
|
537
|
+
if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
|
|
538
|
+
const direction = style?.direction ?? "ltr";
|
|
539
|
+
if (direction === "rtl") attrs.push('direction="rtl"');
|
|
540
|
+
if (style?.align != null || direction === "rtl") {
|
|
541
|
+
const align = style?.align ?? "left";
|
|
542
|
+
const anchor = align === "center" ? "middle" : align === "start" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
|
|
543
|
+
if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
|
|
544
|
+
}
|
|
545
|
+
if (style?.letterSpacing != null && style.letterSpacing !== 0) {
|
|
546
|
+
attrs.push(`letter-spacing="${trimNumber(style.letterSpacing)}"`);
|
|
547
|
+
}
|
|
548
|
+
const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
|
|
549
|
+
if (decoration) attrs.push(`text-decoration="${decoration}"`);
|
|
550
|
+
if (node.fill) {
|
|
551
|
+
if ("color" in node.fill) {
|
|
552
|
+
attrs.push(`fill="${node.fill.color}"`);
|
|
553
|
+
if (node.fill.opacity != null && node.fill.opacity !== 1) {
|
|
554
|
+
attrs.push(`fill-opacity="${trimNumber(node.fill.opacity)}"`);
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
const id = registry.register(node.fill);
|
|
558
|
+
attrs.push(`fill="url(#${id})"`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (const a of coreStrokeAttrs(node.stroke, registry)) attrs.push(a);
|
|
562
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
563
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
564
|
+
}
|
|
565
|
+
if (node.rotation != null && node.rotation !== 0) {
|
|
566
|
+
const cx = node.x + node.width / 2;
|
|
567
|
+
const cy = node.y + node.height / 2;
|
|
568
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
569
|
+
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
570
|
+
}
|
|
571
|
+
const body = node.runs && node.runs.length > 0 ? node.runs.map((r2) => runXml(r2, registry)).join("") : escapeText(node.text);
|
|
572
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
573
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
574
|
+
return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
|
|
575
|
+
}
|
|
576
|
+
function runXml(run, registry) {
|
|
577
|
+
const attrs = [];
|
|
578
|
+
if (run.bold) attrs.push(`font-weight="700"`);
|
|
579
|
+
if (run.italic) attrs.push(`font-style="italic"`);
|
|
580
|
+
if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
|
|
581
|
+
const presetScale = run.script != null && run.baselineShift != null ? SCRIPT_METRICS[run.script].size : void 0;
|
|
582
|
+
const scale = run.fontScale ?? presetScale;
|
|
583
|
+
if (run.fontSize != null) attrs.push(`font-size="${trimNumber(run.fontSize)}"`);
|
|
584
|
+
else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
|
|
585
|
+
if (run.baselineShift != null) {
|
|
586
|
+
attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
|
|
587
|
+
} else if (run.script) {
|
|
588
|
+
attrs.push(`baseline-shift="${run.script}"`);
|
|
589
|
+
}
|
|
590
|
+
if (run.letterSpacing != null) {
|
|
591
|
+
attrs.push(`letter-spacing="${trimNumber(run.letterSpacing)}"`);
|
|
592
|
+
}
|
|
593
|
+
const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
|
|
594
|
+
if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
|
|
595
|
+
if (run.fill) {
|
|
596
|
+
if ("color" in run.fill) {
|
|
597
|
+
attrs.push(`fill="${run.fill.color}"`);
|
|
598
|
+
} else {
|
|
599
|
+
const id = registry.register(run.fill);
|
|
600
|
+
attrs.push(`fill="url(#${id})"`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
for (const a of coreStrokeAttrs(run.stroke, registry)) attrs.push(a);
|
|
604
|
+
const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
|
|
605
|
+
return `${head}${escapeText(run.text)}</tspan>`;
|
|
606
|
+
}
|
|
607
|
+
function textDecorationValue(underline, strikethrough, overline) {
|
|
608
|
+
const tokens = [];
|
|
609
|
+
if (underline) tokens.push("underline");
|
|
610
|
+
if (strikethrough) tokens.push("line-through");
|
|
611
|
+
if (overline) tokens.push("overline");
|
|
612
|
+
return tokens.length > 0 ? tokens.join(" ") : null;
|
|
613
|
+
}
|
|
614
|
+
function escapeAttr2(s) {
|
|
615
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
616
|
+
}
|
|
617
|
+
function escapeText(s) {
|
|
618
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
619
|
+
}
|
|
620
|
+
function pathBounds(path) {
|
|
621
|
+
if (path.kind === "rect") {
|
|
622
|
+
return {
|
|
623
|
+
minX: path.x,
|
|
624
|
+
minY: path.y,
|
|
625
|
+
maxX: path.x + path.width,
|
|
626
|
+
maxY: path.y + path.height
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const b2 = boundsOfPath(path);
|
|
630
|
+
return { minX: b2.x, minY: b2.y, maxX: b2.x + b2.width, maxY: b2.y + b2.height };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/annotations/paint.ts
|
|
634
|
+
var MARK_COLOR = "#e5484d";
|
|
635
|
+
var MARK_WIDTH = 2;
|
|
636
|
+
var TEXT_SIZE = 14;
|
|
637
|
+
var toWorld = (p2, content) => ({
|
|
638
|
+
x: p2.x * content.w,
|
|
639
|
+
y: p2.y * content.h
|
|
640
|
+
});
|
|
641
|
+
function vertices(m2, content) {
|
|
642
|
+
const stored = m2.data.points;
|
|
643
|
+
if (stored && stored.length >= 2) return stored.map((p2) => toWorld(p2, content));
|
|
644
|
+
const { x: x2, y: y2, width, height } = m2.pose;
|
|
645
|
+
return [
|
|
646
|
+
{ x: x2, y: y2 },
|
|
647
|
+
{ x: x2 + width, y: y2 + height }
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
function polyline(points) {
|
|
651
|
+
const b2 = new PathBuilder();
|
|
652
|
+
const first = points[0];
|
|
653
|
+
if (!first) return rectPath(0, 0, 0, 0);
|
|
654
|
+
b2.moveTo(first.x, first.y);
|
|
655
|
+
for (const p2 of points.slice(1)) b2.lineTo(p2.x, p2.y);
|
|
656
|
+
return b2.build();
|
|
657
|
+
}
|
|
658
|
+
function markCommands(m2, content, style = {}) {
|
|
659
|
+
const color = style.color ?? MARK_COLOR;
|
|
660
|
+
const stroke = {
|
|
661
|
+
paint: { color },
|
|
662
|
+
width: MARK_WIDTH,
|
|
663
|
+
cap: "round",
|
|
664
|
+
join: "round",
|
|
665
|
+
...style.stale ? { dash: [6, 4] } : {}
|
|
666
|
+
};
|
|
667
|
+
switch (m2.data.kind) {
|
|
668
|
+
case "rect":
|
|
669
|
+
return [
|
|
670
|
+
{ kind: "path", path: rectPath(m2.pose.x, m2.pose.y, m2.pose.width, m2.pose.height), stroke }
|
|
671
|
+
];
|
|
672
|
+
case "ellipse":
|
|
673
|
+
return [{ kind: "path", path: ellipsePath(m2.pose), stroke }];
|
|
674
|
+
case "line": {
|
|
675
|
+
const [a, b2] = vertices(m2, content);
|
|
676
|
+
return [{ kind: "path", path: linePath(a, b2), stroke }];
|
|
677
|
+
}
|
|
678
|
+
case "arrow": {
|
|
679
|
+
const [a, b2] = vertices(m2, content);
|
|
680
|
+
const marked = { ...stroke, markerEnd: "arrow" };
|
|
681
|
+
const path = linePath(a, b2);
|
|
682
|
+
return [
|
|
683
|
+
{ kind: "path", path, stroke: marked },
|
|
684
|
+
...markerDrawCommands(path, marked, MARK_WIDTH, void 0)
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
case "stroke":
|
|
688
|
+
return [{ kind: "path", path: polyline(vertices(m2, content)), stroke }];
|
|
689
|
+
case "text": {
|
|
690
|
+
const text = m2.data.title;
|
|
691
|
+
if (!text) return [];
|
|
692
|
+
return [
|
|
693
|
+
textCommand(
|
|
694
|
+
m2.pose.x,
|
|
695
|
+
m2.pose.y,
|
|
696
|
+
text,
|
|
697
|
+
{ fontSize: TEXT_SIZE },
|
|
698
|
+
void 0,
|
|
699
|
+
void 0,
|
|
700
|
+
void 0,
|
|
701
|
+
{
|
|
702
|
+
fill: { color }
|
|
703
|
+
}
|
|
704
|
+
)
|
|
705
|
+
];
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/annotations/staleness.ts
|
|
711
|
+
function seenFrom(config, keys) {
|
|
712
|
+
const out = {};
|
|
713
|
+
if (config === null || typeof config !== "object") return out;
|
|
714
|
+
const src = config;
|
|
715
|
+
for (const k2 of keys) {
|
|
716
|
+
if (k2 in src) out[k2] = src[k2];
|
|
717
|
+
}
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
function isStale(seen, config, keys) {
|
|
721
|
+
if (!seen || keys.length === 0) return false;
|
|
722
|
+
if (config === null || typeof config !== "object") return false;
|
|
723
|
+
const src = config;
|
|
724
|
+
for (const k2 of keys) {
|
|
725
|
+
if (!(k2 in seen)) continue;
|
|
726
|
+
if (!Object.is(seen[k2], src[k2])) return true;
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/annotations/drawOne.ts
|
|
732
|
+
function createMarkDrawOne(opts) {
|
|
733
|
+
return (node, pose) => markCommands({ pose, data: node.data }, opts.content, resolveMarkStyle(node.data, opts));
|
|
734
|
+
}
|
|
735
|
+
function resolveMarkStyle(data, opts) {
|
|
736
|
+
return {
|
|
737
|
+
color: opts.meaning?.statuses?.find((s) => s.id === data.status)?.color,
|
|
738
|
+
// Read off the node rather than through the store's `isStale`, which
|
|
739
|
+
// wants a projected Annotation this path does not have.
|
|
740
|
+
stale: isStale(data.seen, opts.config, opts.positionDependsOn ?? [])
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/annotations/svgNodes.ts
|
|
745
|
+
var NO_PAINT = { kind: "none" };
|
|
746
|
+
function toSvgPaint(paint) {
|
|
747
|
+
if (!paint) return NO_PAINT;
|
|
748
|
+
if ("color" in paint && typeof paint.color === "string") {
|
|
749
|
+
return {
|
|
750
|
+
kind: "solid",
|
|
751
|
+
color: paint.color,
|
|
752
|
+
...paint.opacity != null ? { opacity: paint.opacity } : {}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return { kind: "gradient", paint };
|
|
756
|
+
}
|
|
757
|
+
function toSvgStroke(stroke) {
|
|
758
|
+
return {
|
|
759
|
+
paint: toSvgPaint(stroke.paint),
|
|
760
|
+
width: resolveStrokeWidth(stroke.width ?? 1, 1),
|
|
761
|
+
...stroke.cap ? { cap: stroke.cap } : {},
|
|
762
|
+
...stroke.join ? { join: stroke.join } : {},
|
|
763
|
+
...stroke.dash ? { dash: [...stroke.dash] } : {},
|
|
764
|
+
...stroke.miterLimit != null ? { miterLimit: stroke.miterLimit } : {}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function toSvgNode(cmd, m2) {
|
|
768
|
+
if (cmd.kind === "path") {
|
|
769
|
+
return {
|
|
770
|
+
kind: "path",
|
|
771
|
+
path: cmd.path,
|
|
772
|
+
fill: toSvgPaint(cmd.fill),
|
|
773
|
+
...cmd.stroke ? { stroke: toSvgStroke(cmd.stroke) } : {}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
if (cmd.kind === "text") {
|
|
777
|
+
return {
|
|
778
|
+
kind: "text",
|
|
779
|
+
x: cmd.x,
|
|
780
|
+
y: cmd.y,
|
|
781
|
+
width: m2.pose.width,
|
|
782
|
+
height: m2.pose.height,
|
|
783
|
+
text: cmd.runs.map((run) => run.text).join(""),
|
|
784
|
+
style: cmd.style,
|
|
785
|
+
...cmd.runs[0]?.fill ? { fill: cmd.runs[0].fill } : {}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
throw new Error(`[labkit] a mark drew a '${cmd.kind}' command, which has no vector form`);
|
|
789
|
+
}
|
|
790
|
+
function markSvgNodes(m2, content, style = {}) {
|
|
791
|
+
return markCommands(m2, content, style).map((cmd) => toSvgNode(cmd, m2));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/annotations/capture.ts
|
|
795
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
796
|
+
function capturePlan(base, format) {
|
|
797
|
+
if (format === "svg") return "svg-document";
|
|
798
|
+
return base?.kind === "svg" ? "svg-document" : "raster-stack";
|
|
799
|
+
}
|
|
800
|
+
function nestBaseSvg(markup, w2, h) {
|
|
801
|
+
const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
|
|
802
|
+
const root = doc.documentElement;
|
|
803
|
+
if (doc.getElementsByTagName("parsererror").length > 0 || root.localName !== "svg") {
|
|
804
|
+
throw new Error("[labkit] a target's `base()` returned markup that is not an <svg> document");
|
|
805
|
+
}
|
|
806
|
+
root.setAttribute("x", "0");
|
|
807
|
+
root.setAttribute("y", "0");
|
|
808
|
+
root.setAttribute("width", String(w2));
|
|
809
|
+
root.setAttribute("height", String(h));
|
|
810
|
+
return new XMLSerializer().serializeToString(root);
|
|
811
|
+
}
|
|
812
|
+
function embedRasterBase(base, w2, h, onWarn) {
|
|
813
|
+
const href = base.kind === "canvas" ? base.canvas.toDataURL() : base.kind === "image" ? base.src : "";
|
|
814
|
+
if (base.kind === "image" && !href.startsWith("data:")) {
|
|
815
|
+
onWarn?.(`base image '${href}' is a reference, so this SVG is not self-contained`);
|
|
816
|
+
}
|
|
817
|
+
return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w2, height: h }], {
|
|
818
|
+
viewBox: { x: 0, y: 0, width: w2, height: h }
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
function markNodesOf(scene, draw) {
|
|
822
|
+
return [...scene.renderOrder()].flatMap((id) => {
|
|
823
|
+
const node = scene.get(asNodeId$1(String(id)));
|
|
824
|
+
if (!node) return [];
|
|
825
|
+
return markSvgNodes(
|
|
826
|
+
{ pose: node.pose, data: node.data },
|
|
827
|
+
draw.content,
|
|
828
|
+
resolveMarkStyle(node.data, draw)
|
|
829
|
+
);
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
function composeCaptureSvg(args) {
|
|
833
|
+
const { base, scene, draw, scale, onWarn } = args;
|
|
834
|
+
const { w: w2, h } = draw.content;
|
|
835
|
+
const baseXml = base === void 0 ? "" : base.kind === "svg" ? nestBaseSvg(base.markup, w2, h) : embedRasterBase(base, w2, h, onWarn);
|
|
836
|
+
const marksXml = serializeSvg(markNodesOf(scene, draw), {
|
|
837
|
+
viewBox: { x: 0, y: 0, width: w2, height: h },
|
|
838
|
+
onWarn
|
|
839
|
+
});
|
|
840
|
+
const outW = Math.max(1, Math.round(w2 * scale));
|
|
841
|
+
const outH = Math.max(1, Math.round(h * scale));
|
|
842
|
+
return `<svg xmlns="${SVG_NS}" width="${outW}" height="${outH}" viewBox="0 0 ${w2} ${h}">${baseXml}${marksXml}</svg>`;
|
|
843
|
+
}
|
|
844
|
+
function context2d(width, height) {
|
|
845
|
+
const canvas = document.createElement("canvas");
|
|
846
|
+
canvas.width = width;
|
|
847
|
+
canvas.height = height;
|
|
848
|
+
const ctx = canvas.getContext("2d");
|
|
849
|
+
if (!ctx) throw new Error("[labkit] capture needs a 2D canvas context, and this one has none");
|
|
850
|
+
return ctx;
|
|
851
|
+
}
|
|
852
|
+
function toPngBlob(canvas) {
|
|
853
|
+
return new Promise((resolve, reject) => {
|
|
854
|
+
let done;
|
|
855
|
+
try {
|
|
856
|
+
done = (blob) => blob ? resolve(blob) : reject(new Error("[labkit] the browser produced no PNG"));
|
|
857
|
+
canvas.toBlob(done, "image/png");
|
|
858
|
+
} catch (err) {
|
|
859
|
+
reject(
|
|
860
|
+
new Error(
|
|
861
|
+
"[labkit] the capture canvas is tainted, so it cannot be read back \u2014 a base referencing a cross-origin image does this. Embed it as a data: URI.",
|
|
862
|
+
{ cause: err }
|
|
863
|
+
)
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function loadImage(src) {
|
|
869
|
+
return new Promise((resolve, reject) => {
|
|
870
|
+
const img = new Image();
|
|
871
|
+
img.onload = () => resolve(img);
|
|
872
|
+
img.onerror = () => reject(new Error(`[labkit] a capture base failed to load: ${src}`));
|
|
873
|
+
img.src = src;
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
async function rasterizeSvg(svg, width, height) {
|
|
877
|
+
const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
|
|
878
|
+
try {
|
|
879
|
+
const img = await loadImage(url);
|
|
880
|
+
const ctx = context2d(width, height);
|
|
881
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
882
|
+
return await toPngBlob(ctx.canvas);
|
|
883
|
+
} finally {
|
|
884
|
+
URL.revokeObjectURL(url);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async function stackRaster(args) {
|
|
888
|
+
const { base, scene, draw, scale, width, height } = args;
|
|
889
|
+
const ctx = context2d(width, height);
|
|
890
|
+
if (base?.kind === "image") ctx.drawImage(await loadImage(base.src), 0, 0, width, height);
|
|
891
|
+
else if (base?.kind === "canvas") ctx.drawImage(base.canvas, 0, 0, width, height);
|
|
892
|
+
const raster = renderSceneToPixels({
|
|
893
|
+
scene,
|
|
894
|
+
sourceRect: { x: 0, y: 0, width: draw.content.w, height: draw.content.h },
|
|
895
|
+
scale: { x: scale, y: scale },
|
|
896
|
+
drawOne: createMarkDrawOne(draw)
|
|
897
|
+
});
|
|
898
|
+
const marks = context2d(raster.width, raster.height);
|
|
899
|
+
marks.putImageData(
|
|
900
|
+
new ImageData(Uint8ClampedArray.from(raster.data), raster.width, raster.height),
|
|
901
|
+
0,
|
|
902
|
+
0
|
|
903
|
+
);
|
|
904
|
+
ctx.drawImage(marks.canvas, 0, 0, width, height);
|
|
905
|
+
return toPngBlob(ctx.canvas);
|
|
906
|
+
}
|
|
907
|
+
async function captureTarget(args, opts = {}) {
|
|
908
|
+
const format = opts.format ?? "png";
|
|
909
|
+
const scale = opts.scale ?? 2;
|
|
910
|
+
const { content } = args.draw;
|
|
911
|
+
const width = Math.max(1, Math.round(content.w * scale));
|
|
912
|
+
const height = Math.max(1, Math.round(content.h * scale));
|
|
913
|
+
const base = await args.base?.();
|
|
914
|
+
const compose = {
|
|
915
|
+
base,
|
|
916
|
+
scene: args.scene,
|
|
917
|
+
draw: args.draw,
|
|
918
|
+
scale,
|
|
919
|
+
onWarn: args.onWarn
|
|
920
|
+
};
|
|
921
|
+
if (capturePlan(base, format) === "raster-stack") {
|
|
922
|
+
return {
|
|
923
|
+
target: args.target,
|
|
924
|
+
blob: await stackRaster({ ...compose, width, height }),
|
|
925
|
+
format,
|
|
926
|
+
width,
|
|
927
|
+
height
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const svg = composeCaptureSvg(compose);
|
|
931
|
+
const blob = format === "svg" ? new Blob([svg], { type: "image/svg+xml;charset=utf-8" }) : await rasterizeSvg(svg, width, height);
|
|
932
|
+
return { target: args.target, blob, format, width, height };
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/annotations/frac.ts
|
|
936
|
+
function fracToWorld(f2, content) {
|
|
937
|
+
return {
|
|
938
|
+
x: f2.x * content.w,
|
|
939
|
+
y: f2.y * content.h,
|
|
940
|
+
width: f2.w * content.w,
|
|
941
|
+
height: f2.h * content.h
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function worldToFrac(r2, content) {
|
|
945
|
+
const sx = content.w === 0 ? 0 : 1 / content.w;
|
|
946
|
+
const sy = content.h === 0 ? 0 : 1 / content.h;
|
|
947
|
+
return { x: r2.x * sx, y: r2.y * sy, w: r2.width * sx, h: r2.height * sy };
|
|
948
|
+
}
|
|
949
|
+
var DP = 1e4;
|
|
950
|
+
function roundFrac(f2) {
|
|
951
|
+
return {
|
|
952
|
+
x: Math.round(f2.x * DP) / DP,
|
|
953
|
+
y: Math.round(f2.y * DP) / DP,
|
|
954
|
+
w: Math.round(f2.w * DP) / DP,
|
|
955
|
+
h: Math.round(f2.h * DP) / DP
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function fracContains(box, pt, tol = 0) {
|
|
959
|
+
return pt.x >= box.x - tol && pt.x <= box.x + box.w + tol && pt.y >= box.y - tol && pt.y <= box.y + box.h + tol;
|
|
960
|
+
}
|
|
961
|
+
function fracIntersects(outer, inner) {
|
|
962
|
+
return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.w <= outer.x + outer.w && inner.y + inner.h <= outer.y + outer.h;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/annotations/history.ts
|
|
966
|
+
var MarkHistory = class {
|
|
967
|
+
depth = /* @__PURE__ */ new Map();
|
|
968
|
+
undoable = [];
|
|
969
|
+
redoable = [];
|
|
970
|
+
/** Set while this class is driving, so a redo is not read as a new change. */
|
|
971
|
+
replaying = null;
|
|
972
|
+
/** Start following `scene` under `target`. Idempotent per target. */
|
|
973
|
+
track(target, scene) {
|
|
974
|
+
if (this.depth.has(target)) return;
|
|
975
|
+
this.depth.set(target, scene.historyIndex());
|
|
976
|
+
}
|
|
977
|
+
/** Call on every notification from `target`'s scene. */
|
|
978
|
+
observe(target, scene) {
|
|
979
|
+
const now = scene.historyIndex();
|
|
980
|
+
const before = this.depth.get(target) ?? 0;
|
|
981
|
+
this.depth.set(target, now);
|
|
982
|
+
if (now > before) {
|
|
983
|
+
if (this.replaying === "redo") {
|
|
984
|
+
drop(this.redoable, target);
|
|
985
|
+
} else {
|
|
986
|
+
this.redoable = [];
|
|
987
|
+
}
|
|
988
|
+
this.undoable.push(target);
|
|
989
|
+
} else if (now < before) {
|
|
990
|
+
drop(this.undoable, target);
|
|
991
|
+
this.redoable.push(target);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
/** The target `undo` would act on, skipping any whose scene has nothing
|
|
995
|
+
* left — a scene can drop entries this ordering still lists. */
|
|
996
|
+
next(stack, can, at) {
|
|
997
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
998
|
+
const target = stack[i];
|
|
999
|
+
if (target === void 0) continue;
|
|
1000
|
+
const scene = at(target);
|
|
1001
|
+
if (scene && can(scene)) return target;
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
canUndo(at) {
|
|
1006
|
+
return this.next(this.undoable, (s) => s.canUndo(), at) !== null;
|
|
1007
|
+
}
|
|
1008
|
+
canRedo(at) {
|
|
1009
|
+
return this.next(this.redoable, (s) => s.canRedo(), at) !== null;
|
|
1010
|
+
}
|
|
1011
|
+
undo(at) {
|
|
1012
|
+
const target = this.next(this.undoable, (s) => s.canUndo(), at);
|
|
1013
|
+
const scene = target === null ? void 0 : at(target);
|
|
1014
|
+
if (!scene) return false;
|
|
1015
|
+
this.replaying = "undo";
|
|
1016
|
+
try {
|
|
1017
|
+
return scene.undo();
|
|
1018
|
+
} finally {
|
|
1019
|
+
this.replaying = null;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
redo(at) {
|
|
1023
|
+
const target = this.next(this.redoable, (s) => s.canRedo(), at);
|
|
1024
|
+
const scene = target === null ? void 0 : at(target);
|
|
1025
|
+
if (!scene) return false;
|
|
1026
|
+
this.replaying = "redo";
|
|
1027
|
+
try {
|
|
1028
|
+
return scene.redo();
|
|
1029
|
+
} finally {
|
|
1030
|
+
this.replaying = null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
function drop(stack, value) {
|
|
1035
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
1036
|
+
if (stack[i] === value) {
|
|
1037
|
+
stack.splice(i, 1);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/annotations/store.ts
|
|
1044
|
+
function createAnnotationScene() {
|
|
1045
|
+
return createScene({ systemLayers: [{ id: "marks" }] });
|
|
1046
|
+
}
|
|
1047
|
+
var NO_CONTENT = { w: 0, h: 0 };
|
|
1048
|
+
function splitId(id) {
|
|
1049
|
+
const at = id.lastIndexOf("/");
|
|
1050
|
+
return at <= 0 ? void 0 : { target: id.slice(0, at), node: id.slice(at + 1) };
|
|
1051
|
+
}
|
|
1052
|
+
function createAnnotationStore(opts) {
|
|
1053
|
+
const { targets, restore } = opts;
|
|
1054
|
+
const scenes = /* @__PURE__ */ new Map();
|
|
1055
|
+
const subs = /* @__PURE__ */ new Set();
|
|
1056
|
+
const history = new MarkHistory();
|
|
1057
|
+
const notify = () => {
|
|
1058
|
+
for (const fn of subs) fn();
|
|
1059
|
+
};
|
|
1060
|
+
const sceneFor = (target) => {
|
|
1061
|
+
const known = scenes.get(target);
|
|
1062
|
+
if (known) return known;
|
|
1063
|
+
const raw = restore?.[target];
|
|
1064
|
+
const scene = raw ? sceneFromJSON(
|
|
1065
|
+
raw,
|
|
1066
|
+
{}
|
|
1067
|
+
) : createAnnotationScene();
|
|
1068
|
+
scenes.set(target, scene);
|
|
1069
|
+
history.track(target, scene);
|
|
1070
|
+
scene.subscribe(() => {
|
|
1071
|
+
history.observe(target, scene);
|
|
1072
|
+
notify();
|
|
1073
|
+
});
|
|
1074
|
+
return scene;
|
|
1075
|
+
};
|
|
1076
|
+
const historySceneAt = (target) => scenes.get(target);
|
|
1077
|
+
for (const target of Object.keys(restore ?? {})) sceneFor(target);
|
|
1078
|
+
const targetOf = (id) => targets().find((t) => t.id === id);
|
|
1079
|
+
const contentOf = (id) => targetOf(id)?.content ?? NO_CONTENT;
|
|
1080
|
+
const project = (target, node) => {
|
|
1081
|
+
const found = scenes.get(target)?.get(asNodeId$1(node));
|
|
1082
|
+
if (!found) return void 0;
|
|
1083
|
+
return {
|
|
1084
|
+
...found.data,
|
|
1085
|
+
target,
|
|
1086
|
+
id: `${target}/${node}`,
|
|
1087
|
+
frac: roundFrac(worldToFrac(found.pose, contentOf(target)))
|
|
1088
|
+
};
|
|
1089
|
+
};
|
|
1090
|
+
const liveTargets = () => {
|
|
1091
|
+
const declared = targets().map((t) => t.id);
|
|
1092
|
+
const rest = [...scenes.keys()].filter((id) => !declared.includes(id));
|
|
1093
|
+
return [...declared, ...rest];
|
|
1094
|
+
};
|
|
1095
|
+
const marksOn = (target) => {
|
|
1096
|
+
const scene = scenes.get(target);
|
|
1097
|
+
if (!scene) return [];
|
|
1098
|
+
const out = [];
|
|
1099
|
+
for (const id of scene.renderOrder()) {
|
|
1100
|
+
const a = project(target, String(id));
|
|
1101
|
+
if (a) out.push(a);
|
|
1102
|
+
}
|
|
1103
|
+
return out;
|
|
1104
|
+
};
|
|
1105
|
+
const all = () => liveTargets().flatMap(marksOn);
|
|
1106
|
+
const matches = (a, q) => {
|
|
1107
|
+
if (q.target !== void 0 && a.target !== q.target) return false;
|
|
1108
|
+
if (q.kind !== void 0 && a.kind !== q.kind) return false;
|
|
1109
|
+
if (q.status !== void 0 && a.status !== q.status) return false;
|
|
1110
|
+
if (q.tags !== void 0) {
|
|
1111
|
+
const have = a.tags ?? [];
|
|
1112
|
+
for (const t of q.tags) if (!have.includes(t)) return false;
|
|
1113
|
+
}
|
|
1114
|
+
if (q.where && !q.where(a)) return false;
|
|
1115
|
+
return true;
|
|
1116
|
+
};
|
|
1117
|
+
const nodeOf = (id) => {
|
|
1118
|
+
const parts = splitId(id);
|
|
1119
|
+
if (!parts) return void 0;
|
|
1120
|
+
const scene = scenes.get(parts.target);
|
|
1121
|
+
const node = scene?.get(asNodeId$1(parts.node));
|
|
1122
|
+
return node && scene ? { ...parts, scene, node } : void 0;
|
|
1123
|
+
};
|
|
1124
|
+
return {
|
|
1125
|
+
sceneFor,
|
|
1126
|
+
targets,
|
|
1127
|
+
get(id) {
|
|
1128
|
+
const parts = splitId(id);
|
|
1129
|
+
return parts ? project(parts.target, parts.node) : void 0;
|
|
1130
|
+
},
|
|
1131
|
+
query(q) {
|
|
1132
|
+
return q ? all().filter((a) => matches(a, q)) : all();
|
|
1133
|
+
},
|
|
1134
|
+
hitTest(target, pt, tol = 0) {
|
|
1135
|
+
return marksOn(target).filter((a) => fracContains(a.frac, pt, tol)).reverse();
|
|
1136
|
+
},
|
|
1137
|
+
within(target, box) {
|
|
1138
|
+
return marksOn(target).filter((a) => fracIntersects(box, a.frac));
|
|
1139
|
+
},
|
|
1140
|
+
isStale(a, config) {
|
|
1141
|
+
const keys = targetOf(a.target)?.positionDependsOn ?? [];
|
|
1142
|
+
return isStale(a.seen, config, keys);
|
|
1143
|
+
},
|
|
1144
|
+
canUndo() {
|
|
1145
|
+
return history.canUndo(historySceneAt);
|
|
1146
|
+
},
|
|
1147
|
+
canRedo() {
|
|
1148
|
+
return history.canRedo(historySceneAt);
|
|
1149
|
+
},
|
|
1150
|
+
undo() {
|
|
1151
|
+
return history.undo(historySceneAt);
|
|
1152
|
+
},
|
|
1153
|
+
redo() {
|
|
1154
|
+
return history.redo(historySceneAt);
|
|
1155
|
+
},
|
|
1156
|
+
subscribe(fn) {
|
|
1157
|
+
subs.add(fn);
|
|
1158
|
+
return () => {
|
|
1159
|
+
subs.delete(fn);
|
|
1160
|
+
};
|
|
1161
|
+
},
|
|
1162
|
+
add(init, config) {
|
|
1163
|
+
const target = targetOf(init.target);
|
|
1164
|
+
const data = {
|
|
1165
|
+
target: init.target,
|
|
1166
|
+
kind: init.kind,
|
|
1167
|
+
...init.points ? { points: init.points } : {},
|
|
1168
|
+
...init.title !== void 0 ? { title: init.title } : {},
|
|
1169
|
+
...init.status !== void 0 ? { status: init.status } : {},
|
|
1170
|
+
...init.tags !== void 0 ? { tags: init.tags } : {},
|
|
1171
|
+
...init.meta !== void 0 ? { meta: init.meta } : {},
|
|
1172
|
+
seen: seenFrom(config, target?.positionDependsOn ?? [])
|
|
1173
|
+
};
|
|
1174
|
+
const node = sceneFor(init.target).add({
|
|
1175
|
+
kind: "leaf",
|
|
1176
|
+
layer: "marks",
|
|
1177
|
+
pose: fracToWorld(roundFrac(init.frac), target?.content ?? NO_CONTENT),
|
|
1178
|
+
data
|
|
1179
|
+
});
|
|
1180
|
+
return `${init.target}/${String(node)}`;
|
|
1181
|
+
},
|
|
1182
|
+
update(id, patch) {
|
|
1183
|
+
const found = nodeOf(id);
|
|
1184
|
+
if (!found) return;
|
|
1185
|
+
const { frac, ...meaning } = patch;
|
|
1186
|
+
const nid = asNodeId$1(found.node.id);
|
|
1187
|
+
if (Object.keys(meaning).length > 0) {
|
|
1188
|
+
found.scene.update(nid, { data: { ...found.node.data, ...meaning } });
|
|
1189
|
+
}
|
|
1190
|
+
if (frac) {
|
|
1191
|
+
found.scene.setPose(nid, fracToWorld(roundFrac(frac), contentOf(found.target)));
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
setMeta(id, meta) {
|
|
1195
|
+
const found = nodeOf(id);
|
|
1196
|
+
if (!found) return;
|
|
1197
|
+
found.scene.update(asNodeId$1(found.node.id), { data: { ...found.node.data, meta } });
|
|
1198
|
+
},
|
|
1199
|
+
remove(id) {
|
|
1200
|
+
const found = nodeOf(id);
|
|
1201
|
+
if (found) found.scene.remove(asNodeId$1(found.node.id));
|
|
1202
|
+
},
|
|
1203
|
+
async capture(target, captureOpts) {
|
|
1204
|
+
const info = targetOf(target);
|
|
1205
|
+
if (!info) throw new Error(`[labkit] no annotation target called '${target}'`);
|
|
1206
|
+
const result = await captureTarget(
|
|
1207
|
+
{
|
|
1208
|
+
target,
|
|
1209
|
+
scene: sceneFor(target),
|
|
1210
|
+
draw: {
|
|
1211
|
+
content: info.content,
|
|
1212
|
+
positionDependsOn: info.positionDependsOn,
|
|
1213
|
+
config: opts.config?.(),
|
|
1214
|
+
meaning: opts.meaning
|
|
1215
|
+
},
|
|
1216
|
+
base: info.base,
|
|
1217
|
+
onWarn: (message) => console.warn(`[labkit] capture: ${message}`)
|
|
1218
|
+
},
|
|
1219
|
+
captureOpts
|
|
1220
|
+
);
|
|
1221
|
+
opts.onCapture?.(result);
|
|
1222
|
+
return result;
|
|
1223
|
+
},
|
|
1224
|
+
toJSON() {
|
|
1225
|
+
const out = {};
|
|
1226
|
+
for (const [target, scene] of scenes) out[target] = scene.toJSON();
|
|
1227
|
+
return { version: 1, scenes: out };
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function annotationsFromJSON(raw, targets, rest = {}) {
|
|
1232
|
+
const doc = raw;
|
|
1233
|
+
if (doc?.version !== 1 || !doc.scenes) return createAnnotationStore({ targets, ...rest });
|
|
1234
|
+
return createAnnotationStore({ targets, restore: doc.scenes, ...rest });
|
|
1235
|
+
}
|
|
32
1236
|
|
|
1237
|
+
// src/annotations/view.ts
|
|
1238
|
+
function toWeaselView(v) {
|
|
1239
|
+
return { x: v.pan.x, y: v.pan.y, scale: { x: v.zoom, y: v.zoom } };
|
|
1240
|
+
}
|
|
1241
|
+
function fromWeaselView(v) {
|
|
1242
|
+
return { zoom: v.scale.x, pan: { x: v.x, y: v.y } };
|
|
1243
|
+
}
|
|
1244
|
+
function fitView(content, pane) {
|
|
1245
|
+
const zoom = content.w > 0 && content.h > 0 ? Math.min(pane.w / content.w, pane.h / content.h) : 1;
|
|
1246
|
+
return { zoom: Number.isFinite(zoom) && zoom > 0 ? zoom : 1, pan: { x: 0, y: 0 } };
|
|
1247
|
+
}
|
|
1248
|
+
var sameRect = (a, b2) => a !== null && a.x === b2.x && a.y === b2.y && a.w === b2.w && a.h === b2.h;
|
|
1249
|
+
function ToolBridge({ toolId }) {
|
|
1250
|
+
const tools = useActiveToolContext();
|
|
1251
|
+
const setActive = tools.setActive;
|
|
1252
|
+
useEffect(() => {
|
|
1253
|
+
setActive(toolId);
|
|
1254
|
+
}, [toolId, setActive]);
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
function pointsOf(extras, content) {
|
|
1258
|
+
const toFrac = (p2) => ({
|
|
1259
|
+
x: content.w === 0 ? 0 : p2.x / content.w,
|
|
1260
|
+
y: content.h === 0 ? 0 : p2.y / content.h
|
|
1261
|
+
});
|
|
1262
|
+
if (extras.kind === "line") {
|
|
1263
|
+
return [toFrac(extras.a), toFrac(extras.b)];
|
|
1264
|
+
}
|
|
1265
|
+
if (extras.kind === "pencil") {
|
|
1266
|
+
return extras.samples.map(toFrac);
|
|
1267
|
+
}
|
|
1268
|
+
return void 0;
|
|
1269
|
+
}
|
|
1270
|
+
function AnnotationOverlay({
|
|
1271
|
+
target,
|
|
1272
|
+
scene,
|
|
1273
|
+
config,
|
|
1274
|
+
activeToolId,
|
|
1275
|
+
meaning
|
|
1276
|
+
}) {
|
|
1277
|
+
const surface = useSurfaceOptional();
|
|
1278
|
+
const canvas = useSurfaceCanvas();
|
|
1279
|
+
const [rect, setRect] = useState(null);
|
|
1280
|
+
const [input, setInput] = useState(null);
|
|
1281
|
+
const sceneCanvas = useRef(null);
|
|
1282
|
+
const toolRef = useRef(activeToolId);
|
|
1283
|
+
toolRef.current = activeToolId;
|
|
1284
|
+
const configRef = useRef(config);
|
|
1285
|
+
configRef.current = config;
|
|
1286
|
+
const id = target.id;
|
|
1287
|
+
const lastTile = useRef(null);
|
|
1288
|
+
useEffect(() => {
|
|
1289
|
+
const el = target.ref.current ?? null;
|
|
1290
|
+
if (el === lastTile.current) return;
|
|
1291
|
+
lastTile.current = el;
|
|
1292
|
+
surface?.registerTile(id, el);
|
|
1293
|
+
});
|
|
1294
|
+
useEffect(
|
|
1295
|
+
() => () => {
|
|
1296
|
+
lastTile.current = null;
|
|
1297
|
+
surface?.registerTile(id, null);
|
|
1298
|
+
},
|
|
1299
|
+
[surface, id]
|
|
1300
|
+
);
|
|
1301
|
+
useEffect(
|
|
1302
|
+
() => surface?.registerPainter(id, (next) => {
|
|
1303
|
+
setRect((prev) => sameRect(prev, next) ? prev : next);
|
|
1304
|
+
sceneCanvas.current?.requestRedraw();
|
|
1305
|
+
}),
|
|
1306
|
+
[surface, id]
|
|
1307
|
+
);
|
|
1308
|
+
const container = surface?.getContainer() ?? null;
|
|
1309
|
+
if (!container || !rect) return null;
|
|
1310
|
+
const view = toWeaselView(target.view ?? fitView(target.content, { w: rect.w, h: rect.h }));
|
|
1311
|
+
const drawOne = createMarkDrawOne({
|
|
1312
|
+
content: target.content,
|
|
1313
|
+
positionDependsOn: target.positionDependsOn,
|
|
1314
|
+
config,
|
|
1315
|
+
meaning
|
|
1316
|
+
});
|
|
1317
|
+
const factories = {};
|
|
1318
|
+
for (const weaselTool of ANNOTATION_WEASEL_TOOLS) {
|
|
1319
|
+
factories[weaselTool] = (_bounds, extras) => {
|
|
1320
|
+
const info = annotationToolInfo(toolRef.current);
|
|
1321
|
+
const kind = info?.kind;
|
|
1322
|
+
if (!kind) return null;
|
|
1323
|
+
const points = pointsOf(extras, target.content);
|
|
1324
|
+
const data = {
|
|
1325
|
+
target: id,
|
|
1326
|
+
kind,
|
|
1327
|
+
...points ? { points } : {},
|
|
1328
|
+
seen: seenFrom(configRef.current, target.positionDependsOn ?? [])
|
|
1329
|
+
};
|
|
1330
|
+
return { data };
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
const box = {
|
|
1334
|
+
["--lk-anno-x"]: `${rect.x}px`,
|
|
1335
|
+
["--lk-anno-y"]: `${rect.y}px`,
|
|
1336
|
+
["--lk-anno-w"]: `${rect.w}px`,
|
|
1337
|
+
["--lk-anno-h"]: `${rect.h}px`
|
|
1338
|
+
};
|
|
1339
|
+
return createPortal(
|
|
1340
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1341
|
+
/* @__PURE__ */ jsx(
|
|
1342
|
+
"div",
|
|
1343
|
+
{
|
|
1344
|
+
ref: setInput,
|
|
1345
|
+
className: "lk-annotate__input",
|
|
1346
|
+
"data-annotation-target": id,
|
|
1347
|
+
role: "application",
|
|
1348
|
+
"aria-label": `Annotations on ${id}`,
|
|
1349
|
+
tabIndex: 0,
|
|
1350
|
+
style: box
|
|
1351
|
+
}
|
|
1352
|
+
),
|
|
1353
|
+
canvas && input ? (
|
|
1354
|
+
// One scope per pane: a shared <ActionsProvider> lets only the newest
|
|
1355
|
+
// canvas under it respond to input, and the rest go silently dead.
|
|
1356
|
+
/* @__PURE__ */ jsxs(WeaselProvider, { isolate: true, children: [
|
|
1357
|
+
/* @__PURE__ */ jsx(ToolBridge, { toolId: annotationToolInfo(activeToolId)?.weaselTool ?? "select" }),
|
|
1358
|
+
/* @__PURE__ */ jsx(
|
|
1359
|
+
SceneCanvas,
|
|
1360
|
+
{
|
|
1361
|
+
ref: sceneCanvas,
|
|
1362
|
+
scene,
|
|
1363
|
+
width: rect.w,
|
|
1364
|
+
height: rect.h,
|
|
1365
|
+
view,
|
|
1366
|
+
paintInto: { canvas, x: rect.x, y: rect.y },
|
|
1367
|
+
inputElement: input,
|
|
1368
|
+
defaultTools: ANNOTATION_WEASEL_TOOLS,
|
|
1369
|
+
insertNodeFactories: factories,
|
|
1370
|
+
layers: { scene: { drawOne } }
|
|
1371
|
+
}
|
|
1372
|
+
)
|
|
1373
|
+
] })
|
|
1374
|
+
) : null
|
|
1375
|
+
] }),
|
|
1376
|
+
container
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
function AnnotationTargets({
|
|
1380
|
+
capability,
|
|
1381
|
+
state,
|
|
1382
|
+
config,
|
|
1383
|
+
annotations,
|
|
1384
|
+
activeToolId
|
|
1385
|
+
}) {
|
|
1386
|
+
const targets = capability.targets(state, config);
|
|
1387
|
+
return /* @__PURE__ */ jsx(Fragment, { children: targets.map((t) => /* @__PURE__ */ jsx(
|
|
1388
|
+
AnnotationOverlay,
|
|
1389
|
+
{
|
|
1390
|
+
target: t,
|
|
1391
|
+
scene: annotations.sceneFor(t.id),
|
|
1392
|
+
meaning: capability.meaning,
|
|
1393
|
+
config,
|
|
1394
|
+
activeToolId
|
|
1395
|
+
},
|
|
1396
|
+
t.id
|
|
1397
|
+
)) });
|
|
1398
|
+
}
|
|
33
1399
|
var PITCH_LIMIT = Math.PI / 2 - 0.01;
|
|
34
1400
|
var YAW_PER_PX = 8e-3;
|
|
35
1401
|
var PITCH_PER_PX = 8e-3;
|
|
@@ -1242,6 +2608,69 @@ function TrialTitleBar({ title, children }) {
|
|
|
1242
2608
|
children
|
|
1243
2609
|
] });
|
|
1244
2610
|
}
|
|
2611
|
+
function createPanelHostRegistry() {
|
|
2612
|
+
const hosts = /* @__PURE__ */ new Map();
|
|
2613
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2614
|
+
const notify = () => {
|
|
2615
|
+
for (const fn of listeners) fn();
|
|
2616
|
+
};
|
|
2617
|
+
return {
|
|
2618
|
+
set: (key, el) => {
|
|
2619
|
+
if (el) hosts.set(key, el);
|
|
2620
|
+
else hosts.delete(key);
|
|
2621
|
+
notify();
|
|
2622
|
+
},
|
|
2623
|
+
get: (key) => hosts.get(key) ?? null,
|
|
2624
|
+
subscribe: (fn) => {
|
|
2625
|
+
listeners.add(fn);
|
|
2626
|
+
return () => listeners.delete(fn);
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
var PanelHostContext = createContext(null);
|
|
2631
|
+
function usePanelHosts() {
|
|
2632
|
+
return useContext(PanelHostContext);
|
|
2633
|
+
}
|
|
2634
|
+
function Portalled({
|
|
2635
|
+
trialId,
|
|
2636
|
+
contribution,
|
|
2637
|
+
onDock
|
|
2638
|
+
}) {
|
|
2639
|
+
const hosts = usePanelHosts();
|
|
2640
|
+
const key = panelKey(trialId, contribution.id);
|
|
2641
|
+
const host = useSyncExternalStore(
|
|
2642
|
+
(fn) => hosts?.subscribe(fn) ?? (() => {
|
|
2643
|
+
}),
|
|
2644
|
+
() => hosts?.get(key) ?? null,
|
|
2645
|
+
() => null
|
|
2646
|
+
);
|
|
2647
|
+
if (!host || !contribution.item) return null;
|
|
2648
|
+
const item = contribution.item;
|
|
2649
|
+
return createPortal(
|
|
2650
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2651
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-panel-tile__titlebar", children: [
|
|
2652
|
+
/* @__PURE__ */ jsx("span", { className: "lk-panel-tile__title", children: item.title }),
|
|
2653
|
+
/* @__PURE__ */ jsx(
|
|
2654
|
+
"button",
|
|
2655
|
+
{
|
|
2656
|
+
type: "button",
|
|
2657
|
+
className: "lk-panel-tile__dock",
|
|
2658
|
+
"aria-label": `Dock ${item.title}`,
|
|
2659
|
+
title: `Dock ${item.title}`,
|
|
2660
|
+
onClick: () => onDock(contribution.id),
|
|
2661
|
+
children: "\u21E4"
|
|
2662
|
+
}
|
|
2663
|
+
)
|
|
2664
|
+
] }),
|
|
2665
|
+
item.body
|
|
2666
|
+
] }),
|
|
2667
|
+
host
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
function UndockedSections({ trialId, sections, onDock }) {
|
|
2671
|
+
if (sections.length === 0) return null;
|
|
2672
|
+
return /* @__PURE__ */ jsx(Fragment, { children: sections.map((c2) => /* @__PURE__ */ jsx(Portalled, { trialId, contribution: c2, onDock }, c2.id)) });
|
|
2673
|
+
}
|
|
1245
2674
|
var NO_OP = () => {
|
|
1246
2675
|
};
|
|
1247
2676
|
function TrialChrome({
|
|
@@ -1251,6 +2680,7 @@ function TrialChrome({
|
|
|
1251
2680
|
isLastTrial,
|
|
1252
2681
|
undoBindings,
|
|
1253
2682
|
job,
|
|
2683
|
+
loupe,
|
|
1254
2684
|
trialChrome,
|
|
1255
2685
|
chrome,
|
|
1256
2686
|
suppress,
|
|
@@ -1264,6 +2694,13 @@ function TrialChrome({
|
|
|
1264
2694
|
const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
|
|
1265
2695
|
const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
|
|
1266
2696
|
const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
|
|
2697
|
+
const undockPanelAction = useStore(storeCtx.store, (s) => s.undockPanel);
|
|
2698
|
+
const dockPanelAction = useStore(storeCtx.store, (s) => s.dockPanel);
|
|
2699
|
+
const undockedPanels = useStore(storeCtx.store, (s) => s.undockedPanels);
|
|
2700
|
+
const undockedIds = useMemo(
|
|
2701
|
+
() => Object.values(undockedPanels).filter((p2) => p2.trialId === trialId).map((p2) => p2.sectionId),
|
|
2702
|
+
[undockedPanels, trialId]
|
|
2703
|
+
);
|
|
1267
2704
|
const view2d = as2DView(record.view);
|
|
1268
2705
|
const configSchema = useConfigSchema(instrument);
|
|
1269
2706
|
const ctx = useMemo(() => {
|
|
@@ -1281,6 +2718,8 @@ function TrialChrome({
|
|
|
1281
2718
|
canRedo: undoBindings?.canRedo ?? false,
|
|
1282
2719
|
undo: undoBindings?.undo ?? NO_OP,
|
|
1283
2720
|
redo: undoBindings?.redo ?? NO_OP,
|
|
2721
|
+
loupeOn: loupe?.on ?? false,
|
|
2722
|
+
toggleLoupe: loupe?.toggle ?? NO_OP,
|
|
1284
2723
|
configSchema,
|
|
1285
2724
|
configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
|
|
1286
2725
|
config: record.config,
|
|
@@ -1298,6 +2737,9 @@ function TrialChrome({
|
|
|
1298
2737
|
updateTrialState(trialId, nextState);
|
|
1299
2738
|
}
|
|
1300
2739
|
},
|
|
2740
|
+
undockedPanels: undockedIds,
|
|
2741
|
+
undockPanel: (sectionId, as) => undockPanelAction(trialId, sectionId, as),
|
|
2742
|
+
dockPanel: (sectionId) => dockPanelAction(trialId, sectionId),
|
|
1301
2743
|
savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
|
|
1302
2744
|
saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
|
|
1303
2745
|
loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
|
|
@@ -1317,10 +2759,14 @@ function TrialChrome({
|
|
|
1317
2759
|
updateTrialConfig,
|
|
1318
2760
|
updateTrialState,
|
|
1319
2761
|
undoBindings,
|
|
2762
|
+
loupe,
|
|
1320
2763
|
view2d,
|
|
1321
2764
|
activeToolId,
|
|
1322
2765
|
setActiveTool,
|
|
1323
|
-
configSchema
|
|
2766
|
+
configSchema,
|
|
2767
|
+
undockedIds,
|
|
2768
|
+
undockPanelAction,
|
|
2769
|
+
dockPanelAction
|
|
1324
2770
|
]);
|
|
1325
2771
|
const contributions = useMemo(
|
|
1326
2772
|
() => suppressContributions(
|
|
@@ -1359,7 +2805,17 @@ function TrialChrome({
|
|
|
1359
2805
|
/* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: /* @__PURE__ */ jsx(ToolbarRegion, { contributions: inRegion("toolbar"), ctx }) }),
|
|
1360
2806
|
/* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
|
|
1361
2807
|
/* @__PURE__ */ jsx(PaletteRegion, { contributions: inRegion("palette"), ctx }),
|
|
1362
|
-
/* @__PURE__ */
|
|
2808
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-trial__sidebar", children: [
|
|
2809
|
+
/* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }),
|
|
2810
|
+
/* @__PURE__ */ jsx(
|
|
2811
|
+
UndockedSections,
|
|
2812
|
+
{
|
|
2813
|
+
trialId,
|
|
2814
|
+
sections: inRegion("sidebar").filter((c2) => undockedIds.includes(c2.id)),
|
|
2815
|
+
onDock: ctx.dockPanel
|
|
2816
|
+
}
|
|
2817
|
+
)
|
|
2818
|
+
] }),
|
|
1363
2819
|
/* @__PURE__ */ jsxs("div", { className: `lk-trial__content${instrument.canvas ? " lk-trial__content--flush" : ""}`, children: [
|
|
1364
2820
|
children,
|
|
1365
2821
|
/* @__PURE__ */ jsx(ViewportRegion, { contributions: inRegion("viewport"), ctx })
|
|
@@ -1405,10 +2861,12 @@ function Trial({ id, chrome, suppress }) {
|
|
|
1405
2861
|
}
|
|
1406
2862
|
function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
1407
2863
|
const canvasContainerRef = useRef(null);
|
|
2864
|
+
const loupeHostRef = useRef(null);
|
|
1408
2865
|
const updateTrialState = useStore(store, (s) => s.updateTrialState);
|
|
1409
2866
|
const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
|
|
1410
2867
|
const updateTrialView = useStore(store, (s) => s.updateTrialView);
|
|
1411
2868
|
const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
|
|
2869
|
+
const updateTrialAnnotations = useStore(store, (s) => s.updateTrialAnnotations);
|
|
1412
2870
|
const labToolId = useStore(store, (s) => s.activeToolId);
|
|
1413
2871
|
const setLabTool = useStore(store, (s) => s.setLabTool);
|
|
1414
2872
|
const setTrialTool = useStore(store, (s) => s.setTrialTool);
|
|
@@ -1417,6 +2875,11 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1417
2875
|
const bus = busRef.current;
|
|
1418
2876
|
const [layerVisibility, setLayerVisibility] = useState({});
|
|
1419
2877
|
const [layerOrder, setLayerOrder] = useState(null);
|
|
2878
|
+
const [loupeOn, setLoupeOn] = useState(false);
|
|
2879
|
+
const visibleLayers = useMemo(
|
|
2880
|
+
() => (instrument.canvas?.layers ?? []).filter((l) => layerVisibility[l.id] !== false).map((l) => l.id),
|
|
2881
|
+
[instrument.canvas, layerVisibility]
|
|
2882
|
+
);
|
|
1420
2883
|
const undoCap = instrument.undo;
|
|
1421
2884
|
const undoEvents = useMemo(() => new Set(undoCap?.snapshotOn ?? ["state.change"]), [undoCap]);
|
|
1422
2885
|
const maxDepth = undoCap?.maxDepth ?? 50;
|
|
@@ -1428,8 +2891,69 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1428
2891
|
updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
|
|
1429
2892
|
};
|
|
1430
2893
|
const setView = (v) => updateTrialView(record.id, v);
|
|
1431
|
-
const
|
|
1432
|
-
|
|
2894
|
+
const placeView = (size) => {
|
|
2895
|
+
const declared = instrument.canvas?.initialView;
|
|
2896
|
+
if (typeof declared !== "function") return;
|
|
2897
|
+
if (record.view != null) return;
|
|
2898
|
+
setView(declared(size));
|
|
2899
|
+
};
|
|
2900
|
+
const annotationsCap = instrument.annotations;
|
|
2901
|
+
const targetsRef = useRef(() => []);
|
|
2902
|
+
targetsRef.current = () => annotationsCap ? annotationsCap.targets(record.state, record.config) : [];
|
|
2903
|
+
const annotationsRef = useRef(null);
|
|
2904
|
+
const configRef = useRef(record.config);
|
|
2905
|
+
configRef.current = record.config;
|
|
2906
|
+
if (annotationsRef.current === null) {
|
|
2907
|
+
const kept = annotationsCap?.storage ? annotationsCap.storage.load() : record.annotations;
|
|
2908
|
+
annotationsRef.current = annotationsFromJSON(kept, () => targetsRef.current(), {
|
|
2909
|
+
meaning: annotationsCap?.meaning,
|
|
2910
|
+
config: () => configRef.current,
|
|
2911
|
+
onCapture: (result) => annotationsCap?.onCapture?.(result)
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
const annotations = annotationsRef.current;
|
|
2915
|
+
const saveCap = annotationsCap?.storage;
|
|
2916
|
+
const saveRef = useRef(() => {
|
|
2917
|
+
});
|
|
2918
|
+
saveRef.current = (doc) => {
|
|
2919
|
+
if (saveCap) saveCap.save(doc);
|
|
2920
|
+
else updateTrialAnnotations(record.id, doc);
|
|
2921
|
+
};
|
|
2922
|
+
const [markMoves, setMarkMoves] = useState({ undo: false, redo: false });
|
|
2923
|
+
useEffect(() => {
|
|
2924
|
+
if (!annotationsCap) return;
|
|
2925
|
+
const read = () => {
|
|
2926
|
+
const next = { undo: annotations.canUndo(), redo: annotations.canRedo() };
|
|
2927
|
+
setMarkMoves((prev) => prev.undo === next.undo && prev.redo === next.redo ? prev : next);
|
|
2928
|
+
};
|
|
2929
|
+
read();
|
|
2930
|
+
return annotations.subscribe(read);
|
|
2931
|
+
}, [annotations, annotationsCap]);
|
|
2932
|
+
useEffect(() => {
|
|
2933
|
+
if (!annotationsCap) return;
|
|
2934
|
+
let timer = null;
|
|
2935
|
+
let dirty = false;
|
|
2936
|
+
const flush = () => {
|
|
2937
|
+
if (!dirty) return;
|
|
2938
|
+
dirty = false;
|
|
2939
|
+
saveRef.current(annotations.toJSON());
|
|
2940
|
+
};
|
|
2941
|
+
const off = annotations.subscribe(() => {
|
|
2942
|
+
dirty = true;
|
|
2943
|
+
if (timer) clearTimeout(timer);
|
|
2944
|
+
timer = setTimeout(() => {
|
|
2945
|
+
timer = null;
|
|
2946
|
+
flush();
|
|
2947
|
+
}, 250);
|
|
2948
|
+
});
|
|
2949
|
+
return () => {
|
|
2950
|
+
off();
|
|
2951
|
+
if (timer) clearTimeout(timer);
|
|
2952
|
+
flush();
|
|
2953
|
+
};
|
|
2954
|
+
}, [annotations, annotationsCap]);
|
|
2955
|
+
const declaresTools = instrument.tools != null || instrument.annotations != null;
|
|
2956
|
+
const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? (instrument.annotations ? "select" : null) : labToolId;
|
|
1433
2957
|
const setActiveTool = (id) => {
|
|
1434
2958
|
if (declaresTools) setTrialTool(record.id, id);
|
|
1435
2959
|
else setLabTool(id);
|
|
@@ -1468,7 +2992,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1468
2992
|
if (!view2d) return;
|
|
1469
2993
|
updateTrialView(record.id, { ...view2d, zoom: z });
|
|
1470
2994
|
},
|
|
1471
|
-
activeToolId: resolvedToolId
|
|
2995
|
+
activeToolId: resolvedToolId,
|
|
2996
|
+
visibleLayers
|
|
1472
2997
|
},
|
|
1473
2998
|
emit: (event) => {
|
|
1474
2999
|
snapshotIfNeeded(event);
|
|
@@ -1476,22 +3001,36 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1476
3001
|
},
|
|
1477
3002
|
job: jobCap ? job : void 0
|
|
1478
3003
|
};
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
3004
|
+
const undoState = () => {
|
|
3005
|
+
const result = undo(record.undoStack, structuredClone(record.state));
|
|
3006
|
+
if (!result) return;
|
|
3007
|
+
updateTrialState(record.id, result.snapshot);
|
|
3008
|
+
updateTrialUndoStack(record.id, result.stack);
|
|
3009
|
+
};
|
|
3010
|
+
const redoState = () => {
|
|
3011
|
+
const result = redo(record.undoStack, structuredClone(record.state));
|
|
3012
|
+
if (!result) return;
|
|
3013
|
+
updateTrialState(record.id, result.snapshot);
|
|
3014
|
+
updateTrialUndoStack(record.id, result.stack);
|
|
3015
|
+
};
|
|
3016
|
+
const undoBindings = undoCap || annotationsCap ? {
|
|
3017
|
+
canUndo: markMoves.undo || (undoCap ? record.undoStack.past.length > 0 : false),
|
|
3018
|
+
canRedo: markMoves.redo || (undoCap ? record.undoStack.future.length > 0 : false),
|
|
1482
3019
|
undo: () => {
|
|
1483
|
-
|
|
1484
|
-
if (
|
|
1485
|
-
updateTrialState(record.id, result.snapshot);
|
|
1486
|
-
updateTrialUndoStack(record.id, result.stack);
|
|
3020
|
+
if (annotationsCap && annotations.undo()) return;
|
|
3021
|
+
if (undoCap) undoState();
|
|
1487
3022
|
},
|
|
1488
3023
|
redo: () => {
|
|
1489
|
-
|
|
1490
|
-
if (
|
|
1491
|
-
updateTrialState(record.id, result.snapshot);
|
|
1492
|
-
updateTrialUndoStack(record.id, result.stack);
|
|
3024
|
+
if (annotationsCap && annotations.redo()) return;
|
|
3025
|
+
if (undoCap) redoState();
|
|
1493
3026
|
}
|
|
1494
3027
|
} : void 0;
|
|
3028
|
+
const loupeCap = useMemo(() => {
|
|
3029
|
+
const declared = instrument.loupe;
|
|
3030
|
+
if (declared == null) return null;
|
|
3031
|
+
return resolveLoupe(typeof declared === "function" ? declared(record.config) : declared);
|
|
3032
|
+
}, [instrument.loupe, record.config]);
|
|
3033
|
+
const loupeBindings = loupeCap ? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) } : void 0;
|
|
1495
3034
|
const canvasLayers = useMemo(() => {
|
|
1496
3035
|
if (!instrument.canvas) return [];
|
|
1497
3036
|
const baseLayers = instrument.canvas.layers;
|
|
@@ -1499,9 +3038,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1499
3038
|
return ordered.map((layer) => ({
|
|
1500
3039
|
id: layer.id,
|
|
1501
3040
|
visible: layerVisibility[layer.id] !== false,
|
|
1502
|
-
render: (ctx, view) => {
|
|
1503
|
-
ctx
|
|
1504
|
-
ctx.scale(view.zoom, view.zoom);
|
|
3041
|
+
render: (ctx, view, frame) => {
|
|
3042
|
+
applyCamera(ctx, view, frame);
|
|
1505
3043
|
layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom });
|
|
1506
3044
|
}
|
|
1507
3045
|
}));
|
|
@@ -1514,6 +3052,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1514
3052
|
capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
|
|
1515
3053
|
canvasContainerRef,
|
|
1516
3054
|
view: view2d ?? DEFAULT_VIEW,
|
|
3055
|
+
worldSpec: instrument.canvas?.worldSpec,
|
|
1517
3056
|
state: record.state,
|
|
1518
3057
|
config: record.config,
|
|
1519
3058
|
setState: (next) => {
|
|
@@ -1554,22 +3093,44 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1554
3093
|
}
|
|
1555
3094
|
];
|
|
1556
3095
|
}, [canvasLayers, dragDropResult.drag]);
|
|
3096
|
+
const lens = loupeCap ? /* @__PURE__ */ jsx(
|
|
3097
|
+
TrialLoupe,
|
|
3098
|
+
{
|
|
3099
|
+
capability: loupeCap,
|
|
3100
|
+
enabled: loupeOn,
|
|
3101
|
+
state: record.state,
|
|
3102
|
+
config: record.config,
|
|
3103
|
+
view: view2d ?? DEFAULT_VIEW,
|
|
3104
|
+
worldSpec: instrument.canvas?.worldSpec,
|
|
3105
|
+
hostRef: loupeHostRef
|
|
3106
|
+
}
|
|
3107
|
+
) : null;
|
|
1557
3108
|
let body;
|
|
1558
3109
|
if (instrument.canvas) {
|
|
1559
3110
|
body = /* @__PURE__ */ jsxs("div", { ref: canvasContainerRef, className: "lk-trial__canvas-host", children: [
|
|
1560
|
-
/* @__PURE__ */
|
|
3111
|
+
/* @__PURE__ */ jsxs(
|
|
1561
3112
|
CanvasStack,
|
|
1562
3113
|
{
|
|
1563
3114
|
layers: layersWithFeedback,
|
|
1564
3115
|
view: view2d ?? DEFAULT_VIEW,
|
|
1565
3116
|
onViewChange: setView,
|
|
3117
|
+
worldSpec: instrument.canvas.worldSpec,
|
|
3118
|
+
onResize: placeView,
|
|
1566
3119
|
minZoom: instrument.canvas.minZoom,
|
|
1567
3120
|
maxZoom: instrument.canvas.maxZoom,
|
|
1568
|
-
children:
|
|
3121
|
+
children: [
|
|
3122
|
+
instrument.render(renderCtx),
|
|
3123
|
+
lens
|
|
3124
|
+
]
|
|
1569
3125
|
}
|
|
1570
3126
|
),
|
|
1571
3127
|
/* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
|
|
1572
3128
|
] });
|
|
3129
|
+
} else if (lens) {
|
|
3130
|
+
body = /* @__PURE__ */ jsxs("div", { ref: loupeHostRef, className: "lk-trial__loupe-host", children: [
|
|
3131
|
+
instrument.render(renderCtx),
|
|
3132
|
+
lens
|
|
3133
|
+
] });
|
|
1573
3134
|
} else {
|
|
1574
3135
|
body = instrument.render(renderCtx);
|
|
1575
3136
|
}
|
|
@@ -1614,10 +3175,21 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1614
3175
|
}
|
|
1615
3176
|
return out;
|
|
1616
3177
|
}, [paletteNode, layerListNode]);
|
|
1617
|
-
|
|
3178
|
+
const annotationOverlays = annotationsCap ? /* @__PURE__ */ jsx(
|
|
3179
|
+
AnnotationTargets,
|
|
3180
|
+
{
|
|
3181
|
+
capability: annotationsCap,
|
|
3182
|
+
state: record.state,
|
|
3183
|
+
config: record.config,
|
|
3184
|
+
annotations,
|
|
3185
|
+
activeToolId: resolvedToolId
|
|
3186
|
+
}
|
|
3187
|
+
) : null;
|
|
3188
|
+
return /* @__PURE__ */ jsx(TrialIdProvider, { trialId: record.id, children: /* @__PURE__ */ jsx(AnnotationsContext.Provider, { value: annotationsCap ? annotations : null, children: /* @__PURE__ */ jsxs(
|
|
1618
3189
|
TrialChrome,
|
|
1619
3190
|
{
|
|
1620
3191
|
job: jobCap ? job : void 0,
|
|
3192
|
+
loupe: loupeBindings,
|
|
1621
3193
|
trialId: record.id,
|
|
1622
3194
|
record,
|
|
1623
3195
|
instrument,
|
|
@@ -1628,9 +3200,12 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1628
3200
|
suppress,
|
|
1629
3201
|
activeToolId: resolvedToolId,
|
|
1630
3202
|
setActiveTool,
|
|
1631
|
-
children:
|
|
3203
|
+
children: [
|
|
3204
|
+
body,
|
|
3205
|
+
annotationOverlays
|
|
3206
|
+
]
|
|
1632
3207
|
}
|
|
1633
|
-
);
|
|
3208
|
+
) }) });
|
|
1634
3209
|
}
|
|
1635
3210
|
|
|
1636
3211
|
// src/trial/trialOps.ts
|
|
@@ -1643,7 +3218,9 @@ function findInstrument(instruments, name) {
|
|
|
1643
3218
|
return found;
|
|
1644
3219
|
}
|
|
1645
3220
|
function initialView(instrument) {
|
|
1646
|
-
|
|
3221
|
+
const declared = instrument.canvas?.initialView;
|
|
3222
|
+
if (typeof declared === "function") return null;
|
|
3223
|
+
return declared ? structuredClone(declared) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
|
|
1647
3224
|
}
|
|
1648
3225
|
function addTrial(trials, instruments, instrumentName) {
|
|
1649
3226
|
const instrument = findInstrument(instruments, instrumentName);
|
|
@@ -1716,7 +3293,7 @@ function LabHeader() {
|
|
|
1716
3293
|
className: "lk-lab-header__add",
|
|
1717
3294
|
onClick: () => lab.addTrial(only.name),
|
|
1718
3295
|
children: [
|
|
1719
|
-
/* @__PURE__ */ jsx(
|
|
3296
|
+
/* @__PURE__ */ jsx(C, { size: 16 }),
|
|
1720
3297
|
/* @__PURE__ */ jsx("span", { children: "Add trial" })
|
|
1721
3298
|
]
|
|
1722
3299
|
}
|
|
@@ -1738,7 +3315,7 @@ function LabHeader() {
|
|
|
1738
3315
|
)
|
|
1739
3316
|
),
|
|
1740
3317
|
/* @__PURE__ */ jsx(
|
|
1741
|
-
|
|
3318
|
+
r2,
|
|
1742
3319
|
{
|
|
1743
3320
|
className: "lk-lab-header__mode",
|
|
1744
3321
|
ariaLabel: "Color mode",
|
|
@@ -1800,7 +3377,9 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
|
|
|
1800
3377
|
return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
|
|
1801
3378
|
}
|
|
1802
3379
|
var ZONE_ID = asNodeId("lk-workspace");
|
|
1803
|
-
var STRATEGIES = { grid: gridStrategy };
|
|
3380
|
+
var STRATEGIES = { grid: gridStrategy, floating: floatingStrategy };
|
|
3381
|
+
var FLOAT_ZONE_ID = asNodeId("lk-workspace-floating");
|
|
3382
|
+
var PANEL_KIND = "panel";
|
|
1804
3383
|
var KIND = "trial";
|
|
1805
3384
|
function extentOf(store, id) {
|
|
1806
3385
|
const p2 = store.getNode(id)?.membership?.placement;
|
|
@@ -1810,9 +3389,14 @@ function extentOf(store, id) {
|
|
|
1810
3389
|
if (p2.span) out.span = p2.span;
|
|
1811
3390
|
return Object.keys(out).length > 0 ? out : null;
|
|
1812
3391
|
}
|
|
3392
|
+
function PanelFrame({ panel, floating = false }) {
|
|
3393
|
+
const hosts = usePanelHosts();
|
|
3394
|
+
return /* @__PURE__ */ jsx("div", { className: floating ? "lk-panel-tile lk-panel-tile--floating" : "lk-panel-tile", children: /* @__PURE__ */ jsx("div", { className: "lk-panel-tile__body", ref: (el) => hosts?.set(panel.key, el) }) });
|
|
3395
|
+
}
|
|
1813
3396
|
function Workspace({
|
|
1814
3397
|
children,
|
|
1815
3398
|
ids,
|
|
3399
|
+
panels,
|
|
1816
3400
|
resizable = false,
|
|
1817
3401
|
reorderable = false,
|
|
1818
3402
|
onReorder,
|
|
@@ -1823,8 +3407,20 @@ function Workspace({
|
|
|
1823
3407
|
viewport
|
|
1824
3408
|
}) {
|
|
1825
3409
|
const items = Children.toArray(children);
|
|
1826
|
-
const
|
|
1827
|
-
const
|
|
3410
|
+
const tilePanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "tile"), [panels]);
|
|
3411
|
+
const floatPanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "floating"), [panels]);
|
|
3412
|
+
const wantedIds = [
|
|
3413
|
+
...items.map((_2, i) => ids?.[i] ?? `lk-ws-${i}`),
|
|
3414
|
+
...tilePanels.map((p2) => `lk-panel-${p2.key}`)
|
|
3415
|
+
];
|
|
3416
|
+
const idKey = wantedIds.join(",");
|
|
3417
|
+
const idKeyRef = useRef(null);
|
|
3418
|
+
const nodeIdsRef = useRef([]);
|
|
3419
|
+
if (idKeyRef.current !== idKey) {
|
|
3420
|
+
idKeyRef.current = idKey;
|
|
3421
|
+
nodeIdsRef.current = wantedIds.map(asNodeId);
|
|
3422
|
+
}
|
|
3423
|
+
const nodeIds = nodeIdsRef.current;
|
|
1828
3424
|
const layoutRef = useRef(layout);
|
|
1829
3425
|
layoutRef.current = layout;
|
|
1830
3426
|
const onLayoutChangeRef = useRef(onLayoutChange);
|
|
@@ -1839,6 +3435,13 @@ function Workspace({
|
|
|
1839
3435
|
container: { strategyId: "grid", config: { resizable, gap, padding } }
|
|
1840
3436
|
})
|
|
1841
3437
|
);
|
|
3438
|
+
store2.registerNode(
|
|
3439
|
+
createNode({
|
|
3440
|
+
kind: "zone",
|
|
3441
|
+
id: FLOAT_ZONE_ID,
|
|
3442
|
+
container: { strategyId: "floating", config: {} }
|
|
3443
|
+
})
|
|
3444
|
+
);
|
|
1842
3445
|
storeRef.current = store2;
|
|
1843
3446
|
}
|
|
1844
3447
|
const store = storeRef.current;
|
|
@@ -1854,7 +3457,8 @@ function Workspace({
|
|
|
1854
3457
|
}
|
|
1855
3458
|
for (const id of nodeIds) {
|
|
1856
3459
|
if (present.has(id)) continue;
|
|
1857
|
-
|
|
3460
|
+
const kind = String(id).startsWith("lk-panel-") ? PANEL_KIND : KIND;
|
|
3461
|
+
store.registerNode(createNode({ kind, id, parentId: ZONE_ID, focus: true }));
|
|
1858
3462
|
store.showNode(id);
|
|
1859
3463
|
const saved = layoutRef.current?.[id];
|
|
1860
3464
|
if (saved) store.patchPlacement(id, saved);
|
|
@@ -1883,7 +3487,15 @@ function Workspace({
|
|
|
1883
3487
|
);
|
|
1884
3488
|
const chrome = useMemo(() => {
|
|
1885
3489
|
const byId = new Map(nodeIds.map((id, i) => [id, items[i]]));
|
|
3490
|
+
const panelByNode = new Map(
|
|
3491
|
+
tilePanels.map((p2) => [`lk-panel-${p2.key}`, p2])
|
|
3492
|
+
);
|
|
1886
3493
|
return {
|
|
3494
|
+
[PANEL_KIND]: ({ node }) => {
|
|
3495
|
+
const panel = panelByNode.get(String(node.id));
|
|
3496
|
+
if (!panel) return null;
|
|
3497
|
+
return /* @__PURE__ */ jsx(PanelFrame, { panel });
|
|
3498
|
+
},
|
|
1887
3499
|
[KIND]: ({ node }) => {
|
|
1888
3500
|
const content = byId.get(node.id) ?? null;
|
|
1889
3501
|
if (!reorderable) return content;
|
|
@@ -1893,7 +3505,8 @@ function Workspace({
|
|
|
1893
3505
|
);
|
|
1894
3506
|
}
|
|
1895
3507
|
};
|
|
1896
|
-
}, [nodeIds, items, reorderable]);
|
|
3508
|
+
}, [nodeIds, items, reorderable, tilePanels]);
|
|
3509
|
+
const floatingLayer = floatPanels.length === 0 ? null : /* @__PURE__ */ jsx("div", { className: "lk-workspace__floating", children: floatPanels.map((p2) => /* @__PURE__ */ jsx(PanelFrame, { panel: p2, floating: true }, p2.key)) });
|
|
1897
3510
|
const grid = /* @__PURE__ */ jsx(
|
|
1898
3511
|
Container,
|
|
1899
3512
|
{
|
|
@@ -1905,7 +3518,15 @@ function Workspace({
|
|
|
1905
3518
|
onChildOrderChange: reorderable ? commitOrder : void 0
|
|
1906
3519
|
}
|
|
1907
3520
|
);
|
|
1908
|
-
return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children:
|
|
3521
|
+
return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children: floatingLayer ? (
|
|
3522
|
+
// The floating layer insets against the workspace, so it needs a
|
|
3523
|
+
// positioned box around both. Without one it insets against the page
|
|
3524
|
+
// and lands over the lab header.
|
|
3525
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-workspace-host", children: [
|
|
3526
|
+
reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid,
|
|
3527
|
+
floatingLayer
|
|
3528
|
+
] })
|
|
3529
|
+
) : reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
|
|
1909
3530
|
}
|
|
1910
3531
|
function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
|
|
1911
3532
|
const store = createLabStore({ storageKey, storage, initialMode });
|
|
@@ -1968,6 +3589,40 @@ function Lab({
|
|
|
1968
3589
|
const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
|
|
1969
3590
|
const modeValue = useStore(store, (s) => s.mode);
|
|
1970
3591
|
const layout = useStore(store, (s) => s.layout);
|
|
3592
|
+
const undockedPanels = useStore(store, (s) => s.undockedPanels);
|
|
3593
|
+
const panelHostsRef = useRef(null);
|
|
3594
|
+
if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
|
|
3595
|
+
const outerSurface = useSurfaceOptional();
|
|
3596
|
+
const outerCanvas = useSurfaceCanvas();
|
|
3597
|
+
const [ownCanvas, setOwnCanvas] = useState(null);
|
|
3598
|
+
const ownCanvasRef = useRef(null);
|
|
3599
|
+
const bufferRef = useRef({ w: 0, h: 0 });
|
|
3600
|
+
const surfaceRef = useRef(null);
|
|
3601
|
+
const onFrame = useCallback((frame) => {
|
|
3602
|
+
const c2 = ownCanvasRef.current;
|
|
3603
|
+
if (!c2) return;
|
|
3604
|
+
const w2 = Math.round(frame.size.width * frame.dpr);
|
|
3605
|
+
const h = Math.round(frame.size.height * frame.dpr);
|
|
3606
|
+
if (bufferRef.current.w === w2 && bufferRef.current.h === h) return;
|
|
3607
|
+
bufferRef.current = { w: w2, h };
|
|
3608
|
+
c2.width = w2;
|
|
3609
|
+
c2.height = h;
|
|
3610
|
+
c2.style.width = `${frame.size.width}px`;
|
|
3611
|
+
c2.style.height = `${frame.size.height}px`;
|
|
3612
|
+
surfaceRef.current?.invalidateAll();
|
|
3613
|
+
}, []);
|
|
3614
|
+
const ownSurface = useTiledSurface({ onFrame });
|
|
3615
|
+
surfaceRef.current = ownSurface;
|
|
3616
|
+
const surface = outerSurface ?? ownSurface;
|
|
3617
|
+
const surfaceCanvas = outerSurface ? outerCanvas : ownCanvas;
|
|
3618
|
+
const workspacePanels = useMemo(
|
|
3619
|
+
() => Object.entries(undockedPanels).map(([key, panel]) => ({
|
|
3620
|
+
key,
|
|
3621
|
+
title: panel.sectionId,
|
|
3622
|
+
as: panel.as
|
|
3623
|
+
})),
|
|
3624
|
+
[undockedPanels]
|
|
3625
|
+
);
|
|
1971
3626
|
const resolvedMode = useResolvedMode(modeValue);
|
|
1972
3627
|
useEffect(() => {
|
|
1973
3628
|
if (mode && mode !== store.getState().mode) {
|
|
@@ -2047,27 +3702,50 @@ function Lab({
|
|
|
2047
3702
|
/* @__PURE__ */ jsx(LabHeader, {}),
|
|
2048
3703
|
children
|
|
2049
3704
|
] }),
|
|
2050
|
-
children: /* @__PURE__ */
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
3705
|
+
children: /* @__PURE__ */ jsx(PanelHostContext.Provider, { value: panelHostsRef.current, children: /* @__PURE__ */ jsx(SurfaceContext.Provider, { value: surface, children: /* @__PURE__ */ jsx(SurfaceCanvasContext.Provider, { value: surfaceCanvas, children: /* @__PURE__ */ jsxs(
|
|
3706
|
+
"div",
|
|
3707
|
+
{
|
|
3708
|
+
className: "lk-lab__body",
|
|
3709
|
+
ref: outerSurface ? void 0 : ownSurface.containerRef,
|
|
3710
|
+
children: [
|
|
3711
|
+
outerSurface ? null : (
|
|
3712
|
+
// Above the trials and inert: the marks a tile paints have
|
|
3713
|
+
// to sit over the instrument's own DOM, and nothing on this
|
|
3714
|
+
// buffer takes input — each tile has its own input box.
|
|
3715
|
+
/* @__PURE__ */ jsx(
|
|
3716
|
+
"canvas",
|
|
3717
|
+
{
|
|
3718
|
+
className: "lk-lab__surface",
|
|
3719
|
+
ref: (el) => {
|
|
3720
|
+
ownCanvasRef.current = el;
|
|
3721
|
+
setOwnCanvas(el);
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
)
|
|
3725
|
+
),
|
|
3726
|
+
tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
|
|
3727
|
+
/* @__PURE__ */ jsx(
|
|
3728
|
+
Workspace,
|
|
3729
|
+
{
|
|
3730
|
+
panels: workspacePanels,
|
|
3731
|
+
ids: trials.map((w2) => w2.id),
|
|
3732
|
+
resizable: true,
|
|
3733
|
+
reorderable: true,
|
|
3734
|
+
onReorder: (ids) => contextValue.reorderTrials(ids),
|
|
3735
|
+
layout,
|
|
3736
|
+
onLayoutChange: (next) => store.getState().setLayout(next),
|
|
3737
|
+
children: trials.map((w2) => /* @__PURE__ */ jsx(Trial, { id: w2.id, chrome, suppress }, w2.id))
|
|
3738
|
+
}
|
|
3739
|
+
)
|
|
3740
|
+
]
|
|
3741
|
+
}
|
|
3742
|
+
) }) }) })
|
|
2065
3743
|
}
|
|
2066
3744
|
)
|
|
2067
3745
|
}
|
|
2068
3746
|
) }) });
|
|
2069
3747
|
}
|
|
2070
3748
|
|
|
2071
|
-
export { DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, PITCH_LIMIT, Trial, TrialChrome, Workspace, addTrial, applyRules, as2DView, builtinRules, clampPitch, cloneTrial, closeTrial, defineInstrument, f, interstellarTheme, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, resolveConfigSchema, titleCase, useConfigSchema, useLabContext, useOrbit, validateConfigSchema, wrapYaw };
|
|
3749
|
+
export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, PITCH_LIMIT, Trial, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, clampPitch, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, defineInstrument, f, fitView, fracContains, fracIntersects, fracToWorld, fromWeaselView, interstellarTheme, isStale, markCommands, markSvgNodes, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, resolveConfigSchema, resolveMarkStyle, roundFrac, seenFrom, titleCase, toWeaselView, useConfigSchema, useLabContext, useOrbit, validateConfigSchema, worldToFrac, wrapYaw };
|
|
2072
3750
|
//# sourceMappingURL=index.js.map
|
|
2073
3751
|
//# sourceMappingURL=index.js.map
|