@weasel-js/labkit 1.3.0-pre.0 → 1.4.0-pre.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_dts/CanvasStackContext-DtAn_3O-.d.ts +42 -0
- package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
- package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
- package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
- package/dist/_dts/frac-Cp3NivlC.d.ts +728 -0
- package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
- package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
- package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
- package/dist/_dts/{types-AHlQxBNN.d.ts → types-D6s4b7if.d.ts} +2 -2
- package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
- package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
- package/dist/canvas/index.d.ts +43 -25
- package/dist/canvas/index.js +5 -3
- package/dist/chrome/index.d.ts +9 -6
- package/dist/chrome/index.js +7 -6
- package/dist/chunk-2AYGEN57.js +32 -0
- package/dist/chunk-2AYGEN57.js.map +1 -0
- package/dist/{chunk-RVNN4CHQ.js → chunk-3P5TTTA7.js} +16 -8
- package/dist/chunk-3P5TTTA7.js.map +1 -0
- package/dist/{chunk-NS54R52R.js → chunk-3ZELRRBV.js} +45 -40
- package/dist/chunk-3ZELRRBV.js.map +1 -0
- package/dist/{chunk-HFDVGF4X.js → chunk-4EY67BKJ.js} +32 -7
- package/dist/chunk-4EY67BKJ.js.map +1 -0
- package/dist/chunk-5TCEVMPI.js +140 -0
- package/dist/chunk-5TCEVMPI.js.map +1 -0
- package/dist/{chunk-O7NVSCLN.js → chunk-6JMU56CU.js} +133 -134
- package/dist/chunk-6JMU56CU.js.map +1 -0
- package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
- package/dist/chunk-CQLQPQ4P.js.map +1 -0
- package/dist/chunk-CTRKTLYZ.js +33 -0
- package/dist/chunk-CTRKTLYZ.js.map +1 -0
- package/dist/chunk-EQIB2PGC.js +458 -0
- package/dist/chunk-EQIB2PGC.js.map +1 -0
- package/dist/chunk-HEORBLVT.js +7128 -0
- package/dist/chunk-HEORBLVT.js.map +1 -0
- package/dist/{chunk-V7PRSIRQ.js → chunk-JDDIZSLL.js} +4 -4
- package/dist/chunk-JDDIZSLL.js.map +1 -0
- package/dist/chunk-LITE5YFB.js +669 -0
- package/dist/chunk-LITE5YFB.js.map +1 -0
- package/dist/{chunk-Z6HJ5FEM.js → chunk-ROQDRXKW.js} +732 -315
- package/dist/chunk-ROQDRXKW.js.map +1 -0
- package/dist/chunk-SBXNQW4G.js +3 -0
- package/dist/chunk-SBXNQW4G.js.map +1 -0
- package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
- package/dist/chunk-XJ6N32QP.js.map +1 -0
- package/dist/chunk-YQJNT4XQ.js +293 -0
- package/dist/chunk-YQJNT4XQ.js.map +1 -0
- package/dist/controls/index.d.ts +4 -28
- package/dist/controls/index.js +3 -3
- package/dist/dragdrop/index.d.ts +50 -5
- package/dist/dragdrop/index.js +3 -1
- package/dist/index.d.ts +660 -302
- package/dist/index.js +1793 -587
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +4 -2
- 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 +151 -91
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +6 -5
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/styles.css +229 -703
- package/dist/surface/index.d.ts +18 -2
- package/dist/surface/index.js +2 -2
- package/dist/ui/layers/index.d.ts +8 -2
- package/dist/ui/layers/index.js +2 -4
- package/dist/undo/index.d.ts +5 -4
- package/package.json +14 -7
- package/src/annotations/AnnotationOverlay.tsx +205 -0
- package/src/annotations/AnnotationTargets.tsx +37 -0
- package/src/annotations/Annotations.less +129 -0
- package/src/annotations/Annotations.meaning.test.tsx +111 -0
- package/src/annotations/Annotations.overlay.test.tsx +142 -0
- package/src/annotations/AnnotationsContext.ts +49 -0
- package/src/annotations/ExportMenu.test.tsx +86 -0
- package/src/annotations/ExportMenu.tsx +182 -0
- package/src/annotations/MarkList.tsx +75 -0
- package/src/annotations/capture.test.ts +140 -0
- package/src/annotations/capture.ts +237 -0
- package/src/annotations/drawOne.test.ts +56 -0
- package/src/annotations/drawOne.ts +47 -0
- package/src/annotations/frac.test.ts +59 -0
- package/src/annotations/frac.ts +66 -0
- package/src/annotations/history.test.ts +82 -0
- package/src/annotations/history.ts +118 -0
- package/src/annotations/index.ts +37 -0
- package/src/annotations/paint.test.ts +125 -0
- package/src/annotations/paint.ts +130 -0
- package/src/annotations/staleness.test.ts +56 -0
- package/src/annotations/staleness.ts +41 -0
- package/src/annotations/store.test.ts +175 -0
- package/src/annotations/store.ts +317 -0
- package/src/annotations/svgNodes.test.ts +77 -0
- package/src/annotations/svgNodes.ts +74 -0
- package/src/annotations/toolMap.test.ts +36 -0
- package/src/annotations/toolMap.ts +54 -0
- package/src/annotations/types.ts +222 -0
- package/src/annotations/view.test.ts +37 -0
- package/src/annotations/view.ts +40 -0
- package/src/canvas/AGENTS.md +55 -5
- package/src/canvas/CanvasStack.test.tsx +27 -1
- package/src/canvas/CanvasStack.tsx +29 -4
- package/src/canvas/CanvasStackContext.ts +23 -4
- package/src/canvas/camera.test.ts +78 -0
- package/src/canvas/camera.ts +62 -0
- package/src/canvas/canvasCoords.test.ts +26 -0
- package/src/canvas/canvasCoords.ts +20 -7
- package/src/canvas/index.ts +5 -1
- package/src/canvas/useLayerScheduler.ts +7 -3
- package/src/canvas/usePanZoom.test.ts +36 -5
- package/src/canvas/usePanZoom.ts +20 -15
- package/src/canvas/worldSpec.test.ts +63 -0
- package/src/canvas/worldSpec.ts +48 -0
- package/src/chrome/builtins.test.ts +31 -1
- package/src/chrome/builtins.tsx +63 -19
- package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
- package/src/chrome/regions/PaletteRegion.tsx +4 -0
- package/src/chrome/regions/SidebarRegion.tsx +37 -13
- package/src/chrome/regions/ToolbarRegion.tsx +2 -1
- package/src/chrome/regions/ViewportRegion.tsx +9 -1
- package/src/chrome/regions/regions.test.tsx +6 -1
- package/src/chrome/types.ts +20 -0
- package/src/config/builder.test.ts +7 -1
- package/src/config/builder.ts +1 -5
- package/src/config/resolve.test.ts +1 -2
- package/src/controls/ControlPanel.stories.tsx +41 -2
- package/src/controls/ControlPanel.test.tsx +69 -9
- package/src/controls/ControlPanel.tsx +106 -25
- package/src/dragdrop/DragDropRuntime.tsx +8 -2
- package/src/dragdrop/dragDrop.test.tsx +29 -2
- package/src/dragdrop/index.ts +11 -0
- package/src/index.test.ts +22 -0
- package/src/index.ts +136 -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/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.tsx +1 -1
- 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 +25 -17
- 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 -4
- 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/theme/Interstellar.stories.tsx +1 -1
- 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.config.test.tsx +3 -8
- 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/src/ui/layers/index.ts +5 -1
- package/dist/_dts/index-DgcNdEdh.d.ts +0 -314
- package/dist/chunk-4TMMVIDM.js.map +0 -1
- package/dist/chunk-CPUJ3QXL.js.map +0 -1
- package/dist/chunk-HFDVGF4X.js.map +0 -1
- package/dist/chunk-NS54R52R.js.map +0 -1
- package/dist/chunk-O2BULFHX.js +0 -152
- package/dist/chunk-O2BULFHX.js.map +0 -1
- package/dist/chunk-O7NVSCLN.js.map +0 -1
- package/dist/chunk-PO6L3NSH.js +0 -366
- package/dist/chunk-PO6L3NSH.js.map +0 -1
- package/dist/chunk-PQJ5B2U2.js +0 -27
- package/dist/chunk-PQJ5B2U2.js.map +0 -1
- package/dist/chunk-RVNN4CHQ.js.map +0 -1
- package/dist/chunk-TLGRYALP.js +0 -6151
- package/dist/chunk-TLGRYALP.js.map +0 -1
- package/dist/chunk-V7PRSIRQ.js.map +0 -1
- package/dist/chunk-Z6HJ5FEM.js.map +0 -1
- package/dist/chunk-ZZAYVX4X.js +0 -505
- package/dist/chunk-ZZAYVX4X.js.map +0 -1
- package/src/primitives/DragHandleGlyph.tsx +0 -29
- package/src/ui/format.test.ts +0 -33
- package/src/ui/format.ts +0 -30
- package/src/ui/layers/LayerStack.less +0 -143
- package/src/ui/layers/LayerStack.stories.tsx +0 -45
- package/src/ui/layers/LayerStack.test.tsx +0 -170
- package/src/ui/layers/LayerStack.tsx +0 -208
- package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
- package/src/ui/properties/ColorRow.stories.tsx +0 -106
- package/src/ui/properties/CurveField.less +0 -48
- package/src/ui/properties/CurveField.stories.tsx +0 -20
- package/src/ui/properties/CurveField.test.tsx +0 -127
- package/src/ui/properties/CurveField.tsx +0 -184
- package/src/ui/properties/EffectCard.tsx +0 -324
- package/src/ui/properties/Gallery.stories.tsx +0 -178
- package/src/ui/properties/NumberRow.stories.tsx +0 -65
- package/src/ui/properties/PropertyGroup.less +0 -50
- package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
- package/src/ui/properties/PropertyGroup.test.tsx +0 -33
- package/src/ui/properties/PropertyGroup.tsx +0 -45
- package/src/ui/properties/PropertyList.stories.tsx +0 -83
- package/src/ui/properties/PropertyPanel.less +0 -606
- package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
- package/src/ui/properties/PropertyPanel.test.tsx +0 -193
- package/src/ui/properties/PropertyPanel.tsx +0 -469
- package/src/ui/properties/PropertyRow.stories.tsx +0 -53
- package/src/ui/properties/SelectRow.stories.tsx +0 -58
- package/src/ui/properties/SliderRow.stories.tsx +0 -74
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
- package/src/ui/properties/TextRow.stories.tsx +0 -62
- package/src/ui/properties/ToggleRow.stories.tsx +0 -57
- package/src/ui/properties/index.ts +0 -39
- package/src/ui/properties/storyLayouts.tsx +0 -36
package/dist/index.js
CHANGED
|
@@ -1,34 +1,1401 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { createEventBus, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
|
|
2
|
+
export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
|
|
3
|
+
import { useDragDrop, DragOverlay, Palette } from './chunk-5TCEVMPI.js';
|
|
4
|
+
import { useSurfaceOptional, useSurfaceCanvas, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-4EY67BKJ.js';
|
|
5
|
+
export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-4EY67BKJ.js';
|
|
3
6
|
import { useJob } from './chunk-LN6JDUGB.js';
|
|
4
7
|
export { useJob } from './chunk-LN6JDUGB.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
export {
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import {
|
|
8
|
+
import { ANNOTATION_WEASEL_TOOLS, annotationToolInfo, suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, SidebarRegion, ViewportRegion, StatusRegion, AnnotationsContext } from './chunk-LITE5YFB.js';
|
|
9
|
+
export { ANNOTATION_TOOLS, ANNOTATION_WEASEL_TOOLS, AnnotationsContext, ExportMenu, MarkList, PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, annotationToolInfo, builtinContributions, mergeContributions, suppressContributions, useAnnotations, useAnnotationsOptional } from './chunk-LITE5YFB.js';
|
|
10
|
+
import { JobProgress } from './chunk-3P5TTTA7.js';
|
|
11
|
+
export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-3P5TTTA7.js';
|
|
12
|
+
export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-6JMU56CU.js';
|
|
13
|
+
import { LabStoreContext, TrialIdProvider, createLabStore, panelKey, noneAdapter } from './chunk-XJ6N32QP.js';
|
|
14
|
+
export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-XJ6N32QP.js';
|
|
15
|
+
import { fromConfigFields } from './chunk-YQJNT4XQ.js';
|
|
16
|
+
export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-YQJNT4XQ.js';
|
|
17
|
+
import { CanvasStack } from './chunk-3ZELRRBV.js';
|
|
18
|
+
export { CanvasStack } from './chunk-3ZELRRBV.js';
|
|
19
|
+
import { LayerList } from './chunk-JDDIZSLL.js';
|
|
20
|
+
export { LayerList } from './chunk-JDDIZSLL.js';
|
|
21
|
+
import './chunk-SBXNQW4G.js';
|
|
22
|
+
import { C, p, r2 } from './chunk-HEORBLVT.js';
|
|
23
|
+
export { k2 as CheckboxRow, O2 as ColorRow, m2 as CurveField, _3 as EffectCard, y2 as EffectCardList, c3 as LayerStack, j2 as NumberRow, b3 as PropertyGroup, S2 as PropertyList, x3 as PropertyPanel, w as PropertyRow, C2 as PropertySpan, M as SelectRow, E as SliderRow, g as Subpanel, A2 as TextRow, N as ToggleRow } from './chunk-HEORBLVT.js';
|
|
24
|
+
import { resolveLoupe, TrialLoupe } from './chunk-EQIB2PGC.js';
|
|
25
|
+
export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-EQIB2PGC.js';
|
|
26
|
+
export { centerOn, zoomAt } from './chunk-CTRKTLYZ.js';
|
|
27
|
+
import { applyCamera } from './chunk-2AYGEN57.js';
|
|
28
|
+
export { DEFAULT_FRAME, applyCamera, resolveFrame, screenToWorld, worldToScreen } from './chunk-2AYGEN57.js';
|
|
29
|
+
export { CanvasStackContext } from './chunk-CQLQPQ4P.js';
|
|
30
|
+
import { textCommand, linePath, markerDrawCommands, ellipsePath, rectPath, createScene, asNodeId as asNodeId$1, WeaselProvider, SceneCanvas, sceneFromJSON, PathBuilder, renderSceneToPixels, useActiveToolContext, resolveStrokeWidth, getMarker, getPaintKind, boundsOfPath, tileGeometry, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, SCRIPT_METRICS } from './chunk-ROQDRXKW.js';
|
|
31
|
+
import { createContext, useState, useRef, useEffect, useCallback, useContext, useMemo, Children, useLayoutEffect, useSyncExternalStore } from 'react';
|
|
28
32
|
import { createPortal } from 'react-dom';
|
|
33
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
34
|
+
import { useStore } from 'zustand/react';
|
|
29
35
|
import { Provider, StrategyRegistryProvider, DragProvider, Container, useDragHandle } from 'windease/react';
|
|
30
|
-
import { asNodeId, Store, createNode, gridStrategy } from 'windease';
|
|
36
|
+
import { asNodeId, Store, createNode, floatingStrategy, gridStrategy } from 'windease';
|
|
31
37
|
|
|
38
|
+
// ../svg/dist/index.js
|
|
39
|
+
var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
|
|
40
|
+
function multiply(a, b2) {
|
|
41
|
+
return [
|
|
42
|
+
a[0] * b2[0] + a[2] * b2[1],
|
|
43
|
+
a[1] * b2[0] + a[3] * b2[1],
|
|
44
|
+
a[0] * b2[2] + a[2] * b2[3],
|
|
45
|
+
a[1] * b2[2] + a[3] * b2[3],
|
|
46
|
+
a[0] * b2[4] + a[2] * b2[5] + a[4],
|
|
47
|
+
a[1] * b2[4] + a[3] * b2[5] + a[5]
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
function isIdentity(m2, eps = 1e-9) {
|
|
51
|
+
return Math.abs(m2[0] - 1) < eps && Math.abs(m2[1]) < eps && Math.abs(m2[2]) < eps && Math.abs(m2[3] - 1) < eps && Math.abs(m2[4]) < eps && Math.abs(m2[5]) < eps;
|
|
52
|
+
}
|
|
53
|
+
function formatMatrix(m2) {
|
|
54
|
+
if (isIdentity(m2)) return null;
|
|
55
|
+
return `matrix(${m2.map((n) => trimNumber(n)).join(" ")})`;
|
|
56
|
+
}
|
|
57
|
+
function trimNumber(n) {
|
|
58
|
+
if (!Number.isFinite(n)) return "0";
|
|
59
|
+
const r2 = Math.round(n * 1e6) / 1e6;
|
|
60
|
+
if (Number.isInteger(r2)) return String(r2);
|
|
61
|
+
return String(r2);
|
|
62
|
+
}
|
|
63
|
+
function patternSpecOf(paint) {
|
|
64
|
+
return "tile" in paint.pattern ? paint.pattern : null;
|
|
65
|
+
}
|
|
66
|
+
function patternXml(id, paint, onWarn) {
|
|
67
|
+
const spec = patternSpecOf(paint);
|
|
68
|
+
if (!spec) {
|
|
69
|
+
onWarn?.("pattern fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
const geometry = tileGeometry(spec);
|
|
73
|
+
const body = geometry.shapes.map(shapeXml).join("");
|
|
74
|
+
const origin = paint.origin ?? { x: 0, y: 0 };
|
|
75
|
+
const attrs = [
|
|
76
|
+
`id="${id}"`,
|
|
77
|
+
'patternUnits="userSpaceOnUse"',
|
|
78
|
+
`width="${trimNumber(geometry.size)}"`,
|
|
79
|
+
`height="${trimNumber(geometry.size)}"`
|
|
80
|
+
];
|
|
81
|
+
if (origin.x !== 0 || origin.y !== 0) {
|
|
82
|
+
attrs.push(`x="${trimNumber(origin.x)}"`, `y="${trimNumber(origin.y)}"`);
|
|
83
|
+
}
|
|
84
|
+
attrs.push(`data-weasel-tile="${escapeAttr(JSON.stringify(spec))}"`);
|
|
85
|
+
return `<pattern ${attrs.join(" ")}>${body}</pattern>`;
|
|
86
|
+
}
|
|
87
|
+
function shapeXml(shape) {
|
|
88
|
+
switch (shape.kind) {
|
|
89
|
+
case "line":
|
|
90
|
+
return `<line x1="${trimNumber(shape.x1)}" y1="${trimNumber(shape.y1)}" x2="${trimNumber(shape.x2)}" y2="${trimNumber(shape.y2)}" stroke="${shape.color}" stroke-width="${trimNumber(shape.width)}"/>`;
|
|
91
|
+
case "circle":
|
|
92
|
+
return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${shape.color}"/>`;
|
|
93
|
+
case "ellipse": {
|
|
94
|
+
const deg = shape.rotation * 180 / Math.PI;
|
|
95
|
+
const transform = deg === 0 ? "" : ` transform="rotate(${trimNumber(deg)} ${trimNumber(shape.cx)} ${trimNumber(shape.cy)})"`;
|
|
96
|
+
return `<ellipse cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" rx="${trimNumber(shape.rx)}" ry="${trimNumber(shape.ry)}" fill="${shape.color}"${transform}/>`;
|
|
97
|
+
}
|
|
98
|
+
case "rect":
|
|
99
|
+
return `<rect x="${trimNumber(shape.x)}" y="${trimNumber(shape.y)}" width="${trimNumber(shape.width)}" height="${trimNumber(shape.height)}" fill="${shape.color}"/>`;
|
|
100
|
+
default:
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function escapeAttr(s) {
|
|
105
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
106
|
+
}
|
|
107
|
+
function serializePathD(path) {
|
|
108
|
+
if (path.kind === "rect") {
|
|
109
|
+
const { x: x2, y: y2, width, height } = path;
|
|
110
|
+
return `M${trimNumber(x2)} ${trimNumber(y2)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
|
|
111
|
+
}
|
|
112
|
+
const cmds = path.commands;
|
|
113
|
+
const xs = path.coords;
|
|
114
|
+
const out = [];
|
|
115
|
+
let ci = 0;
|
|
116
|
+
for (let i = 0; i < cmds.length; i++) {
|
|
117
|
+
const c2 = cmds[i];
|
|
118
|
+
if (c2 === PATH_M) {
|
|
119
|
+
out.push(`M${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
|
|
120
|
+
ci += 2;
|
|
121
|
+
} else if (c2 === PATH_L) {
|
|
122
|
+
out.push(`L${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
|
|
123
|
+
ci += 2;
|
|
124
|
+
} else if (c2 === PATH_C) {
|
|
125
|
+
out.push(
|
|
126
|
+
`C${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])} ${trimNumber(xs[ci + 4])} ${trimNumber(xs[ci + 5])}`
|
|
127
|
+
);
|
|
128
|
+
ci += 6;
|
|
129
|
+
} else if (c2 === PATH_Q) {
|
|
130
|
+
out.push(
|
|
131
|
+
`Q${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])}`
|
|
132
|
+
);
|
|
133
|
+
ci += 4;
|
|
134
|
+
} else if (c2 === PATH_Z) {
|
|
135
|
+
out.push("Z");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out.join("");
|
|
139
|
+
}
|
|
140
|
+
var PaintServerRegistry = class {
|
|
141
|
+
byPaint = /* @__PURE__ */ new Map();
|
|
142
|
+
order = [];
|
|
143
|
+
counter = 0;
|
|
144
|
+
// Marker keys referenced by any stroke, in first-use order. The `<defs>`
|
|
145
|
+
// id is the marker key itself, not a minted counter id: `parseSvg` reads a
|
|
146
|
+
// `marker-end="url(#id)"` fragment back as the marker key directly (it
|
|
147
|
+
// never inspects the `<marker>` element), so a synthetic id would come
|
|
148
|
+
// back unresolvable and warn on round-trip.
|
|
149
|
+
markerKeys = [];
|
|
150
|
+
markerKeySet = /* @__PURE__ */ new Set();
|
|
151
|
+
register(paint) {
|
|
152
|
+
const existing = this.byPaint.get(paint);
|
|
153
|
+
if (existing) return existing;
|
|
154
|
+
const id = paint.fill === "pattern" ? `pat${this.counter++}` : `grad${this.counter++}`;
|
|
155
|
+
this.byPaint.set(paint, id);
|
|
156
|
+
this.order.push(paint);
|
|
157
|
+
return id;
|
|
158
|
+
}
|
|
159
|
+
/** The `<defs>` id for a marker key — the key itself, minting nothing.
|
|
160
|
+
* `undefined` when nothing is registered under `key`, so a caller emits no
|
|
161
|
+
* attribute at all rather than a `url(#…)` pointing at a def that will
|
|
162
|
+
* never be written. */
|
|
163
|
+
markerId(key) {
|
|
164
|
+
if (getMarker(key) === void 0) return void 0;
|
|
165
|
+
if (!this.markerKeySet.has(key)) {
|
|
166
|
+
this.markerKeySet.add(key);
|
|
167
|
+
this.markerKeys.push(key);
|
|
168
|
+
}
|
|
169
|
+
return key;
|
|
170
|
+
}
|
|
171
|
+
/** Emit `<defs>...</defs>` XML for every registered paint server and marker. */
|
|
172
|
+
toDefsXml(onWarn) {
|
|
173
|
+
if (this.order.length === 0 && this.markerKeys.length === 0) return "";
|
|
174
|
+
const parts = ["<defs>"];
|
|
175
|
+
for (const paint of this.order) {
|
|
176
|
+
parts.push(paintServerXml(this.byPaint.get(paint), paint, onWarn));
|
|
177
|
+
}
|
|
178
|
+
for (const key of this.markerKeys) {
|
|
179
|
+
const entry = getMarker(key);
|
|
180
|
+
if (!entry) continue;
|
|
181
|
+
parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry));
|
|
182
|
+
}
|
|
183
|
+
parts.push("</defs>");
|
|
184
|
+
return parts.join("");
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function defaultMarkerXml(id, entry) {
|
|
188
|
+
const path = entry.path({ size: 1, stroke: { paint: { fill: "solid", color: "#000" } } });
|
|
189
|
+
const d = serializePathD(path);
|
|
190
|
+
const fill = entry.fill === "none" ? "none" : "context-stroke";
|
|
191
|
+
const outline = entry.outline ? ` stroke="context-stroke" stroke-width="${entry.outline.width}" stroke-linecap="round" stroke-linejoin="round"` : "";
|
|
192
|
+
return `<marker id="${id}" markerUnits="strokeWidth" markerWidth="8" markerHeight="8" refX="0" refY="0" orient="auto" overflow="visible"><path d="${d}" fill="${fill}"${outline}/></marker>`;
|
|
193
|
+
}
|
|
194
|
+
function paintServerXml(id, paint, onWarn) {
|
|
195
|
+
if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
|
|
196
|
+
const builtin = gradientXml(id, paint);
|
|
197
|
+
if (builtin) return builtin;
|
|
198
|
+
const custom = getPaintKind(paint.fill)?.toSvg?.(id, paint);
|
|
199
|
+
if (custom) return custom;
|
|
200
|
+
onWarn?.(`${paint.fill} fill has no vector form \u2014 omitted from <defs>`);
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
function gradientUnitsAttr(units) {
|
|
204
|
+
return units === "bounds" ? "objectBoundingBox" : "userSpaceOnUse";
|
|
205
|
+
}
|
|
206
|
+
function gradientXml(id, paint) {
|
|
207
|
+
if (paint.fill === "linear-gradient") {
|
|
208
|
+
const stops = paint.stops.map(stopXml).join("");
|
|
209
|
+
return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}">${stops}</linearGradient>`;
|
|
210
|
+
}
|
|
211
|
+
if (paint.fill === "radial-gradient") {
|
|
212
|
+
const stops = paint.stops.map(stopXml).join("");
|
|
213
|
+
return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}">${stops}</radialGradient>`;
|
|
214
|
+
}
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
function stopXml(s) {
|
|
218
|
+
const m2 = /^#([0-9a-f]{6})([0-9a-f]{2})$/i.exec(s.color);
|
|
219
|
+
if (m2) {
|
|
220
|
+
const alpha = parseInt(m2[2], 16) / 255;
|
|
221
|
+
return `<stop offset="${trimNumber(s.offset)}" stop-color="#${m2[1].toLowerCase()}" stop-opacity="${trimNumber(alpha)}"/>`;
|
|
222
|
+
}
|
|
223
|
+
return `<stop offset="${trimNumber(s.offset)}" stop-color="${s.color}"/>`;
|
|
224
|
+
}
|
|
225
|
+
function serializeSvg(nodes, opts = {}) {
|
|
226
|
+
const registry = new PaintServerRegistry();
|
|
227
|
+
registerPaintServers(nodes, registry);
|
|
228
|
+
const bounds = opts.viewBox ?? computeBounds(nodes);
|
|
229
|
+
const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
|
|
230
|
+
const namespaces = opts.namespaces ?? {};
|
|
231
|
+
const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
|
|
232
|
+
for (const [prefix, uri] of Object.entries(namespaces)) {
|
|
233
|
+
rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
|
|
234
|
+
}
|
|
235
|
+
rootAttrs.push(`viewBox="${vb}"`);
|
|
236
|
+
if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
|
|
237
|
+
if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
|
|
238
|
+
if (opts.documentMeta) {
|
|
239
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
240
|
+
const bucket = opts.documentMeta[prefix];
|
|
241
|
+
if (!bucket?.attrs) continue;
|
|
242
|
+
for (const [name, value] of Object.entries(bucket.attrs)) {
|
|
243
|
+
rootAttrs.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
let docMetaXml = "";
|
|
248
|
+
if (opts.documentMeta) {
|
|
249
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
250
|
+
const bucket = opts.documentMeta[prefix];
|
|
251
|
+
if (!bucket?.elements) continue;
|
|
252
|
+
for (const [localName, list] of Object.entries(bucket.elements)) {
|
|
253
|
+
for (const el of list) docMetaXml += namespacedElementXml(prefix, localName, el);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const defsXml = registry.toDefsXml(opts.onWarn);
|
|
258
|
+
const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces)).join("");
|
|
259
|
+
const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
|
|
260
|
+
return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
|
|
261
|
+
}
|
|
262
|
+
function namespacedElementXml(prefix, localName, el) {
|
|
263
|
+
const attrs = [];
|
|
264
|
+
for (const [name, value] of Object.entries(el.attrs)) {
|
|
265
|
+
attrs.push(`${name}="${escapeAttr2(value)}"`);
|
|
266
|
+
}
|
|
267
|
+
const head = attrs.length > 0 ? `<${prefix}:${localName} ${attrs.join(" ")}>` : `<${prefix}:${localName}>`;
|
|
268
|
+
let body = "";
|
|
269
|
+
if (el.children) {
|
|
270
|
+
for (const [childName, list] of Object.entries(el.children)) {
|
|
271
|
+
for (const child of list) body += namespacedElementXml(prefix, childName, child);
|
|
272
|
+
}
|
|
273
|
+
} else if (el.text != null) {
|
|
274
|
+
body = escapeText(el.text);
|
|
275
|
+
}
|
|
276
|
+
return `${head}${body}</${prefix}:${localName}>`;
|
|
277
|
+
}
|
|
278
|
+
function metaAttrsXml(meta, namespaces) {
|
|
279
|
+
if (!meta) return "";
|
|
280
|
+
const parts = [];
|
|
281
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
282
|
+
const bucket = meta[prefix];
|
|
283
|
+
if (!bucket?.attrs) continue;
|
|
284
|
+
for (const [name, value] of Object.entries(bucket.attrs)) {
|
|
285
|
+
parts.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
289
|
+
}
|
|
290
|
+
function metaElementsXml(meta, namespaces) {
|
|
291
|
+
if (!meta) return "";
|
|
292
|
+
let out = "";
|
|
293
|
+
for (const prefix of Object.keys(namespaces)) {
|
|
294
|
+
const bucket = meta[prefix];
|
|
295
|
+
if (!bucket?.elements) continue;
|
|
296
|
+
for (const [localName, list] of Object.entries(bucket.elements)) {
|
|
297
|
+
for (const el of list) out += namespacedElementXml(prefix, localName, el);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
function registerPaintServers(nodes, registry) {
|
|
303
|
+
for (const n of nodes) {
|
|
304
|
+
if (n.kind === "group") {
|
|
305
|
+
registerPaintServers(n.children, registry);
|
|
306
|
+
} else if (n.kind === "path") {
|
|
307
|
+
if (n.fill.kind === "gradient") registry.register(n.fill.paint);
|
|
308
|
+
if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
|
|
309
|
+
registerMarkers(n.stroke, registry);
|
|
310
|
+
} else if (n.kind === "text") {
|
|
311
|
+
registerTextPaint(n.fill, registry);
|
|
312
|
+
registerTextPaint(n.stroke?.paint, registry);
|
|
313
|
+
registerMarkers(n.stroke, registry);
|
|
314
|
+
for (const run of n.runs ?? []) {
|
|
315
|
+
registerTextPaint(run.fill, registry);
|
|
316
|
+
registerTextPaint(run.stroke?.paint, registry);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function registerTextPaint(paint, registry) {
|
|
322
|
+
if (paint && !("color" in paint)) registry.register(paint);
|
|
323
|
+
}
|
|
324
|
+
function markerKeyOfRef(ref) {
|
|
325
|
+
if (typeof ref === "string") return ref;
|
|
326
|
+
if (ref && typeof ref === "object" && "key" in ref) return String(ref.key);
|
|
327
|
+
return void 0;
|
|
328
|
+
}
|
|
329
|
+
function registerMarkers(stroke, registry) {
|
|
330
|
+
if (!stroke) return;
|
|
331
|
+
for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
|
|
332
|
+
const key = markerKeyOfRef(ref);
|
|
333
|
+
if (key) registry.markerId(key);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function nodeXml(node, registry, namespaces) {
|
|
337
|
+
if (node.kind === "group") return groupXml(node, registry, namespaces);
|
|
338
|
+
if (node.kind === "text") return textXml(node, registry, namespaces);
|
|
339
|
+
if (node.kind === "image") return imageXml(node, namespaces);
|
|
340
|
+
return pathXml(node, registry, namespaces);
|
|
341
|
+
}
|
|
342
|
+
function imageXml(node, namespaces) {
|
|
343
|
+
const attrs = [
|
|
344
|
+
`href="${escapeAttr2(node.href)}"`,
|
|
345
|
+
`x="${trimNumber(node.x)}"`,
|
|
346
|
+
`y="${trimNumber(node.y)}"`,
|
|
347
|
+
`width="${trimNumber(node.width)}"`,
|
|
348
|
+
`height="${trimNumber(node.height)}"`,
|
|
349
|
+
`preserveAspectRatio="none"`
|
|
350
|
+
];
|
|
351
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
352
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
353
|
+
}
|
|
354
|
+
if (node.rotation != null && node.rotation !== 0) {
|
|
355
|
+
const cx = node.x + node.width / 2;
|
|
356
|
+
const cy = node.y + node.height / 2;
|
|
357
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
358
|
+
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
359
|
+
}
|
|
360
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
361
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
362
|
+
if (metaEls) return `<image ${attrs.join(" ")}${metaAttrs}>${metaEls}</image>`;
|
|
363
|
+
return `<image ${attrs.join(" ")}${metaAttrs}/>`;
|
|
364
|
+
}
|
|
365
|
+
function groupXml(node, registry, namespaces) {
|
|
366
|
+
const attrs = [];
|
|
367
|
+
if (node.transform) {
|
|
368
|
+
const m2 = formatMatrix(node.transform);
|
|
369
|
+
if (m2) attrs.push(`transform="${m2}"`);
|
|
370
|
+
}
|
|
371
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
372
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
373
|
+
}
|
|
374
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
375
|
+
const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
|
|
376
|
+
const body = node.children.map((c2) => nodeXml(c2, registry, namespaces)).join("");
|
|
377
|
+
return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
|
|
378
|
+
}
|
|
379
|
+
function pathXml(node, registry, namespaces) {
|
|
380
|
+
const attrs = [`d="${serializePathD(node.path)}"`];
|
|
381
|
+
const fillAttrs = paintAttrs(node.fill, "fill", registry);
|
|
382
|
+
for (const a of fillAttrs) attrs.push(a);
|
|
383
|
+
if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
|
|
384
|
+
attrs.push(`fill-rule="evenodd"`);
|
|
385
|
+
}
|
|
386
|
+
if (node.stroke) {
|
|
387
|
+
const strokeAttrs = strokeAttrsFor(node.stroke, registry);
|
|
388
|
+
for (const a of strokeAttrs) attrs.push(a);
|
|
389
|
+
} else {
|
|
390
|
+
attrs.push('stroke="none"');
|
|
391
|
+
}
|
|
392
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
393
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
394
|
+
}
|
|
395
|
+
if (node.rotation != null && node.rotation !== 0) {
|
|
396
|
+
const b2 = pathBounds(node.path);
|
|
397
|
+
const cx = b2.minX + (b2.maxX - b2.minX) / 2;
|
|
398
|
+
const cy = b2.minY + (b2.maxY - b2.minY) / 2;
|
|
399
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
400
|
+
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
401
|
+
}
|
|
402
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
403
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
404
|
+
if (metaEls) {
|
|
405
|
+
return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
|
|
406
|
+
}
|
|
407
|
+
return `<path ${attrs.join(" ")}${metaAttrs}/>`;
|
|
408
|
+
}
|
|
409
|
+
function paintAttrs(paint, name, registry, includeOpacity = true) {
|
|
410
|
+
if (paint.kind === "none") return [`${name}="none"`];
|
|
411
|
+
if (paint.kind === "solid") {
|
|
412
|
+
const out = [`${name}="${paint.color}"`];
|
|
413
|
+
if (includeOpacity && paint.opacity != null && paint.opacity !== 1) {
|
|
414
|
+
out.push(`${name}-opacity="${trimNumber(paint.opacity)}"`);
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
const id = registry.register(paint.paint);
|
|
419
|
+
return [`${name}="url(#${id})"`];
|
|
420
|
+
}
|
|
421
|
+
function coreStrokeAttrs(stroke, registry) {
|
|
422
|
+
if (!stroke) return [];
|
|
423
|
+
const width = resolveStrokeWidth(stroke.width ?? 1, 1);
|
|
424
|
+
if (!(width > 0)) return [];
|
|
425
|
+
const attrs = [];
|
|
426
|
+
if ("color" in stroke.paint) {
|
|
427
|
+
attrs.push(`stroke="${stroke.paint.color}"`);
|
|
428
|
+
if (stroke.paint.opacity != null && stroke.paint.opacity !== 1) {
|
|
429
|
+
attrs.push(`stroke-opacity="${trimNumber(stroke.paint.opacity)}"`);
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
attrs.push(`stroke="url(#${registry.register(stroke.paint)})"`);
|
|
433
|
+
}
|
|
434
|
+
attrs.push(`stroke-width="${trimNumber(width)}"`);
|
|
435
|
+
if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
436
|
+
if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
437
|
+
if (stroke.dash && stroke.dash.length > 0) {
|
|
438
|
+
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
439
|
+
}
|
|
440
|
+
if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
441
|
+
for (const [field, attr] of [
|
|
442
|
+
["markerStart", "marker-start"],
|
|
443
|
+
["markerMid", "marker-mid"],
|
|
444
|
+
["markerEnd", "marker-end"]
|
|
445
|
+
]) {
|
|
446
|
+
const ref = stroke[field];
|
|
447
|
+
if (ref === void 0) continue;
|
|
448
|
+
const key = typeof ref === "string" ? ref : ref.key;
|
|
449
|
+
const id = registry.markerId(key);
|
|
450
|
+
if (id === void 0) continue;
|
|
451
|
+
attrs.push(`${attr}="url(#${id})"`);
|
|
452
|
+
}
|
|
453
|
+
return attrs;
|
|
454
|
+
}
|
|
455
|
+
function strokeAttrsFor(stroke, registry) {
|
|
456
|
+
const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
|
|
457
|
+
attrs.push(`stroke-width="${trimNumber(stroke.width)}"`);
|
|
458
|
+
const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
|
|
459
|
+
if (opacity != null && opacity !== 1) {
|
|
460
|
+
attrs.push(`stroke-opacity="${trimNumber(opacity)}"`);
|
|
461
|
+
}
|
|
462
|
+
if (stroke.cap) {
|
|
463
|
+
attrs.push(`stroke-linecap="${stroke.cap}"`);
|
|
464
|
+
}
|
|
465
|
+
if (stroke.join) {
|
|
466
|
+
attrs.push(`stroke-linejoin="${stroke.join}"`);
|
|
467
|
+
}
|
|
468
|
+
if (stroke.dash && stroke.dash.length > 0) {
|
|
469
|
+
attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
|
|
470
|
+
}
|
|
471
|
+
if (stroke.miterLimit != null) {
|
|
472
|
+
attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
|
|
473
|
+
}
|
|
474
|
+
for (const [field, attr] of [
|
|
475
|
+
["markerStart", "marker-start"],
|
|
476
|
+
["markerMid", "marker-mid"],
|
|
477
|
+
["markerEnd", "marker-end"]
|
|
478
|
+
]) {
|
|
479
|
+
const ref = stroke[field];
|
|
480
|
+
if (ref === void 0) continue;
|
|
481
|
+
const id = registry.markerId(ref);
|
|
482
|
+
if (id === void 0) continue;
|
|
483
|
+
attrs.push(`${attr}="url(#${id})"`);
|
|
484
|
+
}
|
|
485
|
+
return attrs;
|
|
486
|
+
}
|
|
487
|
+
function computeBounds(nodes) {
|
|
488
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
489
|
+
const visit = (n, ctm) => {
|
|
490
|
+
if (n.kind === "group") {
|
|
491
|
+
const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
|
|
492
|
+
n.children.forEach((c2) => visit(c2, childCtm));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const b2 = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
|
|
496
|
+
const local = n.rotation ? rotateAboutCenter(ctm, b2, n.rotation) : ctm;
|
|
497
|
+
for (const [x2, y2] of corners(b2)) {
|
|
498
|
+
const px = local[0] * x2 + local[2] * y2 + local[4];
|
|
499
|
+
const py = local[1] * x2 + local[3] * y2 + local[5];
|
|
500
|
+
if (px < minX) minX = px;
|
|
501
|
+
if (py < minY) minY = py;
|
|
502
|
+
if (px > maxX) maxX = px;
|
|
503
|
+
if (py > maxY) maxY = py;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
|
|
507
|
+
if (!Number.isFinite(minX)) {
|
|
508
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
509
|
+
}
|
|
510
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
511
|
+
}
|
|
512
|
+
function corners(b2) {
|
|
513
|
+
return [[b2.minX, b2.minY], [b2.maxX, b2.minY], [b2.maxX, b2.maxY], [b2.minX, b2.maxY]];
|
|
514
|
+
}
|
|
515
|
+
function rotateAboutCenter(ctm, b2, angle) {
|
|
516
|
+
const cx = (b2.minX + b2.maxX) / 2;
|
|
517
|
+
const cy = (b2.minY + b2.maxY) / 2;
|
|
518
|
+
const c2 = Math.cos(angle);
|
|
519
|
+
const s = Math.sin(angle);
|
|
520
|
+
return multiply(ctm, [c2, s, -s, c2, cx - cx * c2 + cy * s, cy - cx * s - cy * c2]);
|
|
521
|
+
}
|
|
522
|
+
function textXml(node, registry, namespaces) {
|
|
523
|
+
const attrs = [
|
|
524
|
+
`x="${trimNumber(node.x)}"`,
|
|
525
|
+
`y="${trimNumber(node.y)}"`,
|
|
526
|
+
`dominant-baseline="text-before-edge"`,
|
|
527
|
+
// The runs model stores real line breaks; SVG's default whitespace
|
|
528
|
+
// handling would collapse them (and any indentation) away on re-import.
|
|
529
|
+
`xml:space="preserve"`,
|
|
530
|
+
`data-weasel-width="${trimNumber(node.width)}"`,
|
|
531
|
+
`data-weasel-height="${trimNumber(node.height)}"`
|
|
532
|
+
];
|
|
533
|
+
const style = node.style;
|
|
534
|
+
if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(style.fontSize)}"`);
|
|
535
|
+
if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
|
|
536
|
+
if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
|
|
537
|
+
if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
|
|
538
|
+
const direction = style?.direction ?? "ltr";
|
|
539
|
+
if (direction === "rtl") attrs.push('direction="rtl"');
|
|
540
|
+
if (style?.align != null || direction === "rtl") {
|
|
541
|
+
const align = style?.align ?? "left";
|
|
542
|
+
const anchor = align === "center" ? "middle" : align === "start" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
|
|
543
|
+
if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
|
|
544
|
+
}
|
|
545
|
+
if (style?.letterSpacing != null && style.letterSpacing !== 0) {
|
|
546
|
+
attrs.push(`letter-spacing="${trimNumber(style.letterSpacing)}"`);
|
|
547
|
+
}
|
|
548
|
+
const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
|
|
549
|
+
if (decoration) attrs.push(`text-decoration="${decoration}"`);
|
|
550
|
+
if (node.fill) {
|
|
551
|
+
if ("color" in node.fill) {
|
|
552
|
+
attrs.push(`fill="${node.fill.color}"`);
|
|
553
|
+
if (node.fill.opacity != null && node.fill.opacity !== 1) {
|
|
554
|
+
attrs.push(`fill-opacity="${trimNumber(node.fill.opacity)}"`);
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
const id = registry.register(node.fill);
|
|
558
|
+
attrs.push(`fill="url(#${id})"`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (const a of coreStrokeAttrs(node.stroke, registry)) attrs.push(a);
|
|
562
|
+
if (node.opacity != null && node.opacity !== 1) {
|
|
563
|
+
attrs.push(`opacity="${trimNumber(node.opacity)}"`);
|
|
564
|
+
}
|
|
565
|
+
if (node.rotation != null && node.rotation !== 0) {
|
|
566
|
+
const cx = node.x + node.width / 2;
|
|
567
|
+
const cy = node.y + node.height / 2;
|
|
568
|
+
const deg = node.rotation * 180 / Math.PI;
|
|
569
|
+
attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
|
|
570
|
+
}
|
|
571
|
+
const body = node.runs && node.runs.length > 0 ? node.runs.map((r2) => runXml(r2, registry)).join("") : escapeText(node.text);
|
|
572
|
+
const metaAttrs = metaAttrsXml(node.meta, namespaces);
|
|
573
|
+
const metaEls = metaElementsXml(node.meta, namespaces);
|
|
574
|
+
return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
|
|
575
|
+
}
|
|
576
|
+
function runXml(run, registry) {
|
|
577
|
+
const attrs = [];
|
|
578
|
+
if (run.bold) attrs.push(`font-weight="700"`);
|
|
579
|
+
if (run.italic) attrs.push(`font-style="italic"`);
|
|
580
|
+
if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
|
|
581
|
+
const presetScale = run.script != null && run.baselineShift != null ? SCRIPT_METRICS[run.script].size : void 0;
|
|
582
|
+
const scale = run.fontScale ?? presetScale;
|
|
583
|
+
if (run.fontSize != null) attrs.push(`font-size="${trimNumber(run.fontSize)}"`);
|
|
584
|
+
else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
|
|
585
|
+
if (run.baselineShift != null) {
|
|
586
|
+
attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
|
|
587
|
+
} else if (run.script) {
|
|
588
|
+
attrs.push(`baseline-shift="${run.script}"`);
|
|
589
|
+
}
|
|
590
|
+
if (run.letterSpacing != null) {
|
|
591
|
+
attrs.push(`letter-spacing="${trimNumber(run.letterSpacing)}"`);
|
|
592
|
+
}
|
|
593
|
+
const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
|
|
594
|
+
if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
|
|
595
|
+
if (run.fill) {
|
|
596
|
+
if ("color" in run.fill) {
|
|
597
|
+
attrs.push(`fill="${run.fill.color}"`);
|
|
598
|
+
} else {
|
|
599
|
+
const id = registry.register(run.fill);
|
|
600
|
+
attrs.push(`fill="url(#${id})"`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
for (const a of coreStrokeAttrs(run.stroke, registry)) attrs.push(a);
|
|
604
|
+
const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
|
|
605
|
+
return `${head}${escapeText(run.text)}</tspan>`;
|
|
606
|
+
}
|
|
607
|
+
function textDecorationValue(underline, strikethrough, overline) {
|
|
608
|
+
const tokens = [];
|
|
609
|
+
if (underline) tokens.push("underline");
|
|
610
|
+
if (strikethrough) tokens.push("line-through");
|
|
611
|
+
if (overline) tokens.push("overline");
|
|
612
|
+
return tokens.length > 0 ? tokens.join(" ") : null;
|
|
613
|
+
}
|
|
614
|
+
function escapeAttr2(s) {
|
|
615
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
616
|
+
}
|
|
617
|
+
function escapeText(s) {
|
|
618
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
619
|
+
}
|
|
620
|
+
function pathBounds(path) {
|
|
621
|
+
if (path.kind === "rect") {
|
|
622
|
+
return {
|
|
623
|
+
minX: path.x,
|
|
624
|
+
minY: path.y,
|
|
625
|
+
maxX: path.x + path.width,
|
|
626
|
+
maxY: path.y + path.height
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const b2 = boundsOfPath(path);
|
|
630
|
+
return { minX: b2.x, minY: b2.y, maxX: b2.x + b2.width, maxY: b2.y + b2.height };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/annotations/paint.ts
|
|
634
|
+
var MARK_COLOR = "#e5484d";
|
|
635
|
+
var MARK_WIDTH = 2;
|
|
636
|
+
var TEXT_SIZE = 14;
|
|
637
|
+
var toWorld = (p2, content) => ({
|
|
638
|
+
x: p2.x * content.w,
|
|
639
|
+
y: p2.y * content.h
|
|
640
|
+
});
|
|
641
|
+
function vertices(m2, content) {
|
|
642
|
+
const stored = m2.data.points;
|
|
643
|
+
if (stored && stored.length >= 2) return stored.map((p2) => toWorld(p2, content));
|
|
644
|
+
const { x: x2, y: y2, width, height } = m2.pose;
|
|
645
|
+
return [
|
|
646
|
+
{ x: x2, y: y2 },
|
|
647
|
+
{ x: x2 + width, y: y2 + height }
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
function polyline(points) {
|
|
651
|
+
const b2 = new PathBuilder();
|
|
652
|
+
const first = points[0];
|
|
653
|
+
if (!first) return rectPath(0, 0, 0, 0);
|
|
654
|
+
b2.moveTo(first.x, first.y);
|
|
655
|
+
for (const p2 of points.slice(1)) b2.lineTo(p2.x, p2.y);
|
|
656
|
+
return b2.build();
|
|
657
|
+
}
|
|
658
|
+
function markCommands(m2, content, style = {}) {
|
|
659
|
+
const color = style.color ?? MARK_COLOR;
|
|
660
|
+
const stroke = {
|
|
661
|
+
paint: { color },
|
|
662
|
+
width: MARK_WIDTH,
|
|
663
|
+
cap: "round",
|
|
664
|
+
join: "round",
|
|
665
|
+
...style.stale ? { dash: [6, 4] } : {}
|
|
666
|
+
};
|
|
667
|
+
switch (m2.data.kind) {
|
|
668
|
+
case "rect":
|
|
669
|
+
return [
|
|
670
|
+
{ kind: "path", path: rectPath(m2.pose.x, m2.pose.y, m2.pose.width, m2.pose.height), stroke }
|
|
671
|
+
];
|
|
672
|
+
case "ellipse":
|
|
673
|
+
return [{ kind: "path", path: ellipsePath(m2.pose), stroke }];
|
|
674
|
+
case "line": {
|
|
675
|
+
const [a, b2] = vertices(m2, content);
|
|
676
|
+
return [{ kind: "path", path: linePath(a, b2), stroke }];
|
|
677
|
+
}
|
|
678
|
+
case "arrow": {
|
|
679
|
+
const [a, b2] = vertices(m2, content);
|
|
680
|
+
const marked = { ...stroke, markerEnd: "arrow" };
|
|
681
|
+
const path = linePath(a, b2);
|
|
682
|
+
return [
|
|
683
|
+
{ kind: "path", path, stroke: marked },
|
|
684
|
+
...markerDrawCommands(path, marked, MARK_WIDTH, void 0)
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
case "stroke":
|
|
688
|
+
return [{ kind: "path", path: polyline(vertices(m2, content)), stroke }];
|
|
689
|
+
case "text": {
|
|
690
|
+
const text = m2.data.title;
|
|
691
|
+
if (!text) return [];
|
|
692
|
+
return [
|
|
693
|
+
textCommand(
|
|
694
|
+
m2.pose.x,
|
|
695
|
+
m2.pose.y,
|
|
696
|
+
text,
|
|
697
|
+
{ fontSize: TEXT_SIZE },
|
|
698
|
+
void 0,
|
|
699
|
+
void 0,
|
|
700
|
+
void 0,
|
|
701
|
+
{
|
|
702
|
+
fill: { color }
|
|
703
|
+
}
|
|
704
|
+
)
|
|
705
|
+
];
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/annotations/staleness.ts
|
|
711
|
+
function seenFrom(config, keys) {
|
|
712
|
+
const out = {};
|
|
713
|
+
if (config === null || typeof config !== "object") return out;
|
|
714
|
+
const src = config;
|
|
715
|
+
for (const k2 of keys) {
|
|
716
|
+
if (k2 in src) out[k2] = src[k2];
|
|
717
|
+
}
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
function isStale(seen, config, keys) {
|
|
721
|
+
if (!seen || keys.length === 0) return false;
|
|
722
|
+
if (config === null || typeof config !== "object") return false;
|
|
723
|
+
const src = config;
|
|
724
|
+
for (const k2 of keys) {
|
|
725
|
+
if (!(k2 in seen)) continue;
|
|
726
|
+
if (!Object.is(seen[k2], src[k2])) return true;
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// src/annotations/drawOne.ts
|
|
732
|
+
function createMarkDrawOne(opts) {
|
|
733
|
+
return (node, pose) => markCommands({ pose, data: node.data }, opts.content, resolveMarkStyle(node.data, opts));
|
|
734
|
+
}
|
|
735
|
+
function resolveMarkStyle(data, opts) {
|
|
736
|
+
return {
|
|
737
|
+
color: opts.meaning?.statuses?.find((s) => s.id === data.status)?.color,
|
|
738
|
+
// Read off the node rather than through the store's `isStale`, which
|
|
739
|
+
// wants a projected Annotation this path does not have.
|
|
740
|
+
stale: isStale(data.seen, opts.config, opts.positionDependsOn ?? [])
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/annotations/svgNodes.ts
|
|
745
|
+
var NO_PAINT = { kind: "none" };
|
|
746
|
+
function toSvgPaint(paint) {
|
|
747
|
+
if (!paint) return NO_PAINT;
|
|
748
|
+
if ("color" in paint && typeof paint.color === "string") {
|
|
749
|
+
return {
|
|
750
|
+
kind: "solid",
|
|
751
|
+
color: paint.color,
|
|
752
|
+
...paint.opacity != null ? { opacity: paint.opacity } : {}
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return { kind: "gradient", paint };
|
|
756
|
+
}
|
|
757
|
+
function toSvgStroke(stroke) {
|
|
758
|
+
return {
|
|
759
|
+
paint: toSvgPaint(stroke.paint),
|
|
760
|
+
width: resolveStrokeWidth(stroke.width ?? 1, 1),
|
|
761
|
+
...stroke.cap ? { cap: stroke.cap } : {},
|
|
762
|
+
...stroke.join ? { join: stroke.join } : {},
|
|
763
|
+
...stroke.dash ? { dash: [...stroke.dash] } : {},
|
|
764
|
+
...stroke.miterLimit != null ? { miterLimit: stroke.miterLimit } : {}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function toSvgNode(cmd, m2) {
|
|
768
|
+
if (cmd.kind === "path") {
|
|
769
|
+
return {
|
|
770
|
+
kind: "path",
|
|
771
|
+
path: cmd.path,
|
|
772
|
+
fill: toSvgPaint(cmd.fill),
|
|
773
|
+
...cmd.stroke ? { stroke: toSvgStroke(cmd.stroke) } : {}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
if (cmd.kind === "text") {
|
|
777
|
+
return {
|
|
778
|
+
kind: "text",
|
|
779
|
+
x: cmd.x,
|
|
780
|
+
y: cmd.y,
|
|
781
|
+
width: m2.pose.width,
|
|
782
|
+
height: m2.pose.height,
|
|
783
|
+
text: cmd.runs.map((run) => run.text).join(""),
|
|
784
|
+
style: cmd.style,
|
|
785
|
+
...cmd.runs[0]?.fill ? { fill: cmd.runs[0].fill } : {}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
throw new Error(`[labkit] a mark drew a '${cmd.kind}' command, which has no vector form`);
|
|
789
|
+
}
|
|
790
|
+
function markSvgNodes(m2, content, style = {}) {
|
|
791
|
+
return markCommands(m2, content, style).map((cmd) => toSvgNode(cmd, m2));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/annotations/capture.ts
|
|
795
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
796
|
+
function capturePlan(base, format) {
|
|
797
|
+
if (format === "svg") return "svg-document";
|
|
798
|
+
return base?.kind === "svg" ? "svg-document" : "raster-stack";
|
|
799
|
+
}
|
|
800
|
+
function nestBaseSvg(markup, w2, h) {
|
|
801
|
+
const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
|
|
802
|
+
const root = doc.documentElement;
|
|
803
|
+
if (doc.getElementsByTagName("parsererror").length > 0 || root.localName !== "svg") {
|
|
804
|
+
throw new Error("[labkit] a target's `base()` returned markup that is not an <svg> document");
|
|
805
|
+
}
|
|
806
|
+
root.setAttribute("x", "0");
|
|
807
|
+
root.setAttribute("y", "0");
|
|
808
|
+
root.setAttribute("width", String(w2));
|
|
809
|
+
root.setAttribute("height", String(h));
|
|
810
|
+
return new XMLSerializer().serializeToString(root);
|
|
811
|
+
}
|
|
812
|
+
function embedRasterBase(base, w2, h, onWarn) {
|
|
813
|
+
const href = base.kind === "canvas" ? base.canvas.toDataURL() : base.kind === "image" ? base.src : "";
|
|
814
|
+
if (base.kind === "image" && !href.startsWith("data:")) {
|
|
815
|
+
onWarn?.(`base image '${href}' is a reference, so this SVG is not self-contained`);
|
|
816
|
+
}
|
|
817
|
+
return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w2, height: h }], {
|
|
818
|
+
viewBox: { x: 0, y: 0, width: w2, height: h }
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
function markNodesOf(scene, draw) {
|
|
822
|
+
return [...scene.renderOrder()].flatMap((id) => {
|
|
823
|
+
const node = scene.get(asNodeId$1(String(id)));
|
|
824
|
+
if (!node) return [];
|
|
825
|
+
return markSvgNodes(
|
|
826
|
+
{ pose: node.pose, data: node.data },
|
|
827
|
+
draw.content,
|
|
828
|
+
resolveMarkStyle(node.data, draw)
|
|
829
|
+
);
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
function composeCaptureSvg(args) {
|
|
833
|
+
const { base, scene, draw, scale, onWarn } = args;
|
|
834
|
+
const { w: w2, h } = draw.content;
|
|
835
|
+
const baseXml = base === void 0 ? "" : base.kind === "svg" ? nestBaseSvg(base.markup, w2, h) : embedRasterBase(base, w2, h, onWarn);
|
|
836
|
+
const marksXml = serializeSvg(markNodesOf(scene, draw), {
|
|
837
|
+
viewBox: { x: 0, y: 0, width: w2, height: h },
|
|
838
|
+
onWarn
|
|
839
|
+
});
|
|
840
|
+
const outW = Math.max(1, Math.round(w2 * scale));
|
|
841
|
+
const outH = Math.max(1, Math.round(h * scale));
|
|
842
|
+
return `<svg xmlns="${SVG_NS}" width="${outW}" height="${outH}" viewBox="0 0 ${w2} ${h}">${baseXml}${marksXml}</svg>`;
|
|
843
|
+
}
|
|
844
|
+
function context2d(width, height) {
|
|
845
|
+
const canvas = document.createElement("canvas");
|
|
846
|
+
canvas.width = width;
|
|
847
|
+
canvas.height = height;
|
|
848
|
+
const ctx = canvas.getContext("2d");
|
|
849
|
+
if (!ctx) throw new Error("[labkit] capture needs a 2D canvas context, and this one has none");
|
|
850
|
+
return ctx;
|
|
851
|
+
}
|
|
852
|
+
function toPngBlob(canvas) {
|
|
853
|
+
return new Promise((resolve, reject) => {
|
|
854
|
+
let done;
|
|
855
|
+
try {
|
|
856
|
+
done = (blob) => blob ? resolve(blob) : reject(new Error("[labkit] the browser produced no PNG"));
|
|
857
|
+
canvas.toBlob(done, "image/png");
|
|
858
|
+
} catch (err) {
|
|
859
|
+
reject(
|
|
860
|
+
new Error(
|
|
861
|
+
"[labkit] the capture canvas is tainted, so it cannot be read back \u2014 a base referencing a cross-origin image does this. Embed it as a data: URI.",
|
|
862
|
+
{ cause: err }
|
|
863
|
+
)
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function loadImage(src) {
|
|
869
|
+
return new Promise((resolve, reject) => {
|
|
870
|
+
const img = new Image();
|
|
871
|
+
img.onload = () => resolve(img);
|
|
872
|
+
img.onerror = () => reject(new Error(`[labkit] a capture base failed to load: ${src}`));
|
|
873
|
+
img.src = src;
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
async function rasterizeSvg(svg, width, height) {
|
|
877
|
+
const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
|
|
878
|
+
try {
|
|
879
|
+
const img = await loadImage(url);
|
|
880
|
+
const ctx = context2d(width, height);
|
|
881
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
882
|
+
return await toPngBlob(ctx.canvas);
|
|
883
|
+
} finally {
|
|
884
|
+
URL.revokeObjectURL(url);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async function stackRaster(args) {
|
|
888
|
+
const { base, scene, draw, scale, width, height } = args;
|
|
889
|
+
const ctx = context2d(width, height);
|
|
890
|
+
if (base?.kind === "image") ctx.drawImage(await loadImage(base.src), 0, 0, width, height);
|
|
891
|
+
else if (base?.kind === "canvas") ctx.drawImage(base.canvas, 0, 0, width, height);
|
|
892
|
+
const raster = renderSceneToPixels({
|
|
893
|
+
scene,
|
|
894
|
+
sourceRect: { x: 0, y: 0, width: draw.content.w, height: draw.content.h },
|
|
895
|
+
scale: { x: scale, y: scale },
|
|
896
|
+
drawOne: createMarkDrawOne(draw)
|
|
897
|
+
});
|
|
898
|
+
const marks = context2d(raster.width, raster.height);
|
|
899
|
+
marks.putImageData(
|
|
900
|
+
new ImageData(Uint8ClampedArray.from(raster.data), raster.width, raster.height),
|
|
901
|
+
0,
|
|
902
|
+
0
|
|
903
|
+
);
|
|
904
|
+
ctx.drawImage(marks.canvas, 0, 0, width, height);
|
|
905
|
+
return toPngBlob(ctx.canvas);
|
|
906
|
+
}
|
|
907
|
+
async function captureTarget(args, opts = {}) {
|
|
908
|
+
const format = opts.format ?? "png";
|
|
909
|
+
const scale = opts.scale ?? 2;
|
|
910
|
+
const { content } = args.draw;
|
|
911
|
+
const width = Math.max(1, Math.round(content.w * scale));
|
|
912
|
+
const height = Math.max(1, Math.round(content.h * scale));
|
|
913
|
+
const base = await args.base?.();
|
|
914
|
+
const compose = {
|
|
915
|
+
base,
|
|
916
|
+
scene: args.scene,
|
|
917
|
+
draw: args.draw,
|
|
918
|
+
scale,
|
|
919
|
+
onWarn: args.onWarn
|
|
920
|
+
};
|
|
921
|
+
if (capturePlan(base, format) === "raster-stack") {
|
|
922
|
+
return {
|
|
923
|
+
target: args.target,
|
|
924
|
+
blob: await stackRaster({ ...compose, width, height }),
|
|
925
|
+
format,
|
|
926
|
+
width,
|
|
927
|
+
height
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const svg = composeCaptureSvg(compose);
|
|
931
|
+
const blob = format === "svg" ? new Blob([svg], { type: "image/svg+xml;charset=utf-8" }) : await rasterizeSvg(svg, width, height);
|
|
932
|
+
return { target: args.target, blob, format, width, height };
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/annotations/frac.ts
|
|
936
|
+
function fracToWorld(f2, content) {
|
|
937
|
+
return {
|
|
938
|
+
x: f2.x * content.w,
|
|
939
|
+
y: f2.y * content.h,
|
|
940
|
+
width: f2.w * content.w,
|
|
941
|
+
height: f2.h * content.h
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function worldToFrac(r2, content) {
|
|
945
|
+
const sx = content.w === 0 ? 0 : 1 / content.w;
|
|
946
|
+
const sy = content.h === 0 ? 0 : 1 / content.h;
|
|
947
|
+
return { x: r2.x * sx, y: r2.y * sy, w: r2.width * sx, h: r2.height * sy };
|
|
948
|
+
}
|
|
949
|
+
var DP = 1e4;
|
|
950
|
+
function roundFrac(f2) {
|
|
951
|
+
return {
|
|
952
|
+
x: Math.round(f2.x * DP) / DP,
|
|
953
|
+
y: Math.round(f2.y * DP) / DP,
|
|
954
|
+
w: Math.round(f2.w * DP) / DP,
|
|
955
|
+
h: Math.round(f2.h * DP) / DP
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function fracContains(box, pt, tol = 0) {
|
|
959
|
+
return pt.x >= box.x - tol && pt.x <= box.x + box.w + tol && pt.y >= box.y - tol && pt.y <= box.y + box.h + tol;
|
|
960
|
+
}
|
|
961
|
+
function fracIntersects(outer, inner) {
|
|
962
|
+
return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.w <= outer.x + outer.w && inner.y + inner.h <= outer.y + outer.h;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/annotations/history.ts
|
|
966
|
+
var MarkHistory = class {
|
|
967
|
+
depth = /* @__PURE__ */ new Map();
|
|
968
|
+
undoable = [];
|
|
969
|
+
redoable = [];
|
|
970
|
+
/** Set while this class is driving, so a redo is not read as a new change. */
|
|
971
|
+
replaying = null;
|
|
972
|
+
/** Start following `scene` under `target`. Idempotent per target. */
|
|
973
|
+
track(target, scene) {
|
|
974
|
+
if (this.depth.has(target)) return;
|
|
975
|
+
this.depth.set(target, scene.historyIndex());
|
|
976
|
+
}
|
|
977
|
+
/** Call on every notification from `target`'s scene. */
|
|
978
|
+
observe(target, scene) {
|
|
979
|
+
const now = scene.historyIndex();
|
|
980
|
+
const before = this.depth.get(target) ?? 0;
|
|
981
|
+
this.depth.set(target, now);
|
|
982
|
+
if (now > before) {
|
|
983
|
+
if (this.replaying === "redo") {
|
|
984
|
+
drop(this.redoable, target);
|
|
985
|
+
} else {
|
|
986
|
+
this.redoable = [];
|
|
987
|
+
}
|
|
988
|
+
this.undoable.push(target);
|
|
989
|
+
} else if (now < before) {
|
|
990
|
+
drop(this.undoable, target);
|
|
991
|
+
this.redoable.push(target);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
/** The target `undo` would act on, skipping any whose scene has nothing
|
|
995
|
+
* left — a scene can drop entries this ordering still lists. */
|
|
996
|
+
next(stack, can, at) {
|
|
997
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
998
|
+
const target = stack[i];
|
|
999
|
+
if (target === void 0) continue;
|
|
1000
|
+
const scene = at(target);
|
|
1001
|
+
if (scene && can(scene)) return target;
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
canUndo(at) {
|
|
1006
|
+
return this.next(this.undoable, (s) => s.canUndo(), at) !== null;
|
|
1007
|
+
}
|
|
1008
|
+
canRedo(at) {
|
|
1009
|
+
return this.next(this.redoable, (s) => s.canRedo(), at) !== null;
|
|
1010
|
+
}
|
|
1011
|
+
undo(at) {
|
|
1012
|
+
const target = this.next(this.undoable, (s) => s.canUndo(), at);
|
|
1013
|
+
const scene = target === null ? void 0 : at(target);
|
|
1014
|
+
if (!scene) return false;
|
|
1015
|
+
this.replaying = "undo";
|
|
1016
|
+
try {
|
|
1017
|
+
return scene.undo();
|
|
1018
|
+
} finally {
|
|
1019
|
+
this.replaying = null;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
redo(at) {
|
|
1023
|
+
const target = this.next(this.redoable, (s) => s.canRedo(), at);
|
|
1024
|
+
const scene = target === null ? void 0 : at(target);
|
|
1025
|
+
if (!scene) return false;
|
|
1026
|
+
this.replaying = "redo";
|
|
1027
|
+
try {
|
|
1028
|
+
return scene.redo();
|
|
1029
|
+
} finally {
|
|
1030
|
+
this.replaying = null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
function drop(stack, value) {
|
|
1035
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
1036
|
+
if (stack[i] === value) {
|
|
1037
|
+
stack.splice(i, 1);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/annotations/store.ts
|
|
1044
|
+
function createAnnotationScene() {
|
|
1045
|
+
return createScene({ systemLayers: [{ id: "marks" }] });
|
|
1046
|
+
}
|
|
1047
|
+
var NO_CONTENT = { w: 0, h: 0 };
|
|
1048
|
+
function splitId(id) {
|
|
1049
|
+
const at = id.lastIndexOf("/");
|
|
1050
|
+
return at <= 0 ? void 0 : { target: id.slice(0, at), node: id.slice(at + 1) };
|
|
1051
|
+
}
|
|
1052
|
+
function createAnnotationStore(opts) {
|
|
1053
|
+
const { targets, restore } = opts;
|
|
1054
|
+
const scenes = /* @__PURE__ */ new Map();
|
|
1055
|
+
const subs = /* @__PURE__ */ new Set();
|
|
1056
|
+
const history = new MarkHistory();
|
|
1057
|
+
const notify = () => {
|
|
1058
|
+
for (const fn of subs) fn();
|
|
1059
|
+
};
|
|
1060
|
+
const sceneFor = (target) => {
|
|
1061
|
+
const known = scenes.get(target);
|
|
1062
|
+
if (known) return known;
|
|
1063
|
+
const raw = restore?.[target];
|
|
1064
|
+
const scene = raw ? sceneFromJSON(
|
|
1065
|
+
raw,
|
|
1066
|
+
{}
|
|
1067
|
+
) : createAnnotationScene();
|
|
1068
|
+
scenes.set(target, scene);
|
|
1069
|
+
history.track(target, scene);
|
|
1070
|
+
scene.subscribe(() => {
|
|
1071
|
+
history.observe(target, scene);
|
|
1072
|
+
notify();
|
|
1073
|
+
});
|
|
1074
|
+
return scene;
|
|
1075
|
+
};
|
|
1076
|
+
const historySceneAt = (target) => scenes.get(target);
|
|
1077
|
+
for (const target of Object.keys(restore ?? {})) sceneFor(target);
|
|
1078
|
+
const targetOf = (id) => targets().find((t) => t.id === id);
|
|
1079
|
+
const contentOf = (id) => targetOf(id)?.content ?? NO_CONTENT;
|
|
1080
|
+
const project = (target, node) => {
|
|
1081
|
+
const found = scenes.get(target)?.get(asNodeId$1(node));
|
|
1082
|
+
if (!found) return void 0;
|
|
1083
|
+
return {
|
|
1084
|
+
...found.data,
|
|
1085
|
+
target,
|
|
1086
|
+
id: `${target}/${node}`,
|
|
1087
|
+
frac: roundFrac(worldToFrac(found.pose, contentOf(target)))
|
|
1088
|
+
};
|
|
1089
|
+
};
|
|
1090
|
+
const liveTargets = () => {
|
|
1091
|
+
const declared = targets().map((t) => t.id);
|
|
1092
|
+
const rest = [...scenes.keys()].filter((id) => !declared.includes(id));
|
|
1093
|
+
return [...declared, ...rest];
|
|
1094
|
+
};
|
|
1095
|
+
const marksOn = (target) => {
|
|
1096
|
+
const scene = scenes.get(target);
|
|
1097
|
+
if (!scene) return [];
|
|
1098
|
+
const out = [];
|
|
1099
|
+
for (const id of scene.renderOrder()) {
|
|
1100
|
+
const a = project(target, String(id));
|
|
1101
|
+
if (a) out.push(a);
|
|
1102
|
+
}
|
|
1103
|
+
return out;
|
|
1104
|
+
};
|
|
1105
|
+
const all = () => liveTargets().flatMap(marksOn);
|
|
1106
|
+
const matches = (a, q) => {
|
|
1107
|
+
if (q.target !== void 0 && a.target !== q.target) return false;
|
|
1108
|
+
if (q.kind !== void 0 && a.kind !== q.kind) return false;
|
|
1109
|
+
if (q.status !== void 0 && a.status !== q.status) return false;
|
|
1110
|
+
if (q.tags !== void 0) {
|
|
1111
|
+
const have = a.tags ?? [];
|
|
1112
|
+
for (const t of q.tags) if (!have.includes(t)) return false;
|
|
1113
|
+
}
|
|
1114
|
+
if (q.where && !q.where(a)) return false;
|
|
1115
|
+
return true;
|
|
1116
|
+
};
|
|
1117
|
+
const nodeOf = (id) => {
|
|
1118
|
+
const parts = splitId(id);
|
|
1119
|
+
if (!parts) return void 0;
|
|
1120
|
+
const scene = scenes.get(parts.target);
|
|
1121
|
+
const node = scene?.get(asNodeId$1(parts.node));
|
|
1122
|
+
return node && scene ? { ...parts, scene, node } : void 0;
|
|
1123
|
+
};
|
|
1124
|
+
return {
|
|
1125
|
+
sceneFor,
|
|
1126
|
+
targets,
|
|
1127
|
+
get(id) {
|
|
1128
|
+
const parts = splitId(id);
|
|
1129
|
+
return parts ? project(parts.target, parts.node) : void 0;
|
|
1130
|
+
},
|
|
1131
|
+
query(q) {
|
|
1132
|
+
return q ? all().filter((a) => matches(a, q)) : all();
|
|
1133
|
+
},
|
|
1134
|
+
hitTest(target, pt, tol = 0) {
|
|
1135
|
+
return marksOn(target).filter((a) => fracContains(a.frac, pt, tol)).reverse();
|
|
1136
|
+
},
|
|
1137
|
+
within(target, box) {
|
|
1138
|
+
return marksOn(target).filter((a) => fracIntersects(box, a.frac));
|
|
1139
|
+
},
|
|
1140
|
+
isStale(a, config) {
|
|
1141
|
+
const keys = targetOf(a.target)?.positionDependsOn ?? [];
|
|
1142
|
+
return isStale(a.seen, config, keys);
|
|
1143
|
+
},
|
|
1144
|
+
canUndo() {
|
|
1145
|
+
return history.canUndo(historySceneAt);
|
|
1146
|
+
},
|
|
1147
|
+
canRedo() {
|
|
1148
|
+
return history.canRedo(historySceneAt);
|
|
1149
|
+
},
|
|
1150
|
+
undo() {
|
|
1151
|
+
return history.undo(historySceneAt);
|
|
1152
|
+
},
|
|
1153
|
+
redo() {
|
|
1154
|
+
return history.redo(historySceneAt);
|
|
1155
|
+
},
|
|
1156
|
+
subscribe(fn) {
|
|
1157
|
+
subs.add(fn);
|
|
1158
|
+
return () => {
|
|
1159
|
+
subs.delete(fn);
|
|
1160
|
+
};
|
|
1161
|
+
},
|
|
1162
|
+
add(init, config) {
|
|
1163
|
+
const target = targetOf(init.target);
|
|
1164
|
+
const data = {
|
|
1165
|
+
target: init.target,
|
|
1166
|
+
kind: init.kind,
|
|
1167
|
+
...init.points ? { points: init.points } : {},
|
|
1168
|
+
...init.title !== void 0 ? { title: init.title } : {},
|
|
1169
|
+
...init.status !== void 0 ? { status: init.status } : {},
|
|
1170
|
+
...init.tags !== void 0 ? { tags: init.tags } : {},
|
|
1171
|
+
...init.meta !== void 0 ? { meta: init.meta } : {},
|
|
1172
|
+
seen: seenFrom(config, target?.positionDependsOn ?? [])
|
|
1173
|
+
};
|
|
1174
|
+
const node = sceneFor(init.target).add({
|
|
1175
|
+
kind: "leaf",
|
|
1176
|
+
layer: "marks",
|
|
1177
|
+
pose: fracToWorld(roundFrac(init.frac), target?.content ?? NO_CONTENT),
|
|
1178
|
+
data
|
|
1179
|
+
});
|
|
1180
|
+
return `${init.target}/${String(node)}`;
|
|
1181
|
+
},
|
|
1182
|
+
update(id, patch) {
|
|
1183
|
+
const found = nodeOf(id);
|
|
1184
|
+
if (!found) return;
|
|
1185
|
+
const { frac, ...meaning } = patch;
|
|
1186
|
+
const nid = asNodeId$1(found.node.id);
|
|
1187
|
+
if (Object.keys(meaning).length > 0) {
|
|
1188
|
+
found.scene.update(nid, { data: { ...found.node.data, ...meaning } });
|
|
1189
|
+
}
|
|
1190
|
+
if (frac) {
|
|
1191
|
+
found.scene.setPose(nid, fracToWorld(roundFrac(frac), contentOf(found.target)));
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
setMeta(id, meta) {
|
|
1195
|
+
const found = nodeOf(id);
|
|
1196
|
+
if (!found) return;
|
|
1197
|
+
found.scene.update(asNodeId$1(found.node.id), { data: { ...found.node.data, meta } });
|
|
1198
|
+
},
|
|
1199
|
+
remove(id) {
|
|
1200
|
+
const found = nodeOf(id);
|
|
1201
|
+
if (found) found.scene.remove(asNodeId$1(found.node.id));
|
|
1202
|
+
},
|
|
1203
|
+
async capture(target, captureOpts) {
|
|
1204
|
+
const info = targetOf(target);
|
|
1205
|
+
if (!info) throw new Error(`[labkit] no annotation target called '${target}'`);
|
|
1206
|
+
const result = await captureTarget(
|
|
1207
|
+
{
|
|
1208
|
+
target,
|
|
1209
|
+
scene: sceneFor(target),
|
|
1210
|
+
draw: {
|
|
1211
|
+
content: info.content,
|
|
1212
|
+
positionDependsOn: info.positionDependsOn,
|
|
1213
|
+
config: opts.config?.(),
|
|
1214
|
+
meaning: opts.meaning
|
|
1215
|
+
},
|
|
1216
|
+
base: info.base,
|
|
1217
|
+
onWarn: (message) => console.warn(`[labkit] capture: ${message}`)
|
|
1218
|
+
},
|
|
1219
|
+
captureOpts
|
|
1220
|
+
);
|
|
1221
|
+
opts.onCapture?.(result);
|
|
1222
|
+
return result;
|
|
1223
|
+
},
|
|
1224
|
+
toJSON() {
|
|
1225
|
+
const out = {};
|
|
1226
|
+
for (const [target, scene] of scenes) out[target] = scene.toJSON();
|
|
1227
|
+
return { version: 1, scenes: out };
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function annotationsFromJSON(raw, targets, rest = {}) {
|
|
1232
|
+
const doc = raw;
|
|
1233
|
+
if (doc?.version !== 1 || !doc.scenes) return createAnnotationStore({ targets, ...rest });
|
|
1234
|
+
return createAnnotationStore({ targets, restore: doc.scenes, ...rest });
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/annotations/view.ts
|
|
1238
|
+
function toWeaselView(v) {
|
|
1239
|
+
return { x: v.pan.x, y: v.pan.y, scale: { x: v.zoom, y: v.zoom } };
|
|
1240
|
+
}
|
|
1241
|
+
function fromWeaselView(v) {
|
|
1242
|
+
return { zoom: v.scale.x, pan: { x: v.x, y: v.y } };
|
|
1243
|
+
}
|
|
1244
|
+
function fitView(content, pane) {
|
|
1245
|
+
const zoom = content.w > 0 && content.h > 0 ? Math.min(pane.w / content.w, pane.h / content.h) : 1;
|
|
1246
|
+
return { zoom: Number.isFinite(zoom) && zoom > 0 ? zoom : 1, pan: { x: 0, y: 0 } };
|
|
1247
|
+
}
|
|
1248
|
+
var sameRect = (a, b2) => a !== null && a.x === b2.x && a.y === b2.y && a.w === b2.w && a.h === b2.h;
|
|
1249
|
+
function ToolBridge({ toolId }) {
|
|
1250
|
+
const tools = useActiveToolContext();
|
|
1251
|
+
const setActive = tools.setActive;
|
|
1252
|
+
useEffect(() => {
|
|
1253
|
+
setActive(toolId);
|
|
1254
|
+
}, [toolId, setActive]);
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
function pointsOf(extras, content) {
|
|
1258
|
+
const toFrac = (p2) => ({
|
|
1259
|
+
x: content.w === 0 ? 0 : p2.x / content.w,
|
|
1260
|
+
y: content.h === 0 ? 0 : p2.y / content.h
|
|
1261
|
+
});
|
|
1262
|
+
if (extras.kind === "line") {
|
|
1263
|
+
return [toFrac(extras.a), toFrac(extras.b)];
|
|
1264
|
+
}
|
|
1265
|
+
if (extras.kind === "pencil") {
|
|
1266
|
+
return extras.samples.map(toFrac);
|
|
1267
|
+
}
|
|
1268
|
+
return void 0;
|
|
1269
|
+
}
|
|
1270
|
+
function AnnotationOverlay({
|
|
1271
|
+
target,
|
|
1272
|
+
scene,
|
|
1273
|
+
config,
|
|
1274
|
+
activeToolId,
|
|
1275
|
+
meaning
|
|
1276
|
+
}) {
|
|
1277
|
+
const surface = useSurfaceOptional();
|
|
1278
|
+
const canvas = useSurfaceCanvas();
|
|
1279
|
+
const [rect, setRect] = useState(null);
|
|
1280
|
+
const [input, setInput] = useState(null);
|
|
1281
|
+
const sceneCanvas = useRef(null);
|
|
1282
|
+
const toolRef = useRef(activeToolId);
|
|
1283
|
+
toolRef.current = activeToolId;
|
|
1284
|
+
const configRef = useRef(config);
|
|
1285
|
+
configRef.current = config;
|
|
1286
|
+
const id = target.id;
|
|
1287
|
+
const lastTile = useRef(null);
|
|
1288
|
+
useEffect(() => {
|
|
1289
|
+
const el = target.ref.current ?? null;
|
|
1290
|
+
if (el === lastTile.current) return;
|
|
1291
|
+
lastTile.current = el;
|
|
1292
|
+
surface?.registerTile(id, el);
|
|
1293
|
+
});
|
|
1294
|
+
useEffect(
|
|
1295
|
+
() => () => {
|
|
1296
|
+
lastTile.current = null;
|
|
1297
|
+
surface?.registerTile(id, null);
|
|
1298
|
+
},
|
|
1299
|
+
[surface, id]
|
|
1300
|
+
);
|
|
1301
|
+
useEffect(
|
|
1302
|
+
() => surface?.registerPainter(id, (next) => {
|
|
1303
|
+
setRect((prev) => sameRect(prev, next) ? prev : next);
|
|
1304
|
+
sceneCanvas.current?.requestRedraw();
|
|
1305
|
+
}),
|
|
1306
|
+
[surface, id]
|
|
1307
|
+
);
|
|
1308
|
+
const container = surface?.getContainer() ?? null;
|
|
1309
|
+
if (!container || !rect) return null;
|
|
1310
|
+
const view = toWeaselView(target.view ?? fitView(target.content, { w: rect.w, h: rect.h }));
|
|
1311
|
+
const drawOne = createMarkDrawOne({
|
|
1312
|
+
content: target.content,
|
|
1313
|
+
positionDependsOn: target.positionDependsOn,
|
|
1314
|
+
config,
|
|
1315
|
+
meaning
|
|
1316
|
+
});
|
|
1317
|
+
const factories = {};
|
|
1318
|
+
for (const weaselTool of ANNOTATION_WEASEL_TOOLS) {
|
|
1319
|
+
factories[weaselTool] = (_bounds, extras) => {
|
|
1320
|
+
const info = annotationToolInfo(toolRef.current);
|
|
1321
|
+
const kind = info?.kind;
|
|
1322
|
+
if (!kind) return null;
|
|
1323
|
+
const points = pointsOf(extras, target.content);
|
|
1324
|
+
const data = {
|
|
1325
|
+
target: id,
|
|
1326
|
+
kind,
|
|
1327
|
+
...points ? { points } : {},
|
|
1328
|
+
seen: seenFrom(configRef.current, target.positionDependsOn ?? [])
|
|
1329
|
+
};
|
|
1330
|
+
return { data };
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
const box = {
|
|
1334
|
+
["--lk-anno-x"]: `${rect.x}px`,
|
|
1335
|
+
["--lk-anno-y"]: `${rect.y}px`,
|
|
1336
|
+
["--lk-anno-w"]: `${rect.w}px`,
|
|
1337
|
+
["--lk-anno-h"]: `${rect.h}px`
|
|
1338
|
+
};
|
|
1339
|
+
return createPortal(
|
|
1340
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1341
|
+
/* @__PURE__ */ jsx(
|
|
1342
|
+
"div",
|
|
1343
|
+
{
|
|
1344
|
+
ref: setInput,
|
|
1345
|
+
className: "lk-annotate__input",
|
|
1346
|
+
"data-annotation-target": id,
|
|
1347
|
+
role: "application",
|
|
1348
|
+
"aria-label": `Annotations on ${id}`,
|
|
1349
|
+
tabIndex: 0,
|
|
1350
|
+
style: box
|
|
1351
|
+
}
|
|
1352
|
+
),
|
|
1353
|
+
canvas && input ? (
|
|
1354
|
+
// One scope per pane: a shared <ActionsProvider> lets only the newest
|
|
1355
|
+
// canvas under it respond to input, and the rest go silently dead.
|
|
1356
|
+
/* @__PURE__ */ jsxs(WeaselProvider, { isolate: true, children: [
|
|
1357
|
+
/* @__PURE__ */ jsx(ToolBridge, { toolId: annotationToolInfo(activeToolId)?.weaselTool ?? "select" }),
|
|
1358
|
+
/* @__PURE__ */ jsx(
|
|
1359
|
+
SceneCanvas,
|
|
1360
|
+
{
|
|
1361
|
+
ref: sceneCanvas,
|
|
1362
|
+
scene,
|
|
1363
|
+
width: rect.w,
|
|
1364
|
+
height: rect.h,
|
|
1365
|
+
view,
|
|
1366
|
+
paintInto: { canvas, x: rect.x, y: rect.y },
|
|
1367
|
+
inputElement: input,
|
|
1368
|
+
defaultTools: ANNOTATION_WEASEL_TOOLS,
|
|
1369
|
+
insertNodeFactories: factories,
|
|
1370
|
+
layers: { scene: { drawOne } }
|
|
1371
|
+
}
|
|
1372
|
+
)
|
|
1373
|
+
] })
|
|
1374
|
+
) : null
|
|
1375
|
+
] }),
|
|
1376
|
+
container
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
function AnnotationTargets({
|
|
1380
|
+
capability,
|
|
1381
|
+
state,
|
|
1382
|
+
config,
|
|
1383
|
+
annotations,
|
|
1384
|
+
activeToolId
|
|
1385
|
+
}) {
|
|
1386
|
+
const targets = capability.targets(state, config);
|
|
1387
|
+
return /* @__PURE__ */ jsx(Fragment, { children: targets.map((t) => /* @__PURE__ */ jsx(
|
|
1388
|
+
AnnotationOverlay,
|
|
1389
|
+
{
|
|
1390
|
+
target: t,
|
|
1391
|
+
scene: annotations.sceneFor(t.id),
|
|
1392
|
+
meaning: capability.meaning,
|
|
1393
|
+
config,
|
|
1394
|
+
activeToolId
|
|
1395
|
+
},
|
|
1396
|
+
t.id
|
|
1397
|
+
)) });
|
|
1398
|
+
}
|
|
32
1399
|
var PITCH_LIMIT = Math.PI / 2 - 0.01;
|
|
33
1400
|
var YAW_PER_PX = 8e-3;
|
|
34
1401
|
var PITCH_PER_PX = 8e-3;
|
|
@@ -287,9 +1654,9 @@ function applyRules(seed, ctx, rules) {
|
|
|
287
1654
|
for (const rule of rules) {
|
|
288
1655
|
const patch = rule({ ...ctx, leaf: acc });
|
|
289
1656
|
if (patch === null) continue;
|
|
290
|
-
for (const [
|
|
291
|
-
if (v !== void 0 && acc[
|
|
292
|
-
acc[
|
|
1657
|
+
for (const [k2, v] of Object.entries(patch)) {
|
|
1658
|
+
if (v !== void 0 && acc[k2] === void 0) {
|
|
1659
|
+
acc[k2] = v;
|
|
293
1660
|
}
|
|
294
1661
|
}
|
|
295
1662
|
}
|
|
@@ -299,7 +1666,7 @@ function applyRules(seed, ctx, rules) {
|
|
|
299
1666
|
// src/config/resolve.ts
|
|
300
1667
|
function defined(annotations) {
|
|
301
1668
|
const out = {};
|
|
302
|
-
for (const [
|
|
1669
|
+
for (const [k2, v] of Object.entries(annotations)) if (v !== void 0) out[k2] = v;
|
|
303
1670
|
return out;
|
|
304
1671
|
}
|
|
305
1672
|
function resolveConfigSchema(schema, rules = []) {
|
|
@@ -950,21 +2317,21 @@ var weaselTheme = {
|
|
|
950
2317
|
modes: THEME_SOURCES.weasel.modes
|
|
951
2318
|
};
|
|
952
2319
|
function hexToRgba(hex, alpha) {
|
|
953
|
-
const
|
|
954
|
-
if (!
|
|
955
|
-
const body =
|
|
956
|
-
const full = body.length === 3 ? body.replace(/./g, (
|
|
2320
|
+
const m2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
|
2321
|
+
if (!m2) throw new Error(`Expected a hex color, got "${hex}"`);
|
|
2322
|
+
const body = m2[1];
|
|
2323
|
+
const full = body.length === 3 ? body.replace(/./g, (c2) => c2 + c2) : body;
|
|
957
2324
|
const r2 = Number.parseInt(full.slice(0, 2), 16);
|
|
958
|
-
const
|
|
959
|
-
const
|
|
960
|
-
return `rgba(${r2}, ${
|
|
2325
|
+
const g2 = Number.parseInt(full.slice(2, 4), 16);
|
|
2326
|
+
const b2 = Number.parseInt(full.slice(4, 6), 16);
|
|
2327
|
+
return `rgba(${r2}, ${g2}, ${b2}, ${alpha})`;
|
|
961
2328
|
}
|
|
962
2329
|
var REF = /^\{([^}]+)\}$/;
|
|
963
2330
|
function refTarget(value) {
|
|
964
2331
|
if (typeof value !== "string") return null;
|
|
965
|
-
const
|
|
966
|
-
if (!
|
|
967
|
-
const path =
|
|
2332
|
+
const m2 = REF.exec(value.trim());
|
|
2333
|
+
if (!m2) return null;
|
|
2334
|
+
const path = m2[1];
|
|
968
2335
|
const dot = path.indexOf(".");
|
|
969
2336
|
return dot === -1 ? path : path.slice(dot + 1);
|
|
970
2337
|
}
|
|
@@ -1103,9 +2470,9 @@ function flattenTokens(doc) {
|
|
|
1103
2470
|
for (const [groupName, group] of Object.entries(doc)) {
|
|
1104
2471
|
if (groupName.startsWith("$")) continue;
|
|
1105
2472
|
if (typeof group !== "object" || group === null) continue;
|
|
1106
|
-
const
|
|
1107
|
-
const groupType =
|
|
1108
|
-
for (const [leaf, node] of Object.entries(
|
|
2473
|
+
const g2 = group;
|
|
2474
|
+
const groupType = g2.$type;
|
|
2475
|
+
for (const [leaf, node] of Object.entries(g2)) {
|
|
1109
2476
|
if (leaf.startsWith("$")) continue;
|
|
1110
2477
|
if (!isToken(node)) continue;
|
|
1111
2478
|
const type = node.$type ?? groupType;
|
|
@@ -1210,134 +2577,6 @@ var interstellar_tokens_default = {
|
|
|
1210
2577
|
|
|
1211
2578
|
// src/theme/interstellar.ts
|
|
1212
2579
|
var interstellarTheme = loadDTCG(interstellar_tokens_default);
|
|
1213
|
-
function DragGhost({ item, screenPos }) {
|
|
1214
|
-
const [mounted, setMounted] = useState(false);
|
|
1215
|
-
useEffect(() => setMounted(true), []);
|
|
1216
|
-
if (!mounted || typeof document === "undefined") return null;
|
|
1217
|
-
const style = {
|
|
1218
|
-
left: `${screenPos.x + 12}px`,
|
|
1219
|
-
top: `${screenPos.y + 12}px`
|
|
1220
|
-
};
|
|
1221
|
-
return createPortal(
|
|
1222
|
-
/* @__PURE__ */ jsx("div", { className: "lk-drag-ghost", style, children: /* @__PURE__ */ jsx("span", { className: "lk-drag-ghost__label", children: item.label }) }),
|
|
1223
|
-
document.body
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1226
|
-
function useDragDrop({
|
|
1227
|
-
capability,
|
|
1228
|
-
canvasContainerRef,
|
|
1229
|
-
view,
|
|
1230
|
-
state,
|
|
1231
|
-
config,
|
|
1232
|
-
setState,
|
|
1233
|
-
emit
|
|
1234
|
-
}) {
|
|
1235
|
-
const [drag, setDrag] = useState(null);
|
|
1236
|
-
const dragRef = useRef(null);
|
|
1237
|
-
dragRef.current = drag;
|
|
1238
|
-
const pendingPos = useRef(null);
|
|
1239
|
-
const isOverCanvas = useCallback(
|
|
1240
|
-
(screenPos) => {
|
|
1241
|
-
const el = canvasContainerRef.current;
|
|
1242
|
-
if (!el) return false;
|
|
1243
|
-
const r2 = el.getBoundingClientRect();
|
|
1244
|
-
return screenPos.x >= r2.left && screenPos.x <= r2.right && screenPos.y >= r2.top && screenPos.y <= r2.bottom;
|
|
1245
|
-
},
|
|
1246
|
-
[canvasContainerRef]
|
|
1247
|
-
);
|
|
1248
|
-
const screenToWorldFromContainer = useCallback(
|
|
1249
|
-
(screenPos) => {
|
|
1250
|
-
const el = canvasContainerRef.current;
|
|
1251
|
-
if (!el) return null;
|
|
1252
|
-
const r2 = el.getBoundingClientRect();
|
|
1253
|
-
return screenToWorld({ x: screenPos.x - r2.left, y: screenPos.y - r2.top }, view);
|
|
1254
|
-
},
|
|
1255
|
-
[canvasContainerRef, view]
|
|
1256
|
-
);
|
|
1257
|
-
const frameLoop = useVisibleRaf(() => {
|
|
1258
|
-
const screenPos = pendingPos.current;
|
|
1259
|
-
pendingPos.current = null;
|
|
1260
|
-
const active = dragRef.current;
|
|
1261
|
-
if (!active || !screenPos) return;
|
|
1262
|
-
let feedback = null;
|
|
1263
|
-
if (capability.onDragOver && isOverCanvas(screenPos)) {
|
|
1264
|
-
const world = screenToWorldFromContainer(screenPos);
|
|
1265
|
-
if (world) feedback = capability.onDragOver(world, active.item, state, config);
|
|
1266
|
-
}
|
|
1267
|
-
setDrag({ ...active, screenPos, feedback });
|
|
1268
|
-
});
|
|
1269
|
-
useEffect(() => {
|
|
1270
|
-
if (!drag) return;
|
|
1271
|
-
const handleMove = (e) => {
|
|
1272
|
-
const screenPos = { x: e.clientX, y: e.clientY };
|
|
1273
|
-
const current = dragRef.current;
|
|
1274
|
-
if (!current) return;
|
|
1275
|
-
if (pendingPos.current !== null) {
|
|
1276
|
-
pendingPos.current = screenPos;
|
|
1277
|
-
dragRef.current = { ...current, screenPos };
|
|
1278
|
-
setDrag(dragRef.current);
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
pendingPos.current = screenPos;
|
|
1282
|
-
frameLoop.request();
|
|
1283
|
-
};
|
|
1284
|
-
const handleUp = (e) => {
|
|
1285
|
-
const screenPos = { x: e.clientX, y: e.clientY };
|
|
1286
|
-
const active = dragRef.current;
|
|
1287
|
-
if (!active) return;
|
|
1288
|
-
pendingPos.current = null;
|
|
1289
|
-
frameLoop.cancel();
|
|
1290
|
-
if (isOverCanvas(screenPos)) {
|
|
1291
|
-
const world = screenToWorldFromContainer(screenPos);
|
|
1292
|
-
if (world) {
|
|
1293
|
-
const nextState = capability.onDrop(world, active.item, state, config);
|
|
1294
|
-
setState(nextState);
|
|
1295
|
-
emit("canvas.itemAdded");
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
setDrag(null);
|
|
1299
|
-
};
|
|
1300
|
-
window.addEventListener("pointermove", handleMove);
|
|
1301
|
-
window.addEventListener("pointerup", handleUp);
|
|
1302
|
-
return () => {
|
|
1303
|
-
window.removeEventListener("pointermove", handleMove);
|
|
1304
|
-
window.removeEventListener("pointerup", handleUp);
|
|
1305
|
-
};
|
|
1306
|
-
}, [
|
|
1307
|
-
drag,
|
|
1308
|
-
capability,
|
|
1309
|
-
state,
|
|
1310
|
-
config,
|
|
1311
|
-
setState,
|
|
1312
|
-
emit,
|
|
1313
|
-
frameLoop,
|
|
1314
|
-
isOverCanvas,
|
|
1315
|
-
screenToWorldFromContainer
|
|
1316
|
-
]);
|
|
1317
|
-
const startDrag = useCallback((item, originScreenPos) => {
|
|
1318
|
-
setDrag({ item, screenPos: originScreenPos, feedback: null });
|
|
1319
|
-
}, []);
|
|
1320
|
-
return { drag, startDrag };
|
|
1321
|
-
}
|
|
1322
|
-
function DragOverlay({ drag }) {
|
|
1323
|
-
if (!drag) return null;
|
|
1324
|
-
return /* @__PURE__ */ jsx(DragGhost, { item: drag.item, screenPos: drag.screenPos });
|
|
1325
|
-
}
|
|
1326
|
-
function Palette({ items, onDragStart, className }) {
|
|
1327
|
-
return /* @__PURE__ */ jsx("div", { className: className ? `lk-palette ${className}` : "lk-palette", children: items.map((item) => /* @__PURE__ */ jsx(
|
|
1328
|
-
"button",
|
|
1329
|
-
{
|
|
1330
|
-
type: "button",
|
|
1331
|
-
className: "lk-palette__item",
|
|
1332
|
-
onPointerDown: (e) => {
|
|
1333
|
-
e.preventDefault();
|
|
1334
|
-
onDragStart(item, { x: e.clientX, y: e.clientY });
|
|
1335
|
-
},
|
|
1336
|
-
children: /* @__PURE__ */ jsx("span", { className: "lk-palette__label", children: item.label })
|
|
1337
|
-
},
|
|
1338
|
-
item.id
|
|
1339
|
-
)) });
|
|
1340
|
-
}
|
|
1341
2580
|
|
|
1342
2581
|
// src/state/view.ts
|
|
1343
2582
|
var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
|
|
@@ -1369,6 +2608,69 @@ function TrialTitleBar({ title, children }) {
|
|
|
1369
2608
|
children
|
|
1370
2609
|
] });
|
|
1371
2610
|
}
|
|
2611
|
+
function createPanelHostRegistry() {
|
|
2612
|
+
const hosts = /* @__PURE__ */ new Map();
|
|
2613
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2614
|
+
const notify = () => {
|
|
2615
|
+
for (const fn of listeners) fn();
|
|
2616
|
+
};
|
|
2617
|
+
return {
|
|
2618
|
+
set: (key, el) => {
|
|
2619
|
+
if (el) hosts.set(key, el);
|
|
2620
|
+
else hosts.delete(key);
|
|
2621
|
+
notify();
|
|
2622
|
+
},
|
|
2623
|
+
get: (key) => hosts.get(key) ?? null,
|
|
2624
|
+
subscribe: (fn) => {
|
|
2625
|
+
listeners.add(fn);
|
|
2626
|
+
return () => listeners.delete(fn);
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
var PanelHostContext = createContext(null);
|
|
2631
|
+
function usePanelHosts() {
|
|
2632
|
+
return useContext(PanelHostContext);
|
|
2633
|
+
}
|
|
2634
|
+
function Portalled({
|
|
2635
|
+
trialId,
|
|
2636
|
+
contribution,
|
|
2637
|
+
onDock
|
|
2638
|
+
}) {
|
|
2639
|
+
const hosts = usePanelHosts();
|
|
2640
|
+
const key = panelKey(trialId, contribution.id);
|
|
2641
|
+
const host = useSyncExternalStore(
|
|
2642
|
+
(fn) => hosts?.subscribe(fn) ?? (() => {
|
|
2643
|
+
}),
|
|
2644
|
+
() => hosts?.get(key) ?? null,
|
|
2645
|
+
() => null
|
|
2646
|
+
);
|
|
2647
|
+
if (!host || !contribution.item) return null;
|
|
2648
|
+
const item = contribution.item;
|
|
2649
|
+
return createPortal(
|
|
2650
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2651
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-panel-tile__titlebar", children: [
|
|
2652
|
+
/* @__PURE__ */ jsx("span", { className: "lk-panel-tile__title", children: item.title }),
|
|
2653
|
+
/* @__PURE__ */ jsx(
|
|
2654
|
+
"button",
|
|
2655
|
+
{
|
|
2656
|
+
type: "button",
|
|
2657
|
+
className: "lk-panel-tile__dock",
|
|
2658
|
+
"aria-label": `Dock ${item.title}`,
|
|
2659
|
+
title: `Dock ${item.title}`,
|
|
2660
|
+
onClick: () => onDock(contribution.id),
|
|
2661
|
+
children: "\u21E4"
|
|
2662
|
+
}
|
|
2663
|
+
)
|
|
2664
|
+
] }),
|
|
2665
|
+
item.body
|
|
2666
|
+
] }),
|
|
2667
|
+
host
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
function UndockedSections({ trialId, sections, onDock }) {
|
|
2671
|
+
if (sections.length === 0) return null;
|
|
2672
|
+
return /* @__PURE__ */ jsx(Fragment, { children: sections.map((c2) => /* @__PURE__ */ jsx(Portalled, { trialId, contribution: c2, onDock }, c2.id)) });
|
|
2673
|
+
}
|
|
1372
2674
|
var NO_OP = () => {
|
|
1373
2675
|
};
|
|
1374
2676
|
function TrialChrome({
|
|
@@ -1378,6 +2680,7 @@ function TrialChrome({
|
|
|
1378
2680
|
isLastTrial,
|
|
1379
2681
|
undoBindings,
|
|
1380
2682
|
job,
|
|
2683
|
+
loupe,
|
|
1381
2684
|
trialChrome,
|
|
1382
2685
|
chrome,
|
|
1383
2686
|
suppress,
|
|
@@ -1391,6 +2694,13 @@ function TrialChrome({
|
|
|
1391
2694
|
const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
|
|
1392
2695
|
const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
|
|
1393
2696
|
const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
|
|
2697
|
+
const undockPanelAction = useStore(storeCtx.store, (s) => s.undockPanel);
|
|
2698
|
+
const dockPanelAction = useStore(storeCtx.store, (s) => s.dockPanel);
|
|
2699
|
+
const undockedPanels = useStore(storeCtx.store, (s) => s.undockedPanels);
|
|
2700
|
+
const undockedIds = useMemo(
|
|
2701
|
+
() => Object.values(undockedPanels).filter((p2) => p2.trialId === trialId).map((p2) => p2.sectionId),
|
|
2702
|
+
[undockedPanels, trialId]
|
|
2703
|
+
);
|
|
1394
2704
|
const view2d = as2DView(record.view);
|
|
1395
2705
|
const configSchema = useConfigSchema(instrument);
|
|
1396
2706
|
const ctx = useMemo(() => {
|
|
@@ -1408,6 +2718,8 @@ function TrialChrome({
|
|
|
1408
2718
|
canRedo: undoBindings?.canRedo ?? false,
|
|
1409
2719
|
undo: undoBindings?.undo ?? NO_OP,
|
|
1410
2720
|
redo: undoBindings?.redo ?? NO_OP,
|
|
2721
|
+
loupeOn: loupe?.on ?? false,
|
|
2722
|
+
toggleLoupe: loupe?.toggle ?? NO_OP,
|
|
1411
2723
|
configSchema,
|
|
1412
2724
|
configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
|
|
1413
2725
|
config: record.config,
|
|
@@ -1425,6 +2737,9 @@ function TrialChrome({
|
|
|
1425
2737
|
updateTrialState(trialId, nextState);
|
|
1426
2738
|
}
|
|
1427
2739
|
},
|
|
2740
|
+
undockedPanels: undockedIds,
|
|
2741
|
+
undockPanel: (sectionId, as) => undockPanelAction(trialId, sectionId, as),
|
|
2742
|
+
dockPanel: (sectionId) => dockPanelAction(trialId, sectionId),
|
|
1428
2743
|
savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
|
|
1429
2744
|
saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
|
|
1430
2745
|
loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
|
|
@@ -1444,10 +2759,14 @@ function TrialChrome({
|
|
|
1444
2759
|
updateTrialConfig,
|
|
1445
2760
|
updateTrialState,
|
|
1446
2761
|
undoBindings,
|
|
2762
|
+
loupe,
|
|
1447
2763
|
view2d,
|
|
1448
2764
|
activeToolId,
|
|
1449
2765
|
setActiveTool,
|
|
1450
|
-
configSchema
|
|
2766
|
+
configSchema,
|
|
2767
|
+
undockedIds,
|
|
2768
|
+
undockPanelAction,
|
|
2769
|
+
dockPanelAction
|
|
1451
2770
|
]);
|
|
1452
2771
|
const contributions = useMemo(
|
|
1453
2772
|
() => suppressContributions(
|
|
@@ -1461,7 +2780,7 @@ function TrialChrome({
|
|
|
1461
2780
|
),
|
|
1462
2781
|
[instrument, ctx, trialChrome, chrome, suppress, lab.controls]
|
|
1463
2782
|
);
|
|
1464
|
-
const inRegion = (region) => contributions.filter((
|
|
2783
|
+
const inRegion = (region) => contributions.filter((c2) => c2.region === region);
|
|
1465
2784
|
const handleKeyDown = (e) => {
|
|
1466
2785
|
const mod = e.metaKey || e.ctrlKey;
|
|
1467
2786
|
if (!mod) return;
|
|
@@ -1486,7 +2805,17 @@ function TrialChrome({
|
|
|
1486
2805
|
/* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: /* @__PURE__ */ jsx(ToolbarRegion, { contributions: inRegion("toolbar"), ctx }) }),
|
|
1487
2806
|
/* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
|
|
1488
2807
|
/* @__PURE__ */ jsx(PaletteRegion, { contributions: inRegion("palette"), ctx }),
|
|
1489
|
-
/* @__PURE__ */
|
|
2808
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-trial__sidebar", children: [
|
|
2809
|
+
/* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }),
|
|
2810
|
+
/* @__PURE__ */ jsx(
|
|
2811
|
+
UndockedSections,
|
|
2812
|
+
{
|
|
2813
|
+
trialId,
|
|
2814
|
+
sections: inRegion("sidebar").filter((c2) => undockedIds.includes(c2.id)),
|
|
2815
|
+
onDock: ctx.dockPanel
|
|
2816
|
+
}
|
|
2817
|
+
)
|
|
2818
|
+
] }),
|
|
1490
2819
|
/* @__PURE__ */ jsxs("div", { className: `lk-trial__content${instrument.canvas ? " lk-trial__content--flush" : ""}`, children: [
|
|
1491
2820
|
children,
|
|
1492
2821
|
/* @__PURE__ */ jsx(ViewportRegion, { contributions: inRegion("viewport"), ctx })
|
|
@@ -1504,7 +2833,7 @@ function Trial({ id, chrome, suppress }) {
|
|
|
1504
2833
|
const lab = useLabContext();
|
|
1505
2834
|
const storeCtx = useContext(LabStoreContext);
|
|
1506
2835
|
if (!storeCtx) throw new Error("[labkit] <Trial> requires <LabStoreProvider>");
|
|
1507
|
-
const record = useStore(storeCtx.store, (s) => s.trials.find((
|
|
2836
|
+
const record = useStore(storeCtx.store, (s) => s.trials.find((w2) => w2.id === id));
|
|
1508
2837
|
if (!record) {
|
|
1509
2838
|
return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
|
|
1510
2839
|
"Trial not found: ",
|
|
@@ -1532,10 +2861,12 @@ function Trial({ id, chrome, suppress }) {
|
|
|
1532
2861
|
}
|
|
1533
2862
|
function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
1534
2863
|
const canvasContainerRef = useRef(null);
|
|
2864
|
+
const loupeHostRef = useRef(null);
|
|
1535
2865
|
const updateTrialState = useStore(store, (s) => s.updateTrialState);
|
|
1536
2866
|
const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
|
|
1537
2867
|
const updateTrialView = useStore(store, (s) => s.updateTrialView);
|
|
1538
2868
|
const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
|
|
2869
|
+
const updateTrialAnnotations = useStore(store, (s) => s.updateTrialAnnotations);
|
|
1539
2870
|
const labToolId = useStore(store, (s) => s.activeToolId);
|
|
1540
2871
|
const setLabTool = useStore(store, (s) => s.setLabTool);
|
|
1541
2872
|
const setTrialTool = useStore(store, (s) => s.setTrialTool);
|
|
@@ -1544,19 +2875,85 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1544
2875
|
const bus = busRef.current;
|
|
1545
2876
|
const [layerVisibility, setLayerVisibility] = useState({});
|
|
1546
2877
|
const [layerOrder, setLayerOrder] = useState(null);
|
|
2878
|
+
const [loupeOn, setLoupeOn] = useState(false);
|
|
2879
|
+
const visibleLayers = useMemo(
|
|
2880
|
+
() => (instrument.canvas?.layers ?? []).filter((l) => layerVisibility[l.id] !== false).map((l) => l.id),
|
|
2881
|
+
[instrument.canvas, layerVisibility]
|
|
2882
|
+
);
|
|
1547
2883
|
const undoCap = instrument.undo;
|
|
1548
2884
|
const undoEvents = useMemo(() => new Set(undoCap?.snapshotOn ?? ["state.change"]), [undoCap]);
|
|
1549
2885
|
const maxDepth = undoCap?.maxDepth ?? 50;
|
|
1550
2886
|
const snapshotIfNeeded = (event) => {
|
|
1551
2887
|
if (!undoCap || !undoEvents.has(event)) return;
|
|
1552
|
-
const current = store.getState().trials.find((
|
|
2888
|
+
const current = store.getState().trials.find((w2) => w2.id === record.id);
|
|
1553
2889
|
if (!current) return;
|
|
1554
2890
|
const snap = structuredClone(current.state);
|
|
1555
2891
|
updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
|
|
1556
2892
|
};
|
|
1557
2893
|
const setView = (v) => updateTrialView(record.id, v);
|
|
1558
|
-
const
|
|
1559
|
-
|
|
2894
|
+
const placeView = (size) => {
|
|
2895
|
+
const declared = instrument.canvas?.initialView;
|
|
2896
|
+
if (typeof declared !== "function") return;
|
|
2897
|
+
if (record.view != null) return;
|
|
2898
|
+
setView(declared(size));
|
|
2899
|
+
};
|
|
2900
|
+
const annotationsCap = instrument.annotations;
|
|
2901
|
+
const targetsRef = useRef(() => []);
|
|
2902
|
+
targetsRef.current = () => annotationsCap ? annotationsCap.targets(record.state, record.config) : [];
|
|
2903
|
+
const annotationsRef = useRef(null);
|
|
2904
|
+
const configRef = useRef(record.config);
|
|
2905
|
+
configRef.current = record.config;
|
|
2906
|
+
if (annotationsRef.current === null) {
|
|
2907
|
+
const kept = annotationsCap?.storage ? annotationsCap.storage.load() : record.annotations;
|
|
2908
|
+
annotationsRef.current = annotationsFromJSON(kept, () => targetsRef.current(), {
|
|
2909
|
+
meaning: annotationsCap?.meaning,
|
|
2910
|
+
config: () => configRef.current,
|
|
2911
|
+
onCapture: (result) => annotationsCap?.onCapture?.(result)
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
const annotations = annotationsRef.current;
|
|
2915
|
+
const saveCap = annotationsCap?.storage;
|
|
2916
|
+
const saveRef = useRef(() => {
|
|
2917
|
+
});
|
|
2918
|
+
saveRef.current = (doc) => {
|
|
2919
|
+
if (saveCap) saveCap.save(doc);
|
|
2920
|
+
else updateTrialAnnotations(record.id, doc);
|
|
2921
|
+
};
|
|
2922
|
+
const [markMoves, setMarkMoves] = useState({ undo: false, redo: false });
|
|
2923
|
+
useEffect(() => {
|
|
2924
|
+
if (!annotationsCap) return;
|
|
2925
|
+
const read = () => {
|
|
2926
|
+
const next = { undo: annotations.canUndo(), redo: annotations.canRedo() };
|
|
2927
|
+
setMarkMoves((prev) => prev.undo === next.undo && prev.redo === next.redo ? prev : next);
|
|
2928
|
+
};
|
|
2929
|
+
read();
|
|
2930
|
+
return annotations.subscribe(read);
|
|
2931
|
+
}, [annotations, annotationsCap]);
|
|
2932
|
+
useEffect(() => {
|
|
2933
|
+
if (!annotationsCap) return;
|
|
2934
|
+
let timer = null;
|
|
2935
|
+
let dirty = false;
|
|
2936
|
+
const flush = () => {
|
|
2937
|
+
if (!dirty) return;
|
|
2938
|
+
dirty = false;
|
|
2939
|
+
saveRef.current(annotations.toJSON());
|
|
2940
|
+
};
|
|
2941
|
+
const off = annotations.subscribe(() => {
|
|
2942
|
+
dirty = true;
|
|
2943
|
+
if (timer) clearTimeout(timer);
|
|
2944
|
+
timer = setTimeout(() => {
|
|
2945
|
+
timer = null;
|
|
2946
|
+
flush();
|
|
2947
|
+
}, 250);
|
|
2948
|
+
});
|
|
2949
|
+
return () => {
|
|
2950
|
+
off();
|
|
2951
|
+
if (timer) clearTimeout(timer);
|
|
2952
|
+
flush();
|
|
2953
|
+
};
|
|
2954
|
+
}, [annotations, annotationsCap]);
|
|
2955
|
+
const declaresTools = instrument.tools != null || instrument.annotations != null;
|
|
2956
|
+
const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? (instrument.annotations ? "select" : null) : labToolId;
|
|
1560
2957
|
const setActiveTool = (id) => {
|
|
1561
2958
|
if (declaresTools) setTrialTool(record.id, id);
|
|
1562
2959
|
else setLabTool(id);
|
|
@@ -1595,7 +2992,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1595
2992
|
if (!view2d) return;
|
|
1596
2993
|
updateTrialView(record.id, { ...view2d, zoom: z });
|
|
1597
2994
|
},
|
|
1598
|
-
activeToolId: resolvedToolId
|
|
2995
|
+
activeToolId: resolvedToolId,
|
|
2996
|
+
visibleLayers
|
|
1599
2997
|
},
|
|
1600
2998
|
emit: (event) => {
|
|
1601
2999
|
snapshotIfNeeded(event);
|
|
@@ -1603,32 +3001,45 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1603
3001
|
},
|
|
1604
3002
|
job: jobCap ? job : void 0
|
|
1605
3003
|
};
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
3004
|
+
const undoState = () => {
|
|
3005
|
+
const result = undo(record.undoStack, structuredClone(record.state));
|
|
3006
|
+
if (!result) return;
|
|
3007
|
+
updateTrialState(record.id, result.snapshot);
|
|
3008
|
+
updateTrialUndoStack(record.id, result.stack);
|
|
3009
|
+
};
|
|
3010
|
+
const redoState = () => {
|
|
3011
|
+
const result = redo(record.undoStack, structuredClone(record.state));
|
|
3012
|
+
if (!result) return;
|
|
3013
|
+
updateTrialState(record.id, result.snapshot);
|
|
3014
|
+
updateTrialUndoStack(record.id, result.stack);
|
|
3015
|
+
};
|
|
3016
|
+
const undoBindings = undoCap || annotationsCap ? {
|
|
3017
|
+
canUndo: markMoves.undo || (undoCap ? record.undoStack.past.length > 0 : false),
|
|
3018
|
+
canRedo: markMoves.redo || (undoCap ? record.undoStack.future.length > 0 : false),
|
|
1609
3019
|
undo: () => {
|
|
1610
|
-
|
|
1611
|
-
if (
|
|
1612
|
-
updateTrialState(record.id, result.snapshot);
|
|
1613
|
-
updateTrialUndoStack(record.id, result.stack);
|
|
3020
|
+
if (annotationsCap && annotations.undo()) return;
|
|
3021
|
+
if (undoCap) undoState();
|
|
1614
3022
|
},
|
|
1615
3023
|
redo: () => {
|
|
1616
|
-
|
|
1617
|
-
if (
|
|
1618
|
-
updateTrialState(record.id, result.snapshot);
|
|
1619
|
-
updateTrialUndoStack(record.id, result.stack);
|
|
3024
|
+
if (annotationsCap && annotations.redo()) return;
|
|
3025
|
+
if (undoCap) redoState();
|
|
1620
3026
|
}
|
|
1621
3027
|
} : void 0;
|
|
3028
|
+
const loupeCap = useMemo(() => {
|
|
3029
|
+
const declared = instrument.loupe;
|
|
3030
|
+
if (declared == null) return null;
|
|
3031
|
+
return resolveLoupe(typeof declared === "function" ? declared(record.config) : declared);
|
|
3032
|
+
}, [instrument.loupe, record.config]);
|
|
3033
|
+
const loupeBindings = loupeCap ? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) } : void 0;
|
|
1622
3034
|
const canvasLayers = useMemo(() => {
|
|
1623
3035
|
if (!instrument.canvas) return [];
|
|
1624
3036
|
const baseLayers = instrument.canvas.layers;
|
|
1625
|
-
const ordered = layerOrder ? [...baseLayers].sort((a,
|
|
3037
|
+
const ordered = layerOrder ? [...baseLayers].sort((a, b2) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b2.id)) : baseLayers;
|
|
1626
3038
|
return ordered.map((layer) => ({
|
|
1627
3039
|
id: layer.id,
|
|
1628
3040
|
visible: layerVisibility[layer.id] !== false,
|
|
1629
|
-
render: (ctx, view) => {
|
|
1630
|
-
ctx
|
|
1631
|
-
ctx.scale(view.zoom, view.zoom);
|
|
3041
|
+
render: (ctx, view, frame) => {
|
|
3042
|
+
applyCamera(ctx, view, frame);
|
|
1632
3043
|
layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom });
|
|
1633
3044
|
}
|
|
1634
3045
|
}));
|
|
@@ -1641,6 +3052,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1641
3052
|
capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
|
|
1642
3053
|
canvasContainerRef,
|
|
1643
3054
|
view: view2d ?? DEFAULT_VIEW,
|
|
3055
|
+
worldSpec: instrument.canvas?.worldSpec,
|
|
1644
3056
|
state: record.state,
|
|
1645
3057
|
config: record.config,
|
|
1646
3058
|
setState: (next) => {
|
|
@@ -1654,8 +3066,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1654
3066
|
});
|
|
1655
3067
|
const paletteItems = useMemo(() => {
|
|
1656
3068
|
if (!instrument.dragDrop) return [];
|
|
1657
|
-
const
|
|
1658
|
-
return typeof
|
|
3069
|
+
const p2 = instrument.dragDrop.palette;
|
|
3070
|
+
return typeof p2 === "function" ? p2(record.state, record.config) : p2;
|
|
1659
3071
|
}, [instrument.dragDrop, record.state, record.config]);
|
|
1660
3072
|
const layersWithFeedback = useMemo(() => {
|
|
1661
3073
|
if (!dragDropResult.drag?.feedback) return canvasLayers;
|
|
@@ -1681,22 +3093,44 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1681
3093
|
}
|
|
1682
3094
|
];
|
|
1683
3095
|
}, [canvasLayers, dragDropResult.drag]);
|
|
3096
|
+
const lens = loupeCap ? /* @__PURE__ */ jsx(
|
|
3097
|
+
TrialLoupe,
|
|
3098
|
+
{
|
|
3099
|
+
capability: loupeCap,
|
|
3100
|
+
enabled: loupeOn,
|
|
3101
|
+
state: record.state,
|
|
3102
|
+
config: record.config,
|
|
3103
|
+
view: view2d ?? DEFAULT_VIEW,
|
|
3104
|
+
worldSpec: instrument.canvas?.worldSpec,
|
|
3105
|
+
hostRef: loupeHostRef
|
|
3106
|
+
}
|
|
3107
|
+
) : null;
|
|
1684
3108
|
let body;
|
|
1685
3109
|
if (instrument.canvas) {
|
|
1686
3110
|
body = /* @__PURE__ */ jsxs("div", { ref: canvasContainerRef, className: "lk-trial__canvas-host", children: [
|
|
1687
|
-
/* @__PURE__ */
|
|
3111
|
+
/* @__PURE__ */ jsxs(
|
|
1688
3112
|
CanvasStack,
|
|
1689
3113
|
{
|
|
1690
3114
|
layers: layersWithFeedback,
|
|
1691
3115
|
view: view2d ?? DEFAULT_VIEW,
|
|
1692
3116
|
onViewChange: setView,
|
|
3117
|
+
worldSpec: instrument.canvas.worldSpec,
|
|
3118
|
+
onResize: placeView,
|
|
1693
3119
|
minZoom: instrument.canvas.minZoom,
|
|
1694
3120
|
maxZoom: instrument.canvas.maxZoom,
|
|
1695
|
-
children:
|
|
3121
|
+
children: [
|
|
3122
|
+
instrument.render(renderCtx),
|
|
3123
|
+
lens
|
|
3124
|
+
]
|
|
1696
3125
|
}
|
|
1697
3126
|
),
|
|
1698
3127
|
/* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
|
|
1699
3128
|
] });
|
|
3129
|
+
} else if (lens) {
|
|
3130
|
+
body = /* @__PURE__ */ jsxs("div", { ref: loupeHostRef, className: "lk-trial__loupe-host", children: [
|
|
3131
|
+
instrument.render(renderCtx),
|
|
3132
|
+
lens
|
|
3133
|
+
] });
|
|
1700
3134
|
} else {
|
|
1701
3135
|
body = instrument.render(renderCtx);
|
|
1702
3136
|
}
|
|
@@ -1741,10 +3175,21 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1741
3175
|
}
|
|
1742
3176
|
return out;
|
|
1743
3177
|
}, [paletteNode, layerListNode]);
|
|
1744
|
-
|
|
3178
|
+
const annotationOverlays = annotationsCap ? /* @__PURE__ */ jsx(
|
|
3179
|
+
AnnotationTargets,
|
|
3180
|
+
{
|
|
3181
|
+
capability: annotationsCap,
|
|
3182
|
+
state: record.state,
|
|
3183
|
+
config: record.config,
|
|
3184
|
+
annotations,
|
|
3185
|
+
activeToolId: resolvedToolId
|
|
3186
|
+
}
|
|
3187
|
+
) : null;
|
|
3188
|
+
return /* @__PURE__ */ jsx(TrialIdProvider, { trialId: record.id, children: /* @__PURE__ */ jsx(AnnotationsContext.Provider, { value: annotationsCap ? annotations : null, children: /* @__PURE__ */ jsxs(
|
|
1745
3189
|
TrialChrome,
|
|
1746
3190
|
{
|
|
1747
3191
|
job: jobCap ? job : void 0,
|
|
3192
|
+
loupe: loupeBindings,
|
|
1748
3193
|
trialId: record.id,
|
|
1749
3194
|
record,
|
|
1750
3195
|
instrument,
|
|
@@ -1755,9 +3200,12 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
|
|
|
1755
3200
|
suppress,
|
|
1756
3201
|
activeToolId: resolvedToolId,
|
|
1757
3202
|
setActiveTool,
|
|
1758
|
-
children:
|
|
3203
|
+
children: [
|
|
3204
|
+
body,
|
|
3205
|
+
annotationOverlays
|
|
3206
|
+
]
|
|
1759
3207
|
}
|
|
1760
|
-
);
|
|
3208
|
+
) }) });
|
|
1761
3209
|
}
|
|
1762
3210
|
|
|
1763
3211
|
// src/trial/trialOps.ts
|
|
@@ -1770,7 +3218,9 @@ function findInstrument(instruments, name) {
|
|
|
1770
3218
|
return found;
|
|
1771
3219
|
}
|
|
1772
3220
|
function initialView(instrument) {
|
|
1773
|
-
|
|
3221
|
+
const declared = instrument.canvas?.initialView;
|
|
3222
|
+
if (typeof declared === "function") return null;
|
|
3223
|
+
return declared ? structuredClone(declared) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
|
|
1774
3224
|
}
|
|
1775
3225
|
function addTrial(trials, instruments, instrumentName) {
|
|
1776
3226
|
const instrument = findInstrument(instruments, instrumentName);
|
|
@@ -1787,7 +3237,7 @@ function addTrial(trials, instruments, instrumentName) {
|
|
|
1787
3237
|
return [...trials, record];
|
|
1788
3238
|
}
|
|
1789
3239
|
function cloneTrial(trials, id) {
|
|
1790
|
-
const sourceIdx = trials.findIndex((
|
|
3240
|
+
const sourceIdx = trials.findIndex((w2) => w2.id === id);
|
|
1791
3241
|
const source = trials[sourceIdx];
|
|
1792
3242
|
if (!source) return trials;
|
|
1793
3243
|
const clone = {
|
|
@@ -1802,11 +3252,11 @@ function cloneTrial(trials, id) {
|
|
|
1802
3252
|
}
|
|
1803
3253
|
function closeTrial(trials, id) {
|
|
1804
3254
|
if (trials.length <= 1) return trials;
|
|
1805
|
-
const next = trials.filter((
|
|
3255
|
+
const next = trials.filter((w2) => w2.id !== id);
|
|
1806
3256
|
return next.length === trials.length ? trials : next;
|
|
1807
3257
|
}
|
|
1808
3258
|
function resetTrial(trials, id, instruments) {
|
|
1809
|
-
const idx = trials.findIndex((
|
|
3259
|
+
const idx = trials.findIndex((w2) => w2.id === id);
|
|
1810
3260
|
const current = trials[idx];
|
|
1811
3261
|
if (!current) return trials;
|
|
1812
3262
|
const instrument = findInstrument(instruments, current.instrumentName);
|
|
@@ -1821,10 +3271,10 @@ function resetTrial(trials, id, instruments) {
|
|
|
1821
3271
|
return [...trials.slice(0, idx), reset, ...trials.slice(idx + 1)];
|
|
1822
3272
|
}
|
|
1823
3273
|
function reorderTrials(trials, ids) {
|
|
1824
|
-
const byId = new Map(trials.map((
|
|
1825
|
-
const named = ids.map((id) => byId.get(id)).filter((
|
|
1826
|
-
const seen = new Set(named.map((
|
|
1827
|
-
const rest = trials.filter((
|
|
3274
|
+
const byId = new Map(trials.map((w2) => [w2.id, w2]));
|
|
3275
|
+
const named = ids.map((id) => byId.get(id)).filter((w2) => w2 !== void 0);
|
|
3276
|
+
const seen = new Set(named.map((w2) => w2.id));
|
|
3277
|
+
const rest = trials.filter((w2) => !seen.has(w2.id));
|
|
1828
3278
|
return [...named, ...rest];
|
|
1829
3279
|
}
|
|
1830
3280
|
var MODES = [
|
|
@@ -1843,7 +3293,7 @@ function LabHeader() {
|
|
|
1843
3293
|
className: "lk-lab-header__add",
|
|
1844
3294
|
onClick: () => lab.addTrial(only.name),
|
|
1845
3295
|
children: [
|
|
1846
|
-
/* @__PURE__ */ jsx(
|
|
3296
|
+
/* @__PURE__ */ jsx(C, { size: 16 }),
|
|
1847
3297
|
/* @__PURE__ */ jsx("span", { children: "Add trial" })
|
|
1848
3298
|
]
|
|
1849
3299
|
}
|
|
@@ -1851,7 +3301,7 @@ function LabHeader() {
|
|
|
1851
3301
|
// Held at null so the control stays an "add one" action rather than
|
|
1852
3302
|
// reading as the current instrument.
|
|
1853
3303
|
/* @__PURE__ */ jsx(
|
|
1854
|
-
|
|
3304
|
+
p,
|
|
1855
3305
|
{
|
|
1856
3306
|
className: "lk-lab-header__add-select",
|
|
1857
3307
|
"aria-label": "Add trial",
|
|
@@ -1865,7 +3315,7 @@ function LabHeader() {
|
|
|
1865
3315
|
)
|
|
1866
3316
|
),
|
|
1867
3317
|
/* @__PURE__ */ jsx(
|
|
1868
|
-
|
|
3318
|
+
r2,
|
|
1869
3319
|
{
|
|
1870
3320
|
className: "lk-lab-header__mode",
|
|
1871
3321
|
ariaLabel: "Color mode",
|
|
@@ -1927,19 +3377,26 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
|
|
|
1927
3377
|
return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
|
|
1928
3378
|
}
|
|
1929
3379
|
var ZONE_ID = asNodeId("lk-workspace");
|
|
1930
|
-
var STRATEGIES = { grid: gridStrategy };
|
|
3380
|
+
var STRATEGIES = { grid: gridStrategy, floating: floatingStrategy };
|
|
3381
|
+
var FLOAT_ZONE_ID = asNodeId("lk-workspace-floating");
|
|
3382
|
+
var PANEL_KIND = "panel";
|
|
1931
3383
|
var KIND = "trial";
|
|
1932
3384
|
function extentOf(store, id) {
|
|
1933
|
-
const
|
|
1934
|
-
if (!
|
|
3385
|
+
const p2 = store.getNode(id)?.membership?.placement;
|
|
3386
|
+
if (!p2) return null;
|
|
1935
3387
|
const out = {};
|
|
1936
|
-
if (
|
|
1937
|
-
if (
|
|
3388
|
+
if (p2.size) out.size = p2.size;
|
|
3389
|
+
if (p2.span) out.span = p2.span;
|
|
1938
3390
|
return Object.keys(out).length > 0 ? out : null;
|
|
1939
3391
|
}
|
|
3392
|
+
function PanelFrame({ panel, floating = false }) {
|
|
3393
|
+
const hosts = usePanelHosts();
|
|
3394
|
+
return /* @__PURE__ */ jsx("div", { className: floating ? "lk-panel-tile lk-panel-tile--floating" : "lk-panel-tile", children: /* @__PURE__ */ jsx("div", { className: "lk-panel-tile__body", ref: (el) => hosts?.set(panel.key, el) }) });
|
|
3395
|
+
}
|
|
1940
3396
|
function Workspace({
|
|
1941
3397
|
children,
|
|
1942
3398
|
ids,
|
|
3399
|
+
panels,
|
|
1943
3400
|
resizable = false,
|
|
1944
3401
|
reorderable = false,
|
|
1945
3402
|
onReorder,
|
|
@@ -1950,8 +3407,20 @@ function Workspace({
|
|
|
1950
3407
|
viewport
|
|
1951
3408
|
}) {
|
|
1952
3409
|
const items = Children.toArray(children);
|
|
1953
|
-
const
|
|
1954
|
-
const
|
|
3410
|
+
const tilePanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "tile"), [panels]);
|
|
3411
|
+
const floatPanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "floating"), [panels]);
|
|
3412
|
+
const wantedIds = [
|
|
3413
|
+
...items.map((_2, i) => ids?.[i] ?? `lk-ws-${i}`),
|
|
3414
|
+
...tilePanels.map((p2) => `lk-panel-${p2.key}`)
|
|
3415
|
+
];
|
|
3416
|
+
const idKey = wantedIds.join(",");
|
|
3417
|
+
const idKeyRef = useRef(null);
|
|
3418
|
+
const nodeIdsRef = useRef([]);
|
|
3419
|
+
if (idKeyRef.current !== idKey) {
|
|
3420
|
+
idKeyRef.current = idKey;
|
|
3421
|
+
nodeIdsRef.current = wantedIds.map(asNodeId);
|
|
3422
|
+
}
|
|
3423
|
+
const nodeIds = nodeIdsRef.current;
|
|
1955
3424
|
const layoutRef = useRef(layout);
|
|
1956
3425
|
layoutRef.current = layout;
|
|
1957
3426
|
const onLayoutChangeRef = useRef(onLayoutChange);
|
|
@@ -1966,6 +3435,13 @@ function Workspace({
|
|
|
1966
3435
|
container: { strategyId: "grid", config: { resizable, gap, padding } }
|
|
1967
3436
|
})
|
|
1968
3437
|
);
|
|
3438
|
+
store2.registerNode(
|
|
3439
|
+
createNode({
|
|
3440
|
+
kind: "zone",
|
|
3441
|
+
id: FLOAT_ZONE_ID,
|
|
3442
|
+
container: { strategyId: "floating", config: {} }
|
|
3443
|
+
})
|
|
3444
|
+
);
|
|
1969
3445
|
storeRef.current = store2;
|
|
1970
3446
|
}
|
|
1971
3447
|
const store = storeRef.current;
|
|
@@ -1981,7 +3457,8 @@ function Workspace({
|
|
|
1981
3457
|
}
|
|
1982
3458
|
for (const id of nodeIds) {
|
|
1983
3459
|
if (present.has(id)) continue;
|
|
1984
|
-
|
|
3460
|
+
const kind = String(id).startsWith("lk-panel-") ? PANEL_KIND : KIND;
|
|
3461
|
+
store.registerNode(createNode({ kind, id, parentId: ZONE_ID, focus: true }));
|
|
1985
3462
|
store.showNode(id);
|
|
1986
3463
|
const saved = layoutRef.current?.[id];
|
|
1987
3464
|
if (saved) store.patchPlacement(id, saved);
|
|
@@ -2010,7 +3487,15 @@ function Workspace({
|
|
|
2010
3487
|
);
|
|
2011
3488
|
const chrome = useMemo(() => {
|
|
2012
3489
|
const byId = new Map(nodeIds.map((id, i) => [id, items[i]]));
|
|
3490
|
+
const panelByNode = new Map(
|
|
3491
|
+
tilePanels.map((p2) => [`lk-panel-${p2.key}`, p2])
|
|
3492
|
+
);
|
|
2013
3493
|
return {
|
|
3494
|
+
[PANEL_KIND]: ({ node }) => {
|
|
3495
|
+
const panel = panelByNode.get(String(node.id));
|
|
3496
|
+
if (!panel) return null;
|
|
3497
|
+
return /* @__PURE__ */ jsx(PanelFrame, { panel });
|
|
3498
|
+
},
|
|
2014
3499
|
[KIND]: ({ node }) => {
|
|
2015
3500
|
const content = byId.get(node.id) ?? null;
|
|
2016
3501
|
if (!reorderable) return content;
|
|
@@ -2020,7 +3505,8 @@ function Workspace({
|
|
|
2020
3505
|
);
|
|
2021
3506
|
}
|
|
2022
3507
|
};
|
|
2023
|
-
}, [nodeIds, items, reorderable]);
|
|
3508
|
+
}, [nodeIds, items, reorderable, tilePanels]);
|
|
3509
|
+
const floatingLayer = floatPanels.length === 0 ? null : /* @__PURE__ */ jsx("div", { className: "lk-workspace__floating", children: floatPanels.map((p2) => /* @__PURE__ */ jsx(PanelFrame, { panel: p2, floating: true }, p2.key)) });
|
|
2024
3510
|
const grid = /* @__PURE__ */ jsx(
|
|
2025
3511
|
Container,
|
|
2026
3512
|
{
|
|
@@ -2032,7 +3518,15 @@ function Workspace({
|
|
|
2032
3518
|
onChildOrderChange: reorderable ? commitOrder : void 0
|
|
2033
3519
|
}
|
|
2034
3520
|
);
|
|
2035
|
-
return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children:
|
|
3521
|
+
return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children: floatingLayer ? (
|
|
3522
|
+
// The floating layer insets against the workspace, so it needs a
|
|
3523
|
+
// positioned box around both. Without one it insets against the page
|
|
3524
|
+
// and lands over the lab header.
|
|
3525
|
+
/* @__PURE__ */ jsxs("div", { className: "lk-workspace-host", children: [
|
|
3526
|
+
reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid,
|
|
3527
|
+
floatingLayer
|
|
3528
|
+
] })
|
|
3529
|
+
) : reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
|
|
2036
3530
|
}
|
|
2037
3531
|
function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
|
|
2038
3532
|
const store = createLabStore({ storageKey, storage, initialMode });
|
|
@@ -2054,9 +3548,9 @@ var NEBULA_SLOTS = [
|
|
|
2054
3548
|
{ cx: "85%", cy: "18%", sx: "55%", sy: "80%", stop: "70%" }
|
|
2055
3549
|
];
|
|
2056
3550
|
function buildNebula(colors) {
|
|
2057
|
-
const blobs = colors.map((
|
|
2058
|
-
const
|
|
2059
|
-
return `radial-gradient(ellipse ${
|
|
3551
|
+
const blobs = colors.map((c2, i) => {
|
|
3552
|
+
const p2 = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
|
|
3553
|
+
return `radial-gradient(ellipse ${p2.sx} ${p2.sy} at ${p2.cx} ${p2.cy}, color-mix(in srgb, ${c2} 22%, transparent), transparent ${p2.stop})`;
|
|
2060
3554
|
});
|
|
2061
3555
|
blobs.push("radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)");
|
|
2062
3556
|
return blobs.join(", ");
|
|
@@ -2095,6 +3589,40 @@ function Lab({
|
|
|
2095
3589
|
const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
|
|
2096
3590
|
const modeValue = useStore(store, (s) => s.mode);
|
|
2097
3591
|
const layout = useStore(store, (s) => s.layout);
|
|
3592
|
+
const undockedPanels = useStore(store, (s) => s.undockedPanels);
|
|
3593
|
+
const panelHostsRef = useRef(null);
|
|
3594
|
+
if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
|
|
3595
|
+
const outerSurface = useSurfaceOptional();
|
|
3596
|
+
const outerCanvas = useSurfaceCanvas();
|
|
3597
|
+
const [ownCanvas, setOwnCanvas] = useState(null);
|
|
3598
|
+
const ownCanvasRef = useRef(null);
|
|
3599
|
+
const bufferRef = useRef({ w: 0, h: 0 });
|
|
3600
|
+
const surfaceRef = useRef(null);
|
|
3601
|
+
const onFrame = useCallback((frame) => {
|
|
3602
|
+
const c2 = ownCanvasRef.current;
|
|
3603
|
+
if (!c2) return;
|
|
3604
|
+
const w2 = Math.round(frame.size.width * frame.dpr);
|
|
3605
|
+
const h = Math.round(frame.size.height * frame.dpr);
|
|
3606
|
+
if (bufferRef.current.w === w2 && bufferRef.current.h === h) return;
|
|
3607
|
+
bufferRef.current = { w: w2, h };
|
|
3608
|
+
c2.width = w2;
|
|
3609
|
+
c2.height = h;
|
|
3610
|
+
c2.style.width = `${frame.size.width}px`;
|
|
3611
|
+
c2.style.height = `${frame.size.height}px`;
|
|
3612
|
+
surfaceRef.current?.invalidateAll();
|
|
3613
|
+
}, []);
|
|
3614
|
+
const ownSurface = useTiledSurface({ onFrame });
|
|
3615
|
+
surfaceRef.current = ownSurface;
|
|
3616
|
+
const surface = outerSurface ?? ownSurface;
|
|
3617
|
+
const surfaceCanvas = outerSurface ? outerCanvas : ownCanvas;
|
|
3618
|
+
const workspacePanels = useMemo(
|
|
3619
|
+
() => Object.entries(undockedPanels).map(([key, panel]) => ({
|
|
3620
|
+
key,
|
|
3621
|
+
title: panel.sectionId,
|
|
3622
|
+
as: panel.as
|
|
3623
|
+
})),
|
|
3624
|
+
[undockedPanels]
|
|
3625
|
+
);
|
|
2098
3626
|
const resolvedMode = useResolvedMode(modeValue);
|
|
2099
3627
|
useEffect(() => {
|
|
2100
3628
|
if (mode && mode !== store.getState().mode) {
|
|
@@ -2103,8 +3631,8 @@ function Lab({
|
|
|
2103
3631
|
}, [mode, store]);
|
|
2104
3632
|
const contextValue = useMemo(() => {
|
|
2105
3633
|
const replaceTrials = (next) => {
|
|
2106
|
-
const currentById = new Map(store.getState().trials.map((
|
|
2107
|
-
const merged = next.map((
|
|
3634
|
+
const currentById = new Map(store.getState().trials.map((w2) => [w2.id, w2]));
|
|
3635
|
+
const merged = next.map((w2) => currentById.get(w2.id) ?? w2);
|
|
2108
3636
|
store.setState({ trials: merged });
|
|
2109
3637
|
store.getState().setMode(store.getState().mode);
|
|
2110
3638
|
};
|
|
@@ -2128,11 +3656,11 @@ function Lab({
|
|
|
2128
3656
|
},
|
|
2129
3657
|
resetTrial: (id) => {
|
|
2130
3658
|
const next = resetTrial(store.getState().trials, id, instruments);
|
|
2131
|
-
const record = next.find((
|
|
3659
|
+
const record = next.find((w2) => w2.id === id);
|
|
2132
3660
|
if (!record) return;
|
|
2133
3661
|
store.setState((s) => ({
|
|
2134
3662
|
trials: s.trials.map(
|
|
2135
|
-
(
|
|
3663
|
+
(w2) => w2.id === id ? { ...w2, config: record.config, state: record.state, view: record.view } : w2
|
|
2136
3664
|
)
|
|
2137
3665
|
}));
|
|
2138
3666
|
store.getState().updateTrialView(id, record.view);
|
|
@@ -2149,8 +3677,8 @@ function Lab({
|
|
|
2149
3677
|
store.getState().deleteSnapshot(snapshotId);
|
|
2150
3678
|
},
|
|
2151
3679
|
mode: modeValue,
|
|
2152
|
-
setMode: (
|
|
2153
|
-
store.getState().setMode(
|
|
3680
|
+
setMode: (m2) => {
|
|
3681
|
+
store.getState().setMode(m2);
|
|
2154
3682
|
},
|
|
2155
3683
|
configRules,
|
|
2156
3684
|
controls
|
|
@@ -2174,372 +3702,50 @@ function Lab({
|
|
|
2174
3702
|
/* @__PURE__ */ jsx(LabHeader, {}),
|
|
2175
3703
|
children
|
|
2176
3704
|
] }),
|
|
2177
|
-
children: /* @__PURE__ */
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
function CurveField({
|
|
2198
|
-
values,
|
|
2199
|
-
min,
|
|
2200
|
-
max,
|
|
2201
|
-
step,
|
|
2202
|
-
width,
|
|
2203
|
-
height = 110,
|
|
2204
|
-
defaults,
|
|
2205
|
-
onChange,
|
|
2206
|
-
marks
|
|
2207
|
-
}) {
|
|
2208
|
-
const points = useMemo(() => {
|
|
2209
|
-
const out = [];
|
|
2210
|
-
for (let i = 0; i + 1 < values.length; i += 2) out.push({ x: values[i], y: values[i + 1] });
|
|
2211
|
-
return out;
|
|
2212
|
-
}, [values]);
|
|
2213
|
-
const handleChange = useCallback(
|
|
2214
|
-
(next) => {
|
|
2215
|
-
const flat = new Array(next.length * 2);
|
|
2216
|
-
for (let i = 0; i < next.length; i++) {
|
|
2217
|
-
const ySnap = Math.round(next[i].y / step) * step;
|
|
2218
|
-
const y = Math.max(min, Math.min(max, ySnap));
|
|
2219
|
-
flat[i * 2] = next[i].x;
|
|
2220
|
-
flat[i * 2 + 1] = y;
|
|
2221
|
-
}
|
|
2222
|
-
p("curve-field", "handleChange", {
|
|
2223
|
-
nLen: next.length,
|
|
2224
|
-
flatLen: flat.length,
|
|
2225
|
-
sample: flat.slice(0, 4)
|
|
2226
|
-
});
|
|
2227
|
-
onChange(flat);
|
|
2228
|
-
},
|
|
2229
|
-
[onChange, min, max, step]
|
|
2230
|
-
);
|
|
2231
|
-
const handleFlip = useCallback(() => {
|
|
2232
|
-
const flipped = [];
|
|
2233
|
-
for (let i = 0; i + 1 < values.length; i += 2) {
|
|
2234
|
-
flipped.push({ x: 1 - values[i], y: values[i + 1] });
|
|
2235
|
-
}
|
|
2236
|
-
flipped.sort((a, b) => a.x - b.x);
|
|
2237
|
-
const out = [];
|
|
2238
|
-
for (const p3 of flipped) out.push(p3.x, p3.y);
|
|
2239
|
-
onChange(out);
|
|
2240
|
-
}, [values, onChange]);
|
|
2241
|
-
const handleFlipVertical = useCallback(() => {
|
|
2242
|
-
const mid = (min + max) / 2;
|
|
2243
|
-
const flipped = new Array(values.length);
|
|
2244
|
-
for (let i = 0; i + 1 < values.length; i += 2) {
|
|
2245
|
-
flipped[i] = values[i];
|
|
2246
|
-
flipped[i + 1] = 2 * mid - values[i + 1];
|
|
2247
|
-
}
|
|
2248
|
-
onChange(flipped);
|
|
2249
|
-
}, [values, min, max, onChange]);
|
|
2250
|
-
return /* @__PURE__ */ jsxs("div", { className: "lk-curve-field", children: [
|
|
2251
|
-
/* @__PURE__ */ jsxs("div", { className: "lk-curve-field__plot", children: [
|
|
2252
|
-
/* @__PURE__ */ jsx(
|
|
2253
|
-
F,
|
|
2254
|
-
{
|
|
2255
|
-
value: points,
|
|
2256
|
-
onInput: handleChange,
|
|
2257
|
-
domain: "1d",
|
|
2258
|
-
constrain: "function",
|
|
2259
|
-
xRange: [0, 1],
|
|
2260
|
-
yRange: [min, max],
|
|
2261
|
-
width,
|
|
2262
|
-
height,
|
|
2263
|
-
endpoints: "pinned-x",
|
|
2264
|
-
addPointMode: "click-curve",
|
|
2265
|
-
minPoints: 2,
|
|
2266
|
-
grid: {},
|
|
2267
|
-
history: false
|
|
2268
|
-
}
|
|
2269
|
-
),
|
|
2270
|
-
marks && marks.length > 0 && /* @__PURE__ */ jsx(
|
|
2271
|
-
"svg",
|
|
2272
|
-
{
|
|
2273
|
-
className: "lk-curve-field__marks",
|
|
2274
|
-
width,
|
|
2275
|
-
height,
|
|
2276
|
-
viewBox: `0 0 ${width} ${height}`,
|
|
2277
|
-
"aria-hidden": "true",
|
|
2278
|
-
children: marks.map((m) => {
|
|
2279
|
-
const color = m.color ?? "#ffcc00";
|
|
2280
|
-
if (m.kind === "band") {
|
|
2281
|
-
const x0 = Math.max(0, Math.min(1, m.x[0])) * width;
|
|
2282
|
-
const x1 = Math.max(0, Math.min(1, m.x[1])) * width;
|
|
2283
|
-
const left = Math.min(x0, x1);
|
|
2284
|
-
const w = Math.abs(x1 - x0);
|
|
2285
|
-
return /* @__PURE__ */ jsxs("g", { children: [
|
|
2286
|
-
/* @__PURE__ */ jsx("rect", { x: left, y: 0, width: w, height, fill: color, fillOpacity: 0.4 }),
|
|
2287
|
-
/* @__PURE__ */ jsx(
|
|
2288
|
-
"line",
|
|
2289
|
-
{
|
|
2290
|
-
x1: left,
|
|
2291
|
-
y1: 0,
|
|
2292
|
-
x2: left,
|
|
2293
|
-
y2: height,
|
|
2294
|
-
stroke: color,
|
|
2295
|
-
strokeOpacity: 0.85,
|
|
2296
|
-
strokeWidth: 1
|
|
2297
|
-
}
|
|
3705
|
+
children: /* @__PURE__ */ jsx(PanelHostContext.Provider, { value: panelHostsRef.current, children: /* @__PURE__ */ jsx(SurfaceContext.Provider, { value: surface, children: /* @__PURE__ */ jsx(SurfaceCanvasContext.Provider, { value: surfaceCanvas, children: /* @__PURE__ */ jsxs(
|
|
3706
|
+
"div",
|
|
3707
|
+
{
|
|
3708
|
+
className: "lk-lab__body",
|
|
3709
|
+
ref: outerSurface ? void 0 : ownSurface.containerRef,
|
|
3710
|
+
children: [
|
|
3711
|
+
outerSurface ? null : (
|
|
3712
|
+
// Above the trials and inert: the marks a tile paints have
|
|
3713
|
+
// to sit over the instrument's own DOM, and nothing on this
|
|
3714
|
+
// buffer takes input — each tile has its own input box.
|
|
3715
|
+
/* @__PURE__ */ jsx(
|
|
3716
|
+
"canvas",
|
|
3717
|
+
{
|
|
3718
|
+
className: "lk-lab__surface",
|
|
3719
|
+
ref: (el) => {
|
|
3720
|
+
ownCanvasRef.current = el;
|
|
3721
|
+
setOwnCanvas(el);
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
)
|
|
2298
3725
|
),
|
|
3726
|
+
tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
|
|
2299
3727
|
/* @__PURE__ */ jsx(
|
|
2300
|
-
|
|
3728
|
+
Workspace,
|
|
2301
3729
|
{
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
3730
|
+
panels: workspacePanels,
|
|
3731
|
+
ids: trials.map((w2) => w2.id),
|
|
3732
|
+
resizable: true,
|
|
3733
|
+
reorderable: true,
|
|
3734
|
+
onReorder: (ids) => contextValue.reorderTrials(ids),
|
|
3735
|
+
layout,
|
|
3736
|
+
onLayoutChange: (next) => store.getState().setLayout(next),
|
|
3737
|
+
children: trials.map((w2) => /* @__PURE__ */ jsx(Trial, { id: w2.id, chrome, suppress }, w2.id))
|
|
2309
3738
|
}
|
|
2310
3739
|
)
|
|
2311
|
-
]
|
|
3740
|
+
]
|
|
2312
3741
|
}
|
|
2313
|
-
|
|
2314
|
-
return /* @__PURE__ */ jsx(
|
|
2315
|
-
"line",
|
|
2316
|
-
{
|
|
2317
|
-
x1: x,
|
|
2318
|
-
y1: 0,
|
|
2319
|
-
x2: x,
|
|
2320
|
-
y2: height,
|
|
2321
|
-
stroke: color,
|
|
2322
|
-
strokeWidth: 1
|
|
2323
|
-
},
|
|
2324
|
-
`mark-${m.x}-${color}`
|
|
2325
|
-
);
|
|
2326
|
-
})
|
|
2327
|
-
}
|
|
2328
|
-
)
|
|
2329
|
-
] }),
|
|
2330
|
-
/* @__PURE__ */ jsxs("div", { className: "lk-curve-field__actions", children: [
|
|
2331
|
-
/* @__PURE__ */ jsx("button", { type: "button", className: "lk-curve-field__action", onClick: handleFlip, children: "Flip horizontally" }),
|
|
2332
|
-
/* @__PURE__ */ jsx("button", { type: "button", className: "lk-curve-field__action", onClick: handleFlipVertical, children: "Flip vertically" }),
|
|
2333
|
-
defaults && /* @__PURE__ */ jsx(
|
|
2334
|
-
"button",
|
|
2335
|
-
{
|
|
2336
|
-
type: "button",
|
|
2337
|
-
className: "lk-curve-field__action",
|
|
2338
|
-
onClick: () => onChange([...defaults]),
|
|
2339
|
-
children: "Reset"
|
|
3742
|
+
) }) }) })
|
|
2340
3743
|
}
|
|
2341
3744
|
)
|
|
2342
|
-
] })
|
|
2343
|
-
] });
|
|
2344
|
-
}
|
|
2345
|
-
function Subpanel({ title, children, className }) {
|
|
2346
|
-
const cls = className ? `lk-subpanel ${className}` : "lk-subpanel";
|
|
2347
|
-
return /* @__PURE__ */ jsxs("div", { className: cls, children: [
|
|
2348
|
-
/* @__PURE__ */ jsxs("h4", { className: "lk-subpanel__title", children: [
|
|
2349
|
-
/* @__PURE__ */ jsx("span", { children: title }),
|
|
2350
|
-
/* @__PURE__ */ jsx("hr", {})
|
|
2351
|
-
] }),
|
|
2352
|
-
children
|
|
2353
|
-
] });
|
|
2354
|
-
}
|
|
2355
|
-
function EffectCard({
|
|
2356
|
-
title,
|
|
2357
|
-
primary,
|
|
2358
|
-
accent,
|
|
2359
|
-
index,
|
|
2360
|
-
expanded = true,
|
|
2361
|
-
onToggleExpanded,
|
|
2362
|
-
onRemove,
|
|
2363
|
-
draggable,
|
|
2364
|
-
onPressHandle,
|
|
2365
|
-
onReleaseHandle,
|
|
2366
|
-
onDragStart,
|
|
2367
|
-
onDragEnd,
|
|
2368
|
-
onDragOver,
|
|
2369
|
-
onDragLeave,
|
|
2370
|
-
onDrop,
|
|
2371
|
-
dragging,
|
|
2372
|
-
children,
|
|
2373
|
-
className
|
|
2374
|
-
}) {
|
|
2375
|
-
const stateClass = `${expanded ? "is-expanded" : "is-collapsed"}${dragging ? " is-dragging" : ""}${accent ? " has-accent" : ""}`;
|
|
2376
|
-
const cls = `lk-effect-card ${stateClass}${className ? ` ${className}` : ""}`;
|
|
2377
|
-
const style = accent ? { "--lk-panel-accent": accent } : void 0;
|
|
2378
|
-
const headProps = onToggleExpanded ? {
|
|
2379
|
-
role: "button",
|
|
2380
|
-
tabIndex: 0,
|
|
2381
|
-
onClick: onToggleExpanded,
|
|
2382
|
-
onKeyDown: (e) => {
|
|
2383
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
2384
|
-
e.preventDefault();
|
|
2385
|
-
onToggleExpanded();
|
|
2386
|
-
}
|
|
2387
3745
|
}
|
|
2388
|
-
}
|
|
2389
|
-
return (
|
|
2390
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: native HTML5 drag-and-drop container — drag events are not click/keyboard interactions and have no semantic element; the in-card handle and header carry the a11y affordances.
|
|
2391
|
-
/* @__PURE__ */ jsxs(
|
|
2392
|
-
"div",
|
|
2393
|
-
{
|
|
2394
|
-
className: cls,
|
|
2395
|
-
style,
|
|
2396
|
-
draggable,
|
|
2397
|
-
onDragStart,
|
|
2398
|
-
onDragEnd,
|
|
2399
|
-
onDragOver,
|
|
2400
|
-
onDragLeave,
|
|
2401
|
-
onDrop,
|
|
2402
|
-
children: [
|
|
2403
|
-
/* @__PURE__ */ jsxs("div", { className: "lk-effect-card__head", ...headProps, children: [
|
|
2404
|
-
index != null ? /* @__PURE__ */ jsx(
|
|
2405
|
-
"button",
|
|
2406
|
-
{
|
|
2407
|
-
type: "button",
|
|
2408
|
-
className: "lk-effect-card__index-badge lk-effect-card__handle",
|
|
2409
|
-
title: "Drag to reorder \xB7 click to toggle",
|
|
2410
|
-
"aria-label": `Item ${index + 1} \u2014 drag to reorder, click to toggle`,
|
|
2411
|
-
onMouseDown: onPressHandle,
|
|
2412
|
-
onMouseUp: onReleaseHandle,
|
|
2413
|
-
onClick: (e) => {
|
|
2414
|
-
e.stopPropagation();
|
|
2415
|
-
onToggleExpanded?.();
|
|
2416
|
-
},
|
|
2417
|
-
children: index + 1
|
|
2418
|
-
}
|
|
2419
|
-
) : /* @__PURE__ */ jsx(
|
|
2420
|
-
"span",
|
|
2421
|
-
{
|
|
2422
|
-
className: "lk-effect-card__handle",
|
|
2423
|
-
"aria-hidden": "true",
|
|
2424
|
-
title: "Drag to reorder",
|
|
2425
|
-
onMouseDown: onPressHandle,
|
|
2426
|
-
onMouseUp: onReleaseHandle,
|
|
2427
|
-
children: /* @__PURE__ */ jsx(DragHandleIcon, {})
|
|
2428
|
-
}
|
|
2429
|
-
),
|
|
2430
|
-
/* @__PURE__ */ jsx("span", { className: "lk-effect-card__title", children: title }),
|
|
2431
|
-
primary != null && /* @__PURE__ */ jsx("span", { className: "lk-effect-card__primary", children: primary }),
|
|
2432
|
-
onRemove && /* @__PURE__ */ jsx(
|
|
2433
|
-
"button",
|
|
2434
|
-
{
|
|
2435
|
-
type: "button",
|
|
2436
|
-
className: "lk-effect-card__remove",
|
|
2437
|
-
onClick: (e) => {
|
|
2438
|
-
e.stopPropagation();
|
|
2439
|
-
onRemove();
|
|
2440
|
-
},
|
|
2441
|
-
title: "Remove",
|
|
2442
|
-
"aria-label": "Remove",
|
|
2443
|
-
children: "\u2715"
|
|
2444
|
-
}
|
|
2445
|
-
)
|
|
2446
|
-
] }),
|
|
2447
|
-
expanded && /* @__PURE__ */ jsx("div", { className: "lk-effect-card__body", children })
|
|
2448
|
-
]
|
|
2449
|
-
}
|
|
2450
|
-
)
|
|
2451
|
-
);
|
|
2452
|
-
}
|
|
2453
|
-
function DragHandleIcon() {
|
|
2454
|
-
return /* @__PURE__ */ jsxs("svg", { width: "10", height: "14", viewBox: "0 0 10 14", "aria-hidden": "true", children: [
|
|
2455
|
-
/* @__PURE__ */ jsx("circle", { cx: "3", cy: "3", r: "1.3", fill: "currentColor" }),
|
|
2456
|
-
/* @__PURE__ */ jsx("circle", { cx: "7", cy: "3", r: "1.3", fill: "currentColor" }),
|
|
2457
|
-
/* @__PURE__ */ jsx("circle", { cx: "3", cy: "7", r: "1.3", fill: "currentColor" }),
|
|
2458
|
-
/* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "1.3", fill: "currentColor" }),
|
|
2459
|
-
/* @__PURE__ */ jsx("circle", { cx: "3", cy: "11", r: "1.3", fill: "currentColor" }),
|
|
2460
|
-
/* @__PURE__ */ jsx("circle", { cx: "7", cy: "11", r: "1.3", fill: "currentColor" })
|
|
2461
|
-
] });
|
|
2462
|
-
}
|
|
2463
|
-
function EffectCardList({
|
|
2464
|
-
items,
|
|
2465
|
-
renderItem,
|
|
2466
|
-
onReorder,
|
|
2467
|
-
empty,
|
|
2468
|
-
defaultExpandedIds
|
|
2469
|
-
}) {
|
|
2470
|
-
const [expanded, setExpanded] = useState(() => new Set(defaultExpandedIds ?? []));
|
|
2471
|
-
const [draggingId, setDraggingId] = useState(null);
|
|
2472
|
-
const [pressedId, setPressedId] = useState(null);
|
|
2473
|
-
const [dropHint, setDropHint] = useState(
|
|
2474
|
-
null
|
|
2475
|
-
);
|
|
2476
|
-
if (items.length === 0) {
|
|
2477
|
-
return /* @__PURE__ */ jsx(Fragment, { children: empty ?? /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__empty", children: "No items." }) });
|
|
2478
|
-
}
|
|
2479
|
-
return /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list", children: items.map((item) => {
|
|
2480
|
-
const isExpanded = expanded.has(item.id);
|
|
2481
|
-
const isDragging = draggingId === item.id;
|
|
2482
|
-
const isPressed = pressedId === item.id;
|
|
2483
|
-
const showHintBefore = dropHint && dropHint.id === item.id && dropHint.position === "before";
|
|
2484
|
-
const showHintAfter = dropHint && dropHint.id === item.id && dropHint.position === "after";
|
|
2485
|
-
const toggleExpanded = () => setExpanded((prev) => {
|
|
2486
|
-
const next = new Set(prev);
|
|
2487
|
-
if (next.has(item.id)) next.delete(item.id);
|
|
2488
|
-
else next.add(item.id);
|
|
2489
|
-
return next;
|
|
2490
|
-
});
|
|
2491
|
-
return /* @__PURE__ */ jsxs("div", { className: "lk-effect-card-list__wrap", children: [
|
|
2492
|
-
showHintBefore && /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__drop-hint" }),
|
|
2493
|
-
renderItem(item, {
|
|
2494
|
-
isExpanded,
|
|
2495
|
-
isDragging,
|
|
2496
|
-
toggleExpanded,
|
|
2497
|
-
cardProps: {
|
|
2498
|
-
expanded: isExpanded,
|
|
2499
|
-
dragging: isDragging,
|
|
2500
|
-
draggable: isPressed,
|
|
2501
|
-
onToggleExpanded: toggleExpanded,
|
|
2502
|
-
onPressHandle: () => setPressedId(item.id),
|
|
2503
|
-
onReleaseHandle: () => setPressedId(null),
|
|
2504
|
-
onDragStart: (e) => {
|
|
2505
|
-
e.dataTransfer.effectAllowed = "move";
|
|
2506
|
-
e.dataTransfer.setData("text/plain", String(item.id));
|
|
2507
|
-
setDraggingId(item.id);
|
|
2508
|
-
},
|
|
2509
|
-
onDragEnd: () => {
|
|
2510
|
-
setDraggingId(null);
|
|
2511
|
-
setPressedId(null);
|
|
2512
|
-
setDropHint(null);
|
|
2513
|
-
},
|
|
2514
|
-
onDragOver: (e) => {
|
|
2515
|
-
if (draggingId === null || draggingId === item.id) return;
|
|
2516
|
-
e.preventDefault();
|
|
2517
|
-
e.dataTransfer.dropEffect = "move";
|
|
2518
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
2519
|
-
const position = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
|
|
2520
|
-
setDropHint({ id: item.id, position });
|
|
2521
|
-
},
|
|
2522
|
-
onDragLeave: () => {
|
|
2523
|
-
setDropHint((h) => h && h.id === item.id ? null : h);
|
|
2524
|
-
},
|
|
2525
|
-
onDrop: (e) => {
|
|
2526
|
-
e.preventDefault();
|
|
2527
|
-
if (draggingId === null || draggingId === item.id) {
|
|
2528
|
-
setDropHint(null);
|
|
2529
|
-
return;
|
|
2530
|
-
}
|
|
2531
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
2532
|
-
const position = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
|
|
2533
|
-
onReorder(draggingId, item.id, position);
|
|
2534
|
-
setDropHint(null);
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
}),
|
|
2538
|
-
showHintAfter && /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__drop-hint" })
|
|
2539
|
-
] }, String(item.id));
|
|
2540
|
-
}) });
|
|
3746
|
+
) }) });
|
|
2541
3747
|
}
|
|
2542
3748
|
|
|
2543
|
-
export {
|
|
3749
|
+
export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, PITCH_LIMIT, Trial, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, clampPitch, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, defineInstrument, f, fitView, fracContains, fracIntersects, fracToWorld, fromWeaselView, interstellarTheme, isStale, markCommands, markSvgNodes, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, resolveConfigSchema, resolveMarkStyle, roundFrac, seenFrom, titleCase, toWeaselView, useConfigSchema, useLabContext, useOrbit, validateConfigSchema, worldToFrac, wrapYaw };
|
|
2544
3750
|
//# sourceMappingURL=index.js.map
|
|
2545
3751
|
//# sourceMappingURL=index.js.map
|