@weasel-js/labkit 1.3.0 → 1.4.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -55
- package/dist/_dts/CanvasStackContext-LnCfqNBA.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-C-2c72Ij.d.ts +735 -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 +110 -26
- 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-6OEYTYML.js +669 -0
- package/dist/chunk-6OEYTYML.js.map +1 -0
- package/dist/{chunk-BMW4TDP5.js → chunk-AE5CNVRU.js} +3 -3
- package/dist/{chunk-BMW4TDP5.js.map → chunk-AE5CNVRU.js.map} +1 -1
- 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-E2UQVZ44.js +458 -0
- package/dist/chunk-E2UQVZ44.js.map +1 -0
- package/dist/{chunk-LGKRIUSW.js → chunk-ESIQNQ6K.js} +32 -7
- package/dist/chunk-ESIQNQ6K.js.map +1 -0
- package/dist/{chunk-JDROYQM3.js → chunk-HHGVISVZ.js} +130 -131
- package/dist/chunk-HHGVISVZ.js.map +1 -0
- package/dist/{chunk-BDJXIBRZ.js → chunk-HMFODCOX.js} +126 -29
- package/dist/chunk-HMFODCOX.js.map +1 -0
- package/dist/{chunk-QCICIHEO.js → chunk-L5QJOOLV.js} +839 -320
- package/dist/chunk-L5QJOOLV.js.map +1 -0
- package/dist/{chunk-3XXPU73K.js → chunk-M75ZU6ZZ.js} +9 -7
- package/dist/chunk-M75ZU6ZZ.js.map +1 -0
- package/dist/{chunk-QRYPXSGP.js → chunk-QB5SM4BM.js} +16 -8
- package/dist/chunk-QB5SM4BM.js.map +1 -0
- package/dist/chunk-RBGKL7NF.js +7128 -0
- package/dist/chunk-RBGKL7NF.js.map +1 -0
- package/dist/{chunk-WTI26YTM.js → chunk-TO2FUOKF.js} +40 -13
- package/dist/chunk-TO2FUOKF.js.map +1 -0
- package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
- package/dist/chunk-XJ6N32QP.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 +517 -234
- package/dist/index.js +1769 -143
- 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 +247 -0
- package/src/annotations/store.ts +347 -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 +229 -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 +16 -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 +23 -0
- package/src/index.ts +93 -4
- 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,114 +1,1428 @@
|
|
|
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-M75ZU6ZZ.js';
|
|
4
|
+
import { useSurfaceOptional, useSurfaceCanvas, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-ESIQNQ6K.js';
|
|
5
|
+
export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-ESIQNQ6K.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-6OEYTYML.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-6OEYTYML.js';
|
|
10
|
+
import { JobProgress } from './chunk-QB5SM4BM.js';
|
|
11
|
+
export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-QB5SM4BM.js';
|
|
12
|
+
export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-HHGVISVZ.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-TO2FUOKF.js';
|
|
16
|
+
export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-TO2FUOKF.js';
|
|
17
|
+
import { CanvasStack } from './chunk-HMFODCOX.js';
|
|
18
|
+
export { CanvasStack, PITCH_LIMIT, clampPitch, orbitAfterDrag, orbitAfterWheel, useOrbit, usePanZoom, wrapYaw } from './chunk-HMFODCOX.js';
|
|
19
|
+
import { LayerList } from './chunk-AE5CNVRU.js';
|
|
20
|
+
export { LayerList } from './chunk-AE5CNVRU.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-RBGKL7NF.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-RBGKL7NF.js';
|
|
24
|
+
import { resolveLoupe, TrialLoupe } from './chunk-E2UQVZ44.js';
|
|
25
|
+
export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-E2UQVZ44.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-L5QJOOLV.js';
|
|
31
|
+
import { createContext, useState, useRef, useEffect, useContext, useMemo, Children, useLayoutEffect, useCallback, 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';
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
var
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
function
|
|
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) {
|
|
46
736
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 ?? [])
|
|
50
741
|
};
|
|
51
742
|
}
|
|
52
|
-
|
|
53
|
-
|
|
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) {
|
|
54
758
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
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 } : {}
|
|
57
765
|
};
|
|
58
766
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
selection() {
|
|
1157
|
+
const out = [];
|
|
1158
|
+
for (const target of liveTargets()) {
|
|
1159
|
+
const scene = scenes.get(target);
|
|
1160
|
+
if (!scene) continue;
|
|
1161
|
+
for (const node of scene.getSelection()) {
|
|
1162
|
+
if (scene.get(node)) out.push(`${target}/${String(node)}`);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return out;
|
|
1166
|
+
},
|
|
1167
|
+
setSelection(ids) {
|
|
1168
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
1169
|
+
for (const id of ids) {
|
|
1170
|
+
const found = nodeOf(id);
|
|
1171
|
+
if (!found) continue;
|
|
1172
|
+
const node = String(found.node.id);
|
|
1173
|
+
const on = wanted.get(found.target);
|
|
1174
|
+
if (!on) wanted.set(found.target, [node]);
|
|
1175
|
+
else if (!on.includes(node)) on.push(node);
|
|
1176
|
+
}
|
|
1177
|
+
for (const [target, scene] of scenes) {
|
|
1178
|
+
const next = wanted.get(target) ?? [];
|
|
1179
|
+
const now = scene.getSelection();
|
|
1180
|
+
if (now.length === next.length && next.every((n, i) => String(now[i]) === n)) continue;
|
|
1181
|
+
scene.setSelection(next.map(asNodeId$1));
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
subscribe(fn) {
|
|
1185
|
+
subs.add(fn);
|
|
1186
|
+
return () => {
|
|
1187
|
+
subs.delete(fn);
|
|
1188
|
+
};
|
|
1189
|
+
},
|
|
1190
|
+
add(init, config) {
|
|
1191
|
+
const target = targetOf(init.target);
|
|
1192
|
+
const data = {
|
|
1193
|
+
target: init.target,
|
|
1194
|
+
kind: init.kind,
|
|
1195
|
+
...init.points ? { points: init.points } : {},
|
|
1196
|
+
...init.title !== void 0 ? { title: init.title } : {},
|
|
1197
|
+
...init.status !== void 0 ? { status: init.status } : {},
|
|
1198
|
+
...init.tags !== void 0 ? { tags: init.tags } : {},
|
|
1199
|
+
...init.meta !== void 0 ? { meta: init.meta } : {},
|
|
1200
|
+
seen: seenFrom(config, target?.positionDependsOn ?? [])
|
|
1201
|
+
};
|
|
1202
|
+
const node = sceneFor(init.target).add({
|
|
1203
|
+
kind: "leaf",
|
|
1204
|
+
layer: "marks",
|
|
1205
|
+
pose: fracToWorld(roundFrac(init.frac), target?.content ?? NO_CONTENT),
|
|
1206
|
+
data
|
|
1207
|
+
});
|
|
1208
|
+
return `${init.target}/${String(node)}`;
|
|
1209
|
+
},
|
|
1210
|
+
update(id, patch) {
|
|
1211
|
+
const found = nodeOf(id);
|
|
1212
|
+
if (!found) return;
|
|
1213
|
+
const { frac, ...meaning } = patch;
|
|
1214
|
+
const nid = asNodeId$1(found.node.id);
|
|
1215
|
+
if (Object.keys(meaning).length > 0) {
|
|
1216
|
+
found.scene.update(nid, { data: { ...found.node.data, ...meaning } });
|
|
1217
|
+
}
|
|
1218
|
+
if (frac) {
|
|
1219
|
+
found.scene.setPose(nid, fracToWorld(roundFrac(frac), contentOf(found.target)));
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
setMeta(id, meta) {
|
|
1223
|
+
const found = nodeOf(id);
|
|
1224
|
+
if (!found) return;
|
|
1225
|
+
found.scene.update(asNodeId$1(found.node.id), { data: { ...found.node.data, meta } });
|
|
1226
|
+
},
|
|
1227
|
+
remove(id) {
|
|
1228
|
+
const found = nodeOf(id);
|
|
1229
|
+
if (found) found.scene.remove(asNodeId$1(found.node.id));
|
|
1230
|
+
},
|
|
1231
|
+
async capture(target, captureOpts) {
|
|
1232
|
+
const info = targetOf(target);
|
|
1233
|
+
if (!info) throw new Error(`[labkit] no annotation target called '${target}'`);
|
|
1234
|
+
const result = await captureTarget(
|
|
1235
|
+
{
|
|
1236
|
+
target,
|
|
1237
|
+
scene: sceneFor(target),
|
|
1238
|
+
draw: {
|
|
1239
|
+
content: info.content,
|
|
1240
|
+
positionDependsOn: info.positionDependsOn,
|
|
1241
|
+
config: opts.config?.(),
|
|
1242
|
+
meaning: opts.meaning
|
|
1243
|
+
},
|
|
1244
|
+
base: info.base,
|
|
1245
|
+
onWarn: (message) => console.warn(`[labkit] capture: ${message}`)
|
|
1246
|
+
},
|
|
1247
|
+
captureOpts
|
|
1248
|
+
);
|
|
1249
|
+
opts.onCapture?.(result);
|
|
1250
|
+
return result;
|
|
1251
|
+
},
|
|
1252
|
+
toJSON() {
|
|
1253
|
+
const out = {};
|
|
1254
|
+
for (const [target, scene] of scenes) out[target] = scene.toJSON();
|
|
1255
|
+
return { version: 1, scenes: out };
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
function annotationsFromJSON(raw, targets, rest = {}) {
|
|
1260
|
+
const doc = raw;
|
|
1261
|
+
if (doc?.version !== 1 || !doc.scenes) return createAnnotationStore({ targets, ...rest });
|
|
1262
|
+
return createAnnotationStore({ targets, restore: doc.scenes, ...rest });
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/annotations/view.ts
|
|
1266
|
+
function toWeaselView(v) {
|
|
1267
|
+
return { x: v.pan.x, y: v.pan.y, scale: { x: v.zoom, y: v.zoom } };
|
|
1268
|
+
}
|
|
1269
|
+
function fromWeaselView(v) {
|
|
1270
|
+
return { zoom: v.scale.x, pan: { x: v.x, y: v.y } };
|
|
1271
|
+
}
|
|
1272
|
+
function fitView(content, pane) {
|
|
1273
|
+
const zoom = content.w > 0 && content.h > 0 ? Math.min(pane.w / content.w, pane.h / content.h) : 1;
|
|
1274
|
+
return { zoom: Number.isFinite(zoom) && zoom > 0 ? zoom : 1, pan: { x: 0, y: 0 } };
|
|
1275
|
+
}
|
|
1276
|
+
var sameRect = (a, b2) => a !== null && a.x === b2.x && a.y === b2.y && a.w === b2.w && a.h === b2.h;
|
|
1277
|
+
function ToolBridge({ toolId }) {
|
|
1278
|
+
const tools = useActiveToolContext();
|
|
1279
|
+
const setActive = tools.setActive;
|
|
1280
|
+
useEffect(() => {
|
|
1281
|
+
setActive(toolId);
|
|
1282
|
+
}, [toolId, setActive]);
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
function pointsOf(extras, content) {
|
|
1286
|
+
const toFrac = (p2) => ({
|
|
1287
|
+
x: content.w === 0 ? 0 : p2.x / content.w,
|
|
1288
|
+
y: content.h === 0 ? 0 : p2.y / content.h
|
|
1289
|
+
});
|
|
1290
|
+
if (extras.kind === "line") {
|
|
1291
|
+
return [toFrac(extras.a), toFrac(extras.b)];
|
|
1292
|
+
}
|
|
1293
|
+
if (extras.kind === "pencil") {
|
|
1294
|
+
return extras.samples.map(toFrac);
|
|
1295
|
+
}
|
|
1296
|
+
return void 0;
|
|
1297
|
+
}
|
|
1298
|
+
function AnnotationOverlay({
|
|
1299
|
+
target,
|
|
1300
|
+
scene,
|
|
1301
|
+
config,
|
|
1302
|
+
activeToolId,
|
|
1303
|
+
meaning
|
|
65
1304
|
}) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1305
|
+
const surface = useSurfaceOptional();
|
|
1306
|
+
const canvas = useSurfaceCanvas();
|
|
1307
|
+
const [rect, setRect] = useState(null);
|
|
1308
|
+
const [input, setInput] = useState(null);
|
|
1309
|
+
const sceneCanvas = useRef(null);
|
|
1310
|
+
const toolRef = useRef(activeToolId);
|
|
1311
|
+
toolRef.current = activeToolId;
|
|
1312
|
+
const configRef = useRef(config);
|
|
1313
|
+
configRef.current = config;
|
|
1314
|
+
const id = target.id;
|
|
1315
|
+
const lastTile = useRef(null);
|
|
1316
|
+
useEffect(() => {
|
|
1317
|
+
const el = target.ref.current ?? null;
|
|
1318
|
+
if (el === lastTile.current) return;
|
|
1319
|
+
lastTile.current = el;
|
|
1320
|
+
surface?.registerTile(id, el);
|
|
1321
|
+
});
|
|
1322
|
+
useEffect(
|
|
1323
|
+
() => () => {
|
|
1324
|
+
lastTile.current = null;
|
|
1325
|
+
surface?.registerTile(id, null);
|
|
75
1326
|
},
|
|
76
|
-
[
|
|
1327
|
+
[surface, id]
|
|
77
1328
|
);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
1329
|
+
useEffect(
|
|
1330
|
+
() => surface?.registerPainter(id, (next) => {
|
|
1331
|
+
setRect((prev) => sameRect(prev, next) ? prev : next);
|
|
1332
|
+
sceneCanvas.current?.requestRedraw();
|
|
1333
|
+
}),
|
|
1334
|
+
[surface, id]
|
|
1335
|
+
);
|
|
1336
|
+
const container = surface?.getContainer() ?? null;
|
|
1337
|
+
if (!container || !rect) return null;
|
|
1338
|
+
const view = toWeaselView(target.view ?? fitView(target.content, { w: rect.w, h: rect.h }));
|
|
1339
|
+
const drawOne = createMarkDrawOne({
|
|
1340
|
+
content: target.content,
|
|
1341
|
+
positionDependsOn: target.positionDependsOn,
|
|
1342
|
+
config,
|
|
1343
|
+
meaning
|
|
1344
|
+
});
|
|
1345
|
+
const factories = {};
|
|
1346
|
+
for (const weaselTool of ANNOTATION_WEASEL_TOOLS) {
|
|
1347
|
+
factories[weaselTool] = (_bounds, extras) => {
|
|
1348
|
+
const info = annotationToolInfo(toolRef.current);
|
|
1349
|
+
const kind = info?.kind;
|
|
1350
|
+
if (!kind) return null;
|
|
1351
|
+
const points = pointsOf(extras, target.content);
|
|
1352
|
+
const data = {
|
|
1353
|
+
target: id,
|
|
1354
|
+
kind,
|
|
1355
|
+
...points ? { points } : {},
|
|
1356
|
+
seen: seenFrom(configRef.current, target.positionDependsOn ?? [])
|
|
1357
|
+
};
|
|
1358
|
+
return { data };
|
|
87
1359
|
};
|
|
88
|
-
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
1360
|
+
}
|
|
1361
|
+
const box = {
|
|
1362
|
+
["--lk-anno-x"]: `${rect.x}px`,
|
|
1363
|
+
["--lk-anno-y"]: `${rect.y}px`,
|
|
1364
|
+
["--lk-anno-w"]: `${rect.w}px`,
|
|
1365
|
+
["--lk-anno-h"]: `${rect.h}px`
|
|
1366
|
+
};
|
|
1367
|
+
return createPortal(
|
|
1368
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1369
|
+
/* @__PURE__ */ jsx(
|
|
1370
|
+
"div",
|
|
1371
|
+
{
|
|
1372
|
+
ref: setInput,
|
|
1373
|
+
className: "lk-annotate__input",
|
|
1374
|
+
"data-annotation-target": id,
|
|
1375
|
+
role: "application",
|
|
1376
|
+
"aria-label": `Annotations on ${id}`,
|
|
1377
|
+
tabIndex: 0,
|
|
1378
|
+
style: box
|
|
1379
|
+
}
|
|
1380
|
+
),
|
|
1381
|
+
canvas && input ? (
|
|
1382
|
+
// One scope per pane: a shared <ActionsProvider> lets only the newest
|
|
1383
|
+
// canvas under it respond to input, and the rest go silently dead.
|
|
1384
|
+
/* @__PURE__ */ jsxs(WeaselProvider, { isolate: true, children: [
|
|
1385
|
+
/* @__PURE__ */ jsx(ToolBridge, { toolId: annotationToolInfo(activeToolId)?.weaselTool ?? "select" }),
|
|
1386
|
+
/* @__PURE__ */ jsx(
|
|
1387
|
+
SceneCanvas,
|
|
1388
|
+
{
|
|
1389
|
+
ref: sceneCanvas,
|
|
1390
|
+
scene,
|
|
1391
|
+
width: rect.w,
|
|
1392
|
+
height: rect.h,
|
|
1393
|
+
view,
|
|
1394
|
+
paintInto: { canvas, x: rect.x, y: rect.y },
|
|
1395
|
+
inputElement: input,
|
|
1396
|
+
defaultTools: ANNOTATION_WEASEL_TOOLS,
|
|
1397
|
+
insertNodeFactories: factories,
|
|
1398
|
+
layers: { scene: { drawOne } }
|
|
1399
|
+
}
|
|
1400
|
+
)
|
|
1401
|
+
] })
|
|
1402
|
+
) : null
|
|
1403
|
+
] }),
|
|
1404
|
+
container
|
|
100
1405
|
);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
1406
|
+
}
|
|
1407
|
+
function AnnotationTargets({
|
|
1408
|
+
capability,
|
|
1409
|
+
state,
|
|
1410
|
+
config,
|
|
1411
|
+
annotations,
|
|
1412
|
+
activeToolId
|
|
1413
|
+
}) {
|
|
1414
|
+
const targets = capability.targets(state, config);
|
|
1415
|
+
return /* @__PURE__ */ jsx(Fragment, { children: targets.map((t) => /* @__PURE__ */ jsx(
|
|
1416
|
+
AnnotationOverlay,
|
|
1417
|
+
{
|
|
1418
|
+
target: t,
|
|
1419
|
+
scene: annotations.sceneFor(t.id),
|
|
1420
|
+
meaning: capability.meaning,
|
|
1421
|
+
config,
|
|
1422
|
+
activeToolId
|
|
1423
|
+
},
|
|
1424
|
+
t.id
|
|
1425
|
+
)) });
|
|
112
1426
|
}
|
|
113
1427
|
|
|
114
1428
|
// src/config/builder.ts
|
|
@@ -1242,6 +2556,69 @@ function TrialTitleBar({ title, children }) {
|
|
|
1242
2556
|
children
|
|
1243
2557
|
] });
|
|
1244
2558
|
}
|
|
2559
|
+
function createPanelHostRegistry() {
|
|
2560
|
+
const hosts = /* @__PURE__ */ new Map();
|
|
2561
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2562
|
+
const notify = () => {
|
|
2563
|
+
for (const fn of listeners) fn();
|
|
2564
|
+
};
|
|
2565
|
+
return {
|
|
2566
|
+
set: (key, el) => {
|
|
2567
|
+
if (el) hosts.set(key, el);
|
|
2568
|
+
else hosts.delete(key);
|
|
2569
|
+
notify();
|
|
2570
|
+
},
|
|
2571
|
+
get: (key) => hosts.get(key) ?? null,
|
|
2572
|
+
subscribe: (fn) => {
|
|
2573
|
+
listeners.add(fn);
|
|
2574
|
+
return () => listeners.delete(fn);
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
var PanelHostContext = createContext(null);
|
|
2579
|
+
function usePanelHosts() {
|
|
2580
|
+
return useContext(PanelHostContext);
|
|
2581
|
+
}
|
|
2582
|
+
function Portalled({
|
|
2583
|
+
trialId,
|
|
2584
|
+
contribution,
|
|
2585
|
+
onDock
|
|
2586
|
+
}) {
|
|
2587
|
+
const hosts = usePanelHosts();
|
|
2588
|
+
const key = panelKey(trialId, contribution.id);
|
|
2589
|
+
const host = useSyncExternalStore(
|
|
2590
|
+
(fn) => hosts?.subscribe(fn) ?? (() => {
|
|
2591
|
+
}),
|
|
2592
|
+
() => hosts?.get(key) ?? null,
|
|
2593
|
+
() => null
|
|
2594
|
+
);
|
|
2595
|
+
if (!host || !contribution.item) return null;
|
|
2596
|
+
const item = contribution.item;
|
|
2597
|
+
return createPortal(
|
|
2598
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2599
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-panel-tile__titlebar", children: [
|
|
2600
|
+
/* @__PURE__ */ jsx("span", { className: "lk-panel-tile__title", children: item.title }),
|
|
2601
|
+
/* @__PURE__ */ jsx(
|
|
2602
|
+
"button",
|
|
2603
|
+
{
|
|
2604
|
+
type: "button",
|
|
2605
|
+
className: "lk-panel-tile__dock",
|
|
2606
|
+
"aria-label": `Dock ${item.title}`,
|
|
2607
|
+
title: `Dock ${item.title}`,
|
|
2608
|
+
onClick: () => onDock(contribution.id),
|
|
2609
|
+
children: "\u21E4"
|
|
2610
|
+
}
|
|
2611
|
+
)
|
|
2612
|
+
] }),
|
|
2613
|
+
item.body
|
|
2614
|
+
] }),
|
|
2615
|
+
host
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
function UndockedSections({ trialId, sections, onDock }) {
|
|
2619
|
+
if (sections.length === 0) return null;
|
|
2620
|
+
return /* @__PURE__ */ jsx(Fragment, { children: sections.map((c2) => /* @__PURE__ */ jsx(Portalled, { trialId, contribution: c2, onDock }, c2.id)) });
|
|
2621
|
+
}
|
|
1245
2622
|
var NO_OP = () => {
|
|
1246
2623
|
};
|
|
1247
2624
|
function TrialChrome({
|
|
@@ -1251,6 +2628,7 @@ function TrialChrome({
|
|
|
1251
2628
|
isLastTrial,
|
|
1252
2629
|
undoBindings,
|
|
1253
2630
|
job,
|
|
2631
|
+
loupe,
|
|
1254
2632
|
trialChrome,
|
|
1255
2633
|
chrome,
|
|
1256
2634
|
suppress,
|
|
@@ -1264,6 +2642,13 @@ function TrialChrome({
|
|
|
1264
2642
|
const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
|
|
1265
2643
|
const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
|
|
1266
2644
|
const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
|
|
2645
|
+
const undockPanelAction = useStore(storeCtx.store, (s) => s.undockPanel);
|
|
2646
|
+
const dockPanelAction = useStore(storeCtx.store, (s) => s.dockPanel);
|
|
2647
|
+
const undockedPanels = useStore(storeCtx.store, (s) => s.undockedPanels);
|
|
2648
|
+
const undockedIds = useMemo(
|
|
2649
|
+
() => Object.values(undockedPanels).filter((p2) => p2.trialId === trialId).map((p2) => p2.sectionId),
|
|
2650
|
+
[undockedPanels, trialId]
|
|
2651
|
+
);
|
|
1267
2652
|
const view2d = as2DView(record.view);
|
|
1268
2653
|
const configSchema = useConfigSchema(instrument);
|
|
1269
2654
|
const ctx = useMemo(() => {
|
|
@@ -1281,6 +2666,8 @@ function TrialChrome({
|
|
|
1281
2666
|
canRedo: undoBindings?.canRedo ?? false,
|
|
1282
2667
|
undo: undoBindings?.undo ?? NO_OP,
|
|
1283
2668
|
redo: undoBindings?.redo ?? NO_OP,
|
|
2669
|
+
loupeOn: loupe?.on ?? false,
|
|
2670
|
+
toggleLoupe: loupe?.toggle ?? NO_OP,
|
|
1284
2671
|
configSchema,
|
|
1285
2672
|
configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
|
|
1286
2673
|
config: record.config,
|
|
@@ -1298,6 +2685,9 @@ function TrialChrome({
|
|
|
1298
2685
|
updateTrialState(trialId, nextState);
|
|
1299
2686
|
}
|
|
1300
2687
|
},
|
|
2688
|
+
undockedPanels: undockedIds,
|
|
2689
|
+
undockPanel: (sectionId, as) => undockPanelAction(trialId, sectionId, as),
|
|
2690
|
+
dockPanel: (sectionId) => dockPanelAction(trialId, sectionId),
|
|
1301
2691
|
savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
|
|
1302
2692
|
saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
|
|
1303
2693
|
loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
|
|
@@ -1317,10 +2707,14 @@ function TrialChrome({
|
|
|
1317
2707
|
updateTrialConfig,
|
|
1318
2708
|
updateTrialState,
|
|
1319
2709
|
undoBindings,
|
|
2710
|
+
loupe,
|
|
1320
2711
|
view2d,
|
|
1321
2712
|
activeToolId,
|
|
1322
2713
|
setActiveTool,
|
|
1323
|
-
configSchema
|
|
2714
|
+
configSchema,
|
|
2715
|
+
undockedIds,
|
|
2716
|
+
undockPanelAction,
|
|
2717
|
+
dockPanelAction
|
|
1324
2718
|
]);
|
|
1325
2719
|
const contributions = useMemo(
|
|
1326
2720
|
() => suppressContributions(
|
|
@@ -1359,7 +2753,17 @@ function TrialChrome({
|
|
|
1359
2753
|
/* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: /* @__PURE__ */ jsx(ToolbarRegion, { contributions: inRegion("toolbar"), ctx }) }),
|
|
1360
2754
|
/* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
|
|
1361
2755
|
/* @__PURE__ */ jsx(PaletteRegion, { contributions: inRegion("palette"), ctx }),
|
|
1362
|
-
/* @__PURE__ */
|
|
2756
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-trial__sidebar", children: [
|
|
2757
|
+
/* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }),
|
|
2758
|
+
/* @__PURE__ */ jsx(
|
|
2759
|
+
UndockedSections,
|
|
2760
|
+
{
|
|
2761
|
+
trialId,
|
|
2762
|
+
sections: inRegion("sidebar").filter((c2) => undockedIds.includes(c2.id)),
|
|
2763
|
+
onDock: ctx.dockPanel
|
|
2764
|
+
}
|
|
2765
|
+
)
|
|
2766
|
+
] }),
|
|
1363
2767
|
/* @__PURE__ */ jsxs("div", { className: `lk-trial__content${instrument.canvas ? " lk-trial__content--flush" : ""}`, children: [
|
|
1364
2768
|
children,
|
|
1365
2769
|
/* @__PURE__ */ jsx(ViewportRegion, { contributions: inRegion("viewport"), ctx })
|
|
@@ -1405,10 +2809,12 @@ function Trial({ id, chrome, suppress }) {
|
|
|
1405
2809
|
}
|
|
1406
2810
|
function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
1407
2811
|
const canvasContainerRef = useRef(null);
|
|
2812
|
+
const loupeHostRef = useRef(null);
|
|
1408
2813
|
const updateTrialState = useStore(store, (s) => s.updateTrialState);
|
|
1409
2814
|
const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
|
|
1410
2815
|
const updateTrialView = useStore(store, (s) => s.updateTrialView);
|
|
1411
2816
|
const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
|
|
2817
|
+
const updateTrialAnnotations = useStore(store, (s) => s.updateTrialAnnotations);
|
|
1412
2818
|
const labToolId = useStore(store, (s) => s.activeToolId);
|
|
1413
2819
|
const setLabTool = useStore(store, (s) => s.setLabTool);
|
|
1414
2820
|
const setTrialTool = useStore(store, (s) => s.setTrialTool);
|
|
@@ -1417,6 +2823,11 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1417
2823
|
const bus = busRef.current;
|
|
1418
2824
|
const [layerVisibility, setLayerVisibility] = useState({});
|
|
1419
2825
|
const [layerOrder, setLayerOrder] = useState(null);
|
|
2826
|
+
const [loupeOn, setLoupeOn] = useState(false);
|
|
2827
|
+
const visibleLayers = useMemo(
|
|
2828
|
+
() => (instrument.canvas?.layers ?? []).filter((l) => layerVisibility[l.id] !== false).map((l) => l.id),
|
|
2829
|
+
[instrument.canvas, layerVisibility]
|
|
2830
|
+
);
|
|
1420
2831
|
const undoCap = instrument.undo;
|
|
1421
2832
|
const undoEvents = useMemo(() => new Set(undoCap?.snapshotOn ?? ["state.change"]), [undoCap]);
|
|
1422
2833
|
const maxDepth = undoCap?.maxDepth ?? 50;
|
|
@@ -1428,8 +2839,69 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1428
2839
|
updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
|
|
1429
2840
|
};
|
|
1430
2841
|
const setView = (v) => updateTrialView(record.id, v);
|
|
1431
|
-
const
|
|
1432
|
-
|
|
2842
|
+
const placeView = (size) => {
|
|
2843
|
+
const declared = instrument.canvas?.initialView;
|
|
2844
|
+
if (typeof declared !== "function") return;
|
|
2845
|
+
if (record.view != null) return;
|
|
2846
|
+
setView(declared(size));
|
|
2847
|
+
};
|
|
2848
|
+
const annotationsCap = instrument.annotations;
|
|
2849
|
+
const targetsRef = useRef(() => []);
|
|
2850
|
+
targetsRef.current = () => annotationsCap ? annotationsCap.targets(record.state, record.config) : [];
|
|
2851
|
+
const annotationsRef = useRef(null);
|
|
2852
|
+
const configRef = useRef(record.config);
|
|
2853
|
+
configRef.current = record.config;
|
|
2854
|
+
if (annotationsRef.current === null) {
|
|
2855
|
+
const kept = annotationsCap?.storage ? annotationsCap.storage.load() : record.annotations;
|
|
2856
|
+
annotationsRef.current = annotationsFromJSON(kept, () => targetsRef.current(), {
|
|
2857
|
+
meaning: annotationsCap?.meaning,
|
|
2858
|
+
config: () => configRef.current,
|
|
2859
|
+
onCapture: (result) => annotationsCap?.onCapture?.(result)
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
const annotations = annotationsRef.current;
|
|
2863
|
+
const saveCap = annotationsCap?.storage;
|
|
2864
|
+
const saveRef = useRef(() => {
|
|
2865
|
+
});
|
|
2866
|
+
saveRef.current = (doc) => {
|
|
2867
|
+
if (saveCap) saveCap.save(doc);
|
|
2868
|
+
else updateTrialAnnotations(record.id, doc);
|
|
2869
|
+
};
|
|
2870
|
+
const [markMoves, setMarkMoves] = useState({ undo: false, redo: false });
|
|
2871
|
+
useEffect(() => {
|
|
2872
|
+
if (!annotationsCap) return;
|
|
2873
|
+
const read = () => {
|
|
2874
|
+
const next = { undo: annotations.canUndo(), redo: annotations.canRedo() };
|
|
2875
|
+
setMarkMoves((prev) => prev.undo === next.undo && prev.redo === next.redo ? prev : next);
|
|
2876
|
+
};
|
|
2877
|
+
read();
|
|
2878
|
+
return annotations.subscribe(read);
|
|
2879
|
+
}, [annotations, annotationsCap]);
|
|
2880
|
+
useEffect(() => {
|
|
2881
|
+
if (!annotationsCap) return;
|
|
2882
|
+
let timer = null;
|
|
2883
|
+
let dirty = false;
|
|
2884
|
+
const flush = () => {
|
|
2885
|
+
if (!dirty) return;
|
|
2886
|
+
dirty = false;
|
|
2887
|
+
saveRef.current(annotations.toJSON());
|
|
2888
|
+
};
|
|
2889
|
+
const off = annotations.subscribe(() => {
|
|
2890
|
+
dirty = true;
|
|
2891
|
+
if (timer) clearTimeout(timer);
|
|
2892
|
+
timer = setTimeout(() => {
|
|
2893
|
+
timer = null;
|
|
2894
|
+
flush();
|
|
2895
|
+
}, 250);
|
|
2896
|
+
});
|
|
2897
|
+
return () => {
|
|
2898
|
+
off();
|
|
2899
|
+
if (timer) clearTimeout(timer);
|
|
2900
|
+
flush();
|
|
2901
|
+
};
|
|
2902
|
+
}, [annotations, annotationsCap]);
|
|
2903
|
+
const declaresTools = instrument.tools != null || instrument.annotations != null;
|
|
2904
|
+
const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? (instrument.annotations ? "select" : null) : labToolId;
|
|
1433
2905
|
const setActiveTool = (id) => {
|
|
1434
2906
|
if (declaresTools) setTrialTool(record.id, id);
|
|
1435
2907
|
else setLabTool(id);
|
|
@@ -1468,7 +2940,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1468
2940
|
if (!view2d) return;
|
|
1469
2941
|
updateTrialView(record.id, { ...view2d, zoom: z });
|
|
1470
2942
|
},
|
|
1471
|
-
activeToolId: resolvedToolId
|
|
2943
|
+
activeToolId: resolvedToolId,
|
|
2944
|
+
visibleLayers
|
|
1472
2945
|
},
|
|
1473
2946
|
emit: (event) => {
|
|
1474
2947
|
snapshotIfNeeded(event);
|
|
@@ -1476,22 +2949,36 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1476
2949
|
},
|
|
1477
2950
|
job: jobCap ? job : void 0
|
|
1478
2951
|
};
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
2952
|
+
const undoState = () => {
|
|
2953
|
+
const result = undo(record.undoStack, structuredClone(record.state));
|
|
2954
|
+
if (!result) return;
|
|
2955
|
+
updateTrialState(record.id, result.snapshot);
|
|
2956
|
+
updateTrialUndoStack(record.id, result.stack);
|
|
2957
|
+
};
|
|
2958
|
+
const redoState = () => {
|
|
2959
|
+
const result = redo(record.undoStack, structuredClone(record.state));
|
|
2960
|
+
if (!result) return;
|
|
2961
|
+
updateTrialState(record.id, result.snapshot);
|
|
2962
|
+
updateTrialUndoStack(record.id, result.stack);
|
|
2963
|
+
};
|
|
2964
|
+
const undoBindings = undoCap || annotationsCap ? {
|
|
2965
|
+
canUndo: markMoves.undo || (undoCap ? record.undoStack.past.length > 0 : false),
|
|
2966
|
+
canRedo: markMoves.redo || (undoCap ? record.undoStack.future.length > 0 : false),
|
|
1482
2967
|
undo: () => {
|
|
1483
|
-
|
|
1484
|
-
if (
|
|
1485
|
-
updateTrialState(record.id, result.snapshot);
|
|
1486
|
-
updateTrialUndoStack(record.id, result.stack);
|
|
2968
|
+
if (annotationsCap && annotations.undo()) return;
|
|
2969
|
+
if (undoCap) undoState();
|
|
1487
2970
|
},
|
|
1488
2971
|
redo: () => {
|
|
1489
|
-
|
|
1490
|
-
if (
|
|
1491
|
-
updateTrialState(record.id, result.snapshot);
|
|
1492
|
-
updateTrialUndoStack(record.id, result.stack);
|
|
2972
|
+
if (annotationsCap && annotations.redo()) return;
|
|
2973
|
+
if (undoCap) redoState();
|
|
1493
2974
|
}
|
|
1494
2975
|
} : void 0;
|
|
2976
|
+
const loupeCap = useMemo(() => {
|
|
2977
|
+
const declared = instrument.loupe;
|
|
2978
|
+
if (declared == null) return null;
|
|
2979
|
+
return resolveLoupe(typeof declared === "function" ? declared(record.config) : declared);
|
|
2980
|
+
}, [instrument.loupe, record.config]);
|
|
2981
|
+
const loupeBindings = loupeCap ? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) } : void 0;
|
|
1495
2982
|
const canvasLayers = useMemo(() => {
|
|
1496
2983
|
if (!instrument.canvas) return [];
|
|
1497
2984
|
const baseLayers = instrument.canvas.layers;
|
|
@@ -1499,9 +2986,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1499
2986
|
return ordered.map((layer) => ({
|
|
1500
2987
|
id: layer.id,
|
|
1501
2988
|
visible: layerVisibility[layer.id] !== false,
|
|
1502
|
-
render: (ctx, view) => {
|
|
1503
|
-
ctx
|
|
1504
|
-
ctx.scale(view.zoom, view.zoom);
|
|
2989
|
+
render: (ctx, view, frame) => {
|
|
2990
|
+
applyCamera(ctx, view, frame);
|
|
1505
2991
|
layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom });
|
|
1506
2992
|
}
|
|
1507
2993
|
}));
|
|
@@ -1514,6 +3000,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1514
3000
|
capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
|
|
1515
3001
|
canvasContainerRef,
|
|
1516
3002
|
view: view2d ?? DEFAULT_VIEW,
|
|
3003
|
+
worldSpec: instrument.canvas?.worldSpec,
|
|
1517
3004
|
state: record.state,
|
|
1518
3005
|
config: record.config,
|
|
1519
3006
|
setState: (next) => {
|
|
@@ -1554,22 +3041,44 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1554
3041
|
}
|
|
1555
3042
|
];
|
|
1556
3043
|
}, [canvasLayers, dragDropResult.drag]);
|
|
3044
|
+
const lens = loupeCap ? /* @__PURE__ */ jsx(
|
|
3045
|
+
TrialLoupe,
|
|
3046
|
+
{
|
|
3047
|
+
capability: loupeCap,
|
|
3048
|
+
enabled: loupeOn,
|
|
3049
|
+
state: record.state,
|
|
3050
|
+
config: record.config,
|
|
3051
|
+
view: view2d ?? DEFAULT_VIEW,
|
|
3052
|
+
worldSpec: instrument.canvas?.worldSpec,
|
|
3053
|
+
hostRef: loupeHostRef
|
|
3054
|
+
}
|
|
3055
|
+
) : null;
|
|
1557
3056
|
let body;
|
|
1558
3057
|
if (instrument.canvas) {
|
|
1559
3058
|
body = /* @__PURE__ */ jsxs("div", { ref: canvasContainerRef, className: "lk-trial__canvas-host", children: [
|
|
1560
|
-
/* @__PURE__ */
|
|
3059
|
+
/* @__PURE__ */ jsxs(
|
|
1561
3060
|
CanvasStack,
|
|
1562
3061
|
{
|
|
1563
3062
|
layers: layersWithFeedback,
|
|
1564
3063
|
view: view2d ?? DEFAULT_VIEW,
|
|
1565
3064
|
onViewChange: setView,
|
|
3065
|
+
worldSpec: instrument.canvas.worldSpec,
|
|
3066
|
+
onResize: placeView,
|
|
1566
3067
|
minZoom: instrument.canvas.minZoom,
|
|
1567
3068
|
maxZoom: instrument.canvas.maxZoom,
|
|
1568
|
-
children:
|
|
3069
|
+
children: [
|
|
3070
|
+
instrument.render(renderCtx),
|
|
3071
|
+
lens
|
|
3072
|
+
]
|
|
1569
3073
|
}
|
|
1570
3074
|
),
|
|
1571
3075
|
/* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
|
|
1572
3076
|
] });
|
|
3077
|
+
} else if (lens) {
|
|
3078
|
+
body = /* @__PURE__ */ jsxs("div", { ref: loupeHostRef, className: "lk-trial__loupe-host", children: [
|
|
3079
|
+
instrument.render(renderCtx),
|
|
3080
|
+
lens
|
|
3081
|
+
] });
|
|
1573
3082
|
} else {
|
|
1574
3083
|
body = instrument.render(renderCtx);
|
|
1575
3084
|
}
|
|
@@ -1614,10 +3123,21 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1614
3123
|
}
|
|
1615
3124
|
return out;
|
|
1616
3125
|
}, [paletteNode, layerListNode]);
|
|
1617
|
-
|
|
3126
|
+
const annotationOverlays = annotationsCap ? /* @__PURE__ */ jsx(
|
|
3127
|
+
AnnotationTargets,
|
|
3128
|
+
{
|
|
3129
|
+
capability: annotationsCap,
|
|
3130
|
+
state: record.state,
|
|
3131
|
+
config: record.config,
|
|
3132
|
+
annotations,
|
|
3133
|
+
activeToolId: resolvedToolId
|
|
3134
|
+
}
|
|
3135
|
+
) : null;
|
|
3136
|
+
return /* @__PURE__ */ jsx(TrialIdProvider, { trialId: record.id, children: /* @__PURE__ */ jsx(AnnotationsContext.Provider, { value: annotationsCap ? annotations : null, children: /* @__PURE__ */ jsxs(
|
|
1618
3137
|
TrialChrome,
|
|
1619
3138
|
{
|
|
1620
3139
|
job: jobCap ? job : void 0,
|
|
3140
|
+
loupe: loupeBindings,
|
|
1621
3141
|
trialId: record.id,
|
|
1622
3142
|
record,
|
|
1623
3143
|
instrument,
|
|
@@ -1628,9 +3148,12 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1628
3148
|
suppress,
|
|
1629
3149
|
activeToolId: resolvedToolId,
|
|
1630
3150
|
setActiveTool,
|
|
1631
|
-
children:
|
|
3151
|
+
children: [
|
|
3152
|
+
body,
|
|
3153
|
+
annotationOverlays
|
|
3154
|
+
]
|
|
1632
3155
|
}
|
|
1633
|
-
);
|
|
3156
|
+
) }) });
|
|
1634
3157
|
}
|
|
1635
3158
|
|
|
1636
3159
|
// src/trial/trialOps.ts
|
|
@@ -1643,7 +3166,9 @@ function findInstrument(instruments, name) {
|
|
|
1643
3166
|
return found;
|
|
1644
3167
|
}
|
|
1645
3168
|
function initialView(instrument) {
|
|
1646
|
-
|
|
3169
|
+
const declared = instrument.canvas?.initialView;
|
|
3170
|
+
if (typeof declared === "function") return null;
|
|
3171
|
+
return declared ? structuredClone(declared) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
|
|
1647
3172
|
}
|
|
1648
3173
|
function addTrial(trials, instruments, instrumentName) {
|
|
1649
3174
|
const instrument = findInstrument(instruments, instrumentName);
|
|
@@ -1716,7 +3241,7 @@ function LabHeader() {
|
|
|
1716
3241
|
className: "lk-lab-header__add",
|
|
1717
3242
|
onClick: () => lab.addTrial(only.name),
|
|
1718
3243
|
children: [
|
|
1719
|
-
/* @__PURE__ */ jsx(
|
|
3244
|
+
/* @__PURE__ */ jsx(C, { size: 16 }),
|
|
1720
3245
|
/* @__PURE__ */ jsx("span", { children: "Add trial" })
|
|
1721
3246
|
]
|
|
1722
3247
|
}
|
|
@@ -1738,7 +3263,7 @@ function LabHeader() {
|
|
|
1738
3263
|
)
|
|
1739
3264
|
),
|
|
1740
3265
|
/* @__PURE__ */ jsx(
|
|
1741
|
-
|
|
3266
|
+
r2,
|
|
1742
3267
|
{
|
|
1743
3268
|
className: "lk-lab-header__mode",
|
|
1744
3269
|
ariaLabel: "Color mode",
|
|
@@ -1800,7 +3325,9 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
|
|
|
1800
3325
|
return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
|
|
1801
3326
|
}
|
|
1802
3327
|
var ZONE_ID = asNodeId("lk-workspace");
|
|
1803
|
-
var STRATEGIES = { grid: gridStrategy };
|
|
3328
|
+
var STRATEGIES = { grid: gridStrategy, floating: floatingStrategy };
|
|
3329
|
+
var FLOAT_ZONE_ID = asNodeId("lk-workspace-floating");
|
|
3330
|
+
var PANEL_KIND = "panel";
|
|
1804
3331
|
var KIND = "trial";
|
|
1805
3332
|
function extentOf(store, id) {
|
|
1806
3333
|
const p2 = store.getNode(id)?.membership?.placement;
|
|
@@ -1810,9 +3337,14 @@ function extentOf(store, id) {
|
|
|
1810
3337
|
if (p2.span) out.span = p2.span;
|
|
1811
3338
|
return Object.keys(out).length > 0 ? out : null;
|
|
1812
3339
|
}
|
|
3340
|
+
function PanelFrame({ panel, floating = false }) {
|
|
3341
|
+
const hosts = usePanelHosts();
|
|
3342
|
+
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) }) });
|
|
3343
|
+
}
|
|
1813
3344
|
function Workspace({
|
|
1814
3345
|
children,
|
|
1815
3346
|
ids,
|
|
3347
|
+
panels,
|
|
1816
3348
|
resizable = false,
|
|
1817
3349
|
reorderable = false,
|
|
1818
3350
|
onReorder,
|
|
@@ -1823,8 +3355,20 @@ function Workspace({
|
|
|
1823
3355
|
viewport
|
|
1824
3356
|
}) {
|
|
1825
3357
|
const items = Children.toArray(children);
|
|
1826
|
-
const
|
|
1827
|
-
const
|
|
3358
|
+
const tilePanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "tile"), [panels]);
|
|
3359
|
+
const floatPanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "floating"), [panels]);
|
|
3360
|
+
const wantedIds = [
|
|
3361
|
+
...items.map((_2, i) => ids?.[i] ?? `lk-ws-${i}`),
|
|
3362
|
+
...tilePanels.map((p2) => `lk-panel-${p2.key}`)
|
|
3363
|
+
];
|
|
3364
|
+
const idKey = wantedIds.join(",");
|
|
3365
|
+
const idKeyRef = useRef(null);
|
|
3366
|
+
const nodeIdsRef = useRef([]);
|
|
3367
|
+
if (idKeyRef.current !== idKey) {
|
|
3368
|
+
idKeyRef.current = idKey;
|
|
3369
|
+
nodeIdsRef.current = wantedIds.map(asNodeId);
|
|
3370
|
+
}
|
|
3371
|
+
const nodeIds = nodeIdsRef.current;
|
|
1828
3372
|
const layoutRef = useRef(layout);
|
|
1829
3373
|
layoutRef.current = layout;
|
|
1830
3374
|
const onLayoutChangeRef = useRef(onLayoutChange);
|
|
@@ -1839,6 +3383,13 @@ function Workspace({
|
|
|
1839
3383
|
container: { strategyId: "grid", config: { resizable, gap, padding } }
|
|
1840
3384
|
})
|
|
1841
3385
|
);
|
|
3386
|
+
store2.registerNode(
|
|
3387
|
+
createNode({
|
|
3388
|
+
kind: "zone",
|
|
3389
|
+
id: FLOAT_ZONE_ID,
|
|
3390
|
+
container: { strategyId: "floating", config: {} }
|
|
3391
|
+
})
|
|
3392
|
+
);
|
|
1842
3393
|
storeRef.current = store2;
|
|
1843
3394
|
}
|
|
1844
3395
|
const store = storeRef.current;
|
|
@@ -1854,7 +3405,8 @@ function Workspace({
|
|
|
1854
3405
|
}
|
|
1855
3406
|
for (const id of nodeIds) {
|
|
1856
3407
|
if (present.has(id)) continue;
|
|
1857
|
-
|
|
3408
|
+
const kind = String(id).startsWith("lk-panel-") ? PANEL_KIND : KIND;
|
|
3409
|
+
store.registerNode(createNode({ kind, id, parentId: ZONE_ID, focus: true }));
|
|
1858
3410
|
store.showNode(id);
|
|
1859
3411
|
const saved = layoutRef.current?.[id];
|
|
1860
3412
|
if (saved) store.patchPlacement(id, saved);
|
|
@@ -1883,7 +3435,15 @@ function Workspace({
|
|
|
1883
3435
|
);
|
|
1884
3436
|
const chrome = useMemo(() => {
|
|
1885
3437
|
const byId = new Map(nodeIds.map((id, i) => [id, items[i]]));
|
|
3438
|
+
const panelByNode = new Map(
|
|
3439
|
+
tilePanels.map((p2) => [`lk-panel-${p2.key}`, p2])
|
|
3440
|
+
);
|
|
1886
3441
|
return {
|
|
3442
|
+
[PANEL_KIND]: ({ node }) => {
|
|
3443
|
+
const panel = panelByNode.get(String(node.id));
|
|
3444
|
+
if (!panel) return null;
|
|
3445
|
+
return /* @__PURE__ */ jsx(PanelFrame, { panel });
|
|
3446
|
+
},
|
|
1887
3447
|
[KIND]: ({ node }) => {
|
|
1888
3448
|
const content = byId.get(node.id) ?? null;
|
|
1889
3449
|
if (!reorderable) return content;
|
|
@@ -1893,7 +3453,8 @@ function Workspace({
|
|
|
1893
3453
|
);
|
|
1894
3454
|
}
|
|
1895
3455
|
};
|
|
1896
|
-
}, [nodeIds, items, reorderable]);
|
|
3456
|
+
}, [nodeIds, items, reorderable, tilePanels]);
|
|
3457
|
+
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
3458
|
const grid = /* @__PURE__ */ jsx(
|
|
1898
3459
|
Container,
|
|
1899
3460
|
{
|
|
@@ -1905,7 +3466,15 @@ function Workspace({
|
|
|
1905
3466
|
onChildOrderChange: reorderable ? commitOrder : void 0
|
|
1906
3467
|
}
|
|
1907
3468
|
);
|
|
1908
|
-
return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children:
|
|
3469
|
+
return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children: floatingLayer ? (
|
|
3470
|
+
// The floating layer insets against the workspace, so it needs a
|
|
3471
|
+
// positioned box around both. Without one it insets against the page
|
|
3472
|
+
// and lands over the lab header.
|
|
3473
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-workspace-host", children: [
|
|
3474
|
+
reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid,
|
|
3475
|
+
floatingLayer
|
|
3476
|
+
] })
|
|
3477
|
+
) : reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
|
|
1909
3478
|
}
|
|
1910
3479
|
function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
|
|
1911
3480
|
const store = createLabStore({ storageKey, storage, initialMode });
|
|
@@ -1968,6 +3537,40 @@ function Lab({
|
|
|
1968
3537
|
const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
|
|
1969
3538
|
const modeValue = useStore(store, (s) => s.mode);
|
|
1970
3539
|
const layout = useStore(store, (s) => s.layout);
|
|
3540
|
+
const undockedPanels = useStore(store, (s) => s.undockedPanels);
|
|
3541
|
+
const panelHostsRef = useRef(null);
|
|
3542
|
+
if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
|
|
3543
|
+
const outerSurface = useSurfaceOptional();
|
|
3544
|
+
const outerCanvas = useSurfaceCanvas();
|
|
3545
|
+
const [ownCanvas, setOwnCanvas] = useState(null);
|
|
3546
|
+
const ownCanvasRef = useRef(null);
|
|
3547
|
+
const bufferRef = useRef({ w: 0, h: 0 });
|
|
3548
|
+
const surfaceRef = useRef(null);
|
|
3549
|
+
const onFrame = useCallback((frame) => {
|
|
3550
|
+
const c2 = ownCanvasRef.current;
|
|
3551
|
+
if (!c2) return;
|
|
3552
|
+
const w2 = Math.round(frame.size.width * frame.dpr);
|
|
3553
|
+
const h = Math.round(frame.size.height * frame.dpr);
|
|
3554
|
+
if (bufferRef.current.w === w2 && bufferRef.current.h === h) return;
|
|
3555
|
+
bufferRef.current = { w: w2, h };
|
|
3556
|
+
c2.width = w2;
|
|
3557
|
+
c2.height = h;
|
|
3558
|
+
c2.style.width = `${frame.size.width}px`;
|
|
3559
|
+
c2.style.height = `${frame.size.height}px`;
|
|
3560
|
+
surfaceRef.current?.invalidateAll();
|
|
3561
|
+
}, []);
|
|
3562
|
+
const ownSurface = useTiledSurface({ onFrame });
|
|
3563
|
+
surfaceRef.current = ownSurface;
|
|
3564
|
+
const surface = outerSurface ?? ownSurface;
|
|
3565
|
+
const surfaceCanvas = outerSurface ? outerCanvas : ownCanvas;
|
|
3566
|
+
const workspacePanels = useMemo(
|
|
3567
|
+
() => Object.entries(undockedPanels).map(([key, panel]) => ({
|
|
3568
|
+
key,
|
|
3569
|
+
title: panel.sectionId,
|
|
3570
|
+
as: panel.as
|
|
3571
|
+
})),
|
|
3572
|
+
[undockedPanels]
|
|
3573
|
+
);
|
|
1971
3574
|
const resolvedMode = useResolvedMode(modeValue);
|
|
1972
3575
|
useEffect(() => {
|
|
1973
3576
|
if (mode && mode !== store.getState().mode) {
|
|
@@ -2047,27 +3650,50 @@ function Lab({
|
|
|
2047
3650
|
/* @__PURE__ */ jsx(LabHeader, {}),
|
|
2048
3651
|
children
|
|
2049
3652
|
] }),
|
|
2050
|
-
children: /* @__PURE__ */
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
3653
|
+
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(
|
|
3654
|
+
"div",
|
|
3655
|
+
{
|
|
3656
|
+
className: "lk-lab__body",
|
|
3657
|
+
ref: outerSurface ? void 0 : ownSurface.containerRef,
|
|
3658
|
+
children: [
|
|
3659
|
+
outerSurface ? null : (
|
|
3660
|
+
// Above the trials and inert: the marks a tile paints have
|
|
3661
|
+
// to sit over the instrument's own DOM, and nothing on this
|
|
3662
|
+
// buffer takes input — each tile has its own input box.
|
|
3663
|
+
/* @__PURE__ */ jsx(
|
|
3664
|
+
"canvas",
|
|
3665
|
+
{
|
|
3666
|
+
className: "lk-lab__surface",
|
|
3667
|
+
ref: (el) => {
|
|
3668
|
+
ownCanvasRef.current = el;
|
|
3669
|
+
setOwnCanvas(el);
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
)
|
|
3673
|
+
),
|
|
3674
|
+
tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
|
|
3675
|
+
/* @__PURE__ */ jsx(
|
|
3676
|
+
Workspace,
|
|
3677
|
+
{
|
|
3678
|
+
panels: workspacePanels,
|
|
3679
|
+
ids: trials.map((w2) => w2.id),
|
|
3680
|
+
resizable: true,
|
|
3681
|
+
reorderable: true,
|
|
3682
|
+
onReorder: (ids) => contextValue.reorderTrials(ids),
|
|
3683
|
+
layout,
|
|
3684
|
+
onLayoutChange: (next) => store.getState().setLayout(next),
|
|
3685
|
+
children: trials.map((w2) => /* @__PURE__ */ jsx(Trial, { id: w2.id, chrome, suppress }, w2.id))
|
|
3686
|
+
}
|
|
3687
|
+
)
|
|
3688
|
+
]
|
|
3689
|
+
}
|
|
3690
|
+
) }) }) })
|
|
2065
3691
|
}
|
|
2066
3692
|
)
|
|
2067
3693
|
}
|
|
2068
3694
|
) }) });
|
|
2069
3695
|
}
|
|
2070
3696
|
|
|
2071
|
-
export { DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell,
|
|
3697
|
+
export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, Trial, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, defineInstrument, f, fitView, fracContains, fracIntersects, fracToWorld, fromWeaselView, interstellarTheme, isStale, markCommands, markSvgNodes, reorderTrials, resetTrial, resolveConfigSchema, resolveMarkStyle, roundFrac, seenFrom, titleCase, toWeaselView, useConfigSchema, useLabContext, validateConfigSchema, worldToFrac };
|
|
2072
3698
|
//# sourceMappingURL=index.js.map
|
|
2073
3699
|
//# sourceMappingURL=index.js.map
|