@griddle/vue 0.1.6 → 0.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@griddle/vue",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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.6"
56
+ "@griddle/core": "^0.1.8"
57
57
  },
58
58
  "devDependencies": {
59
59
  "vue": "^3.4.0",
60
- "@griddle/core": "^0.1.6",
60
+ "@griddle/core": "^0.1.8",
61
61
  "vite": "^5.4.0",
62
62
  "@vitejs/plugin-vue": "^5.2.0",
63
63
  "vue-tsc": "^2.2.0"
@@ -112,6 +112,13 @@ import {
112
112
  import type { TileLayout } from '@griddle/core';
113
113
  import type { GriddleApi } from './useGriddle.js';
114
114
  import { animateReposition, liftTransition } from './animation.js';
115
+ import {
116
+ measureInteractionScale,
117
+ clampInteractionCell,
118
+ resolveResizePreview,
119
+ toLocalInteractionDelta,
120
+ type InteractionScale,
121
+ } from './interaction.js';
115
122
 
116
123
  const props = defineProps<{
117
124
  api: GriddleApi;
@@ -208,6 +215,7 @@ interface DragVisual {
208
215
  const drag = ref<DragVisual | null>(null);
209
216
  let dragStartPointerX = 0;
210
217
  let dragStartPointerY = 0;
218
+ let interactionScale: InteractionScale = { x: 1, y: 1 };
211
219
 
212
220
  interface GroupDragVisual {
213
221
  tileIds: string[];
@@ -277,6 +285,7 @@ function beginPendingDrag(pending: PendingDragState): boolean {
277
285
 
278
286
  dragStartPointerX = pending.startPointerX;
279
287
  dragStartPointerY = pending.startPointerY;
288
+ interactionScale = measureInteractionScale(scrollEl.value);
280
289
 
281
290
  if (pending.mode === 'pin') {
282
291
  const layout = computeTileLayout({
@@ -399,11 +408,20 @@ function onBackgroundPointerDown(e: PointerEvent) {
399
408
 
400
409
  const el = scrollEl.value;
401
410
  if (!el) return;
411
+ interactionScale = measureInteractionScale(el);
402
412
  const rect = el.getBoundingClientRect();
403
- const x = e.clientX - rect.left + el.scrollLeft;
404
- const y = e.clientY - rect.top + el.scrollTop;
405
- const col = Math.floor(x / colSize.value);
406
- 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
+ );
407
425
  drawState.value = { anchorCol: col, anchorRow: row, currentCol: col, currentRow: row };
408
426
  (el as HTMLDivElement).setPointerCapture(e.pointerId);
409
427
  }
@@ -411,6 +429,7 @@ function onBackgroundPointerDown(e: PointerEvent) {
411
429
  function onResizeHandleDown(e: PointerEvent, tile: Tile, c: Corner) {
412
430
  if (tile.resizable === false) return;
413
431
  (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
432
+ interactionScale = measureInteractionScale(scrollEl.value);
414
433
  resize.value = {
415
434
  tileId: tile.id,
416
435
  corner: c,
@@ -434,10 +453,18 @@ function onPointerMove(e: PointerEvent) {
434
453
  const el = scrollEl.value;
435
454
  if (el) {
436
455
  const rect = el.getBoundingClientRect();
437
- const x = e.clientX - rect.left + el.scrollLeft;
438
- const y = e.clientY - rect.top + el.scrollTop;
439
- const col = Math.max(0, Math.floor(x / colSize.value));
440
- 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
+ );
441
468
  drawState.value = { ...drawState.value, currentCol: col, currentRow: row };
442
469
  }
443
470
  return;
@@ -461,15 +488,21 @@ function onPointerMove(e: PointerEvent) {
461
488
  const d = drag.value;
462
489
  const r = resize.value;
463
490
  if (pd) {
464
- const dx = e.clientX - pd.startPointerX;
465
- const dy = e.clientY - pd.startPointerY;
491
+ const { dx, dy } = toLocalInteractionDelta(
492
+ e.clientX - pd.startPointerX,
493
+ e.clientY - pd.startPointerY,
494
+ interactionScale,
495
+ );
466
496
  const newPinPx = { x: pd.startPinPx.x + dx, y: pd.startPinPx.y + dy };
467
497
  const newPin = pixelsToPin(newPinPx, config.value);
468
498
  props.api.grid.setTilePinned(pd.tileId, newPin);
469
499
  }
470
500
  if (gd) {
471
- const dx = e.clientX - dragStartPointerX;
472
- const dy = e.clientY - dragStartPointerY;
501
+ const { dx, dy } = toLocalInteractionDelta(
502
+ e.clientX - dragStartPointerX,
503
+ e.clientY - dragStartPointerY,
504
+ interactionScale,
505
+ );
473
506
  const dcol = Math.round(dx / colSize.value);
474
507
  const drow = Math.round(dy / rowSize.value);
475
508
  const result = groupDragController.update({ dcol, drow });
@@ -483,8 +516,11 @@ function onPointerMove(e: PointerEvent) {
483
516
  if (result.changed) syncTiles();
484
517
  }
485
518
  if (d) {
486
- const dx = e.clientX - dragStartPointerX;
487
- const dy = e.clientY - dragStartPointerY;
519
+ const { dx, dy } = toLocalInteractionDelta(
520
+ e.clientX - dragStartPointerX,
521
+ e.clientY - dragStartPointerY,
522
+ interactionScale,
523
+ );
488
524
  const candidateCol = d.pickupCol + Math.round(dx / colSize.value);
489
525
  const candidateRow = d.pickupRow + Math.round(dy / rowSize.value);
490
526
  const result = dragController.update({ col: candidateCol, row: candidateRow });
@@ -501,24 +537,35 @@ function onPointerMove(e: PointerEvent) {
501
537
  if (result.changed) syncTiles();
502
538
  }
503
539
  if (r) {
504
- const dx = e.clientX - r.startPointerX;
505
- const dy = e.clientY - r.startPointerY;
506
- let dw = 0, dh = 0, dcol = 0, drow = 0;
540
+ const { dx, dy } = toLocalInteractionDelta(
541
+ e.clientX - r.startPointerX,
542
+ e.clientY - r.startPointerY,
543
+ interactionScale,
544
+ );
507
545
  const stepsX = Math.round(dx / colSize.value);
508
546
  const stepsY = Math.round(dy / rowSize.value);
509
- if (r.corner === 'se' || r.corner === 'ne') dw = stepsX;
510
- if (r.corner === 'se' || r.corner === 'sw') dh = stepsY;
511
- if (r.corner === 'sw' || r.corner === 'nw') { dw = -stepsX; dcol = stepsX; }
512
- if (r.corner === 'ne' || r.corner === 'nw') { dh = -stepsY; drow = stepsY; }
513
547
  const tile = props.api.grid.getTile(r.tileId);
514
- const minW = tile?.minW ?? 1;
515
- const minH = tile?.minH ?? 1;
516
- const maxW = tile?.maxW ?? Infinity;
517
- const maxH = tile?.maxH ?? Infinity;
518
- const nW = Math.min(maxW, Math.max(minW, r.startW + dw));
519
- const nH = Math.min(maxH, Math.max(minH, r.startH + dh));
520
- const nC = r.startCol + dcol;
521
- 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;
522
569
  if (nW !== r.previewW || nH !== r.previewH || nC !== r.previewCol || nR !== r.previewRow) {
523
570
  resize.value = { ...r, previewW: nW, previewH: nH, previewCol: nC, previewRow: nR };
524
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
  }
@@ -0,0 +1,153 @@
1
+ export interface InteractionScale {
2
+ x: number;
3
+ y: number;
4
+ }
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
+
31
+ type MeasurableElement = Pick<
32
+ HTMLElement,
33
+ 'getBoundingClientRect' | 'offsetWidth' | 'offsetHeight'
34
+ >;
35
+
36
+ function validScale(value: number): number | null {
37
+ return Number.isFinite(value) && value > 0 ? value : null;
38
+ }
39
+
40
+ /**
41
+ * Measure how CSS transforms map an element's local coordinate space into
42
+ * viewport pixels. PointerEvent.clientX/clientY are viewport coordinates,
43
+ * while Griddle's cells and inline transforms use the element's local CSS
44
+ * pixels, so drag math must cross this boundary explicitly.
45
+ */
46
+ export function measureInteractionScale(
47
+ element: MeasurableElement | null,
48
+ ): InteractionScale {
49
+ if (!element) return { x: 1, y: 1 };
50
+
51
+ const rect = element.getBoundingClientRect();
52
+ const measuredX = validScale(
53
+ element.offsetWidth > 0 ? rect.width / element.offsetWidth : NaN,
54
+ );
55
+ const measuredY = validScale(
56
+ element.offsetHeight > 0 ? rect.height / element.offsetHeight : NaN,
57
+ );
58
+
59
+ // A content-sized grid can temporarily report a zero height while mounting.
60
+ // Prefer the other measured axis before falling back to an unscaled value;
61
+ // this also handles the common uniform transform: scale(...) host.
62
+ return {
63
+ x: measuredX ?? measuredY ?? 1,
64
+ y: measuredY ?? measuredX ?? 1,
65
+ };
66
+ }
67
+
68
+ /** Convert a viewport-space pointer delta into the grid's local CSS pixels. */
69
+ export function toLocalInteractionDelta(
70
+ dx: number,
71
+ dy: number,
72
+ scale: InteractionScale,
73
+ ): { dx: number; dy: number } {
74
+ return {
75
+ dx: dx / scale.x,
76
+ dy: dy / scale.y,
77
+ };
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
+ }