Calendar drifts position when resized smaller #58

Open
opened 2026-04-22 12:20:51 +00:00 by eslamnawara · 5 comments
Member

Summary

Resizing the calendar by dragging the top-left corner anchor inward (to make
the calendar smaller) causes the calendar to move across the canvas instead
of shrinking in place. The more the user shrinks it, the further it drifts
from its original position.

Steps to reproduce

  1. Place a calendar on the whiteboard.
  2. Select it so the resize transformer appears.
  3. Grab the top-left corner anchor.
  4. Drag it inward (toward the bottom-right) to shrink the calendar.

Expected

The calendar shrinks while its bottom-right corner stays anchored in place
(standard behavior for a top-left resize handle). The calendar's overall
position on the canvas should not drift.

Actual

The calendar both shrinks and translates — its position moves down and to the
right as the drag progresses. When resized to a very small size, the calendar
ends up in a visibly different location from where it started.

## Summary Resizing the calendar by dragging the top-left corner anchor inward (to make the calendar smaller) causes the calendar to move across the canvas instead of shrinking in place. The more the user shrinks it, the further it drifts from its original position. ## Steps to reproduce 1. Place a calendar on the whiteboard. 2. Select it so the resize transformer appears. 3. Grab the top-left corner anchor. 4. Drag it inward (toward the bottom-right) to shrink the calendar. ## Expected The calendar shrinks while its bottom-right corner stays anchored in place (standard behavior for a top-left resize handle). The calendar's overall position on the canvas should not drift. ## Actual The calendar both shrinks and translates — its position moves down and to the right as the drag progresses. When resized to a very small size, the calendar ends up in a visibly different location from where it started.
Author
Member

still an issue after setting brave shield down

still an issue after setting brave shield down
Member

Implementation Spec for Issue #58

Objective

Eliminate position drift during resize. When the user drags the top-left, top-*, or *-left anchor handles of any object's transformer, the object must shrink in place — its pivot corner (opposite the dragged anchor) must stay anchored, and the object must not translate across the canvas. This applies to all object types, not just calendar.

