fix(resize): keep pivot corner fixed across all object types #68

Merged
AhmedHanafy725 merged 5 commits from development_fix_resize_drift_all_objects into development 2026-04-23 11:29:15 +00:00
Member

Summary

Resizing any object via the top-left (or any left/top) anchor now shrinks it in place. The pivot corner (opposite the dragged anchor) stays anchored even when the new size is clamped to the type's minimum. Applies to every object type, not just calendar.

Closes #58

Root cause

Konva's Transformer sets node.x/node.y each tick assuming the visible size equals oldSize * scale. When our code rounds or clamps the committed size to something different (e.g. calendar's per-view minimum, kanban's colWidth >= 100), left/top-anchored drags shift the pivot corner by the delta. The more we clamp, the more the object drifts.

Changes

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js
    • New helper correctResizeDrift(node, expectedW, expectedH, newW, newH). Reads the active anchor from the transformer; shifts node.x by (expectedW - newW) when a left anchor is active and node.y by (expectedH - newH) when a top anchor is active. No-op for rotater and for right/bottom-only anchors.
    • Wired into the existing transform live-redraw handler for both calendar and kanban. For kanban, old bg width/height are read BEFORE WhiteboardKanban.redraw destroys children; new bg is re-found afterwards.
    • Exposed as WhiteboardTools.correctResizeDrift.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js
    • applyTransform now calls the helper in every branch that mutates size: sticky, shape (Rect, Ellipse, RegularPolygon, Star, Path, Line), frame, document, image, webframe, emoji, calendar, kanban, mindmap. text and drawing/generic branches are deliberately skipped because they do not resize meaningfully.
    • Mindmap uses getClientRect({skipTransform: true}) to read its intrinsic bbox and compares expectedW = intrinsic.width * scaleX vs newW = intrinsic.width * finalClampedScale (mindmap bakes a uniform scale onto the group instead of resetting to 1).
    • The shape fallback was tightened from a bare else to else if (cls === 'Rect'), so an unknown cls falls through without a bogus drift correction. All six shape classes currently used are explicitly handled.

Preserved

  • PR #63 live-redraw timing and transformer.forceUpdate().
  • PR #66 kanban _colLayout WeakMap (rebuilt per render as before).
  • Rotation behaviour (rotater anchor does not invoke the helper).
  • Right-only and bottom-only anchor drags (already correct; helper is a no-op for those).
  • Multi-object transform (same active anchor applies to every selected node simultaneously).

Test Results

  • cargo check --workspace: pass
  • cargo test --workspace --lib: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo fmt --check: pass

JS-only change; Rust checks confirm no regression. Manual QA required to verify the pivot corner stays anchored for every object type and every anchor handle, including at the clamp minima.

## Summary Resizing any object via the top-left (or any left/top) anchor now shrinks it in place. The pivot corner (opposite the dragged anchor) stays anchored even when the new size is clamped to the type's minimum. Applies to every object type, not just calendar. ## Related Issue Closes https://forge.ourworld.tf/lhumina_code/hero_whiteboard/issues/58 ## Root cause Konva's Transformer sets `node.x`/`node.y` each tick assuming the visible size equals `oldSize * scale`. When our code rounds or clamps the committed size to something different (e.g. calendar's per-view minimum, kanban's `colWidth >= 100`), left/top-anchored drags shift the pivot corner by the delta. The more we clamp, the more the object drifts. ## Changes - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` - New helper `correctResizeDrift(node, expectedW, expectedH, newW, newH)`. Reads the active anchor from the transformer; shifts `node.x` by `(expectedW - newW)` when a left anchor is active and `node.y` by `(expectedH - newH)` when a top anchor is active. No-op for rotater and for right/bottom-only anchors. - Wired into the existing `transform` live-redraw handler for both calendar and kanban. For kanban, old bg width/height are read BEFORE `WhiteboardKanban.redraw` destroys children; new bg is re-found afterwards. - Exposed as `WhiteboardTools.correctResizeDrift`. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` - `applyTransform` now calls the helper in every branch that mutates size: `sticky`, `shape` (Rect, Ellipse, RegularPolygon, Star, Path, Line), `frame`, `document`, `image`, `webframe`, `emoji`, `calendar`, `kanban`, `mindmap`. `text` and `drawing`/generic branches are deliberately skipped because they do not resize meaningfully. - Mindmap uses `getClientRect({skipTransform: true})` to read its intrinsic bbox and compares `expectedW = intrinsic.width * scaleX` vs `newW = intrinsic.width * finalClampedScale` (mindmap bakes a uniform scale onto the group instead of resetting to 1). - The shape fallback was tightened from a bare `else` to `else if (cls === 'Rect')`, so an unknown `cls` falls through without a bogus drift correction. All six shape classes currently used are explicitly handled. ## Preserved - PR #63 live-redraw timing and `transformer.forceUpdate()`. - PR #66 kanban `_colLayout` WeakMap (rebuilt per render as before). - Rotation behaviour (rotater anchor does not invoke the helper). - Right-only and bottom-only anchor drags (already correct; helper is a no-op for those). - Multi-object transform (same active anchor applies to every selected node simultaneously). ## Test Results - `cargo check --workspace`: pass - `cargo test --workspace --lib`: pass - `cargo clippy --workspace -- -D warnings`: pass - `cargo fmt --check`: pass JS-only change; Rust checks confirm no regression. Manual QA required to verify the pivot corner stays anchored for every object type and every anchor handle, including at the clamp minima.
fix(resize): keep pivot corner fixed across all object types
Some checks failed
CI / build (pull_request) Has been cancelled
788a7ccb0a
Konva's Transformer sets node.x / node.y each tick assuming the visible
size equals oldSize * scale. When our applyTransform and live-redraw
paths round or clamp the committed size to something other than
oldSize * scale, left/top-anchored drags shift the pivot corner by the
delta, producing visible drift (most obvious on calendar past its
minimum).

