insomni-plot 0.1.0-alpha.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/LICENSE.md +674 -0
- package/README.md +81 -0
- package/dist/core.d.mts +340 -0
- package/dist/core.mjs +1047 -0
- package/dist/index.d.mts +3426 -0
- package/dist/index.mjs +12762 -0
- package/dist/interactions-DEFL_F4E.mjs +5395 -0
- package/dist/range-presets-CzECsu3V.d.mts +1523 -0
- package/package.json +34 -0
- package/src/annotations.d.ts +121 -0
- package/src/annotations.ts +438 -0
- package/src/axis.d.ts +184 -0
- package/src/axis.test.ts +131 -0
- package/src/axis.ts +765 -0
- package/src/colorbar.d.ts +69 -0
- package/src/colorbar.ts +294 -0
- package/src/colors.d.ts +57 -0
- package/src/colors.test.ts +28 -0
- package/src/colors.ts +486 -0
- package/src/core.ts +299 -0
- package/src/format.d.ts +54 -0
- package/src/format.ts +138 -0
- package/src/grammar/accessibility.d.ts +147 -0
- package/src/grammar/accessibility.test.ts +199 -0
- package/src/grammar/accessibility.ts +443 -0
- package/src/grammar/aes.d.ts +35 -0
- package/src/grammar/aes.test.ts +75 -0
- package/src/grammar/aes.ts +120 -0
- package/src/grammar/annotations.d.ts +86 -0
- package/src/grammar/annotations.test.ts +68 -0
- package/src/grammar/annotations.ts +336 -0
- package/src/grammar/attach-brush.d.ts +44 -0
- package/src/grammar/attach-brush.test.ts +214 -0
- package/src/grammar/attach-brush.ts +111 -0
- package/src/grammar/attach-presets.d.ts +33 -0
- package/src/grammar/attach-presets.test.ts +106 -0
- package/src/grammar/attach-presets.ts +215 -0
- package/src/grammar/chart.d.ts +952 -0
- package/src/grammar/chart.test.ts +118 -0
- package/src/grammar/chart.ts +1172 -0
- package/src/grammar/color-utils.d.ts +29 -0
- package/src/grammar/color-utils.test.ts +53 -0
- package/src/grammar/color-utils.ts +66 -0
- package/src/grammar/constants.d.ts +45 -0
- package/src/grammar/constants.ts +61 -0
- package/src/grammar/coord.d.ts +183 -0
- package/src/grammar/coord.test.ts +355 -0
- package/src/grammar/coord.ts +619 -0
- package/src/grammar/data/pivot.d.ts +57 -0
- package/src/grammar/data/pivot.ts +107 -0
- package/src/grammar/emphasis-driver.d.ts +69 -0
- package/src/grammar/emphasis-driver.test.ts +199 -0
- package/src/grammar/emphasis-driver.ts +205 -0
- package/src/grammar/equality.d.ts +3 -0
- package/src/grammar/equality.ts +40 -0
- package/src/grammar/facet.d.ts +63 -0
- package/src/grammar/facet.test.ts +60 -0
- package/src/grammar/facet.ts +175 -0
- package/src/grammar/geoms/_categorical.d.ts +94 -0
- package/src/grammar/geoms/_categorical.ts +0 -0
- package/src/grammar/geoms/_distribution.d.ts +52 -0
- package/src/grammar/geoms/_distribution.ts +125 -0
- package/src/grammar/geoms/_mark.d.ts +69 -0
- package/src/grammar/geoms/_mark.ts +136 -0
- package/src/grammar/geoms/_shape.d.ts +41 -0
- package/src/grammar/geoms/_shape.ts +74 -0
- package/src/grammar/geoms/aggregate.d.ts +95 -0
- package/src/grammar/geoms/aggregate.test.ts +554 -0
- package/src/grammar/geoms/aggregate.ts +840 -0
- package/src/grammar/geoms/area.d.ts +32 -0
- package/src/grammar/geoms/area.test.ts +165 -0
- package/src/grammar/geoms/area.ts +578 -0
- package/src/grammar/geoms/band.d.ts +27 -0
- package/src/grammar/geoms/band.test.ts +57 -0
- package/src/grammar/geoms/band.ts +126 -0
- package/src/grammar/geoms/bar.d.ts +56 -0
- package/src/grammar/geoms/bar.test.ts +367 -0
- package/src/grammar/geoms/bar.ts +1054 -0
- package/src/grammar/geoms/boxplot.d.ts +129 -0
- package/src/grammar/geoms/boxplot.test.ts +299 -0
- package/src/grammar/geoms/boxplot.ts +834 -0
- package/src/grammar/geoms/connected-scatter.d.ts +27 -0
- package/src/grammar/geoms/connected-scatter.test.ts +157 -0
- package/src/grammar/geoms/connected-scatter.ts +63 -0
- package/src/grammar/geoms/emphasis.d.ts +76 -0
- package/src/grammar/geoms/emphasis.test.ts +135 -0
- package/src/grammar/geoms/emphasis.ts +162 -0
- package/src/grammar/geoms/histogram.d.ts +75 -0
- package/src/grammar/geoms/histogram.test.ts +262 -0
- package/src/grammar/geoms/histogram.ts +740 -0
- package/src/grammar/geoms/index.d.ts +20 -0
- package/src/grammar/geoms/index.ts +77 -0
- package/src/grammar/geoms/interval.d.ts +31 -0
- package/src/grammar/geoms/interval.test.ts +154 -0
- package/src/grammar/geoms/interval.ts +342 -0
- package/src/grammar/geoms/line.d.ts +38 -0
- package/src/grammar/geoms/line.test.ts +247 -0
- package/src/grammar/geoms/line.ts +659 -0
- package/src/grammar/geoms/point.d.ts +57 -0
- package/src/grammar/geoms/point.test.ts +163 -0
- package/src/grammar/geoms/point.ts +545 -0
- package/src/grammar/geoms/polar.test.ts +216 -0
- package/src/grammar/geoms/ribbon.d.ts +21 -0
- package/src/grammar/geoms/ribbon.test.ts +170 -0
- package/src/grammar/geoms/ribbon.ts +87 -0
- package/src/grammar/geoms/ridgeline.d.ts +89 -0
- package/src/grammar/geoms/ridgeline.test.ts +247 -0
- package/src/grammar/geoms/ridgeline.ts +1164 -0
- package/src/grammar/geoms/rolling.d.ts +43 -0
- package/src/grammar/geoms/rolling.test.ts +217 -0
- package/src/grammar/geoms/rolling.ts +387 -0
- package/src/grammar/geoms/rug.d.ts +28 -0
- package/src/grammar/geoms/rug.test.ts +126 -0
- package/src/grammar/geoms/rug.ts +214 -0
- package/src/grammar/geoms/rule.d.ts +23 -0
- package/src/grammar/geoms/rule.test.ts +69 -0
- package/src/grammar/geoms/rule.ts +212 -0
- package/src/grammar/geoms/smooth.d.ts +54 -0
- package/src/grammar/geoms/smooth.test.ts +78 -0
- package/src/grammar/geoms/smooth.ts +337 -0
- package/src/grammar/geoms/text.d.ts +29 -0
- package/src/grammar/geoms/text.test.ts +64 -0
- package/src/grammar/geoms/text.ts +234 -0
- package/src/grammar/geoms/tile.d.ts +61 -0
- package/src/grammar/geoms/tile.test.ts +157 -0
- package/src/grammar/geoms/tile.ts +621 -0
- package/src/grammar/geoms/types.d.ts +319 -0
- package/src/grammar/geoms/types.ts +362 -0
- package/src/grammar/geoms/violin.d.ts +85 -0
- package/src/grammar/geoms/violin.test.ts +187 -0
- package/src/grammar/geoms/violin.ts +672 -0
- package/src/grammar/index.d.ts +22 -0
- package/src/grammar/index.ts +269 -0
- package/src/grammar/interactions/_disposable.d.ts +5 -0
- package/src/grammar/interactions/_disposable.ts +23 -0
- package/src/grammar/interactions/_z.d.ts +4 -0
- package/src/grammar/interactions/_z.ts +16 -0
- package/src/grammar/interactions/brush-selection.test.ts +262 -0
- package/src/grammar/interactions/brush.d.ts +63 -0
- package/src/grammar/interactions/brush.test.ts +483 -0
- package/src/grammar/interactions/brush.ts +452 -0
- package/src/grammar/interactions/crosshair.d.ts +19 -0
- package/src/grammar/interactions/crosshair.test.ts +127 -0
- package/src/grammar/interactions/crosshair.ts +76 -0
- package/src/grammar/interactions/hit-layer.d.ts +64 -0
- package/src/grammar/interactions/hit-layer.ts +246 -0
- package/src/grammar/interactions/legend.d.ts +19 -0
- package/src/grammar/interactions/legend.ts +101 -0
- package/src/grammar/interactions/menu.d.ts +93 -0
- package/src/grammar/interactions/menu.test.ts +373 -0
- package/src/grammar/interactions/menu.ts +342 -0
- package/src/grammar/interactions/selection.d.ts +25 -0
- package/src/grammar/interactions/selection.test.ts +289 -0
- package/src/grammar/interactions/selection.ts +142 -0
- package/src/grammar/interactions/series-readout.d.ts +91 -0
- package/src/grammar/interactions/series-readout.test.ts +668 -0
- package/src/grammar/interactions/series-readout.ts +422 -0
- package/src/grammar/interactions/series-snap.d.ts +70 -0
- package/src/grammar/interactions/series-snap.test.ts +214 -0
- package/src/grammar/interactions/series-snap.ts +218 -0
- package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
- package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
- package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
- package/src/grammar/interactions/tooltip.d.ts +140 -0
- package/src/grammar/interactions/tooltip.test.ts +406 -0
- package/src/grammar/interactions/tooltip.ts +622 -0
- package/src/grammar/interactions/transitions.d.ts +34 -0
- package/src/grammar/interactions/transitions.test.ts +172 -0
- package/src/grammar/interactions/transitions.ts +160 -0
- package/src/grammar/layout.d.ts +68 -0
- package/src/grammar/layout.ts +186 -0
- package/src/grammar/legend-merge.test.ts +332 -0
- package/src/grammar/mount.d.ts +78 -0
- package/src/grammar/mount.test.ts +479 -0
- package/src/grammar/mount.ts +2112 -0
- package/src/grammar/palettes.d.ts +54 -0
- package/src/grammar/palettes.test.ts +80 -0
- package/src/grammar/palettes.ts +167 -0
- package/src/grammar/pan-zoom.test.ts +398 -0
- package/src/grammar/phylo.d.ts +65 -0
- package/src/grammar/phylo.test.ts +59 -0
- package/src/grammar/phylo.ts +112 -0
- package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
- package/src/grammar/pipeline.d.ts +158 -0
- package/src/grammar/pipeline.test.ts +463 -0
- package/src/grammar/pipeline.ts +1233 -0
- package/src/grammar/profiling.d.ts +8 -0
- package/src/grammar/profiling.ts +24 -0
- package/src/grammar/scales.d.ts +188 -0
- package/src/grammar/scales.test.ts +181 -0
- package/src/grammar/scales.ts +800 -0
- package/src/grammar/svg.d.ts +3 -0
- package/src/grammar/svg.ts +39 -0
- package/src/grammar/theme.d.ts +261 -0
- package/src/grammar/theme.test.ts +105 -0
- package/src/grammar/theme.ts +490 -0
- package/src/heatmap/cpu.ts +109 -0
- package/src/heatmap/gpu.ts +565 -0
- package/src/heatmap/types.ts +177 -0
- package/src/heatmap.browser.test.ts +308 -0
- package/src/heatmap.test.ts +320 -0
- package/src/heatmap.ts +123 -0
- package/src/index.d.ts +1 -0
- package/src/index.ts +8 -0
- package/src/interactions.d.ts +48 -0
- package/src/interactions.test.ts +226 -0
- package/src/interactions.ts +394 -0
- package/src/layout/box.d.ts +48 -0
- package/src/layout/box.test.ts +107 -0
- package/src/layout/box.ts +143 -0
- package/src/legend.d.ts +115 -0
- package/src/legend.ts +422 -0
- package/src/marks/curve.d.ts +43 -0
- package/src/marks/curve.ts +244 -0
- package/src/marks/stack.d.ts +53 -0
- package/src/marks/stack.ts +184 -0
- package/src/marks.d.ts +273 -0
- package/src/marks.test.ts +541 -0
- package/src/marks.ts +1292 -0
- package/src/navigator.test.ts +174 -0
- package/src/navigator.ts +393 -0
- package/src/range-presets.d.ts +113 -0
- package/src/range-presets.test.ts +345 -0
- package/src/range-presets.ts +349 -0
- package/src/scales.d.ts +98 -0
- package/src/scales.test.ts +103 -0
- package/src/scales.ts +695 -0
- package/src/stats/index.d.ts +200 -0
- package/src/stats/index.test.ts +349 -0
- package/src/stats/index.ts +740 -0
- package/src/stats/regression.d.ts +38 -0
- package/src/stats/regression.test.ts +56 -0
- package/src/stats/regression.ts +396 -0
- package/src/stats/rolling-window.d.ts +55 -0
- package/src/stats/rolling-window.test.ts +237 -0
- package/src/stats/rolling-window.ts +256 -0
- package/src/test-setup.ts +19 -0
- package/src/viewport/axis-state.d.ts +72 -0
- package/src/viewport/axis-state.ts +476 -0
- package/src/viewport.d.ts +170 -0
- package/src/viewport.test.ts +363 -0
- package/src/viewport.ts +510 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Grammar-level brush wiring
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Owns a single drag-capable InteractionNode covering the plot frame. Drag
|
|
5
|
+
// gestures route through the generic `Brush` primitive; on each change we
|
|
6
|
+
// project the rect against the current hit-test positions and emit the
|
|
7
|
+
// matching `HoveredHit[]` via `onChange`. The grammar mount mirrors that
|
|
8
|
+
// payload onto `MountedPlot.brushed`, parallel to (and independent of) the
|
|
9
|
+
// click `selected` signal.
|
|
10
|
+
//
|
|
11
|
+
// Background tap clears the brush — same dismissal pattern as click selection.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type Brush,
|
|
15
|
+
type BrushAxis,
|
|
16
|
+
type BrushEdge,
|
|
17
|
+
type BrushRect,
|
|
18
|
+
type Color,
|
|
19
|
+
createBrush,
|
|
20
|
+
type CrosshairBounds,
|
|
21
|
+
isCoarsePointer,
|
|
22
|
+
type InteractionManager,
|
|
23
|
+
type InteractionNode,
|
|
24
|
+
type Invalidator,
|
|
25
|
+
type Layer,
|
|
26
|
+
withAlpha,
|
|
27
|
+
} from "insomni";
|
|
28
|
+
|
|
29
|
+
import type { CompiledHitTest, HoveredHit } from "../geoms/types.ts";
|
|
30
|
+
import type { Theme } from "../theme.ts";
|
|
31
|
+
import { createDisposable } from "./_disposable.ts";
|
|
32
|
+
import { Z_BRUSH_DRAG, Z_BRUSH_HANDLE_CORNER, Z_BRUSH_HANDLE_EDGE } from "./_z.ts";
|
|
33
|
+
|
|
34
|
+
export interface GrammarBrushConfig {
|
|
35
|
+
/** Which axes the brush spans. Default `"xy"`. */
|
|
36
|
+
axis?: BrushAxis;
|
|
37
|
+
/** Translucent fill color. Default theme.axis.color × 0.12 alpha. */
|
|
38
|
+
fillColor?: Color;
|
|
39
|
+
/** Outline color. Default theme.axis.color × 0.6 alpha. */
|
|
40
|
+
strokeColor?: Color;
|
|
41
|
+
/** Outline width in CSS px. Default 1. */
|
|
42
|
+
strokeWidth?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Enable edge/corner resize handles on the committed rect. Default `true`.
|
|
45
|
+
* Disable to lock a brush rect to its initial drag.
|
|
46
|
+
*/
|
|
47
|
+
handles?: boolean;
|
|
48
|
+
/** Hit-test extent of each handle in CSS px. Default 8. */
|
|
49
|
+
handleSize?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Enable drag-to-move on the committed rect interior. Default `true`. The
|
|
52
|
+
* rect's size is held constant during the move; off-axis movement is locked
|
|
53
|
+
* for `axis: "x"` / `"y"` brushes. Disable to lock a brush rect in place.
|
|
54
|
+
*/
|
|
55
|
+
translate?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Snap brush rect edges to the nearest hit-test position along the chosen
|
|
58
|
+
* axis. `true` snaps on the brush's active `axis`; pass `"x"` / `"y"` /
|
|
59
|
+
* `"xy"` to override. Default `false`. Useful for axis-locked range brushes
|
|
60
|
+
* over discrete x-ticks. The snap is computed against the live hit-test
|
|
61
|
+
* positions (refreshed on every `sync()`), so it follows the data.
|
|
62
|
+
*/
|
|
63
|
+
snap?: boolean | BrushAxis;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface GrammarBrushOptions extends GrammarBrushConfig {
|
|
67
|
+
/**
|
|
68
|
+
* Fired when the brushed set changes. Receives one `HoveredHit` per data
|
|
69
|
+
* row whose hit-test position falls within the brush rect.
|
|
70
|
+
*/
|
|
71
|
+
onChange?(hits: HoveredHit[]): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface GrammarBrushDeps {
|
|
75
|
+
manager: InteractionManager;
|
|
76
|
+
/** Plot-frame bounds in element-local CSS px. The brush only fires inside this rect. */
|
|
77
|
+
bounds: () => CrosshairBounds;
|
|
78
|
+
hudLayer: () => Layer;
|
|
79
|
+
theme: () => Theme;
|
|
80
|
+
invalidator: Invalidator;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface GrammarBrush {
|
|
84
|
+
/** Replace the active hit-test set after each pipeline run. */
|
|
85
|
+
sync<T>(hits: readonly CompiledHitTest<T>[]): void;
|
|
86
|
+
/** Read-only snapshot of the current brushed hits. */
|
|
87
|
+
current(): HoveredHit[];
|
|
88
|
+
/** Current brush rect, or null when idle. */
|
|
89
|
+
rect(): BrushRect | null;
|
|
90
|
+
/** Imperatively clear the brush. */
|
|
91
|
+
clear(): void;
|
|
92
|
+
/** Draw the brush overlay into the hud layer. */
|
|
93
|
+
draw(): void;
|
|
94
|
+
dispose(): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function pointInRect(px: number, py: number, r: BrushRect): boolean {
|
|
98
|
+
return px >= r.x && px <= r.x + r.width && py >= r.y && py <= r.y + r.height;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createGrammarBrush(
|
|
102
|
+
deps: GrammarBrushDeps,
|
|
103
|
+
opts: GrammarBrushOptions = {},
|
|
104
|
+
): GrammarBrush {
|
|
105
|
+
const themeAxis = deps.theme().axis.color;
|
|
106
|
+
const defaultFill: Color = withAlpha(themeAxis, themeAxis.a * 0.12);
|
|
107
|
+
const defaultStroke: Color = withAlpha(themeAxis, themeAxis.a * 0.6);
|
|
108
|
+
|
|
109
|
+
// Cached hit-test set; refreshed on every sync.
|
|
110
|
+
let hits: readonly CompiledHitTest<unknown>[] = [];
|
|
111
|
+
let brushed: HoveredHit[] = [];
|
|
112
|
+
|
|
113
|
+
// Snap config. `true` = snap on the brush's active axis; explicit axis
|
|
114
|
+
// overrides. Snap reads `hits` so it tracks the latest hit-test positions.
|
|
115
|
+
const brushAxis: BrushAxis = opts.axis ?? "xy";
|
|
116
|
+
const snapMode: BrushAxis | false =
|
|
117
|
+
opts.snap === undefined || opts.snap === false
|
|
118
|
+
? false
|
|
119
|
+
: opts.snap === true
|
|
120
|
+
? brushAxis
|
|
121
|
+
: opts.snap;
|
|
122
|
+
const snapX = snapMode === "x" || snapMode === "xy";
|
|
123
|
+
const snapY = snapMode === "y" || snapMode === "xy";
|
|
124
|
+
|
|
125
|
+
function nearestHitValue(v: number, axis: "x" | "y"): number {
|
|
126
|
+
let best = v;
|
|
127
|
+
let bestDist = Infinity;
|
|
128
|
+
for (const hit of hits) {
|
|
129
|
+
const positions = hit.positions;
|
|
130
|
+
const n = positions.length / 2;
|
|
131
|
+
const offset = axis === "x" ? 0 : 1;
|
|
132
|
+
for (let i = 0; i < n; i++) {
|
|
133
|
+
const p = positions[i * 2 + offset]!;
|
|
134
|
+
if (!Number.isFinite(p)) continue;
|
|
135
|
+
const d = Math.abs(p - v);
|
|
136
|
+
if (d < bestDist) {
|
|
137
|
+
bestDist = d;
|
|
138
|
+
best = p;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return best;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function snapRect(rect: BrushRect): BrushRect {
|
|
146
|
+
if (!snapX && !snapY) return rect;
|
|
147
|
+
let { x, y, width, height } = rect;
|
|
148
|
+
if (snapX) {
|
|
149
|
+
const x1 = nearestHitValue(x, "x");
|
|
150
|
+
const x2 = nearestHitValue(x + width, "x");
|
|
151
|
+
x = Math.min(x1, x2);
|
|
152
|
+
width = Math.abs(x2 - x1);
|
|
153
|
+
}
|
|
154
|
+
if (snapY) {
|
|
155
|
+
const y1 = nearestHitValue(y, "y");
|
|
156
|
+
const y2 = nearestHitValue(y + height, "y");
|
|
157
|
+
y = Math.min(y1, y2);
|
|
158
|
+
height = Math.abs(y2 - y1);
|
|
159
|
+
}
|
|
160
|
+
return { x, y, width, height };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const recomputeBrushed = (rect: BrushRect | null): boolean => {
|
|
164
|
+
const next: HoveredHit[] = [];
|
|
165
|
+
if (rect && (rect.width > 0 || rect.height > 0)) {
|
|
166
|
+
for (const hit of hits) {
|
|
167
|
+
const positions = hit.positions;
|
|
168
|
+
const indices = hit.dataIndex;
|
|
169
|
+
const n = indices.length;
|
|
170
|
+
for (let i = 0; i < n; i++) {
|
|
171
|
+
const px = positions[i * 2]!;
|
|
172
|
+
const py = positions[i * 2 + 1]!;
|
|
173
|
+
if (!pointInRect(px, py, rect)) continue;
|
|
174
|
+
next.push({
|
|
175
|
+
geomKind: hit.geomKind,
|
|
176
|
+
dataIndex: indices[i]!,
|
|
177
|
+
seriesKey: hit.seriesKey?.[i],
|
|
178
|
+
data: hit.data as readonly unknown[],
|
|
179
|
+
x: px,
|
|
180
|
+
y: py,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const changed =
|
|
186
|
+
next.length !== brushed.length ||
|
|
187
|
+
next.some(
|
|
188
|
+
(h, i) =>
|
|
189
|
+
h.dataIndex !== brushed[i]?.dataIndex ||
|
|
190
|
+
h.geomKind !== brushed[i]?.geomKind ||
|
|
191
|
+
h.seriesKey !== brushed[i]?.seriesKey,
|
|
192
|
+
);
|
|
193
|
+
brushed = next;
|
|
194
|
+
return changed;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const brush: Brush = createBrush({
|
|
198
|
+
axis: brushAxis,
|
|
199
|
+
bounds: deps.bounds,
|
|
200
|
+
style: {
|
|
201
|
+
fill: opts.fillColor ?? defaultFill,
|
|
202
|
+
stroke: opts.strokeColor ?? defaultStroke,
|
|
203
|
+
strokeWidth: opts.strokeWidth ?? 1,
|
|
204
|
+
},
|
|
205
|
+
invalidator: deps.invalidator,
|
|
206
|
+
snap: snapX || snapY ? snapRect : undefined,
|
|
207
|
+
onChange: (rect) => {
|
|
208
|
+
if (recomputeBrushed(rect)) opts.onChange?.(brushed.slice());
|
|
209
|
+
},
|
|
210
|
+
onEnd: (rect) => {
|
|
211
|
+
// Final emission so callers see the post-clamp committed rect even when
|
|
212
|
+
// the rect didn't change since the last onChange.
|
|
213
|
+
recomputeBrushed(rect);
|
|
214
|
+
opts.onChange?.(brushed.slice());
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Drag node sits inside the plot frame. We deliberately don't register an
|
|
219
|
+
// onPress here — clicks fall through to background-tap, which we use to
|
|
220
|
+
// clear the brush below.
|
|
221
|
+
const node: InteractionNode = deps.manager.add({
|
|
222
|
+
space: "ui",
|
|
223
|
+
bounds: deps.bounds,
|
|
224
|
+
// Lower than tooltip (1000+) and selection (900+) PointCloud nodes so
|
|
225
|
+
// those still claim hover/press at the same spot. Drag routes
|
|
226
|
+
// independently anyway, but a low z keeps insertion-order ties sane.
|
|
227
|
+
zIndex: Z_BRUSH_DRAG,
|
|
228
|
+
cursor: "crosshair",
|
|
229
|
+
onDragStart: (info) => {
|
|
230
|
+
brush.begin(info.x, info.y);
|
|
231
|
+
},
|
|
232
|
+
onDragMove: (info) => {
|
|
233
|
+
brush.update(info.x, info.y);
|
|
234
|
+
},
|
|
235
|
+
onDragEnd: (info) => {
|
|
236
|
+
brush.update(info.x, info.y);
|
|
237
|
+
brush.end();
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// -----------------------------------------------------------------------
|
|
242
|
+
// Resize handles — 4 edges + 4 corners overlaid on the committed rect.
|
|
243
|
+
// Each handle is a fixed-size hit zone tracking its slot on the live rect;
|
|
244
|
+
// when there's no rect, bounds collapse to zero so the node never hits.
|
|
245
|
+
// -----------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
const handlesEnabled = opts.handles !== false;
|
|
248
|
+
const defaultHandle = isCoarsePointer() ? 24 : 8;
|
|
249
|
+
const handleSize = Math.max(2, opts.handleSize ?? defaultHandle);
|
|
250
|
+
const allowVertical = brushAxis === "xy" || brushAxis === "y";
|
|
251
|
+
const allowHorizontal = brushAxis === "xy" || brushAxis === "x";
|
|
252
|
+
// Filter to edges that match the brush axis. For axis "x" only east/west
|
|
253
|
+
// make sense; the off-axis is locked to the bounds so vertical resize is
|
|
254
|
+
// a no-op and the cursor would be misleading.
|
|
255
|
+
const ALL_EDGES: BrushEdge[] = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
|
|
256
|
+
const handleEdges: BrushEdge[] = ALL_EDGES.filter((e) => {
|
|
257
|
+
if (!allowVertical && (e === "n" || e === "s")) return false;
|
|
258
|
+
if (!allowHorizontal && (e === "e" || e === "w")) return false;
|
|
259
|
+
if (brushAxis === "x" && (e === "ne" || e === "nw" || e === "se" || e === "sw")) return false;
|
|
260
|
+
if (brushAxis === "y" && (e === "ne" || e === "nw" || e === "se" || e === "sw")) return false;
|
|
261
|
+
return true;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Per-edge cursor + bbox derivation. Corner bboxes are h×h squares centered
|
|
265
|
+
// on the corner; edge bboxes hug the side and shrink inward at the corners
|
|
266
|
+
// so they don't overlap the corner squares.
|
|
267
|
+
function cursorFor(edge: BrushEdge): string {
|
|
268
|
+
switch (edge) {
|
|
269
|
+
case "n":
|
|
270
|
+
case "s":
|
|
271
|
+
return "ns-resize";
|
|
272
|
+
case "e":
|
|
273
|
+
case "w":
|
|
274
|
+
return "ew-resize";
|
|
275
|
+
case "ne":
|
|
276
|
+
case "sw":
|
|
277
|
+
return "nesw-resize";
|
|
278
|
+
case "nw":
|
|
279
|
+
case "se":
|
|
280
|
+
return "nwse-resize";
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function handleBounds(edge: BrushEdge): BrushRect {
|
|
285
|
+
const r = brush.rect;
|
|
286
|
+
if (r === null) return { x: 0, y: 0, width: 0, height: 0 };
|
|
287
|
+
const h = handleSize;
|
|
288
|
+
const half = h / 2;
|
|
289
|
+
const x1 = r.x;
|
|
290
|
+
const y1 = r.y;
|
|
291
|
+
const x2 = r.x + r.width;
|
|
292
|
+
const y2 = r.y + r.height;
|
|
293
|
+
switch (edge) {
|
|
294
|
+
case "nw":
|
|
295
|
+
return { x: x1 - half, y: y1 - half, width: h, height: h };
|
|
296
|
+
case "ne":
|
|
297
|
+
return { x: x2 - half, y: y1 - half, width: h, height: h };
|
|
298
|
+
case "sw":
|
|
299
|
+
return { x: x1 - half, y: y2 - half, width: h, height: h };
|
|
300
|
+
case "se":
|
|
301
|
+
return { x: x2 - half, y: y2 - half, width: h, height: h };
|
|
302
|
+
case "n":
|
|
303
|
+
return {
|
|
304
|
+
x: x1 + half,
|
|
305
|
+
y: y1 - half,
|
|
306
|
+
width: Math.max(0, r.width - h),
|
|
307
|
+
height: h,
|
|
308
|
+
};
|
|
309
|
+
case "s":
|
|
310
|
+
return {
|
|
311
|
+
x: x1 + half,
|
|
312
|
+
y: y2 - half,
|
|
313
|
+
width: Math.max(0, r.width - h),
|
|
314
|
+
height: h,
|
|
315
|
+
};
|
|
316
|
+
case "w":
|
|
317
|
+
return {
|
|
318
|
+
x: x1 - half,
|
|
319
|
+
y: y1 + half,
|
|
320
|
+
width: h,
|
|
321
|
+
height: Math.max(0, r.height - h),
|
|
322
|
+
};
|
|
323
|
+
case "e":
|
|
324
|
+
return {
|
|
325
|
+
x: x2 - half,
|
|
326
|
+
y: y1 + half,
|
|
327
|
+
width: h,
|
|
328
|
+
height: Math.max(0, r.height - h),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// -----------------------------------------------------------------------
|
|
334
|
+
// Translate node — drag the rect interior to move it without changing size.
|
|
335
|
+
// Sits above the create node (so dragging inside the rect translates rather
|
|
336
|
+
// than starting a new brush) but below resize handles (so handles win on
|
|
337
|
+
// edges). Bounds collapse to zero when there's no rect, making it inert.
|
|
338
|
+
// -----------------------------------------------------------------------
|
|
339
|
+
const translateEnabled = opts.translate !== false;
|
|
340
|
+
function moveBounds(): BrushRect {
|
|
341
|
+
const r = brush.rect;
|
|
342
|
+
if (r === null || (r.width <= 0 && r.height <= 0)) {
|
|
343
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
344
|
+
}
|
|
345
|
+
// Inset by handleSize/2 on each axis when handles are enabled so the
|
|
346
|
+
// resize zones get exclusive ownership at the edges and corners.
|
|
347
|
+
const inset = handlesEnabled ? handleSize / 2 : 0;
|
|
348
|
+
return {
|
|
349
|
+
x: r.x + inset,
|
|
350
|
+
y: r.y + inset,
|
|
351
|
+
width: Math.max(0, r.width - 2 * inset),
|
|
352
|
+
height: Math.max(0, r.height - 2 * inset),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const translateNode: InteractionNode | null = translateEnabled
|
|
356
|
+
? deps.manager.add({
|
|
357
|
+
space: "ui",
|
|
358
|
+
bounds: moveBounds,
|
|
359
|
+
zIndex: Z_BRUSH_HANDLE_EDGE,
|
|
360
|
+
cursor: "move",
|
|
361
|
+
onPress: () => {},
|
|
362
|
+
onDragStart: (info) => {
|
|
363
|
+
brush.beginTranslate(info.x, info.y);
|
|
364
|
+
},
|
|
365
|
+
onDragMove: (info) => {
|
|
366
|
+
brush.update(info.x, info.y);
|
|
367
|
+
},
|
|
368
|
+
onDragEnd: (info) => {
|
|
369
|
+
brush.update(info.x, info.y);
|
|
370
|
+
brush.end();
|
|
371
|
+
},
|
|
372
|
+
})
|
|
373
|
+
: null;
|
|
374
|
+
|
|
375
|
+
const handleNodes: InteractionNode[] = handlesEnabled
|
|
376
|
+
? handleEdges.map((edge) =>
|
|
377
|
+
deps.manager.add({
|
|
378
|
+
space: "ui",
|
|
379
|
+
bounds: () => handleBounds(edge),
|
|
380
|
+
// Above the create drag node so handles claim the gesture inside
|
|
381
|
+
// the rect; corners get a small extra bump so they win over
|
|
382
|
+
// overlapping edge zones at small rect sizes.
|
|
383
|
+
zIndex:
|
|
384
|
+
edge === "ne" || edge === "nw" || edge === "se" || edge === "sw"
|
|
385
|
+
? Z_BRUSH_HANDLE_CORNER
|
|
386
|
+
: Z_BRUSH_HANDLE_EDGE,
|
|
387
|
+
cursor: cursorFor(edge),
|
|
388
|
+
// Empty onPress claims the press so a click without drag doesn't
|
|
389
|
+
// fall through to background-tap (which would clear the brush).
|
|
390
|
+
onPress: () => {},
|
|
391
|
+
onDragStart: (info) => {
|
|
392
|
+
brush.beginResize(edge, info.x, info.y);
|
|
393
|
+
},
|
|
394
|
+
onDragMove: (info) => {
|
|
395
|
+
brush.update(info.x, info.y);
|
|
396
|
+
},
|
|
397
|
+
onDragEnd: (info) => {
|
|
398
|
+
brush.update(info.x, info.y);
|
|
399
|
+
brush.end();
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
)
|
|
403
|
+
: [];
|
|
404
|
+
|
|
405
|
+
const unsubscribeBgTap = deps.manager.onBackgroundTap(() => {
|
|
406
|
+
if (brush.rect !== null) brush.cancel();
|
|
407
|
+
if (brushed.length > 0) {
|
|
408
|
+
brushed = [];
|
|
409
|
+
opts.onChange?.([]);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const d = createDisposable(() => {
|
|
414
|
+
unsubscribeBgTap();
|
|
415
|
+
node.destroy();
|
|
416
|
+
translateNode?.destroy();
|
|
417
|
+
for (const h of handleNodes) h.destroy();
|
|
418
|
+
brush.dispose();
|
|
419
|
+
brushed = [];
|
|
420
|
+
hits = [];
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
sync(next) {
|
|
425
|
+
if (d.isDisposed) return;
|
|
426
|
+
hits = next as readonly CompiledHitTest<unknown>[];
|
|
427
|
+
// Refresh brushed positions/membership against the new hit-test data so
|
|
428
|
+
// a static rect over a re-rendered chart still reports current rows.
|
|
429
|
+
if (brush.rect !== null) {
|
|
430
|
+
if (recomputeBrushed(brush.rect)) opts.onChange?.(brushed.slice());
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
current() {
|
|
434
|
+
return brushed.slice();
|
|
435
|
+
},
|
|
436
|
+
rect() {
|
|
437
|
+
return brush.rect;
|
|
438
|
+
},
|
|
439
|
+
clear() {
|
|
440
|
+
brush.cancel();
|
|
441
|
+
if (brushed.length > 0) {
|
|
442
|
+
brushed = [];
|
|
443
|
+
opts.onChange?.([]);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
draw() {
|
|
447
|
+
if (d.isDisposed) return;
|
|
448
|
+
brush.draw(deps.hudLayer());
|
|
449
|
+
},
|
|
450
|
+
dispose: () => d.dispose(),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CrosshairBounds, type Invalidator, type Layer, type Vec2 } from "insomni";
|
|
2
|
+
import type { CrosshairConfig } from "../chart.ts";
|
|
3
|
+
import type { Theme } from "../theme.ts";
|
|
4
|
+
export interface GrammarCrosshairDeps {
|
|
5
|
+
hudLayer: () => Layer;
|
|
6
|
+
theme: () => Theme;
|
|
7
|
+
bounds: () => CrosshairBounds;
|
|
8
|
+
invalidator: Invalidator;
|
|
9
|
+
}
|
|
10
|
+
export interface GrammarCrosshair {
|
|
11
|
+
/** Set the snapped position, or `null` to hide. */
|
|
12
|
+
setPosition(p: Vec2 | null): void;
|
|
13
|
+
/** Toggle visibility (e.g., off for band x-scales). Hides immediately. */
|
|
14
|
+
setEnabled(enabled: boolean): void;
|
|
15
|
+
/** Draw guide line(s) into the hud layer. */
|
|
16
|
+
draw(): void;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createGrammarCrosshair(deps: GrammarCrosshairDeps, opts?: CrosshairConfig): GrammarCrosshair;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createInvalidator, type Layer } from "insomni";
|
|
2
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
3
|
+
|
|
4
|
+
import { themeDefault } from "../theme.ts";
|
|
5
|
+
import { createGrammarCrosshair } from "./crosshair.ts";
|
|
6
|
+
|
|
7
|
+
interface RecordedSegment {
|
|
8
|
+
x1: number;
|
|
9
|
+
y1: number;
|
|
10
|
+
x2: number;
|
|
11
|
+
y2: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fakeLayer(): { segments: RecordedSegment[]; layer: Layer } {
|
|
15
|
+
const segments: RecordedSegment[] = [];
|
|
16
|
+
const layer = {
|
|
17
|
+
pushSegment(s: { x1: number; y1: number; x2: number; y2: number }) {
|
|
18
|
+
segments.push({ x1: s.x1, y1: s.y1, x2: s.x2, y2: s.y2 });
|
|
19
|
+
return layer;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
return { segments, layer: layer as unknown as Layer };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const bounds = () => ({ x: 0, y: 0, width: 100, height: 200 });
|
|
26
|
+
|
|
27
|
+
describe("grammar crosshair", () => {
|
|
28
|
+
test("setPosition draws a vertical line by default", () => {
|
|
29
|
+
const { segments, layer } = fakeLayer();
|
|
30
|
+
const inv = createInvalidator();
|
|
31
|
+
const ch = createGrammarCrosshair({
|
|
32
|
+
hudLayer: () => layer,
|
|
33
|
+
theme: () => themeDefault,
|
|
34
|
+
bounds,
|
|
35
|
+
invalidator: inv,
|
|
36
|
+
});
|
|
37
|
+
ch.setPosition({ x: 40, y: 80 });
|
|
38
|
+
ch.draw();
|
|
39
|
+
expect(segments).toHaveLength(1);
|
|
40
|
+
expect(segments[0]!.x1).toBe(40);
|
|
41
|
+
expect(segments[0]!.x2).toBe(40);
|
|
42
|
+
expect(segments[0]!.y1).toBe(0);
|
|
43
|
+
expect(segments[0]!.y2).toBe(200);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("axis: 'xy' draws both vertical and horizontal lines", () => {
|
|
47
|
+
const { segments, layer } = fakeLayer();
|
|
48
|
+
const inv = createInvalidator();
|
|
49
|
+
const ch = createGrammarCrosshair(
|
|
50
|
+
{
|
|
51
|
+
hudLayer: () => layer,
|
|
52
|
+
theme: () => themeDefault,
|
|
53
|
+
bounds,
|
|
54
|
+
invalidator: inv,
|
|
55
|
+
},
|
|
56
|
+
{ axis: "xy" },
|
|
57
|
+
);
|
|
58
|
+
ch.setPosition({ x: 40, y: 80 });
|
|
59
|
+
ch.draw();
|
|
60
|
+
expect(segments).toHaveLength(2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("setPosition(null) hides the crosshair", () => {
|
|
64
|
+
const { segments, layer } = fakeLayer();
|
|
65
|
+
const inv = createInvalidator();
|
|
66
|
+
const ch = createGrammarCrosshair({
|
|
67
|
+
hudLayer: () => layer,
|
|
68
|
+
theme: () => themeDefault,
|
|
69
|
+
bounds,
|
|
70
|
+
invalidator: inv,
|
|
71
|
+
});
|
|
72
|
+
ch.setPosition({ x: 40, y: 80 });
|
|
73
|
+
ch.setPosition(null);
|
|
74
|
+
ch.draw();
|
|
75
|
+
expect(segments).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("setEnabled(false) drops position and skips draw", () => {
|
|
79
|
+
const { segments, layer } = fakeLayer();
|
|
80
|
+
const inv = createInvalidator();
|
|
81
|
+
const ch = createGrammarCrosshair({
|
|
82
|
+
hudLayer: () => layer,
|
|
83
|
+
theme: () => themeDefault,
|
|
84
|
+
bounds,
|
|
85
|
+
invalidator: inv,
|
|
86
|
+
});
|
|
87
|
+
ch.setPosition({ x: 40, y: 80 });
|
|
88
|
+
ch.setEnabled(false);
|
|
89
|
+
ch.draw();
|
|
90
|
+
expect(segments).toHaveLength(0);
|
|
91
|
+
// Re-enabling does not restore the prior position (caller must repush).
|
|
92
|
+
ch.setEnabled(true);
|
|
93
|
+
ch.draw();
|
|
94
|
+
expect(segments).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("setPosition is a no-op while disabled", () => {
|
|
98
|
+
const { segments, layer } = fakeLayer();
|
|
99
|
+
const inv = createInvalidator();
|
|
100
|
+
const ch = createGrammarCrosshair({
|
|
101
|
+
hudLayer: () => layer,
|
|
102
|
+
theme: () => themeDefault,
|
|
103
|
+
bounds,
|
|
104
|
+
invalidator: inv,
|
|
105
|
+
});
|
|
106
|
+
ch.setEnabled(false);
|
|
107
|
+
ch.setPosition({ x: 40, y: 80 });
|
|
108
|
+
ch.setEnabled(true);
|
|
109
|
+
ch.draw();
|
|
110
|
+
expect(segments).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("dispose stops further draws", () => {
|
|
114
|
+
const { segments, layer } = fakeLayer();
|
|
115
|
+
const inv = createInvalidator();
|
|
116
|
+
const ch = createGrammarCrosshair({
|
|
117
|
+
hudLayer: () => layer,
|
|
118
|
+
theme: () => themeDefault,
|
|
119
|
+
bounds,
|
|
120
|
+
invalidator: inv,
|
|
121
|
+
});
|
|
122
|
+
ch.setPosition({ x: 40, y: 80 });
|
|
123
|
+
ch.dispose();
|
|
124
|
+
ch.draw();
|
|
125
|
+
expect(segments).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Grammar-level crosshair wiring
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Wraps insomni's generic `createCrosshair` for plot charts. The mount creates
|
|
5
|
+
// this once; the grammar tooltip pushes hover positions in via `setPosition`,
|
|
6
|
+
// and the mount toggles `setEnabled(false)` when the active x-scale is `band`
|
|
7
|
+
// (where a snapping crosshair adds nothing to a categorical layout).
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type Color,
|
|
11
|
+
createCrosshair,
|
|
12
|
+
type Crosshair,
|
|
13
|
+
type CrosshairBounds,
|
|
14
|
+
type Invalidator,
|
|
15
|
+
type Layer,
|
|
16
|
+
type Vec2,
|
|
17
|
+
withAlpha,
|
|
18
|
+
} from "insomni";
|
|
19
|
+
import type { CrosshairConfig } from "../chart.ts";
|
|
20
|
+
import type { Theme } from "../theme.ts";
|
|
21
|
+
import { createDisposable } from "./_disposable.ts";
|
|
22
|
+
|
|
23
|
+
export interface GrammarCrosshairDeps {
|
|
24
|
+
hudLayer: () => Layer;
|
|
25
|
+
theme: () => Theme;
|
|
26
|
+
bounds: () => CrosshairBounds;
|
|
27
|
+
invalidator: Invalidator;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GrammarCrosshair {
|
|
31
|
+
/** Set the snapped position, or `null` to hide. */
|
|
32
|
+
setPosition(p: Vec2 | null): void;
|
|
33
|
+
/** Toggle visibility (e.g., off for band x-scales). Hides immediately. */
|
|
34
|
+
setEnabled(enabled: boolean): void;
|
|
35
|
+
/** Draw guide line(s) into the hud layer. */
|
|
36
|
+
draw(): void;
|
|
37
|
+
dispose(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createGrammarCrosshair(
|
|
41
|
+
deps: GrammarCrosshairDeps,
|
|
42
|
+
opts: CrosshairConfig = {},
|
|
43
|
+
): GrammarCrosshair {
|
|
44
|
+
const themeAxisColor = deps.theme().axis.color;
|
|
45
|
+
const defaultColor: Color = withAlpha(themeAxisColor, themeAxisColor.a * 0.6);
|
|
46
|
+
const crosshair: Crosshair = createCrosshair({
|
|
47
|
+
axis: opts.axis ?? "x",
|
|
48
|
+
bounds: deps.bounds,
|
|
49
|
+
style: {
|
|
50
|
+
color: opts.color ?? defaultColor,
|
|
51
|
+
width: opts.width ?? 1,
|
|
52
|
+
},
|
|
53
|
+
invalidator: deps.invalidator,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let enabled = true;
|
|
57
|
+
const d = createDisposable(() => crosshair.dispose());
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
setPosition(p) {
|
|
61
|
+
if (d.isDisposed || !enabled) return;
|
|
62
|
+
crosshair.setPosition(p);
|
|
63
|
+
},
|
|
64
|
+
setEnabled(next) {
|
|
65
|
+
if (d.isDisposed) return;
|
|
66
|
+
if (enabled === next) return;
|
|
67
|
+
enabled = next;
|
|
68
|
+
if (!enabled) crosshair.setPosition(null);
|
|
69
|
+
},
|
|
70
|
+
draw() {
|
|
71
|
+
if (d.isDisposed || !enabled) return;
|
|
72
|
+
crosshair.draw(deps.hudLayer());
|
|
73
|
+
},
|
|
74
|
+
dispose: () => d.dispose(),
|
|
75
|
+
};
|
|
76
|
+
}
|