Skip to content

fix: preserve position during SE/S/E resize (prevent rounding drift)#3241

Merged
adumesny merged 1 commit intogridstack:masterfrom
KokXinTan:fix/resize-position-drift
Mar 19, 2026
Merged

fix: preserve position during SE/S/E resize (prevent rounding drift)#3241
adumesny merged 1 commit intogridstack:masterfrom
KokXinTan:fix/resize-position-drift

Conversation

@KokXinTan
Copy link
Copy Markdown
Contributor

@KokXinTan KokXinTan commented Mar 18, 2026

Problem

During resize events, _dragOrResize unconditionally recalculates x and y from pixel positions for all resize handle directions. For SE/S/E handles, the top-left corner is anchored and should never move. The pixel → grid round-trip through Math.round() introduces rounding errors that cause position drift.

History

This was introduced in commit a13fad9 (April 18, 2021, v4.2.1) — "fix sizing from top/left" (#1728, #1731). The previous code only handled left-side resizing with a targeted comparison. The fix replaced it with an unconditional Math.round(left / cellWidth) and Math.round(top / cellHeight) for all handles — correctly fixing N/W resize but introducing a rounding drift regression for SE/S/E handles.

Root Cause

// gridstack.ts, _dragOrResize, resize branch:
// if we size on left/top side this might move us, so get possible new position as well
const left = ui.position.left + mLeft;
const top = ui.position.top + mTop;
p.x = Math.round(left / cellWidth);   // ← recalculates x always
p.y = Math.round(top / cellHeight);   // ← recalculates y always

The comment is correct — this is needed for N/W/NW/NE/SW handles. But for SE/S/E handles the top-left is anchored and should remain node._orig.x / node._orig.y.

Why it drifts

The pixel → grid → pixel round-trip is lossy:

Original: y = 100, cellHeight = 3.526px
Pixel: 100 × 3.526 = 352.6px
Browser renders: ~355.0px (sub-pixel variation)
Back to grid: Math.round(355.0 / 3.526) = Math.round(100.68) = 101 ← DRIFTED

This is visible on any grid where cellHeight or cellWidth is small enough for Math.round() to tip over — not just extreme fine grids. Fractional cellHeight from vh/rem units also triggers it on standard column counts.

Reproduction

const grid = GridStack.init({
  column: 480,
  cellHeight: '0.38vh',  // ~3.5px per cell
  float: true,
  minRow: 240,
  maxRow: 240,
  resizable: { handles: 'se' }
});
grid.addWidget({ x: 100, y: 100, w: 50, h: 30 });
// Resize using SE handle → widget y drifts from 100 to 101 or 99

Fix

Two small changes:

  1. dd-resizable.ts: Expose the resize handle direction by attaching resizeDir to the synthesized event in _resizing()
  2. gridstack.ts: In _dragOrResize, only recalculate x/y when the handle actually moves the top-left corner (direction contains 'n' or 'w'). For SE/S/E handles, p already has the correct values from { ...node._orig }.

Testing

  • Fine grid (480 columns, cellHeight: '0.38vh'): SE resize no longer drifts position
  • Standard grid (12 columns): No behavior change
  • NW/N/W resize: Position still updates correctly
  • No new test failures (33 pre-existing failures in master, none added)

Related Issues

During resize events, _dragOrResize unconditionally recalculates x/y
from pixel positions for all handle directions. For SE/S/E handles,
the top-left corner is anchored and should never move. The pixel→grid
round-trip through Math.round() introduces rounding errors that cause
position drift, especially on fine grids with small cell sizes (e.g.
column:480, cellHeight:'0.38vh' ≈ 3.5px per cell).

This fix:
- Passes the resize handle direction (dir) from DDResizable._resizing()
  through the event object as event.resizeDir
- In _dragOrResize, only recalculates x/y when the handle moves the
  top-left corner (contains 'n' or 'w')
- For SE/S/E handles, preserves the original position from node._orig

Since p is initialized from { ...node._orig }, x/y naturally keep their
original values when not overwritten.

Fixes gridstack#385, relates to gridstack#811, gridstack#1356
Copy link
Copy Markdown
Member

@adumesny adumesny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the workaround. Still not sold why there are expected rounding errors, which would affect draggin from N/W then...

this.temporalRect = this._getChange(event, dir);
this._applyChange();
const ev = Utils.initEvent<MouseEvent>(event, { type: 'resize', target: this.el });
(ev as any).resizeDir = dir; // expose handle direction so _dragOrResize can avoid position drift
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a custom type for proper TS support.

@adumesny adumesny merged commit 8ec3dc9 into gridstack:master Mar 19, 2026
adumesny added a commit to adumesny/gridstack.js that referenced this pull request Mar 19, 2026
* added proper type checking to gridstack#3241
@adumesny adumesny mentioned this pull request Mar 19, 2026
3 tasks
adumesny added a commit that referenced this pull request Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants