@bicharts/chart-host 0.6.83 → 0.6.85

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.
@@ -0,0 +1,665 @@
1
+ /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
+ import {
3
+ DRAG_NOT_CLICK_PX,
4
+ gestureWasDrag,
5
+ isInsideControl,
6
+ rowIdxsFromMark
7
+ } from "./chunk-IURROJT7.mjs";
8
+ import {
9
+ AXIS_FILTER_CLASS,
10
+ CONTROL_CLASS,
11
+ LEGEND_MARK_CLASS,
12
+ MARK_CLASS,
13
+ ROW_IDX_ATTR,
14
+ SNAPSHOT_SVG_ATTR
15
+ } from "./chunk-MR6TL7QZ.mjs";
16
+
17
+ // src/lasso.ts
18
+ var LASSO_ROW_CAP = 5e3;
19
+ var LASSO_DRAG_PX = DRAG_NOT_CLICK_PX;
20
+ var LASSO_SAMPLE_PX = 3;
21
+ var LASSO_MAX_VERTICES = 400;
22
+ var LASSO_RESTORE_MAX_VERTICES = 2e3;
23
+ var LASSO_RESTORE_MAX_SHAPES = 32;
24
+ var LASSO_EVENT = "llm-lasso";
25
+ var LASSO_SURFACE_CLASS = "lch-lasso-surface";
26
+ var LASSO_OUTLINE_CLASS = "lch-lasso-outline";
27
+ var LASSO_COUNT_CLASS = "lch-lasso-count";
28
+ var LASSO_NOTE_CLASS = "lch-lasso-note";
29
+ var LASSO_PLOT_CLASS = "lch-plot";
30
+ var LASSO_MODE_ATTR = "data-lch-lasso-mode";
31
+ var LASSO_KNOB_KEY = "lasso";
32
+ var LASSO_AXES_SLOT = "__lchLassoAxes";
33
+ var LASSO_SLOT = "__lchLasso";
34
+ var LASSO_DEFAULT_ACTIONS = Object.freeze(["select"]);
35
+ var SVGNS = "http://www.w3.org/2000/svg";
36
+ var MARKISH = `.${MARK_CLASS}[${ROW_IDX_ATTR}]:not([${ROW_IDX_ATTR}=""]), .${LEGEND_MARK_CLASS}, .${AXIS_FILTER_CLASS}`;
37
+ var settingOf = (o) => o?.lasso;
38
+ function lassoRowCap(setting) {
39
+ const n = Number(setting?.rowCap);
40
+ return Number.isFinite(n) && n >= 1 ? Math.floor(n) : LASSO_ROW_CAP;
41
+ }
42
+ function lassoActions(setting) {
43
+ const a = setting?.actions;
44
+ if (!Array.isArray(a)) return [...LASSO_DEFAULT_ACTIONS];
45
+ const out = [];
46
+ for (const n of a) if (typeof n === "string" && n.trim() && !out.includes(n.trim())) out.push(n.trim());
47
+ return out;
48
+ }
49
+ function lassoVerdict(setting, rows) {
50
+ if (!setting || setting.capable !== true) return "not-capable";
51
+ if (setting.enabled === false) return "disabled";
52
+ const n = Number(rows);
53
+ if (Number.isFinite(n) && n > lassoRowCap(setting)) return "over-cap";
54
+ return "on";
55
+ }
56
+ function makeShape(mode, pts) {
57
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
58
+ for (const p of pts) {
59
+ if (p.x < minX) minX = p.x;
60
+ if (p.x > maxX) maxX = p.x;
61
+ if (p.y < minY) minY = p.y;
62
+ if (p.y > maxY) maxY = p.y;
63
+ }
64
+ return { mode, pts, minX, minY, maxX, maxY };
65
+ }
66
+ function rectCorners(a, b) {
67
+ return [{ x: a.x, y: a.y }, { x: b.x, y: a.y }, { x: b.x, y: b.y }, { x: a.x, y: b.y }];
68
+ }
69
+ function pointInShape(shape, x, y) {
70
+ const p = shape.pts;
71
+ if (p.length < 3) return false;
72
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
73
+ for (const q of p) {
74
+ if (q.x < minX) minX = q.x;
75
+ if (q.x > maxX) maxX = q.x;
76
+ if (q.y < minY) minY = q.y;
77
+ if (q.y > maxY) maxY = q.y;
78
+ }
79
+ if (x < minX || x > maxX || y < minY || y > maxY) return false;
80
+ if (shape.mode === "rect") return true;
81
+ let inside = false;
82
+ for (let i = 0, j = p.length - 1; i < p.length; j = i++) {
83
+ const xi = p[i].x, yi = p[i].y, xj = p[j].x, yj = p[j].y;
84
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
85
+ }
86
+ return inside;
87
+ }
88
+ function thin(pts, max) {
89
+ if (pts.length <= max) return pts;
90
+ const step = pts.length / max;
91
+ const out = [];
92
+ for (let i = 0; i < max; i++) out.push(pts[Math.floor(i * step)]);
93
+ out.push(pts[pts.length - 1]);
94
+ return out;
95
+ }
96
+ function frameOf(surface) {
97
+ const r = surface.getBoundingClientRect();
98
+ const num = (n) => {
99
+ const v = parseFloat(surface.getAttribute(n) || "0");
100
+ return Number.isFinite(v) ? v : 0;
101
+ };
102
+ return { left: r.left, top: r.top, width: r.width, height: r.height, sx: num("x"), sy: num("y"), sw: num("width"), sh: num("height") };
103
+ }
104
+ var frameUsable = (f) => f.width > 0 && f.height > 0;
105
+ var insideFrame = (f, x, y) => frameUsable(f) && x >= f.left && x <= f.left + f.width && y >= f.top && y <= f.top + f.height;
106
+ var toLocal = (f, p) => [
107
+ f.sx + (f.width > 0 ? (p.x - f.left) * f.sw / f.width : p.x - f.left),
108
+ f.sy + (f.height > 0 ? (p.y - f.top) * f.sh / f.height : p.y - f.top)
109
+ ];
110
+ var fromLocal = (f, p) => ({
111
+ x: f.left + (f.sw > 0 ? (p[0] - f.sx) * f.width / f.sw : p[0] - f.sx),
112
+ y: f.top + (f.sh > 0 ? (p[1] - f.sy) * f.height / f.sh : p[1] - f.sy)
113
+ });
114
+ var finiteNumber = (v) => {
115
+ const n = v instanceof Date ? +v : typeof v === "number" ? v : NaN;
116
+ return Number.isFinite(n) ? n : NaN;
117
+ };
118
+ var finitePair = (v) => {
119
+ if (!v || typeof v !== "object") return null;
120
+ const a = finiteNumber(v[0]), b = finiteNumber(v[1]);
121
+ return Number.isFinite(a) && Number.isFinite(b) ? [a, b] : null;
122
+ };
123
+ function axesOf(container) {
124
+ const a = container[LASSO_AXES_SLOT];
125
+ return a && typeof a.toData === "function" && typeof a.toScreen === "function" ? a : null;
126
+ }
127
+ function toStored(shapes, f, axes) {
128
+ if (axes) {
129
+ try {
130
+ const out = [];
131
+ for (const s of shapes) {
132
+ const pts = [];
133
+ for (const p of s.pts) {
134
+ const d = finitePair(axes.toData(toLocal(f, p)));
135
+ if (!d) throw new Error("no data coordinate");
136
+ pts.push(d);
137
+ }
138
+ out.push({ mode: s.mode, pts });
139
+ }
140
+ return { v: 1, space: "data", shapes: out };
141
+ } catch {
142
+ }
143
+ }
144
+ const w = f.width > 0 ? f.width : 1, h = f.height > 0 ? f.height : 1;
145
+ return {
146
+ v: 1,
147
+ space: "unit",
148
+ shapes: shapes.map((s) => ({ mode: s.mode, pts: s.pts.map((p) => [(p.x - f.left) / w, (p.y - f.top) / h]) }))
149
+ };
150
+ }
151
+ function fromStored(raw, f, axes) {
152
+ try {
153
+ if (!raw || typeof raw !== "object") return [];
154
+ const o = raw;
155
+ const space = o.space === "data" ? "data" : o.space === "unit" ? "unit" : null;
156
+ if (!space || !Array.isArray(o.shapes) || o.shapes.length === 0 || o.shapes.length > LASSO_RESTORE_MAX_SHAPES) return [];
157
+ if (space === "data" && !axes) return [];
158
+ if (!frameUsable(f)) return [];
159
+ const out = [];
160
+ for (const s of o.shapes) {
161
+ if (!s || typeof s !== "object" || !Array.isArray(s.pts)) return [];
162
+ const mode = s.mode === "rect" ? "rect" : "free";
163
+ if (s.pts.length < 3 || s.pts.length > LASSO_RESTORE_MAX_VERTICES) return [];
164
+ if (mode === "rect" && s.pts.length !== 4) return [];
165
+ const pts = [];
166
+ for (const v of s.pts) {
167
+ const p = finitePair(v);
168
+ if (!p) return [];
169
+ if (space === "unit") {
170
+ pts.push({ x: f.left + p[0] * f.width, y: f.top + p[1] * f.height });
171
+ } else {
172
+ const scr = finitePair(axes.toScreen(p));
173
+ if (!scr) return [];
174
+ pts.push(fromLocal(f, scr));
175
+ }
176
+ }
177
+ const shape = makeShape(mode, pts);
178
+ const outside = shape.maxX < f.left || shape.minX > f.left + f.width || shape.maxY < f.top || shape.minY > f.top + f.height;
179
+ if (!outside) out.push(shape);
180
+ }
181
+ return out;
182
+ } catch {
183
+ return [];
184
+ }
185
+ }
186
+ function gatherCentres(container) {
187
+ const out = [];
188
+ for (const el of Array.from(container.querySelectorAll(`.${MARK_CLASS}[${ROW_IDX_ATTR}]`))) {
189
+ const rows = rowIdxsFromMark(el);
190
+ if (!rows.length) continue;
191
+ const r = el.getBoundingClientRect();
192
+ if (!(r.width > 0 || r.height > 0)) continue;
193
+ out.push({ x: r.left + r.width / 2, y: r.top + r.height / 2, el, rows });
194
+ }
195
+ return out;
196
+ }
197
+ function markHidden(el, win) {
198
+ try {
199
+ const cs = win?.getComputedStyle?.(el);
200
+ return !!cs && (cs.visibility === "hidden" || cs.display === "none");
201
+ } catch {
202
+ return false;
203
+ }
204
+ }
205
+ function rowsInside(centres, shapes, win, skipHidden = true) {
206
+ const set = /* @__PURE__ */ new Set();
207
+ for (const c of centres) {
208
+ if (!shapes.some((s) => pointInShape(s, c.x, c.y))) continue;
209
+ if (skipHidden && markHidden(c.el, win)) continue;
210
+ for (const r of c.rows) set.add(r);
211
+ }
212
+ return Array.from(set).sort((a, b) => a - b);
213
+ }
214
+ var sameRows = (a, b) => a.length === b.length && a.every((r, i) => r === b[i]);
215
+ var ACTIONS = /* @__PURE__ */ new Map([
216
+ // The host's own selection: the same publish a mark click makes, so the dim and the subscribers follow.
217
+ ["select", (ctx) => ctx.env.select(ctx.rows)]
218
+ ]);
219
+ function registerLassoAction(name, action) {
220
+ if (typeof name === "string" && name && typeof action === "function") ACTIONS.set(name, action);
221
+ }
222
+ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
223
+ function readStored(options, container) {
224
+ const ui = options?.uiState;
225
+ const knobs = isObject(ui) ? ui.knobs : void 0;
226
+ if (isObject(knobs) && knobs[LASSO_KNOB_KEY] !== void 0) return knobs[LASSO_KNOB_KEY];
227
+ const bag = container.__lchKnobs;
228
+ return isObject(bag) ? bag[LASSO_KNOB_KEY] : void 0;
229
+ }
230
+ function writeStored(container, options, value) {
231
+ const holder = container;
232
+ if (!isObject(holder.__lchKnobs)) holder.__lchKnobs = {};
233
+ const bag = holder.__lchKnobs;
234
+ const had = bag[LASSO_KNOB_KEY] !== void 0 || readStored(options, container) !== void 0;
235
+ if (value === null && !had) return;
236
+ if (value === null) delete bag[LASSO_KNOB_KEY];
237
+ else bag[LASSO_KNOB_KEY] = value;
238
+ if (typeof options.setUiState !== "function") return;
239
+ const ui = isObject(options.uiState) ? options.uiState : {};
240
+ const live = holder.__lchUiLiveOpts === options && holder.__lchUiLiveSeed === options.uiState ? holder.__lchUiLive : null;
241
+ const base = isObject(live) ? live : ui;
242
+ const next = { ...base };
243
+ const knobs = { ...isObject(base.knobs) ? base.knobs : {}, ...bag };
244
+ if (value === null) delete knobs[LASSO_KNOB_KEY];
245
+ next.knobs = knobs;
246
+ holder.__lchUiLive = next;
247
+ holder.__lchUiLiveOpts = options;
248
+ holder.__lchUiLiveSeed = options.uiState;
249
+ try {
250
+ options.setUiState(next);
251
+ } catch {
252
+ }
253
+ }
254
+ function svgBox(svg) {
255
+ const vb = (svg.getAttribute("viewBox") || "").trim().split(/[\s,]+/).map(Number);
256
+ if (vb.length === 4 && vb.every(Number.isFinite) && vb[2] > 0 && vb[3] > 0) return { x: vb[0], y: vb[1], w: vb[2], h: vb[3] };
257
+ const r = svg.getBoundingClientRect();
258
+ if (r.width > 0 && r.height > 0) return { x: 0, y: 0, w: r.width, h: r.height };
259
+ const w = parseFloat(svg.getAttribute("width") || ""), h = parseFloat(svg.getAttribute("height") || "");
260
+ return w > 0 && h > 0 ? { x: 0, y: 0, w, h } : null;
261
+ }
262
+ function findPlot(container) {
263
+ const grouped = container.querySelector(`.${LASSO_PLOT_CLASS}`);
264
+ const tag = grouped?.tagName?.toLowerCase();
265
+ if (grouped && grouped.namespaceURI === SVGNS && (tag === "g" || tag === "svg")) {
266
+ if (tag === "svg") {
267
+ const b3 = svgBox(grouped);
268
+ return b3 ? { host: grouped, box: b3 } : null;
269
+ }
270
+ try {
271
+ const b3 = grouped.getBBox?.();
272
+ if (b3 && b3.width > 0 && b3.height > 0) return { host: grouped, box: { x: b3.x, y: b3.y, w: b3.width, h: b3.height } };
273
+ } catch {
274
+ }
275
+ const svg2 = grouped.ownerSVGElement ?? grouped.closest("svg");
276
+ const b2 = svg2 ? svgBox(svg2) : null;
277
+ return b2 ? { host: grouped, box: b2 } : null;
278
+ }
279
+ const svg = container.querySelector(`svg[${SNAPSHOT_SVG_ATTR}]`) ?? container.querySelector("svg");
280
+ const b = svg ? svgBox(svg) : null;
281
+ return svg && b ? { host: svg, box: b } : null;
282
+ }
283
+ function fmtCount(n, culture) {
284
+ try {
285
+ return new Intl.NumberFormat(culture || void 0).format(n);
286
+ } catch {
287
+ return String(n);
288
+ }
289
+ }
290
+ function showNote(env, rows, cap) {
291
+ const { container } = env;
292
+ const doc = container.ownerDocument;
293
+ const win = doc.defaultView;
294
+ const opts = env.options();
295
+ const note = doc.createElement("div");
296
+ note.className = LASSO_NOTE_CLASS;
297
+ note.setAttribute("role", "note");
298
+ note.textContent = `Lasso selection is off at ${fmtCount(rows, opts?.cultureCode)} rows (limit ${fmtCount(cap, opts?.cultureCode)})`;
299
+ Object.assign(note.style, {
300
+ position: "absolute",
301
+ right: "6px",
302
+ bottom: "2px",
303
+ font: "11px/1.2 system-ui, sans-serif",
304
+ opacity: "0.75",
305
+ pointerEvents: "none",
306
+ whiteSpace: "nowrap",
307
+ zIndex: "2",
308
+ color: opts?.themeFg || "inherit"
309
+ });
310
+ try {
311
+ if (win?.getComputedStyle(container).position === "static") container.style.position = "relative";
312
+ } catch {
313
+ }
314
+ container.appendChild(note);
315
+ const handle = {
316
+ surface: null,
317
+ rows: () => [],
318
+ clear: () => {
319
+ },
320
+ destroy: () => {
321
+ note.remove();
322
+ if (container[LASSO_SLOT] === handle) container[LASSO_SLOT] = null;
323
+ }
324
+ };
325
+ container[LASSO_SLOT] = handle;
326
+ return handle;
327
+ }
328
+ function installLasso(env) {
329
+ const { container } = env;
330
+ const doc = container.ownerDocument;
331
+ const win = doc.defaultView ?? globalThis;
332
+ const holder = container;
333
+ try {
334
+ holder[LASSO_SLOT]?.destroy();
335
+ } catch {
336
+ }
337
+ holder[LASSO_SLOT] = null;
338
+ const setting = settingOf(env.options());
339
+ const verdict = lassoVerdict(setting, env.rows);
340
+ if (verdict === "not-capable" || verdict === "disabled") return null;
341
+ if (verdict === "over-cap") return showNote(env, env.rows, lassoRowCap(setting));
342
+ const plot = findPlot(container);
343
+ if (!plot) return null;
344
+ const surface = doc.createElementNS(SVGNS, "rect");
345
+ surface.setAttribute("class", `${CONTROL_CLASS} ${LASSO_SURFACE_CLASS}`);
346
+ surface.setAttribute("x", String(plot.box.x));
347
+ surface.setAttribute("y", String(plot.box.y));
348
+ surface.setAttribute("width", String(plot.box.w));
349
+ surface.setAttribute("height", String(plot.box.h));
350
+ surface.setAttribute("fill", "transparent");
351
+ surface.setAttribute("pointer-events", "all");
352
+ surface.setAttribute("tabindex", "0");
353
+ surface.setAttribute("aria-label", "Selection area: drag to draw around marks, Escape to clear");
354
+ Object.assign(surface.style, { cursor: "crosshair", touchAction: "none", userSelect: "none" });
355
+ plot.host.insertBefore(surface, plot.host.firstChild);
356
+ const outline = doc.createElementNS(SVGNS, "path");
357
+ outline.setAttribute("class", LASSO_OUTLINE_CLASS);
358
+ outline.setAttribute("pointer-events", "none");
359
+ outline.setAttribute("fill-opacity", "0.12");
360
+ outline.setAttribute("stroke-width", "1.5");
361
+ outline.setAttribute("stroke-dasharray", "5 3");
362
+ outline.setAttribute("d", "");
363
+ const count = doc.createElementNS(SVGNS, "text");
364
+ count.setAttribute("class", LASSO_COUNT_CLASS);
365
+ count.setAttribute("pointer-events", "none");
366
+ count.setAttribute("font-size", "12");
367
+ count.setAttribute("x", String(plot.box.x + 6));
368
+ count.setAttribute("y", String(plot.box.y + 16));
369
+ Object.assign(count.style, { userSelect: "none" });
370
+ plot.host.appendChild(outline);
371
+ plot.host.appendChild(count);
372
+ const paintInk = () => {
373
+ const o = env.options();
374
+ const fg = o?.themeFg || "currentColor";
375
+ outline.setAttribute("stroke", fg);
376
+ outline.setAttribute("fill", o?.themeAccent || fg);
377
+ count.setAttribute("fill", fg);
378
+ };
379
+ paintInk();
380
+ let shapes = [];
381
+ let rowsNow = [];
382
+ let gesture = null;
383
+ let press = null;
384
+ let tail = false;
385
+ let centres = [];
386
+ const frame = () => frameOf(surface);
387
+ const modeNow = () => container.getAttribute(LASSO_MODE_ATTR) === "rect" ? "rect" : "free";
388
+ const dFor = (list, f) => list.map((s) => {
389
+ const loc = s.pts.map((p) => toLocal(f, p));
390
+ return "M" + loc.map((p) => `${round(p[0])} ${round(p[1])}`).join("L") + "Z";
391
+ }).join("");
392
+ const round = (n) => Math.round(n * 100) / 100;
393
+ const drawRest = () => {
394
+ outline.setAttribute("d", shapes.length ? dFor(shapes, frame()) : "");
395
+ };
396
+ const setCount = (n) => {
397
+ count.textContent = n === null ? "" : `${fmtCount(n, env.options()?.cultureCode)} selected`;
398
+ };
399
+ const detail = (rows, list, source) => {
400
+ const stored = toStored(list, frame(), axesOf(container));
401
+ const last = stored.shapes[stored.shapes.length - 1];
402
+ return {
403
+ rows,
404
+ polygon: last ? last.pts : [],
405
+ polygons: stored.shapes.map((s) => s.pts),
406
+ mode: last ? last.mode : modeNow(),
407
+ space: stored.space,
408
+ source
409
+ };
410
+ };
411
+ const emit = (d) => {
412
+ const Ev = win.CustomEvent ?? globalThis.CustomEvent;
413
+ try {
414
+ container.dispatchEvent(new Ev(LASSO_EVENT, { detail: d, bubbles: true }));
415
+ } catch {
416
+ }
417
+ };
418
+ const persist = (list) => {
419
+ try {
420
+ writeStored(container, env.options(), list ? toStored(list, frame(), axesOf(container)) : null);
421
+ } catch {
422
+ }
423
+ };
424
+ const commit = (list, rows) => {
425
+ shapes = list;
426
+ rowsNow = rows;
427
+ drawRest();
428
+ setCount(null);
429
+ persist(list);
430
+ const ctx = { rows: rows.slice(), mode: list[list.length - 1].mode, source: "user", env };
431
+ for (const name of lassoActions(settingOf(env.options()))) {
432
+ const run = ACTIONS.get(name);
433
+ if (!run) continue;
434
+ try {
435
+ run(ctx);
436
+ } catch {
437
+ }
438
+ }
439
+ emit(detail(rows, list, "user"));
440
+ };
441
+ const clear = (opts = {}) => {
442
+ const had = shapes.length > 0;
443
+ if (gesture) endGesture();
444
+ shapes = [];
445
+ rowsNow = [];
446
+ drawRest();
447
+ setCount(null);
448
+ if (opts.publish !== false) {
449
+ try {
450
+ env.clearSelection();
451
+ } catch {
452
+ }
453
+ }
454
+ if (had) {
455
+ persist(null);
456
+ emit(detail([], [], opts.source ?? "user"));
457
+ }
458
+ };
459
+ const beginsOffMark = (target, x, y) => {
460
+ const t = target;
461
+ if (!t || t.nodeType !== 1) return false;
462
+ if (t !== surface) {
463
+ if (!plot.host.contains(t)) return false;
464
+ if (t.closest?.(MARKISH)) return false;
465
+ if (isInsideControl(t, container)) return false;
466
+ }
467
+ try {
468
+ const stack = doc.elementsFromPoint?.(x, y);
469
+ if (stack) {
470
+ for (const el of stack) if (el !== surface && container.contains(el) && el.closest?.(MARKISH)) return false;
471
+ }
472
+ } catch {
473
+ }
474
+ return true;
475
+ };
476
+ const stopSelect = (e) => e.preventDefault();
477
+ const listen = (on) => {
478
+ const m = on ? "addEventListener" : "removeEventListener";
479
+ doc[m]("pointermove", onMove);
480
+ doc[m]("pointerup", onUp);
481
+ doc[m]("pointercancel", onCancel);
482
+ doc[m]("selectstart", stopSelect);
483
+ };
484
+ const endGesture = () => {
485
+ if (!gesture) return;
486
+ const wasDragging = gesture.dragging;
487
+ gesture = null;
488
+ listen(false);
489
+ centres = [];
490
+ setCount(null);
491
+ if (wasDragging) {
492
+ tail = true;
493
+ drawRest();
494
+ }
495
+ };
496
+ const liveShape = (g) => g.mode === "rect" ? makeShape("rect", rectCorners(g.start, g.end)) : makeShape("free", [...g.pts, g.end]);
497
+ const paintLive = (g) => {
498
+ const f = frame();
499
+ const live = liveShape(g);
500
+ const parts = g.additive ? [...shapes, live] : [live];
501
+ outline.setAttribute("d", dFor(parts, f));
502
+ setCount(rowsInside(centres, parts, win, false).length);
503
+ };
504
+ function onDown(e) {
505
+ tail = false;
506
+ const x = Number(e.clientX), y = Number(e.clientY);
507
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return;
508
+ press = { x, y };
509
+ if (gesture || e.button != null && e.button !== 0 || e.isPrimary === false) return;
510
+ const f = frame();
511
+ if (!insideFrame(f, x, y) || !beginsOffMark(e.target, x, y)) return;
512
+ gesture = {
513
+ id: e.pointerId,
514
+ start: { x, y },
515
+ pts: [{ x, y }],
516
+ end: { x, y },
517
+ additive: !!(e.ctrlKey || e.metaKey || e.shiftKey),
518
+ mode: modeNow(),
519
+ dragging: false
520
+ };
521
+ try {
522
+ if (e.pointerId != null && typeof surface.setPointerCapture === "function") surface.setPointerCapture(e.pointerId);
523
+ } catch {
524
+ }
525
+ listen(true);
526
+ }
527
+ function onMove(e) {
528
+ const g = gesture;
529
+ if (!g || g.id != null && e.pointerId != null && e.pointerId !== g.id) return;
530
+ const p = { x: Number(e.clientX), y: Number(e.clientY) };
531
+ if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) return;
532
+ if (!g.dragging) {
533
+ if (!gestureWasDrag(g.start, p, LASSO_DRAG_PX)) return;
534
+ g.dragging = true;
535
+ centres = gatherCentres(container);
536
+ paintInk();
537
+ }
538
+ g.end = p;
539
+ if (g.mode === "free") {
540
+ const last = g.pts[g.pts.length - 1];
541
+ if (Math.hypot(p.x - last.x, p.y - last.y) >= LASSO_SAMPLE_PX) g.pts.push(p);
542
+ }
543
+ paintLive(g);
544
+ }
545
+ function onUp(e) {
546
+ const g = gesture;
547
+ if (!g || g.id != null && e.pointerId != null && e.pointerId !== g.id) return;
548
+ if (!g.dragging) {
549
+ endGesture();
550
+ return;
551
+ }
552
+ const x = Number(e.clientX), y = Number(e.clientY);
553
+ if (Number.isFinite(x) && Number.isFinite(y)) g.end = { x, y };
554
+ const additive = g.additive;
555
+ const mode = g.mode;
556
+ let pts = mode === "rect" ? rectCorners(g.start, g.end) : [...g.pts, g.end];
557
+ endGesture();
558
+ if (mode === "free") {
559
+ if (pts.length < 3) return;
560
+ pts = thin(pts, LASSO_MAX_VERTICES);
561
+ }
562
+ const shape = makeShape(mode, pts);
563
+ const fresh = gatherCentres(container);
564
+ const added = rowsInside(fresh, [shape], win);
565
+ if (!added.length) return;
566
+ const list = additive ? [...shapes, shape] : [shape];
567
+ const rows = additive ? rowsInside(fresh, list, win) : added;
568
+ if (additive && sameRows(rows, rowsNow)) return;
569
+ commit(list, rows);
570
+ try {
571
+ surface.focus?.({ preventScroll: true });
572
+ } catch {
573
+ }
574
+ }
575
+ function onCancel(e) {
576
+ const g = gesture;
577
+ if (!g || g.id != null && e.pointerId != null && e.pointerId !== g.id) return;
578
+ endGesture();
579
+ }
580
+ function onClick(e) {
581
+ const wasDrag = tail || Number(e.detail) > 0 && !!press && gestureWasDrag(press, { x: Number(e.clientX), y: Number(e.clientY) }, LASSO_DRAG_PX);
582
+ tail = false;
583
+ press = null;
584
+ if (e.target !== surface || wasDrag) return;
585
+ if (e.ctrlKey || e.metaKey || e.shiftKey) return;
586
+ clear({ publish: true, source: "user" });
587
+ }
588
+ function onKey(e) {
589
+ if (e.key !== "Escape" || e.defaultPrevented) return;
590
+ if (gesture) {
591
+ endGesture();
592
+ e.preventDefault?.();
593
+ return;
594
+ }
595
+ if (shapes.length) {
596
+ clear({ publish: true, source: "user" });
597
+ e.preventDefault?.();
598
+ }
599
+ }
600
+ container.addEventListener("pointerdown", onDown, true);
601
+ container.addEventListener("click", onClick, true);
602
+ container.addEventListener("keydown", onKey);
603
+ const handle = {
604
+ surface,
605
+ rows: () => rowsNow.slice(),
606
+ clear,
607
+ destroy() {
608
+ if (gesture) {
609
+ gesture = null;
610
+ listen(false);
611
+ }
612
+ container.removeEventListener("pointerdown", onDown, true);
613
+ container.removeEventListener("click", onClick, true);
614
+ container.removeEventListener("keydown", onKey);
615
+ surface.remove();
616
+ outline.remove();
617
+ count.remove();
618
+ if (holder[LASSO_SLOT] === handle) holder[LASSO_SLOT] = null;
619
+ }
620
+ };
621
+ holder[LASSO_SLOT] = handle;
622
+ const restored = fromStored(readStored(env.options(), container), frame(), axesOf(container));
623
+ if (restored.length) {
624
+ const rows = rowsInside(gatherCentres(container), restored, win);
625
+ if (rows.length) {
626
+ shapes = restored;
627
+ rowsNow = rows;
628
+ drawRest();
629
+ emit(detail(rows, restored, "restore"));
630
+ }
631
+ }
632
+ return handle;
633
+ }
634
+ function clearLasso(container, opts) {
635
+ try {
636
+ container?.[LASSO_SLOT]?.clear(opts);
637
+ } catch {
638
+ }
639
+ }
640
+
641
+ export {
642
+ LASSO_ROW_CAP,
643
+ LASSO_DRAG_PX,
644
+ LASSO_SAMPLE_PX,
645
+ LASSO_MAX_VERTICES,
646
+ LASSO_RESTORE_MAX_VERTICES,
647
+ LASSO_RESTORE_MAX_SHAPES,
648
+ LASSO_EVENT,
649
+ LASSO_SURFACE_CLASS,
650
+ LASSO_OUTLINE_CLASS,
651
+ LASSO_COUNT_CLASS,
652
+ LASSO_NOTE_CLASS,
653
+ LASSO_PLOT_CLASS,
654
+ LASSO_MODE_ATTR,
655
+ LASSO_KNOB_KEY,
656
+ LASSO_AXES_SLOT,
657
+ LASSO_DEFAULT_ACTIONS,
658
+ lassoRowCap,
659
+ lassoActions,
660
+ lassoVerdict,
661
+ pointInShape,
662
+ registerLassoAction,
663
+ installLasso,
664
+ clearLasso
665
+ };