Requirements

  • Dragging any corner/edge anchor smaller shrinks the object in place — the opposite corner/edge (the pivot) never moves.
  • Behaviour is unchanged for right-side / bottom-side anchor drags (where node.x / node.y were already fixed and Konva's default positioning was correct).
  • The fix is applied both to the live-redraw path (transform handler in tools.js, currently for calendar + kanban) and to the final transformend conversion path (applyTransform in objects.js, for every relevant type branch).
  • No refactor of object modules. One shared helper, used in two call sites.
  • Rotation is not covered (this fix targets rounding/clamping drift on scale→size conversion, not rotation math).

Root cause

Konva's Transformer maintains:

visibleBox.right  = node.x() + oldSize.w * scaleX
visibleBox.bottom = node.y() + oldSize.h * scaleY

and positions node.x / node.y so the pivot corner stays at the user's expected location. The key identity is that Konva uses oldSize * scale — not our later-committed newSize — as the size.

Our code then does:

newW = Math.max(minW, Math.round(oldW * scaleX));
newH = Math.max(minH, Math.round(oldH * scaleY));
node.scaleX(1); node.scaleY(1);

For clamp (especially), newW !== oldW * scaleX. For left-anchor or top-anchor drags, Konva shifted node.x / node.y assuming the pre-clamp size; after we commit the clamped size, the pivot edge moves by the delta → visible drift.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js — add shared helper correctResizeDrift, expose it, and call it inside the transform live-redraw handler (calendar + kanban branches).
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js — call WhiteboardTools.correctResizeDrift(...) inside every applyTransform type branch that mutates size.

Implementation Plan

Step 1 — Add shared helper in tools.js

Files: tools.js

  • Near the top of the WhiteboardTools IIFE (after transformer is declared), add:
    var LEFT_ANCHORS = { 'top-left': 1, 'middle-left': 1, 'bottom-left': 1 };
    var TOP_ANCHORS  = { 'top-left': 1, 'top-center': 1, 'top-right': 1 };
    
    function correctResizeDrift(node, expectedW, expectedH, newW, newH) {
        if (!transformer) return;
        var anchor = transformer.getActiveAnchor ? transformer.getActiveAnchor() : null;
        if (!anchor || anchor === 'rotater') return;
        if (LEFT_ANCHORS[anchor]) node.x(node.x() + (expectedW - newW));
        if (TOP_ANCHORS[anchor])  node.y(node.y() + (expectedH - newH));
    }
    
  • Export via the module's return object: correctResizeDrift: correctResizeDrift.

Dependencies: none.

Step 2 — Wire into the transform live-redraw handler in tools.js

Files: tools.js

  • Calendar branch: before mutating bg, capture oldW = bg.width(), oldH = bg.height(), expectedW = oldW * sx, expectedH = oldH * sy. Apply clamp + round as today. After node.scaleX(1); node.scaleY(1);, call correctResizeDrift(node, expectedW, expectedH, newW, newH); before WhiteboardCalendar.redraw(node);.
  • Kanban branch: read oldBg = node.findOne('.bg'); oldW = oldBg.width(); oldH = oldBg.height(); BEFORE mutating _kanbanState and calling WhiteboardKanban.redraw(node) (because renderKanban destroys children). After redraw, read newBg = node.findOne('.bg'); compute newW = newBg.width(), newH = newBg.height(); call correctResizeDrift.

Dependencies: Step 1.

Step 3 — Wire into applyTransform branches in objects.js

Files: objects.js

  • For each type branch that mutates size, capture oldW / oldH (per table below), compute expectedW = oldW * scaleX, expectedH = oldH * scaleY, commit newW / newH via existing logic, reset scale to 1, then call WhiteboardTools.correctResizeDrift(node, expectedW, expectedH, newW, newH).

Per-type oldW/oldH source:

Type oldW source oldH source
sticky / frame / document / image / webframe / emoji bg.width() bg.height()
text skip (bg is auto-sized from content; drift correction not meaningful)
shape — Rect/rounded_rect bg.width() bg.height()
shape — Ellipse bg.radiusX()*2 bg.radiusY()*2
shape — RegularPolygon bg.radius()*2 same
shape — Star bg.outerRadius()*2 same
shape — Path/Line bg._nominalWidth bg._nominalHeight
calendar bg.width() bg.height()
kanban bg.width() (before redraw destroys it) bg.height()
mindmap intrinsic = node.getClientRect({skipTransform:true}); intrinsic.width intrinsic.heightexpectedW = intrinsic.width * scaleX, newW = intrinsic.width * finalClampedScale
drawing / generic skip (no size mutation, so no drift)
  • Shape branch: compute expectedW/H once above the cls switch from the relevant oldW/oldH source per class, capture newW/H inside each sub-branch, call the helper once at the end of the shape branch after the outer node.scaleX(1); node.scaleY(1);.
  • Kanban branch: mirror Step 2 (read oldBg.width/height BEFORE state mutation + redraw).
  • Text branch: no call (size is content-derived).
  • Drawing / generic: no call (size unchanged).

Dependencies: Step 1.

Acceptance Criteria

  • Dragging the top-left anchor of a calendar inward keeps the bottom-right corner fixed at its initial canvas position from start to end of the drag, including when minW/minH clamping kicks in.
  • Same test passes for: sticky, text, shape (Rect, Ellipse, Circle, RegularPolygon, Star, Path/Line), frame, document, image, webframe, kanban, mindmap, emoji, drawing.
  • All anchor handles preserve their correct pivot:
    • top-left → bottom-right fixed
    • top-center → bottom edge fixed
    • top-right → bottom-left fixed
    • middle-left → right edge fixed
    • middle-right → left edge fixed (unchanged)
    • bottom-left → top-right fixed
    • bottom-center → top edge fixed (unchanged)
    • bottom-right → top-left fixed (unchanged)
  • Clamping at minimum size no longer produces visible drift (the clamp overshoot is absorbed into node.x / node.y, not into position).
  • Rotater anchor untouched — rotation behaviour unchanged.
  • No regression on multi-object transform (same anchor applies to all selected nodes; each gets its own correction).

Notes

The math, restated. Konva places node.x = pivot.x - oldW * scaleX for left-anchor drags. We commit size = newW, so the rendered right edge is node.x + newW. To restore right = pivot.x, shift node.x += expectedW - newW where expectedW = oldW * scaleX. Symmetric for y with TOP anchors.

Why bg dimensions, not getClientRect. bg is exact, matches what every branch already mutates, and its value before the mutation is the authoritative oldW/oldH. getClientRect would need callers to juggle before/after scale-reset and be re-derived from what we ultimately care about (bg).

Mindmap is special because it doesn't reset scale to 1 — it bakes a clamped uniform scale onto the group. The intrinsic (unscaled) bbox provides oldW/oldH; expectedW = intrinsic.w * scaleX, newW = intrinsic.w * clampedUniformScale. The helper handles the remaining drift from the scaleX vs finalScale asymmetry.

Interaction with PR #63 / PR #66. PR #63 introduced the transform live-redraw; this spec layers one helper call per branch on top with no change to redraw timing. PR #66's _colLayout WeakMap is rebuilt per render and does not change here. Kanban reads oldBg.width()/height() BEFORE renderKanban destroys children.

Out of scope. Rotation drift (different math). Drift inside shape sub-bg positions (bg.x(round(bg.x()*scaleX))) — the outer correction makes the user-visible pivot stable regardless. Drawing's pre-existing size-snap-back behaviour (separate bug).

## Implementation Spec for Issue #58 ### Objective Eliminate position drift during resize. When the user drags the top-left, top-*, or *-left anchor handles of any object's transformer, the object must shrink in place — its pivot corner (opposite the dragged anchor) must stay anchored, and the object must not translate across the canvas. This applies to all object types, not just calendar. ### Requirements - Dragging any corner/edge anchor smaller shrinks the object in place — the opposite corner/edge (the pivot) never moves. - Behaviour is unchanged for right-side / bottom-side anchor drags (where `node.x` / `node.y` were already fixed and Konva's default positioning was correct). - The fix is applied both to the live-redraw path (`transform` handler in `tools.js`, currently for calendar + kanban) and to the final `transformend` conversion path (`applyTransform` in `objects.js`, for every relevant type branch). - No refactor of object modules. One shared helper, used in two call sites. - Rotation is not covered (this fix targets rounding/clamping drift on scale→size conversion, not rotation math). ### Root cause Konva's Transformer maintains: ``` visibleBox.right = node.x() + oldSize.w * scaleX visibleBox.bottom = node.y() + oldSize.h * scaleY ``` and positions `node.x` / `node.y` so the pivot corner stays at the user's expected location. The key identity is that Konva uses `oldSize * scale` — not our later-committed `newSize` — as the size. Our code then does: ``` newW = Math.max(minW, Math.round(oldW * scaleX)); newH = Math.max(minH, Math.round(oldH * scaleY)); node.scaleX(1); node.scaleY(1); ``` For clamp (especially), `newW !== oldW * scaleX`. For left-anchor or top-anchor drags, Konva shifted `node.x` / `node.y` assuming the pre-clamp size; after we commit the clamped size, the pivot edge moves by the delta → visible drift. ### Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` — add shared helper `correctResizeDrift`, expose it, and call it inside the `transform` live-redraw handler (calendar + kanban branches). - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` — call `WhiteboardTools.correctResizeDrift(...)` inside every `applyTransform` type branch that mutates size. ### Implementation Plan #### Step 1 — Add shared helper in tools.js Files: `tools.js` - Near the top of the `WhiteboardTools` IIFE (after `transformer` is declared), add: ``` var LEFT_ANCHORS = { 'top-left': 1, 'middle-left': 1, 'bottom-left': 1 }; var TOP_ANCHORS = { 'top-left': 1, 'top-center': 1, 'top-right': 1 }; function correctResizeDrift(node, expectedW, expectedH, newW, newH) { if (!transformer) return; var anchor = transformer.getActiveAnchor ? transformer.getActiveAnchor() : null; if (!anchor || anchor === 'rotater') return; if (LEFT_ANCHORS[anchor]) node.x(node.x() + (expectedW - newW)); if (TOP_ANCHORS[anchor]) node.y(node.y() + (expectedH - newH)); } ``` - Export via the module's return object: `correctResizeDrift: correctResizeDrift`. Dependencies: none. #### Step 2 — Wire into the `transform` live-redraw handler in tools.js Files: `tools.js` - **Calendar branch**: before mutating bg, capture `oldW = bg.width()`, `oldH = bg.height()`, `expectedW = oldW * sx`, `expectedH = oldH * sy`. Apply clamp + round as today. After `node.scaleX(1); node.scaleY(1);`, call `correctResizeDrift(node, expectedW, expectedH, newW, newH);` before `WhiteboardCalendar.redraw(node);`. - **Kanban branch**: read `oldBg = node.findOne('.bg'); oldW = oldBg.width(); oldH = oldBg.height();` BEFORE mutating `_kanbanState` and calling `WhiteboardKanban.redraw(node)` (because `renderKanban` destroys children). After redraw, read `newBg = node.findOne('.bg')`; compute `newW = newBg.width()`, `newH = newBg.height()`; call `correctResizeDrift`. Dependencies: Step 1. #### Step 3 — Wire into `applyTransform` branches in objects.js Files: `objects.js` - For each type branch that mutates size, capture `oldW` / `oldH` (per table below), compute `expectedW = oldW * scaleX`, `expectedH = oldH * scaleY`, commit `newW` / `newH` via existing logic, reset scale to 1, then call `WhiteboardTools.correctResizeDrift(node, expectedW, expectedH, newW, newH)`. Per-type oldW/oldH source: | Type | oldW source | oldH source | |------|-------------|-------------| | sticky / frame / document / image / webframe / emoji | `bg.width()` | `bg.height()` | | text | skip (bg is auto-sized from content; drift correction not meaningful) | | shape — Rect/rounded_rect | `bg.width()` | `bg.height()` | | shape — Ellipse | `bg.radiusX()*2` | `bg.radiusY()*2` | | shape — RegularPolygon | `bg.radius()*2` | same | | shape — Star | `bg.outerRadius()*2` | same | | shape — Path/Line | `bg._nominalWidth` | `bg._nominalHeight` | | calendar | `bg.width()` | `bg.height()` | | kanban | `bg.width()` (before redraw destroys it) | `bg.height()` | | mindmap | `intrinsic = node.getClientRect({skipTransform:true}); intrinsic.width` | `intrinsic.height` — `expectedW = intrinsic.width * scaleX`, `newW = intrinsic.width * finalClampedScale` | | drawing / generic | skip (no size mutation, so no drift) | - Shape branch: compute `expectedW/H` once above the `cls` switch from the relevant oldW/oldH source per class, capture `newW/H` inside each sub-branch, call the helper once at the end of the shape branch after the outer `node.scaleX(1); node.scaleY(1);`. - Kanban branch: mirror Step 2 (read oldBg.width/height BEFORE state mutation + redraw). - Text branch: no call (size is content-derived). - Drawing / generic: no call (size unchanged). Dependencies: Step 1. ### Acceptance Criteria - [ ] Dragging the top-left anchor of a calendar inward keeps the bottom-right corner fixed at its initial canvas position from start to end of the drag, including when minW/minH clamping kicks in. - [ ] Same test passes for: sticky, text, shape (Rect, Ellipse, Circle, RegularPolygon, Star, Path/Line), frame, document, image, webframe, kanban, mindmap, emoji, drawing. - [ ] All anchor handles preserve their correct pivot: - `top-left` → bottom-right fixed - `top-center` → bottom edge fixed - `top-right` → bottom-left fixed - `middle-left` → right edge fixed - `middle-right` → left edge fixed (unchanged) - `bottom-left` → top-right fixed - `bottom-center` → top edge fixed (unchanged) - `bottom-right` → top-left fixed (unchanged) - [ ] Clamping at minimum size no longer produces visible drift (the clamp overshoot is absorbed into `node.x` / `node.y`, not into position). - [ ] Rotater anchor untouched — rotation behaviour unchanged. - [ ] No regression on multi-object transform (same anchor applies to all selected nodes; each gets its own correction). ### Notes **The math, restated.** Konva places `node.x = pivot.x - oldW * scaleX` for left-anchor drags. We commit size = `newW`, so the rendered right edge is `node.x + newW`. To restore `right = pivot.x`, shift `node.x += expectedW - newW` where `expectedW = oldW * scaleX`. Symmetric for y with TOP anchors. **Why bg dimensions, not getClientRect.** bg is exact, matches what every branch already mutates, and its value before the mutation is the authoritative `oldW/oldH`. `getClientRect` would need callers to juggle before/after scale-reset and be re-derived from what we ultimately care about (bg). **Mindmap is special** because it doesn't reset scale to 1 — it bakes a clamped uniform scale onto the group. The intrinsic (unscaled) bbox provides `oldW/oldH`; `expectedW = intrinsic.w * scaleX`, `newW = intrinsic.w * clampedUniformScale`. The helper handles the remaining drift from the `scaleX vs finalScale` asymmetry. **Interaction with PR #63 / PR #66.** PR #63 introduced the `transform` live-redraw; this spec layers one helper call per branch on top with no change to redraw timing. PR #66's `_colLayout` WeakMap is rebuilt per render and does not change here. Kanban reads `oldBg.width()/height()` BEFORE `renderKanban` destroys children. **Out of scope.** Rotation drift (different math). Drift inside shape sub-bg positions (`bg.x(round(bg.x()*scaleX))`) — the outer correction makes the user-visible pivot stable regardless. Drawing's pre-existing size-snap-back behaviour (separate bug).
Member

Test Results

JavaScript-only change in tools.js and objects.js; Rust workspace validated for regressions.

Check Result
cargo check --workspace pass
cargo test --workspace --lib pass (0 executed)
cargo clippy --workspace -- -D warnings pass
cargo fmt --check pass

No Rust tests exercise the JavaScript whiteboard modules, so the suite confirms regression safety on the Rust side. Manual verification against a running UI is required to confirm the pivot corner stays anchored across every object type, anchor handle, and clamp edge case listed in the spec's Acceptance Criteria.

## Test Results JavaScript-only change in `tools.js` and `objects.js`; Rust workspace validated for regressions. | Check | Result | |---|---| | `cargo check --workspace` | pass | | `cargo test --workspace --lib` | pass (0 executed) | | `cargo clippy --workspace -- -D warnings` | pass | | `cargo fmt --check` | pass | No Rust tests exercise the JavaScript whiteboard modules, so the suite confirms regression safety on the Rust side. Manual verification against a running UI is required to confirm the pivot corner stays anchored across every object type, anchor handle, and clamp edge case listed in the spec's Acceptance Criteria.
Member

Implementation Summary

Position drift during resize is eliminated for every object type on the canvas, not just calendar. Dragging any left or top anchor shrinks the object in place — the pivot corner stays fixed — even when the new size is clamped to the type's minimum.

Files changed

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js (+35 / -2): shared correctResizeDrift helper plus wiring for the live-redraw path (calendar + kanban).
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js (+169 / -12): wiring inside every applyTransform type branch that mutates size.

Root cause

Konva's Transformer sets node.x / node.y assuming the visible size equals oldSize * scale. When our code rounds or clamps the committed size to newSize, left/top-anchored drags shift the pivot by the delta, producing visible drift — most obvious on calendar, which clamps to a per-view minimum, but present to some degree anywhere size is rounded or clamped.

Fix

A single helper in tools.js:

function correctResizeDrift(node, expectedW, expectedH, newW, newH) {
    if (!transformer) return;
    var anchor = transformer.getActiveAnchor();
    if (!anchor || anchor === 'rotater') return;
    if (LEFT_ANCHORS[anchor]) node.x(node.x() + (expectedW - newW));
    if (TOP_ANCHORS[anchor]) node.y(node.y() + (expectedH - newH));
}

Applied at two call sites:

  • tools.js transform live-redraw: calendar and kanban branches capture old bg dimensions before mutating / redrawing, compute expected = old * scale, and call the helper with the new bg dimensions after redraw.
  • objects.js applyTransform: every type branch that mutates size (sticky, shape with each cls, frame, document, image, webframe, emoji, kanban, calendar, mindmap) captures oldW/oldH, computes expected/new, and calls the helper after node.scaleX(1); node.scaleY(1);. text and drawing/generic are skipped because they do not meaningfully resize.

Mindmap is handled specially because it does not reset scale to 1 — it bakes a clamped uniform scale onto the group. The helper is called with the intrinsic bbox (via getClientRect({skipTransform: true})) multiplied by scaleX/scaleY (expected) vs. the clamped final scale (new).

In addition, the shape else fallback was tightened to else if (cls === 'Rect') so an unknown shape class falls through without a bogus drift correction; the six known shape classes (Rect, Ellipse, RegularPolygon, Star, Path, Line) are all explicitly handled.

Preserved behaviour

  • PR #63 (calendar + kanban live-redraw): unchanged timing, same transformer.forceUpdate() call; the helper is an additional call after each branch's scale reset.
  • PR #66 (kanban _colLayout WeakMap): rebuilt by renderKanban as before; we read oldBg.width()/height() BEFORE calling WhiteboardKanban.redraw(node) so we don't dereference a destroyed node.
  • Rotater anchor: untouched by the helper. Right-only and bottom-only anchor drags continue to work identically.
  • Multi-object transform: the helper uses transformer.getActiveAnchor() which returns the same anchor for every selected node, so each gets its own correction simultaneously.

Test results

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

Manual QA required: verify each object type's pivot corner stays put when the top-left anchor is dragged inward, including at the clamp minimum; test the remaining seven anchor handles; confirm no regression on rotation and on multi-select transforms.

## Implementation Summary Position drift during resize is eliminated for every object type on the canvas, not just calendar. Dragging any left or top anchor shrinks the object in place — the pivot corner stays fixed — even when the new size is clamped to the type's minimum. ### Files changed - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` (+35 / -2): shared `correctResizeDrift` helper plus wiring for the live-redraw path (calendar + kanban). - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` (+169 / -12): wiring inside every `applyTransform` type branch that mutates size. ### Root cause Konva's Transformer sets `node.x` / `node.y` assuming the visible size equals `oldSize * scale`. When our code rounds or clamps the committed size to `newSize`, left/top-anchored drags shift the pivot by the delta, producing visible drift — most obvious on calendar, which clamps to a per-view minimum, but present to some degree anywhere size is rounded or clamped. ### Fix A single helper in `tools.js`: ``` function correctResizeDrift(node, expectedW, expectedH, newW, newH) { if (!transformer) return; var anchor = transformer.getActiveAnchor(); if (!anchor || anchor === 'rotater') return; if (LEFT_ANCHORS[anchor]) node.x(node.x() + (expectedW - newW)); if (TOP_ANCHORS[anchor]) node.y(node.y() + (expectedH - newH)); } ``` Applied at two call sites: - `tools.js` `transform` live-redraw: calendar and kanban branches capture old bg dimensions before mutating / redrawing, compute `expected = old * scale`, and call the helper with the new bg dimensions after redraw. - `objects.js` `applyTransform`: every type branch that mutates size (sticky, shape with each `cls`, frame, document, image, webframe, emoji, kanban, calendar, mindmap) captures `oldW/oldH`, computes expected/new, and calls the helper after `node.scaleX(1); node.scaleY(1);`. `text` and `drawing`/generic are skipped because they do not meaningfully resize. Mindmap is handled specially because it does not reset scale to 1 — it bakes a clamped uniform scale onto the group. The helper is called with the intrinsic bbox (via `getClientRect({skipTransform: true})`) multiplied by `scaleX/scaleY` (expected) vs. the clamped final scale (new). In addition, the shape `else` fallback was tightened to `else if (cls === 'Rect')` so an unknown shape class falls through without a bogus drift correction; the six known shape classes (Rect, Ellipse, RegularPolygon, Star, Path, Line) are all explicitly handled. ### Preserved behaviour - PR #63 (calendar + kanban live-redraw): unchanged timing, same `transformer.forceUpdate()` call; the helper is an additional call after each branch's scale reset. - PR #66 (kanban `_colLayout` WeakMap): rebuilt by `renderKanban` as before; we read `oldBg.width()/height()` BEFORE calling `WhiteboardKanban.redraw(node)` so we don't dereference a destroyed node. - Rotater anchor: untouched by the helper. Right-only and bottom-only anchor drags continue to work identically. - Multi-object transform: the helper uses `transformer.getActiveAnchor()` which returns the same anchor for every selected node, so each gets its own correction simultaneously. ### Test results - `cargo check --workspace`: pass - `cargo test --workspace --lib`: pass - `cargo clippy --workspace -- -D warnings`: pass - `cargo fmt --check`: pass Manual QA required: verify each object type's pivot corner stays put when the top-left anchor is dragged inward, including at the clamp minimum; test the remaining seven anchor handles; confirm no regression on rotation and on multi-select transforms.
Member

Pull request opened: #68

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_whiteboard/pulls/68 This PR implements the changes discussed in this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#58
No description provided.