Introduce a shared correctResizeDrift helper in tools.js that shifts
node.x by (expectedW - newW) for left anchors and node.y by
(expectedH - newH) for top anchors. Wire it into the calendar and
kanban live-redraw branches and into every applyTransform branch that
mutates size: sticky, shape (Rect, Ellipse, RegularPolygon, Star,
Path, Line), frame, document, image, webframe, emoji, calendar,
kanban, and mindmap. Text and drawing branches are skipped because
they do not resize.

Mindmap is handled by reading the intrinsic bbox via
getClientRect({skipTransform: true}) and comparing expectedW =
intrinsic.width * scaleX against newW = intrinsic.width *
finalClampedScale, since mindmap bakes a uniform scale onto the group
instead of resetting scale to 1.

The shape else-fallback is tightened to `else if (cls === 'Rect')`
so unknown classes fall through without a bogus drift correction;
all six currently used shape classes are explicitly handled.

#58
fix(resize): compute expected size from transformstart snapshot
Some checks failed
CI / build (pull_request) Failing after 2s
c6bca71e35
Konva's Transformer computes each tick's scale relative to the bounding
box sampled at transformstart, not the current mutated bg. The previous
drift fix computed expectedW = currentBgWidth * scaleX, which matched
Konva on tick 1 but diverged on later ticks once our live-redraw had
mutated bg. Once the clamp engaged, the mismatch between our expected
and Konva's internal expected compounded each tick into visible drift.

Snapshot bg.width / bg.height (and the kanban colWidth / cardHeight)
at transformstart, use those as the basis for expectedW / expectedH on
every tick, and clear the snapshot at transformend. Scale commits now
always derive from the original values so correctResizeDrift sees the
true Konva-expected size and compensates for clamp overshoot exactly.
fix(resize): re-anchor node to snapshotted pivot instead of scale-derived math
All checks were successful
CI / build (pull_request) Successful in 2m10s
a3b7052781
The previous drift correction shifted node.x by (expectedW - newW), which
required expectedW to match Konva's internal per-tick scale math. For
left-anchor drags past the minimum, Konva keeps committing ever-smaller
scales while our clamp keeps committing the same min size, so the
difference (expectedW - newW) grows each tick and the shift compounded
into visible drift regardless of whether we sampled bg width at tick
start or at transformstart.

Snapshot the initial visible right edge (node.x + bg.width) and bottom
edge at transformstart, and after each live redraw set node.x =
pivotX - newW and node.y = pivotY - newH directly. The new node
position no longer depends on Konva's scale math, so clamping at the
minimum keeps the pivot exactly where it was when the drag began.
fix(resize): enforce per-type minimum via boundBoxFunc instead of clamping
All checks were successful
CI / build (pull_request) Successful in 2m10s
231c1cf054
The previous approach clamped bg.width / bg.height inside the transform
handler after Konva had already committed a smaller scale. Konva kept
tracking the cursor past our minimum while we held the size fixed, so
the user saw the object continue to translate across the canvas even
though the box had stopped shrinking. Position adjustments in the
handler could not fix this because Konva re-computed node.x/y from the
active pointer on every subsequent tick.

Move the minimum enforcement into Konva's own boundBoxFunc. When the
user drags past a calendar's view-specific minimum or a kanban's
computed minimum, boundBoxFunc returns the old box; Konva refuses the
transform entirely and the node and transformer handles both freeze
until the cursor moves back above the minimum. The transform handler
now applies only the scale -> bg conversion (with Math.round for sub
pixel cleanup) — no clamp, no drift.
fix(resize): let Konva handle pivot, enforce min via boundBoxFunc only
All checks were successful
CI / build (pull_request) Successful in 2m14s
3a76c8a24e
Previous attempts tried to correct drift by mutating node.x / node.y
inside the transform handler based on initW * sx. That math did not
match what Konva commits per tick (sx is measured against the node's
current width, which we were mutating every tick), so enlarging
produced the wrong size and shrinking below the minimum still drifted.

Drop all of the init-size snapshotting, pivot tracking, and
correctResizeDrift helper. The handler now just commits bg.width =
round(bg.width() * sx) (same as the original working code) and
resets scale to 1 — Konva's own math keeps the pivot fixed because
our committed width matches Konva's expected width for that tick.

Per-type minimums stay enforced inside boundBoxFunc. When the user
drags past a calendar's view-specific minimum or a kanban's computed
minimum, boundBoxFunc returns oldBox; Konva refuses the transform
entirely, so position and scale freeze until the cursor moves back
above the minimum.

Also revert the per-branch correctResizeDrift wiring in objects.js —
no longer needed now that Konva handles the pivot during drag and
applyTransform at transformend already commits size the same way the
original code did.
AhmedHanafy725 merged commit 3dcdd15f53 into development 2026-04-23 11:29:15 +00:00
AhmedHanafy725 deleted branch development_fix_resize_drift_all_objects 2026-04-23 11:29:19 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard!68
No description provided.