@griddle/vue 0.1.7 → 0.1.9

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.
@@ -1 +1 @@
1
- {"version":3,"file":"interaction.js","sources":["../src/interaction.ts"],"sourcesContent":["export interface InteractionScale {\n x: number;\n y: number;\n}\n\ntype MeasurableElement = Pick<\n HTMLElement,\n 'getBoundingClientRect' | 'offsetWidth' | 'offsetHeight'\n>;\n\nfunction validScale(value: number): number | null {\n return Number.isFinite(value) && value > 0 ? value : null;\n}\n\n/**\n * Measure how CSS transforms map an element's local coordinate space into\n * viewport pixels. PointerEvent.clientX/clientY are viewport coordinates,\n * while Griddle's cells and inline transforms use the element's local CSS\n * pixels, so drag math must cross this boundary explicitly.\n */\nexport function measureInteractionScale(\n element: MeasurableElement | null,\n): InteractionScale {\n if (!element) return { x: 1, y: 1 };\n\n const rect = element.getBoundingClientRect();\n const measuredX = validScale(\n element.offsetWidth > 0 ? rect.width / element.offsetWidth : NaN,\n );\n const measuredY = validScale(\n element.offsetHeight > 0 ? rect.height / element.offsetHeight : NaN,\n );\n\n // A content-sized grid can temporarily report a zero height while mounting.\n // Prefer the other measured axis before falling back to an unscaled value;\n // this also handles the common uniform transform: scale(...) host.\n return {\n x: measuredX ?? measuredY ?? 1,\n y: measuredY ?? measuredX ?? 1,\n };\n}\n\n/** Convert a viewport-space pointer delta into the grid's local CSS pixels. */\nexport function toLocalInteractionDelta(\n dx: number,\n dy: number,\n scale: InteractionScale,\n): { dx: number; dy: number } {\n return {\n dx: dx / scale.x,\n dy: dy / scale.y,\n };\n}\n"],"names":["validScale","value","measureInteractionScale","element","rect","measuredX","measuredY","toLocalInteractionDelta","dx","dy","scale"],"mappings":"AAUA,SAASA,EAAWC,GAA8B;AAChD,SAAO,OAAO,SAASA,CAAK,KAAKA,IAAQ,IAAIA,IAAQ;AACvD;AAQO,SAASC,EACdC,GACkB;AAClB,MAAI,CAACA,EAAS,QAAO,EAAE,GAAG,GAAG,GAAG,EAAA;AAEhC,QAAMC,IAAOD,EAAQ,sBAAA,GACfE,IAAYL;AAAA,IAChBG,EAAQ,cAAc,IAAIC,EAAK,QAAQD,EAAQ,cAAc;AAAA,EAAA,GAEzDG,IAAYN;AAAA,IAChBG,EAAQ,eAAe,IAAIC,EAAK,SAASD,EAAQ,eAAe;AAAA,EAAA;AAMlE,SAAO;AAAA,IACL,GAAGE,KAAaC,KAAa;AAAA,IAC7B,GAAGA,KAAaD,KAAa;AAAA,EAAA;AAEjC;AAGO,SAASE,EACdC,GACAC,GACAC,GAC4B;AAC5B,SAAO;AAAA,IACL,IAAIF,IAAKE,EAAM;AAAA,IACf,IAAID,IAAKC,EAAM;AAAA,EAAA;AAEnB;"}
1
+ {"version":3,"file":"interaction.js","sources":["../src/interaction.ts"],"sourcesContent":["export interface InteractionScale {\n x: number;\n y: number;\n}\n\nexport interface ResizePreviewInput {\n corner: 'nw' | 'ne' | 'sw' | 'se';\n startCol: number;\n startRow: number;\n startW: number;\n startH: number;\n stepsX: number;\n stepsY: number;\n minW: number;\n minH: number;\n maxW: number;\n maxH: number;\n cols: number;\n rows: number;\n infiniteX: boolean;\n infiniteY: boolean;\n}\n\nexport interface ResizePreview {\n col: number;\n row: number;\n w: number;\n h: number;\n}\n\ntype MeasurableElement = Pick<\n HTMLElement,\n 'getBoundingClientRect' | 'offsetWidth' | 'offsetHeight'\n>;\n\nfunction validScale(value: number): number | null {\n return Number.isFinite(value) && value > 0 ? value : null;\n}\n\n/**\n * Measure how CSS transforms map an element's local coordinate space into\n * viewport pixels. PointerEvent.clientX/clientY are viewport coordinates,\n * while Griddle's cells and inline transforms use the element's local CSS\n * pixels, so drag math must cross this boundary explicitly.\n */\nexport function measureInteractionScale(\n element: MeasurableElement | null,\n): InteractionScale {\n if (!element) return { x: 1, y: 1 };\n\n const rect = element.getBoundingClientRect();\n const measuredX = validScale(\n element.offsetWidth > 0 ? rect.width / element.offsetWidth : NaN,\n );\n const measuredY = validScale(\n element.offsetHeight > 0 ? rect.height / element.offsetHeight : NaN,\n );\n\n // A content-sized grid can temporarily report a zero height while mounting.\n // Prefer the other measured axis before falling back to an unscaled value;\n // this also handles the common uniform transform: scale(...) host.\n return {\n x: measuredX ?? measuredY ?? 1,\n y: measuredY ?? measuredX ?? 1,\n };\n}\n\n/** Convert a viewport-space pointer delta into the grid's local CSS pixels. */\nexport function toLocalInteractionDelta(\n dx: number,\n dy: number,\n scale: InteractionScale,\n): { dx: number; dy: number } {\n return {\n dx: dx / scale.x,\n dy: dy / scale.y,\n };\n}\n\nfunction clamp(value: number, lower: number, upper: number): number {\n if (upper < lower) return upper;\n return Math.min(upper, Math.max(lower, value));\n}\n\n/**\n * Resolve a corner resize while preserving the opposite edges and trimming\n * the dragged edges at finite grid bounds.\n */\nexport function resolveResizePreview({\n corner,\n startCol,\n startRow,\n startW,\n startH,\n stepsX,\n stepsY,\n minW,\n minH,\n maxW,\n maxH,\n cols,\n rows,\n infiniteX,\n infiniteY,\n}: ResizePreviewInput): ResizePreview {\n const startRight = startCol + startW;\n const startBottom = startRow + startH;\n const east = corner === 'ne' || corner === 'se';\n const south = corner === 'se' || corner === 'sw';\n\n let col = startCol;\n let right = startRight;\n if (east) {\n const maxRight = Math.min(\n startCol + maxW,\n infiniteX ? Infinity : cols,\n );\n right = clamp(startRight + stepsX, startCol + minW, maxRight);\n } else {\n const minLeft = Math.max(startRight - maxW, 0);\n col = clamp(startCol + stepsX, minLeft, startRight - minW);\n }\n\n let row = startRow;\n let bottom = startBottom;\n if (south) {\n const maxBottom = Math.min(\n startRow + maxH,\n infiniteY ? Infinity : rows,\n );\n bottom = clamp(startBottom + stepsY, startRow + minH, maxBottom);\n } else {\n const minTop = Math.max(startBottom - maxH, 0);\n row = clamp(startRow + stepsY, minTop, startBottom - minH);\n }\n\n return {\n col,\n row,\n w: right - col,\n h: bottom - row,\n };\n}\n\n/** Clamp a pointer-derived cell index to the cells addressable by the grid. */\nexport function clampInteractionCell(\n cell: number,\n extent: number,\n infinite: boolean,\n): number {\n const nonNegative = Math.max(0, cell);\n return infinite ? nonNegative : Math.min(extent - 1, nonNegative);\n}\n"],"names":["validScale","value","measureInteractionScale","element","rect","measuredX","measuredY","toLocalInteractionDelta","dx","dy","scale","clamp","lower","upper","resolveResizePreview","corner","startCol","startRow","startW","startH","stepsX","stepsY","minW","minH","maxW","maxH","cols","rows","infiniteX","infiniteY","startRight","startBottom","east","south","col","right","maxRight","minLeft","row","bottom","maxBottom","minTop","clampInteractionCell","cell","extent","infinite","nonNegative"],"mappings":"AAmCA,SAASA,EAAWC,GAA8B;AAChD,SAAO,OAAO,SAASA,CAAK,KAAKA,IAAQ,IAAIA,IAAQ;AACvD;AAQO,SAASC,EACdC,GACkB;AAClB,MAAI,CAACA,EAAS,QAAO,EAAE,GAAG,GAAG,GAAG,EAAA;AAEhC,QAAMC,IAAOD,EAAQ,sBAAA,GACfE,IAAYL;AAAA,IAChBG,EAAQ,cAAc,IAAIC,EAAK,QAAQD,EAAQ,cAAc;AAAA,EAAA,GAEzDG,IAAYN;AAAA,IAChBG,EAAQ,eAAe,IAAIC,EAAK,SAASD,EAAQ,eAAe;AAAA,EAAA;AAMlE,SAAO;AAAA,IACL,GAAGE,KAAaC,KAAa;AAAA,IAC7B,GAAGA,KAAaD,KAAa;AAAA,EAAA;AAEjC;AAGO,SAASE,EACdC,GACAC,GACAC,GAC4B;AAC5B,SAAO;AAAA,IACL,IAAIF,IAAKE,EAAM;AAAA,IACf,IAAID,IAAKC,EAAM;AAAA,EAAA;AAEnB;AAEA,SAASC,EAAMV,GAAeW,GAAeC,GAAuB;AAClE,SAAIA,IAAQD,IAAcC,IACnB,KAAK,IAAIA,GAAO,KAAK,IAAID,GAAOX,CAAK,CAAC;AAC/C;AAMO,SAASa,EAAqB;AAAA,EACnC,QAAAC;AAAA,EACA,UAAAC;AAAA,EACA,UAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,MAAAC;AAAA,EACA,MAAAC;AAAA,EACA,MAAAC;AAAA,EACA,MAAAC;AAAA,EACA,MAAAC;AAAA,EACA,MAAAC;AAAA,EACA,WAAAC;AAAA,EACA,WAAAC;AACF,GAAsC;AACpC,QAAMC,IAAad,IAAWE,GACxBa,IAAcd,IAAWE,GACzBa,IAAOjB,MAAW,QAAQA,MAAW,MACrCkB,IAAQlB,MAAW,QAAQA,MAAW;AAE5C,MAAImB,IAAMlB,GACNmB,IAAQL;AACZ,MAAIE,GAAM;AACR,UAAMI,IAAW,KAAK;AAAA,MACpBpB,IAAWQ;AAAA,MACXI,IAAY,QAAWF;AAAA,IAAA;AAEzB,IAAAS,IAAQxB,EAAMmB,IAAaV,GAAQJ,IAAWM,GAAMc,CAAQ;AAAA,EAC9D,OAAO;AACL,UAAMC,IAAU,KAAK,IAAIP,IAAaN,GAAM,CAAC;AAC7C,IAAAU,IAAMvB,EAAMK,IAAWI,GAAQiB,GAASP,IAAaR,CAAI;AAAA,EAC3D;AAEA,MAAIgB,IAAMrB,GACNsB,IAASR;AACb,MAAIE,GAAO;AACT,UAAMO,IAAY,KAAK;AAAA,MACrBvB,IAAWQ;AAAA,MACXI,IAAY,QAAWF;AAAA,IAAA;AAEzB,IAAAY,IAAS5B,EAAMoB,IAAcV,GAAQJ,IAAWM,GAAMiB,CAAS;AAAA,EACjE,OAAO;AACL,UAAMC,IAAS,KAAK,IAAIV,IAAcN,GAAM,CAAC;AAC7C,IAAAa,IAAM3B,EAAMM,IAAWI,GAAQoB,GAAQV,IAAcR,CAAI;AAAA,EAC3D;AAEA,SAAO;AAAA,IACL,KAAAW;AAAA,IACA,KAAAI;AAAA,IACA,GAAGH,IAAQD;AAAA,IACX,GAAGK,IAASD;AAAA,EAAA;AAEhB;AAGO,SAASI,EACdC,GACAC,GACAC,GACQ;AACR,QAAMC,IAAc,KAAK,IAAI,GAAGH,CAAI;AACpC,SAAOE,IAAWC,IAAc,KAAK,IAAIF,IAAS,GAAGE,CAAW;AAClE;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@griddle/vue",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Vue 3 bindings for Griddle — a headless, zero-dependency grid/canvas engine.",
5
5
  "keywords": [
6
6
  "grid",
@@ -53,11 +53,11 @@
53
53
  },
54
54
  "peerDependencies": {
55
55
  "vue": "^3.3.0",
56
- "@griddle/core": "^0.1.7"
56
+ "@griddle/core": "^0.1.9"
57
57
  },
58
58
  "devDependencies": {
59
59
  "vue": "^3.4.0",
60
- "@griddle/core": "^0.1.7",
60
+ "@griddle/core": "^0.1.9",
61
61
  "vite": "^5.4.0",
62
62
  "@vitejs/plugin-vue": "^5.2.0",
63
63
  "vue-tsc": "^2.2.0"
@@ -114,6 +114,8 @@ import type { GriddleApi } from './useGriddle.js';
114
114
  import { animateReposition, liftTransition } from './animation.js';
115
115
  import {
116
116
  measureInteractionScale,
117
+ clampInteractionCell,
118
+ resolveResizePreview,
117
119
  toLocalInteractionDelta,
118
120
  type InteractionScale,
119
121
  } from './interaction.js';
@@ -406,11 +408,20 @@ function onBackgroundPointerDown(e: PointerEvent) {
406
408
 
407
409
  const el = scrollEl.value;
408
410
  if (!el) return;
411
+ interactionScale = measureInteractionScale(el);
409
412
  const rect = el.getBoundingClientRect();
410
- const x = e.clientX - rect.left + el.scrollLeft;
411
- const y = e.clientY - rect.top + el.scrollTop;
412
- const col = Math.floor(x / colSize.value);
413
- const row = Math.floor(y / rowSize.value);
413
+ const x = (e.clientX - rect.left) / interactionScale.x + el.scrollLeft;
414
+ const y = (e.clientY - rect.top) / interactionScale.y + el.scrollTop;
415
+ const col = clampInteractionCell(
416
+ Math.floor(x / colSize.value),
417
+ config.value.cols,
418
+ config.value.infiniteX ?? config.value.cols === Infinity,
419
+ );
420
+ const row = clampInteractionCell(
421
+ Math.floor(y / rowSize.value),
422
+ config.value.rows,
423
+ config.value.infiniteY ?? config.value.rows === Infinity,
424
+ );
414
425
  drawState.value = { anchorCol: col, anchorRow: row, currentCol: col, currentRow: row };
415
426
  (el as HTMLDivElement).setPointerCapture(e.pointerId);
416
427
  }
@@ -442,10 +453,18 @@ function onPointerMove(e: PointerEvent) {
442
453
  const el = scrollEl.value;
443
454
  if (el) {
444
455
  const rect = el.getBoundingClientRect();
445
- const x = e.clientX - rect.left + el.scrollLeft;
446
- const y = e.clientY - rect.top + el.scrollTop;
447
- const col = Math.max(0, Math.floor(x / colSize.value));
448
- const row = Math.max(0, Math.floor(y / rowSize.value));
456
+ const x = (e.clientX - rect.left) / interactionScale.x + el.scrollLeft;
457
+ const y = (e.clientY - rect.top) / interactionScale.y + el.scrollTop;
458
+ const col = clampInteractionCell(
459
+ Math.floor(x / colSize.value),
460
+ config.value.cols,
461
+ config.value.infiniteX ?? config.value.cols === Infinity,
462
+ );
463
+ const row = clampInteractionCell(
464
+ Math.floor(y / rowSize.value),
465
+ config.value.rows,
466
+ config.value.infiniteY ?? config.value.rows === Infinity,
467
+ );
449
468
  drawState.value = { ...drawState.value, currentCol: col, currentRow: row };
450
469
  }
451
470
  return;
@@ -523,22 +542,30 @@ function onPointerMove(e: PointerEvent) {
523
542
  e.clientY - r.startPointerY,
524
543
  interactionScale,
525
544
  );
526
- let dw = 0, dh = 0, dcol = 0, drow = 0;
527
545
  const stepsX = Math.round(dx / colSize.value);
528
546
  const stepsY = Math.round(dy / rowSize.value);
529
- if (r.corner === 'se' || r.corner === 'ne') dw = stepsX;
530
- if (r.corner === 'se' || r.corner === 'sw') dh = stepsY;
531
- if (r.corner === 'sw' || r.corner === 'nw') { dw = -stepsX; dcol = stepsX; }
532
- if (r.corner === 'ne' || r.corner === 'nw') { dh = -stepsY; drow = stepsY; }
533
547
  const tile = props.api.grid.getTile(r.tileId);
534
- const minW = tile?.minW ?? 1;
535
- const minH = tile?.minH ?? 1;
536
- const maxW = tile?.maxW ?? Infinity;
537
- const maxH = tile?.maxH ?? Infinity;
538
- const nW = Math.min(maxW, Math.max(minW, r.startW + dw));
539
- const nH = Math.min(maxH, Math.max(minH, r.startH + dh));
540
- const nC = r.startCol + dcol;
541
- const nR = r.startRow + drow;
548
+ const preview = resolveResizePreview({
549
+ corner: r.corner,
550
+ startCol: r.startCol,
551
+ startRow: r.startRow,
552
+ startW: r.startW,
553
+ startH: r.startH,
554
+ stepsX,
555
+ stepsY,
556
+ minW: tile?.minW ?? 1,
557
+ minH: tile?.minH ?? 1,
558
+ maxW: tile?.maxW ?? Infinity,
559
+ maxH: tile?.maxH ?? Infinity,
560
+ cols: config.value.cols,
561
+ rows: config.value.rows,
562
+ infiniteX: config.value.infiniteX ?? config.value.cols === Infinity,
563
+ infiniteY: config.value.infiniteY ?? config.value.rows === Infinity,
564
+ });
565
+ const nW = preview.w;
566
+ const nH = preview.h;
567
+ const nC = preview.col;
568
+ const nR = preview.row;
542
569
  if (nW !== r.previewW || nH !== r.previewH || nC !== r.previewCol || nR !== r.previewRow) {
543
570
  resize.value = { ...r, previewW: nW, previewH: nH, previewCol: nC, previewRow: nR };
544
571
  }
package/src/LoopGrid.vue CHANGED
@@ -80,6 +80,7 @@ import {
80
80
  import type { LoopTileInstance } from '@griddle/core';
81
81
  import type { GriddleApi } from './useGriddle.js';
82
82
  import { animateReposition } from './animation.js';
83
+ import { resolveResizePreview } from './interaction.js';
83
84
 
84
85
  const props = defineProps<{
85
86
  api: GriddleApi;
@@ -359,22 +360,30 @@ function onPointerMove(e: PointerEvent) {
359
360
  if (r) {
360
361
  const dx = e.clientX - r.startPointerX;
361
362
  const dy = e.clientY - r.startPointerY;
362
- let dw = 0, dh = 0, dcol = 0, drow = 0;
363
363
  const stepsX = Math.round(dx / colSize.value);
364
364
  const stepsY = Math.round(dy / rowSize.value);
365
- if (r.corner === 'se' || r.corner === 'ne') dw = stepsX;
366
- if (r.corner === 'se' || r.corner === 'sw') dh = stepsY;
367
- if (r.corner === 'sw' || r.corner === 'nw') { dw = -stepsX; dcol = stepsX; }
368
- if (r.corner === 'ne' || r.corner === 'nw') { dh = -stepsY; drow = stepsY; }
369
365
  const tile = props.api.grid.getTile(r.tileId);
370
- const minW = tile?.minW ?? 1;
371
- const minH = tile?.minH ?? 1;
372
- const maxW = Math.min(tile?.maxW ?? Infinity, config.value.cols);
373
- const maxH = Math.min(tile?.maxH ?? Infinity, config.value.rows);
374
- const nW = Math.min(maxW, Math.max(minW, r.startW + dw));
375
- const nH = Math.min(maxH, Math.max(minH, r.startH + dh));
376
- const nC = r.startCol + dcol;
377
- const nR = r.startRow + drow;
366
+ const preview = resolveResizePreview({
367
+ corner: r.corner,
368
+ startCol: r.startCol,
369
+ startRow: r.startRow,
370
+ startW: r.startW,
371
+ startH: r.startH,
372
+ stepsX,
373
+ stepsY,
374
+ minW: tile?.minW ?? 1,
375
+ minH: tile?.minH ?? 1,
376
+ maxW: tile?.maxW ?? Infinity,
377
+ maxH: tile?.maxH ?? Infinity,
378
+ cols: config.value.cols,
379
+ rows: config.value.rows,
380
+ infiniteX: config.value.infiniteX ?? config.value.cols === Infinity,
381
+ infiniteY: config.value.infiniteY ?? config.value.rows === Infinity,
382
+ });
383
+ const nW = preview.w;
384
+ const nH = preview.h;
385
+ const nC = preview.col;
386
+ const nR = preview.row;
378
387
  if (nW !== r.previewW || nH !== r.previewH || nC !== r.previewCol || nR !== r.previewRow) {
379
388
  resize.value = { ...r, previewW: nW, previewH: nH, previewCol: nC, previewRow: nR };
380
389
  }
@@ -3,6 +3,31 @@ export interface InteractionScale {
3
3
  y: number;
4
4
  }
5
5
 
6
+ export interface ResizePreviewInput {
7
+ corner: 'nw' | 'ne' | 'sw' | 'se';
8
+ startCol: number;
9
+ startRow: number;
10
+ startW: number;
11
+ startH: number;
12
+ stepsX: number;
13
+ stepsY: number;
14
+ minW: number;
15
+ minH: number;
16
+ maxW: number;
17
+ maxH: number;
18
+ cols: number;
19
+ rows: number;
20
+ infiniteX: boolean;
21
+ infiniteY: boolean;
22
+ }
23
+
24
+ export interface ResizePreview {
25
+ col: number;
26
+ row: number;
27
+ w: number;
28
+ h: number;
29
+ }
30
+
6
31
  type MeasurableElement = Pick<
7
32
  HTMLElement,
8
33
  'getBoundingClientRect' | 'offsetWidth' | 'offsetHeight'
@@ -51,3 +76,78 @@ export function toLocalInteractionDelta(
51
76
  dy: dy / scale.y,
52
77
  };
53
78
  }
79
+
80
+ function clamp(value: number, lower: number, upper: number): number {
81
+ if (upper < lower) return upper;
82
+ return Math.min(upper, Math.max(lower, value));
83
+ }
84
+
85
+ /**
86
+ * Resolve a corner resize while preserving the opposite edges and trimming
87
+ * the dragged edges at finite grid bounds.
88
+ */
89
+ export function resolveResizePreview({
90
+ corner,
91
+ startCol,
92
+ startRow,
93
+ startW,
94
+ startH,
95
+ stepsX,
96
+ stepsY,
97
+ minW,
98
+ minH,
99
+ maxW,
100
+ maxH,
101
+ cols,
102
+ rows,
103
+ infiniteX,
104
+ infiniteY,
105
+ }: ResizePreviewInput): ResizePreview {
106
+ const startRight = startCol + startW;
107
+ const startBottom = startRow + startH;
108
+ const east = corner === 'ne' || corner === 'se';
109
+ const south = corner === 'se' || corner === 'sw';
110
+
111
+ let col = startCol;
112
+ let right = startRight;
113
+ if (east) {
114
+ const maxRight = Math.min(
115
+ startCol + maxW,
116
+ infiniteX ? Infinity : cols,
117
+ );
118
+ right = clamp(startRight + stepsX, startCol + minW, maxRight);
119
+ } else {
120
+ const minLeft = Math.max(startRight - maxW, 0);
121
+ col = clamp(startCol + stepsX, minLeft, startRight - minW);
122
+ }
123
+
124
+ let row = startRow;
125
+ let bottom = startBottom;
126
+ if (south) {
127
+ const maxBottom = Math.min(
128
+ startRow + maxH,
129
+ infiniteY ? Infinity : rows,
130
+ );
131
+ bottom = clamp(startBottom + stepsY, startRow + minH, maxBottom);
132
+ } else {
133
+ const minTop = Math.max(startBottom - maxH, 0);
134
+ row = clamp(startRow + stepsY, minTop, startBottom - minH);
135
+ }
136
+
137
+ return {
138
+ col,
139
+ row,
140
+ w: right - col,
141
+ h: bottom - row,
142
+ };
143
+ }
144
+
145
+ /** Clamp a pointer-derived cell index to the cells addressable by the grid. */
146
+ export function clampInteractionCell(
147
+ cell: number,
148
+ extent: number,
149
+ infinite: boolean,
150
+ ): number {
151
+ const nonNegative = Math.max(0, cell);
152
+ return infinite ? nonNegative : Math.min(extent - 1, nonNegative);
153
+ }