@weasel-js/labkit 1.3.0 → 1.4.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -55
- package/dist/_dts/CanvasStackContext-LnCfqNBA.d.ts +42 -0
- package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
- package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
- package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
- package/dist/_dts/frac-C-2c72Ij.d.ts +735 -0
- package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
- package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
- package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
- package/dist/_dts/{types-DCGEcMO_.d.ts → types-D6s4b7if.d.ts} +1 -1
- package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
- package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
- package/dist/canvas/index.d.ts +110 -26
- package/dist/canvas/index.js +5 -4
- package/dist/chrome/index.d.ts +9 -6
- package/dist/chrome/index.js +6 -6
- package/dist/chunk-2AYGEN57.js +32 -0
- package/dist/chunk-2AYGEN57.js.map +1 -0
- package/dist/chunk-6OEYTYML.js +669 -0
- package/dist/chunk-6OEYTYML.js.map +1 -0
- package/dist/{chunk-BMW4TDP5.js → chunk-AE5CNVRU.js} +3 -3
- package/dist/{chunk-BMW4TDP5.js.map → chunk-AE5CNVRU.js.map} +1 -1
- package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
- package/dist/chunk-CQLQPQ4P.js.map +1 -0
- package/dist/chunk-CTRKTLYZ.js +33 -0
- package/dist/chunk-CTRKTLYZ.js.map +1 -0
- package/dist/chunk-E2UQVZ44.js +458 -0
- package/dist/chunk-E2UQVZ44.js.map +1 -0
- package/dist/{chunk-LGKRIUSW.js → chunk-ESIQNQ6K.js} +32 -7
- package/dist/chunk-ESIQNQ6K.js.map +1 -0
- package/dist/{chunk-JDROYQM3.js → chunk-HHGVISVZ.js} +130 -131
- package/dist/chunk-HHGVISVZ.js.map +1 -0
- package/dist/{chunk-BDJXIBRZ.js → chunk-HMFODCOX.js} +126 -29
- package/dist/chunk-HMFODCOX.js.map +1 -0
- package/dist/{chunk-QCICIHEO.js → chunk-L5QJOOLV.js} +839 -320
- package/dist/chunk-L5QJOOLV.js.map +1 -0
- package/dist/{chunk-3XXPU73K.js → chunk-M75ZU6ZZ.js} +9 -7
- package/dist/chunk-M75ZU6ZZ.js.map +1 -0
- package/dist/{chunk-QRYPXSGP.js → chunk-QB5SM4BM.js} +16 -8
- package/dist/chunk-QB5SM4BM.js.map +1 -0
- package/dist/chunk-RBGKL7NF.js +7128 -0
- package/dist/chunk-RBGKL7NF.js.map +1 -0
- package/dist/{chunk-WTI26YTM.js → chunk-TO2FUOKF.js} +40 -13
- package/dist/chunk-TO2FUOKF.js.map +1 -0
- package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
- package/dist/chunk-XJ6N32QP.js.map +1 -0
- package/dist/controls/index.d.ts +4 -28
- package/dist/controls/index.js +3 -3
- package/dist/dragdrop/index.d.ts +9 -5
- package/dist/dragdrop/index.js +3 -3
- package/dist/index.d.ts +517 -234
- package/dist/index.js +1769 -143
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -5
- package/dist/layers/index.js +3 -3
- package/dist/loupe/index.d.ts +187 -0
- package/dist/loupe/index.js +7 -0
- package/dist/loupe/index.js.map +1 -0
- package/dist/passthrough/weasel-canvas.d.ts +3 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +138 -91
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.js +5 -5
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/styles.css +229 -1
- package/dist/surface/index.d.ts +18 -2
- package/dist/surface/index.js +2 -2
- package/dist/ui/layers/index.js +2 -2
- package/dist/undo/index.d.ts +5 -4
- package/package.json +14 -7
- package/src/annotations/AnnotationOverlay.tsx +205 -0
- package/src/annotations/AnnotationTargets.tsx +37 -0
- package/src/annotations/Annotations.less +129 -0
- package/src/annotations/Annotations.meaning.test.tsx +111 -0
- package/src/annotations/Annotations.overlay.test.tsx +142 -0
- package/src/annotations/AnnotationsContext.ts +49 -0
- package/src/annotations/ExportMenu.test.tsx +86 -0
- package/src/annotations/ExportMenu.tsx +182 -0
- package/src/annotations/MarkList.tsx +75 -0
- package/src/annotations/capture.test.ts +140 -0
- package/src/annotations/capture.ts +237 -0
- package/src/annotations/drawOne.test.ts +56 -0
- package/src/annotations/drawOne.ts +47 -0
- package/src/annotations/frac.test.ts +59 -0
- package/src/annotations/frac.ts +66 -0
- package/src/annotations/history.test.ts +82 -0
- package/src/annotations/history.ts +118 -0
- package/src/annotations/index.ts +37 -0
- package/src/annotations/paint.test.ts +125 -0
- package/src/annotations/paint.ts +130 -0
- package/src/annotations/staleness.test.ts +56 -0
- package/src/annotations/staleness.ts +41 -0
- package/src/annotations/store.test.ts +247 -0
- package/src/annotations/store.ts +347 -0
- package/src/annotations/svgNodes.test.ts +77 -0
- package/src/annotations/svgNodes.ts +74 -0
- package/src/annotations/toolMap.test.ts +36 -0
- package/src/annotations/toolMap.ts +54 -0
- package/src/annotations/types.ts +229 -0
- package/src/annotations/view.test.ts +37 -0
- package/src/annotations/view.ts +40 -0
- package/src/canvas/AGENTS.md +55 -5
- package/src/canvas/CanvasStack.test.tsx +27 -1
- package/src/canvas/CanvasStack.tsx +29 -4
- package/src/canvas/CanvasStackContext.ts +23 -4
- package/src/canvas/camera.test.ts +78 -0
- package/src/canvas/camera.ts +62 -0
- package/src/canvas/canvasCoords.test.ts +26 -0
- package/src/canvas/canvasCoords.ts +20 -7
- package/src/canvas/index.ts +16 -1
- package/src/canvas/useLayerScheduler.ts +7 -3
- package/src/canvas/usePanZoom.test.ts +36 -5
- package/src/canvas/usePanZoom.ts +20 -15
- package/src/canvas/worldSpec.test.ts +63 -0
- package/src/canvas/worldSpec.ts +48 -0
- package/src/chrome/builtins.test.ts +31 -1
- package/src/chrome/builtins.tsx +63 -19
- package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
- package/src/chrome/regions/PaletteRegion.tsx +4 -0
- package/src/chrome/regions/SidebarRegion.tsx +37 -13
- package/src/chrome/regions/ToolbarRegion.tsx +2 -1
- package/src/chrome/regions/ViewportRegion.tsx +9 -1
- package/src/chrome/regions/regions.test.tsx +6 -1
- package/src/chrome/types.ts +20 -0
- package/src/controls/ControlPanel.test.tsx +38 -0
- package/src/controls/ControlPanel.tsx +50 -3
- package/src/dragdrop/DragDropRuntime.tsx +8 -2
- package/src/dragdrop/dragDrop.test.tsx +29 -2
- package/src/index.test.ts +23 -0
- package/src/index.ts +93 -4
- package/src/instrument/SineWave.smoke.test.tsx +1 -0
- package/src/instrument/types.ts +24 -1
- package/src/lab/Lab.surface.test.tsx +162 -0
- package/src/lab/Lab.tsx +102 -17
- package/src/lab/Lab.undock.test.tsx +85 -0
- package/src/lab/LabFullChrome.stories.tsx +20 -0
- package/src/lab/LabShell.less +11 -0
- package/src/lab/Workspace.less +73 -0
- package/src/lab/Workspace.surface.test.tsx +2 -0
- package/src/lab/Workspace.tsx +97 -8
- package/src/lab/index.ts +1 -1
- package/src/lab/panelHost.ts +37 -0
- package/src/loupe/AGENTS.md +49 -0
- package/src/loupe/CanvasLoupe.tsx +83 -0
- package/src/loupe/DomLoupe.tsx +62 -0
- package/src/loupe/Loupe.less +38 -0
- package/src/loupe/LoupeBubble.tsx +32 -0
- package/src/loupe/TrialLoupe.tsx +93 -0
- package/src/loupe/canvasLens.test.ts +205 -0
- package/src/loupe/canvasLens.ts +141 -0
- package/src/loupe/index.ts +19 -0
- package/src/loupe/types.test.ts +29 -0
- package/src/loupe/types.ts +90 -0
- package/src/loupe/useHostSize.ts +30 -0
- package/src/loupe/useLoupe.test.tsx +197 -0
- package/src/loupe/useLoupe.ts +189 -0
- package/src/passthrough/weasel-ui.ts +3 -0
- package/src/primitives/FloatingPanel.test.tsx +76 -1
- package/src/primitives/FloatingPanel.tsx +19 -3
- package/src/primitives/ScaleIndicator.test.tsx +7 -2
- package/src/primitives/Sidebar.less +18 -0
- package/src/primitives/Toolbar.less +8 -0
- package/src/primitives/Toolbar.tsx +5 -0
- package/src/primitives/useRovingTabIndex.test.ts +21 -0
- package/src/primitives/useRovingTabIndex.ts +26 -17
- package/src/state/document.test.ts +23 -0
- package/src/state/document.ts +14 -2
- package/src/state/index.ts +2 -0
- package/src/state/store.test.ts +24 -0
- package/src/state/store.ts +35 -1
- package/src/state/types.ts +11 -0
- package/src/state/undock.test.ts +40 -0
- package/src/state/undock.ts +39 -0
- package/src/styles.less +2 -0
- package/src/surface/SurfaceContext.ts +4 -0
- package/src/surface/index.ts +8 -3
- package/src/surface/useSurfaceTile.test.tsx +2 -0
- package/src/surface/useSurfaceTile.ts +7 -1
- package/src/surface/useTiledSurface.test.tsx +58 -0
- package/src/surface/useTiledSurface.ts +38 -3
- package/src/trial/Trial.annotations.persist.test.tsx +166 -0
- package/src/trial/Trial.annotations.test.tsx +121 -0
- package/src/trial/Trial.canvas.test.tsx +76 -1
- package/src/trial/Trial.less +19 -0
- package/src/trial/Trial.loupe.test.tsx +128 -0
- package/src/trial/Trial.stories.tsx +1 -0
- package/src/trial/Trial.test.tsx +12 -0
- package/src/trial/Trial.trialId.test.tsx +36 -0
- package/src/trial/Trial.tsx +215 -40
- package/src/trial/TrialChrome.tsx +35 -0
- package/src/trial/UndockedSections.tsx +65 -0
- package/src/trial/trialOps.test.ts +21 -0
- package/src/trial/trialOps.ts +5 -3
- package/dist/_dts/types-BttTIent.d.ts +0 -314
- package/dist/chunk-3XXPU73K.js.map +0 -1
- package/dist/chunk-4TMMVIDM.js.map +0 -1
- package/dist/chunk-6X5RBC6G.js +0 -366
- package/dist/chunk-6X5RBC6G.js.map +0 -1
- package/dist/chunk-BDJXIBRZ.js.map +0 -1
- package/dist/chunk-CPUJ3QXL.js.map +0 -1
- package/dist/chunk-JDROYQM3.js.map +0 -1
- package/dist/chunk-LGKRIUSW.js.map +0 -1
- package/dist/chunk-QCICIHEO.js.map +0 -1
- package/dist/chunk-QRYPXSGP.js.map +0 -1
- package/dist/chunk-RGT4EJIB.js +0 -6961
- package/dist/chunk-RGT4EJIB.js.map +0 -1
- package/dist/chunk-T66HU3HX.js +0 -17
- package/dist/chunk-T66HU3HX.js.map +0 -1
- package/dist/chunk-WTI26YTM.js.map +0 -1
package/src/lab/Workspace.less
CHANGED
|
@@ -9,3 +9,76 @@
|
|
|
9
9
|
height: 100%;
|
|
10
10
|
min-height: 0;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
// An undocked sidebar section. A tile version is placed by the grid like any
|
|
14
|
+
// trial; the floating version sits above the grid in its own layer.
|
|
15
|
+
.lk-panel-tile {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
height: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
border: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
21
|
+
border-radius: var(--wzl-radius-sm);
|
|
22
|
+
background: var(--wzl-surface);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.lk-panel-tile__titlebar {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: var(--wzl-space-sm);
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
padding: 0 var(--wzl-space-sm);
|
|
31
|
+
border-bottom: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.lk-panel-tile__title {
|
|
35
|
+
flex: 1;
|
|
36
|
+
color: var(--wzl-fg-muted);
|
|
37
|
+
font-family: var(--wzl-font-display);
|
|
38
|
+
font-size: var(--wzl-font-size-sm);
|
|
39
|
+
letter-spacing: 0.08em;
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.lk-panel-tile__dock {
|
|
44
|
+
border: 0;
|
|
45
|
+
background: transparent;
|
|
46
|
+
color: var(--wzl-fg-subtle);
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
|
|
49
|
+
&:hover {
|
|
50
|
+
color: var(--wzl-fg);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.lk-panel-tile__body {
|
|
55
|
+
flex: 1;
|
|
56
|
+
min-height: 0;
|
|
57
|
+
overflow: auto;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.lk-workspace-host {
|
|
61
|
+
position: relative;
|
|
62
|
+
width: 100%;
|
|
63
|
+
height: 100%;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.lk-workspace__floating {
|
|
67
|
+
position: absolute;
|
|
68
|
+
inset: 0;
|
|
69
|
+
pointer-events: none;
|
|
70
|
+
|
|
71
|
+
> * {
|
|
72
|
+
pointer-events: auto;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.lk-panel-tile--floating {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: var(--wzl-space-md);
|
|
79
|
+
right: var(--wzl-space-md);
|
|
80
|
+
width: 260px;
|
|
81
|
+
height: auto;
|
|
82
|
+
max-height: 60%;
|
|
83
|
+
box-shadow: var(--wzl-shadow-panel, 0 2px 8px rgb(0 0 0 / 0.3));
|
|
84
|
+
}
|
package/src/lab/Workspace.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
useMemo,
|
|
8
8
|
useRef,
|
|
9
9
|
} from 'react';
|
|
10
|
-
import { asNodeId, createNode, gridStrategy, type NodeId, Store } from 'windease';
|
|
10
|
+
import { asNodeId, createNode, floatingStrategy, gridStrategy, type NodeId, Store } from 'windease';
|
|
11
11
|
import {
|
|
12
12
|
type ChromeMap,
|
|
13
13
|
Container,
|
|
@@ -18,9 +18,12 @@ import {
|
|
|
18
18
|
|
|
19
19
|
import { useSurfaceOptional } from '../surface/useSurfaceTile';
|
|
20
20
|
import { TrialDragContext } from '../trial/TrialDragContext';
|
|
21
|
+
import { usePanelHosts } from './panelHost';
|
|
21
22
|
|
|
22
23
|
const ZONE_ID = asNodeId('lk-workspace');
|
|
23
|
-
const STRATEGIES = { grid: gridStrategy as never };
|
|
24
|
+
const STRATEGIES = { grid: gridStrategy as never, floating: floatingStrategy as never };
|
|
25
|
+
const FLOAT_ZONE_ID = asNodeId('lk-workspace-floating');
|
|
26
|
+
const PANEL_KIND = 'panel';
|
|
24
27
|
const KIND = 'trial';
|
|
25
28
|
|
|
26
29
|
/** A tile's persisted extent, keyed by the id its caller gave it. Grid resizes
|
|
@@ -56,6 +59,11 @@ export interface WorkspaceProps {
|
|
|
56
59
|
onLayoutChange?: (layout: TrialLayout) => void;
|
|
57
60
|
gap?: number;
|
|
58
61
|
padding?: number;
|
|
62
|
+
/** Undocked sidebar panels to render alongside the trials. A `'tile'` panel
|
|
63
|
+
* joins the grid as a peer of the trials; a `'floating'` one goes into the
|
|
64
|
+
* floating zone above it. The body is portalled in by the trial that owns
|
|
65
|
+
* it, so all this renders is the frame and the host. */
|
|
66
|
+
panels?: readonly PanelDescriptor[];
|
|
59
67
|
/**
|
|
60
68
|
* Fixed tiling extent. Omit in an app — the grid measures its own box. Supply
|
|
61
69
|
* it where nothing measures, notably jsdom: at a zero measurement the grid
|
|
@@ -79,10 +87,39 @@ function extentOf(store: Store, id: NodeId): TrialLayout[string] | null {
|
|
|
79
87
|
* Tiles are absolutely positioned at the rects `gridStrategy` computes, not
|
|
80
88
|
* laid out by CSS — `windease/styles.css` (folded into
|
|
81
89
|
* `@weasel-js/labkit/styles.css`) carries the rules that positioning depends on.
|
|
90
|
+
*
|
|
91
|
+
* `panels` are undocked sidebar sections. A `'tile'` panel is registered as a
|
|
92
|
+
* peer of the trials under `PANEL_KIND`, so the grid places and resizes it like
|
|
93
|
+
* one; a `'floating'` panel goes in the overlay above. Either way this renders
|
|
94
|
+
* only the frame and an empty host: the section's content is portalled in by
|
|
95
|
+
* the trial that owns it, which is what keeps a torn-out section inside its
|
|
96
|
+
* trial's React tree instead of rebuilding it as a sibling.
|
|
82
97
|
*/
|
|
98
|
+
|
|
99
|
+
/** The frame an undocked panel gets: a box, and the host element its trial
|
|
100
|
+
* portals into. The title and the dock control come through the portal with
|
|
101
|
+
* the body — the workspace knows a panel's key, not what is in it. */
|
|
102
|
+
function PanelFrame({ panel, floating = false }: { panel: PanelDescriptor; floating?: boolean }) {
|
|
103
|
+
const hosts = usePanelHosts();
|
|
104
|
+
return (
|
|
105
|
+
<div className={floating ? 'lk-panel-tile lk-panel-tile--floating' : 'lk-panel-tile'}>
|
|
106
|
+
<div className="lk-panel-tile__body" ref={(el) => hosts?.set(panel.key, el)} />
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One undocked panel, as the workspace needs to know it. */
|
|
112
|
+
export interface PanelDescriptor {
|
|
113
|
+
/** Stable key; also the portal host key the owning trial writes into. */
|
|
114
|
+
key: string;
|
|
115
|
+
title: string;
|
|
116
|
+
as: 'tile' | 'floating';
|
|
117
|
+
}
|
|
118
|
+
|
|
83
119
|
export function Workspace({
|
|
84
120
|
children,
|
|
85
121
|
ids,
|
|
122
|
+
panels,
|
|
86
123
|
resizable = false,
|
|
87
124
|
reorderable = false,
|
|
88
125
|
onReorder,
|
|
@@ -93,9 +130,24 @@ export function Workspace({
|
|
|
93
130
|
viewport,
|
|
94
131
|
}: WorkspaceProps) {
|
|
95
132
|
const items = Children.toArray(children);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
133
|
+
const tilePanels = useMemo(() => (panels ?? []).filter((p) => p.as === 'tile'), [panels]);
|
|
134
|
+
const floatPanels = useMemo(() => (panels ?? []).filter((p) => p.as === 'floating'), [panels]);
|
|
135
|
+
// Identity has to be stable while the id *contents* are unchanged: the sync
|
|
136
|
+
// effect below keys off it, and a fresh array every render would re-register
|
|
137
|
+
// every tile forever. A ref keyed on the joined ids says that directly, where
|
|
138
|
+
// a `useMemo` on the same key can only say it by suppressing both linters.
|
|
139
|
+
const wantedIds = [
|
|
140
|
+
...items.map((_, i) => ids?.[i] ?? `lk-ws-${i}`),
|
|
141
|
+
...tilePanels.map((p) => `lk-panel-${p.key}`),
|
|
142
|
+
];
|
|
143
|
+
const idKey = wantedIds.join(',');
|
|
144
|
+
const idKeyRef = useRef<string | null>(null);
|
|
145
|
+
const nodeIdsRef = useRef<NodeId[]>([]);
|
|
146
|
+
if (idKeyRef.current !== idKey) {
|
|
147
|
+
idKeyRef.current = idKey;
|
|
148
|
+
nodeIdsRef.current = wantedIds.map(asNodeId);
|
|
149
|
+
}
|
|
150
|
+
const nodeIds = nodeIdsRef.current;
|
|
99
151
|
|
|
100
152
|
// Held in refs rather than depended on: a fresh object each render would
|
|
101
153
|
// re-run the sync effect, and only a newly registered tile reads `layout`.
|
|
@@ -117,6 +169,13 @@ export function Workspace({
|
|
|
117
169
|
container: { strategyId: 'grid', config: { resizable, gap, padding } },
|
|
118
170
|
}),
|
|
119
171
|
);
|
|
172
|
+
store.registerNode(
|
|
173
|
+
createNode({
|
|
174
|
+
kind: 'zone',
|
|
175
|
+
id: FLOAT_ZONE_ID,
|
|
176
|
+
container: { strategyId: 'floating', config: {} },
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
120
179
|
storeRef.current = store;
|
|
121
180
|
}
|
|
122
181
|
const store = storeRef.current;
|
|
@@ -137,7 +196,8 @@ export function Workspace({
|
|
|
137
196
|
}
|
|
138
197
|
for (const id of nodeIds) {
|
|
139
198
|
if (present.has(id)) continue;
|
|
140
|
-
|
|
199
|
+
const kind = String(id).startsWith('lk-panel-') ? PANEL_KIND : KIND;
|
|
200
|
+
store.registerNode(createNode({ kind, id, parentId: ZONE_ID, focus: true }));
|
|
141
201
|
store.showNode(id);
|
|
142
202
|
const saved = layoutRef.current?.[id];
|
|
143
203
|
if (saved) store.patchPlacement(id, saved);
|
|
@@ -171,7 +231,15 @@ export function Workspace({
|
|
|
171
231
|
|
|
172
232
|
const chrome = useMemo<ChromeMap>(() => {
|
|
173
233
|
const byId = new Map<string, ReactNode>(nodeIds.map((id, i) => [id, items[i]]));
|
|
234
|
+
const panelByNode = new Map<string, PanelDescriptor>(
|
|
235
|
+
tilePanels.map((p) => [`lk-panel-${p.key}`, p]),
|
|
236
|
+
);
|
|
174
237
|
return {
|
|
238
|
+
[PANEL_KIND]: ({ node }) => {
|
|
239
|
+
const panel = panelByNode.get(String(node.id));
|
|
240
|
+
if (!panel) return null;
|
|
241
|
+
return <PanelFrame panel={panel} />;
|
|
242
|
+
},
|
|
175
243
|
[KIND]: ({ node }) => {
|
|
176
244
|
const content = byId.get(node.id) ?? null;
|
|
177
245
|
if (!reorderable) return content;
|
|
@@ -183,7 +251,16 @@ export function Workspace({
|
|
|
183
251
|
);
|
|
184
252
|
},
|
|
185
253
|
};
|
|
186
|
-
}, [nodeIds, items, reorderable]);
|
|
254
|
+
}, [nodeIds, items, reorderable, tilePanels]);
|
|
255
|
+
|
|
256
|
+
const floatingLayer =
|
|
257
|
+
floatPanels.length === 0 ? null : (
|
|
258
|
+
<div className="lk-workspace__floating">
|
|
259
|
+
{floatPanels.map((p) => (
|
|
260
|
+
<PanelFrame key={p.key} panel={p} floating />
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
187
264
|
|
|
188
265
|
const grid = (
|
|
189
266
|
<Container
|
|
@@ -199,7 +276,19 @@ export function Workspace({
|
|
|
199
276
|
return (
|
|
200
277
|
<Provider store={store}>
|
|
201
278
|
<StrategyRegistryProvider strategies={STRATEGIES}>
|
|
202
|
-
{
|
|
279
|
+
{floatingLayer ? (
|
|
280
|
+
// The floating layer insets against the workspace, so it needs a
|
|
281
|
+
// positioned box around both. Without one it insets against the page
|
|
282
|
+
// and lands over the lab header.
|
|
283
|
+
<div className="lk-workspace-host">
|
|
284
|
+
{reorderable ? <DragProvider>{grid}</DragProvider> : grid}
|
|
285
|
+
{floatingLayer}
|
|
286
|
+
</div>
|
|
287
|
+
) : reorderable ? (
|
|
288
|
+
<DragProvider>{grid}</DragProvider>
|
|
289
|
+
) : (
|
|
290
|
+
grid
|
|
291
|
+
)}
|
|
203
292
|
</StrategyRegistryProvider>
|
|
204
293
|
</Provider>
|
|
205
294
|
);
|
package/src/lab/index.ts
CHANGED
|
@@ -6,5 +6,5 @@ export type { LabPaletteProps } from './LabPalette';
|
|
|
6
6
|
export { LabPalette } from './LabPalette';
|
|
7
7
|
export type { LabShellProps } from './LabShell';
|
|
8
8
|
export { LabShell } from './LabShell';
|
|
9
|
-
export type { TrialLayout, WorkspaceProps } from './Workspace';
|
|
9
|
+
export type { PanelDescriptor, TrialLayout, WorkspaceProps } from './Workspace';
|
|
10
10
|
export { Workspace } from './Workspace';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Where an undocked panel's DOM lives. The panel's *content* stays owned by
|
|
4
|
+
* its trial — it is React-portalled into the host — so a torn-out section
|
|
5
|
+
* keeps the trial's context, state and subscriptions instead of being rebuilt
|
|
6
|
+
* as a sibling of the trial it came from. */
|
|
7
|
+
export interface PanelHostRegistry {
|
|
8
|
+
set: (key: string, el: HTMLElement | null) => void;
|
|
9
|
+
get: (key: string) => HTMLElement | null;
|
|
10
|
+
subscribe: (fn: () => void) => () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createPanelHostRegistry(): PanelHostRegistry {
|
|
14
|
+
const hosts = new Map<string, HTMLElement>();
|
|
15
|
+
const listeners = new Set<() => void>();
|
|
16
|
+
const notify = () => {
|
|
17
|
+
for (const fn of listeners) fn();
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
set: (key, el) => {
|
|
21
|
+
if (el) hosts.set(key, el);
|
|
22
|
+
else hosts.delete(key);
|
|
23
|
+
notify();
|
|
24
|
+
},
|
|
25
|
+
get: (key) => hosts.get(key) ?? null,
|
|
26
|
+
subscribe: (fn) => {
|
|
27
|
+
listeners.add(fn);
|
|
28
|
+
return () => listeners.delete(fn);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const PanelHostContext = createContext<PanelHostRegistry | null>(null);
|
|
34
|
+
|
|
35
|
+
export function usePanelHosts(): PanelHostRegistry | null {
|
|
36
|
+
return useContext(PanelHostContext);
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Loupe — Agent Guide
|
|
2
|
+
|
|
3
|
+
`src/loupe/` is the `loupe` instrument capability: a magnifier a trial can turn
|
|
4
|
+
on, painted by whichever painter suits the instrument's content.
|
|
5
|
+
|
|
6
|
+
The magnifier itself is not here. `@weasel-js/loupe` holds the model — aim,
|
|
7
|
+
factor, mode, colour, picking — over a `LoupeSurface` it asks five questions.
|
|
8
|
+
This directory binds that model to a labkit trial and draws it.
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | Role |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `types.ts` | `LoupeCapability`, and `resolveLoupe` filling in every default |
|
|
15
|
+
| `useLoupe.ts` | The model over a host element, plus all of the input |
|
|
16
|
+
| `TrialLoupe.tsx` | Picks the painter and mounts the lens |
|
|
17
|
+
| `LoupeBubble.tsx` | The circular clip, positioned on the aim |
|
|
18
|
+
| `CanvasLoupe.tsx` | Painter for a `<CanvasStack>` |
|
|
19
|
+
| `canvasLens.ts` | That painter's geometry and drawing, with no React in it |
|
|
20
|
+
| `DomLoupe.tsx` | Painter for DOM content |
|
|
21
|
+
| `useHostSize.ts` | The host's measured box, for the DOM stage |
|
|
22
|
+
|
|
23
|
+
## Which painter
|
|
24
|
+
|
|
25
|
+
`LoupeCapability.render` decides. Absent, the lens re-runs the instrument's own
|
|
26
|
+
canvas layers through `lensCamera` — sharp at any factor, and `mode: 'pixel'`
|
|
27
|
+
enlarges the presented pixels with smoothing off instead. Present, the
|
|
28
|
+
instrument is handed a camera and draws itself again; a DOM loupe is always
|
|
29
|
+
`vector`, since DOM has no framebuffer to enlarge.
|
|
30
|
+
|
|
31
|
+
The canvas painter needs the stack's own pixels and layers, which is why
|
|
32
|
+
`TrialLoupe` mounts *inside* `<CanvasStack>` for a drawing instrument and reads
|
|
33
|
+
`CanvasStackContext`'s `surface`. A DOM instrument gets a
|
|
34
|
+
`.lk-trial__loupe-host` wrapper from `Trial` and the lens tracks that.
|
|
35
|
+
|
|
36
|
+
## Traps
|
|
37
|
+
|
|
38
|
+
**jsdom cannot see magnification.** Everything assertable there is state — aim
|
|
39
|
+
moved, factor clamped, mode switched, the lens raised and put away. That the
|
|
40
|
+
lens shows the right region is a screenshot.
|
|
41
|
+
|
|
42
|
+
**Do not dispose the model when React unmounts.** `dispose` is one-way, and
|
|
43
|
+
StrictMode mounts / unmounts / mounts every effect — so disposing in the
|
|
44
|
+
cleanup leaves a magnifier that draws but silently ignores every aim. It owns
|
|
45
|
+
no resources; unmounting only reports the lens gone.
|
|
46
|
+
|
|
47
|
+
**The wheel has to be taken from pan/zoom by hand.** `usePanZoom` is a React
|
|
48
|
+
handler on the same element, so the lens listens in the capture phase and stops
|
|
49
|
+
propagation. See the loupe entry in `docs/TODO.md` for what replaces this.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useVisibleRaf } from '@weasel-js/core';
|
|
2
|
+
import type { LoupeMode, LoupePoint } from '@weasel-js/loupe';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import type { CanvasStackSurface } from '../canvas/CanvasStackContext';
|
|
5
|
+
import type { WorldFrame, WorldSpec } from '../canvas/worldSpec';
|
|
6
|
+
import type { ViewTransform } from '../instrument/types';
|
|
7
|
+
import { drawCanvasLens } from './canvasLens';
|
|
8
|
+
|
|
9
|
+
/** Props for `<CanvasLoupe>`. */
|
|
10
|
+
export interface CanvasLoupeProps {
|
|
11
|
+
aim: LoupePoint;
|
|
12
|
+
factor: number;
|
|
13
|
+
mode: LoupeMode;
|
|
14
|
+
diameter: number;
|
|
15
|
+
/** The stack being magnified: its layers, its pixels, and its measured box. */
|
|
16
|
+
surface: CanvasStackSurface;
|
|
17
|
+
view: ViewTransform;
|
|
18
|
+
frame: WorldFrame;
|
|
19
|
+
worldSpec?: WorldSpec;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Paints a lens over a `<CanvasStack>` by re-drawing the stack's layers through
|
|
24
|
+
* a zoomed camera — so the magnified content is as sharp as the original, at
|
|
25
|
+
* any factor. `pixel` mode enlarges what the stack presented instead.
|
|
26
|
+
*/
|
|
27
|
+
export function CanvasLoupe({
|
|
28
|
+
aim,
|
|
29
|
+
factor,
|
|
30
|
+
mode,
|
|
31
|
+
diameter,
|
|
32
|
+
surface,
|
|
33
|
+
view,
|
|
34
|
+
frame,
|
|
35
|
+
worldSpec,
|
|
36
|
+
}: CanvasLoupeProps) {
|
|
37
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
38
|
+
const dpr = surface.size.dpr;
|
|
39
|
+
|
|
40
|
+
// Whatever the stack redraws every frame, the lens has to redraw too — an
|
|
41
|
+
// instrument that animates from its own loop never re-renders this component.
|
|
42
|
+
const args = { aim, factor, mode, diameter, dpr, view, frame, worldSpec, surface };
|
|
43
|
+
const argsRef = useRef(args);
|
|
44
|
+
argsRef.current = args;
|
|
45
|
+
|
|
46
|
+
const loop = useVisibleRaf(
|
|
47
|
+
() => {
|
|
48
|
+
loop.request();
|
|
49
|
+
const canvas = canvasRef.current;
|
|
50
|
+
const ctx = canvas?.getContext('2d');
|
|
51
|
+
const canvases = argsRef.current.surface.canvases.current;
|
|
52
|
+
if (!ctx || !canvases) return;
|
|
53
|
+
const a = argsRef.current;
|
|
54
|
+
drawCanvasLens(ctx, {
|
|
55
|
+
aim: a.aim,
|
|
56
|
+
factor: a.factor,
|
|
57
|
+
diameter: a.diameter,
|
|
58
|
+
dpr: a.dpr,
|
|
59
|
+
mode: a.mode,
|
|
60
|
+
outer: a.view,
|
|
61
|
+
outerFrame: a.frame,
|
|
62
|
+
worldSpec: a.worldSpec,
|
|
63
|
+
layers: a.surface.layers,
|
|
64
|
+
canvases,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
{ target: canvasRef },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
loop.request();
|
|
72
|
+
return () => loop.cancel();
|
|
73
|
+
}, [loop]);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<canvas
|
|
77
|
+
ref={canvasRef}
|
|
78
|
+
className="lk-loupe__canvas"
|
|
79
|
+
width={Math.max(1, Math.round(diameter * dpr))}
|
|
80
|
+
height={Math.max(1, Math.round(diameter * dpr))}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LoupeMode, LoupePoint } from '@weasel-js/loupe';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
import { zoomAt } from '../canvas/camera';
|
|
4
|
+
import type { ViewportSize, WorldFrame } from '../canvas/worldSpec';
|
|
5
|
+
import type { ViewTransform } from '../instrument/types';
|
|
6
|
+
import type { LoupeRenderArgs } from './types';
|
|
7
|
+
|
|
8
|
+
/** Props for `<DomLoupe>`. */
|
|
9
|
+
export interface DomLoupeProps {
|
|
10
|
+
aim: LoupePoint;
|
|
11
|
+
factor: number;
|
|
12
|
+
mode: LoupeMode;
|
|
13
|
+
diameter: number;
|
|
14
|
+
/** The host the instrument draws into, which the stage reproduces. */
|
|
15
|
+
size: ViewportSize;
|
|
16
|
+
/** The trial's own camera. */
|
|
17
|
+
view: ViewTransform;
|
|
18
|
+
frame?: WorldFrame;
|
|
19
|
+
state: unknown;
|
|
20
|
+
config: unknown;
|
|
21
|
+
render: (args: LoupeRenderArgs) => React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Paints a lens over DOM content by asking the instrument to draw itself again
|
|
26
|
+
* at a magnified camera.
|
|
27
|
+
*
|
|
28
|
+
* The stage is a full copy of the host, so the instrument's own layout still
|
|
29
|
+
* holds; the camera is composed about the aimed point, which keeps that point
|
|
30
|
+
* where it already was, and the stage is then slid by `diameter / 2 - aim` to
|
|
31
|
+
* bring it to the middle of the lens.
|
|
32
|
+
*/
|
|
33
|
+
export function DomLoupe({
|
|
34
|
+
aim,
|
|
35
|
+
factor,
|
|
36
|
+
mode,
|
|
37
|
+
diameter,
|
|
38
|
+
size,
|
|
39
|
+
view,
|
|
40
|
+
frame,
|
|
41
|
+
state,
|
|
42
|
+
config,
|
|
43
|
+
render,
|
|
44
|
+
}: DomLoupeProps) {
|
|
45
|
+
const style: CSSProperties = {
|
|
46
|
+
width: `${size.width}px`,
|
|
47
|
+
height: `${size.height}px`,
|
|
48
|
+
transform: `translate(${diameter / 2 - aim.x}px, ${diameter / 2 - aim.y}px)`,
|
|
49
|
+
};
|
|
50
|
+
return (
|
|
51
|
+
<div className="lk-loupe__stage" style={style}>
|
|
52
|
+
{render({
|
|
53
|
+
state,
|
|
54
|
+
config,
|
|
55
|
+
view: zoomAt(view, factor, aim, { frame }),
|
|
56
|
+
factor,
|
|
57
|
+
mode,
|
|
58
|
+
size,
|
|
59
|
+
})}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.lk-loupe {
|
|
2
|
+
position: absolute;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
border-radius: 50%;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
// Nothing under the lens stops working while it is up: the pan, the wheel and
|
|
8
|
+
// any overlay of the instrument's own keep receiving the pointer.
|
|
9
|
+
pointer-events: none;
|
|
10
|
+
border: var(--wzl-border-w) solid var(--wzl-border-raised);
|
|
11
|
+
box-shadow: 0 2px 8px var(--wzl-shadow);
|
|
12
|
+
// A painter's ground shows through wherever it draws nothing, and a vector
|
|
13
|
+
// lens leaves the gaps between marks transparent. This is the trial's own
|
|
14
|
+
// ground, for the part of a DOM lens its stage does not reach.
|
|
15
|
+
background: var(--wzl-surface-raised);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// `.lk-canvas-stack__overlay > *` turns pointer events back on for an
|
|
19
|
+
// instrument's own overlay; the lens is the one child that must stay inert.
|
|
20
|
+
.lk-canvas-stack__overlay > .lk-loupe {
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.lk-loupe__canvas {
|
|
25
|
+
display: block;
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 100%;
|
|
28
|
+
// The stack's own ground, so the lens reads as a window onto it rather than
|
|
29
|
+
// as a panel sitting over it.
|
|
30
|
+
background: var(--wzl-surface-sunken);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.lk-loupe__stage {
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: 0;
|
|
36
|
+
left: 0;
|
|
37
|
+
transform-origin: 0 0;
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LoupePoint } from '@weasel-js/loupe';
|
|
2
|
+
import type { CSSProperties, ReactNode, RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
/** Props for `<LoupeBubble>`. */
|
|
5
|
+
export interface LoupeBubbleProps {
|
|
6
|
+
/** Where the lens is aimed, in its container's own pixels. */
|
|
7
|
+
aim: LoupePoint;
|
|
8
|
+
diameter: number;
|
|
9
|
+
hostRef?: RefObject<HTMLDivElement | null>;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The lens itself: a circle centred on the aimed point, clipping whatever a
|
|
15
|
+
* painter draws into it.
|
|
16
|
+
*
|
|
17
|
+
* It takes no pointer events, so the pan, the wheel and anything underneath
|
|
18
|
+
* keep working while it is up — and it is `aria-hidden`, since it magnifies
|
|
19
|
+
* content already on the page rather than adding any.
|
|
20
|
+
*/
|
|
21
|
+
export function LoupeBubble({ aim, diameter, hostRef, children }: LoupeBubbleProps) {
|
|
22
|
+
const style: CSSProperties = {
|
|
23
|
+
width: `${diameter}px`,
|
|
24
|
+
height: `${diameter}px`,
|
|
25
|
+
transform: `translate(${aim.x - diameter / 2}px, ${aim.y - diameter / 2}px)`,
|
|
26
|
+
};
|
|
27
|
+
return (
|
|
28
|
+
<div ref={hostRef} className="lk-loupe" style={style} aria-hidden="true">
|
|
29
|
+
{children}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type RefObject, useContext, useMemo } from 'react';
|
|
2
|
+
import { CanvasStackContext } from '../canvas/CanvasStackContext';
|
|
3
|
+
import type { WorldSpec } from '../canvas/worldSpec';
|
|
4
|
+
import type { ViewTransform } from '../instrument/types';
|
|
5
|
+
import { CanvasLoupe } from './CanvasLoupe';
|
|
6
|
+
import { sampleStack } from './canvasLens';
|
|
7
|
+
import { DomLoupe } from './DomLoupe';
|
|
8
|
+
import { LoupeBubble } from './LoupeBubble';
|
|
9
|
+
import type { ResolvedLoupe } from './types';
|
|
10
|
+
import { useHostSize } from './useHostSize';
|
|
11
|
+
import { useLoupe } from './useLoupe';
|
|
12
|
+
|
|
13
|
+
/** Props for `<TrialLoupe>`. */
|
|
14
|
+
export interface TrialLoupeProps {
|
|
15
|
+
capability: ResolvedLoupe;
|
|
16
|
+
/** Whether the trial's loupe is turned on. Hold-to-peek shows it anyway. */
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
state: unknown;
|
|
19
|
+
config: unknown;
|
|
20
|
+
/** The trial's own camera. */
|
|
21
|
+
view: ViewTransform;
|
|
22
|
+
worldSpec?: WorldSpec;
|
|
23
|
+
/** The element the lens tracks. A loupe mounted inside a `<CanvasStack>`
|
|
24
|
+
* takes the stack's own element instead, and this goes unused. */
|
|
25
|
+
hostRef?: RefObject<HTMLElement | null>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Stable stand-in, so a loupe with nothing to track does not re-bind its
|
|
29
|
+
* listeners on every render. */
|
|
30
|
+
const NO_HOST: RefObject<HTMLElement | null> = { current: null };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A trial's loupe, painted by whichever painter suits its content: the canvas
|
|
34
|
+
* stack it is mounted inside, or the instrument's own `render` at a magnified
|
|
35
|
+
* camera.
|
|
36
|
+
*/
|
|
37
|
+
export function TrialLoupe({
|
|
38
|
+
capability,
|
|
39
|
+
enabled,
|
|
40
|
+
state,
|
|
41
|
+
config,
|
|
42
|
+
view,
|
|
43
|
+
worldSpec,
|
|
44
|
+
hostRef,
|
|
45
|
+
}: TrialLoupeProps) {
|
|
46
|
+
const stack = useContext(CanvasStackContext);
|
|
47
|
+
const surface = capability.render ? undefined : stack?.surface;
|
|
48
|
+
const host = surface?.element ?? hostRef ?? NO_HOST;
|
|
49
|
+
|
|
50
|
+
const sample = useMemo(() => {
|
|
51
|
+
if (!surface) return undefined;
|
|
52
|
+
return (p: { x: number; y: number }): string | null => {
|
|
53
|
+
const canvases = surface.canvases.current;
|
|
54
|
+
return canvases ? sampleStack(surface.layers, canvases, p, surface.size.dpr) : null;
|
|
55
|
+
};
|
|
56
|
+
}, [surface]);
|
|
57
|
+
|
|
58
|
+
const loupe = useLoupe({ capability, hostRef: host, enabled, sample });
|
|
59
|
+
const measured = useHostSize(host);
|
|
60
|
+
const size = surface?.size ?? measured;
|
|
61
|
+
|
|
62
|
+
if (!loupe.visible) return null;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<LoupeBubble aim={loupe.aim} diameter={capability.diameter}>
|
|
66
|
+
{capability.render ? (
|
|
67
|
+
<DomLoupe
|
|
68
|
+
aim={loupe.aim}
|
|
69
|
+
factor={loupe.factor}
|
|
70
|
+
mode={loupe.mode}
|
|
71
|
+
diameter={capability.diameter}
|
|
72
|
+
size={size}
|
|
73
|
+
view={view}
|
|
74
|
+
frame={stack?.frame}
|
|
75
|
+
state={state}
|
|
76
|
+
config={config}
|
|
77
|
+
render={capability.render}
|
|
78
|
+
/>
|
|
79
|
+
) : surface && stack ? (
|
|
80
|
+
<CanvasLoupe
|
|
81
|
+
aim={loupe.aim}
|
|
82
|
+
factor={loupe.factor}
|
|
83
|
+
mode={loupe.mode}
|
|
84
|
+
diameter={capability.diameter}
|
|
85
|
+
surface={surface}
|
|
86
|
+
view={stack.view}
|
|
87
|
+
frame={stack.frame}
|
|
88
|
+
worldSpec={worldSpec}
|
|
89
|
+
/>
|
|
90
|
+
) : null}
|
|
91
|
+
</LoupeBubble>
|
|
92
|
+
);
|
|
93
|
+
}
|