Resize minimum-size limit grows as the user zooms out, making objects unshrinkable at low zoom #77

Open
opened 2026-04-27 10:45:53 +00:00 by AhmedHanafy725 · 3 comments
Member

Bug

The live resize floor enforced by the Konva Transformer's boundBoxFunc is interpreted as world units, but Konva supplies the box dimensions in absolute (stage-scaled) coordinates. As a result, the effective world-unit minimum grows as the user zooms out — at low zoom levels objects become impossible to shrink at all, even though their on-screen size is already small.

Reproduction

  1. Open the same board in two browser windows.
  2. Set window A to ~33% zoom (Ctrl+wheel out a few times) and window B to ~17% zoom.
  3. Place a calendar (or any object) in both windows.
  4. In window A, drag a corner handle inward — the calendar shrinks.
  5. In window B, drag a corner handle inward — the calendar refuses to shrink (or only shrinks a tiny amount).

Reproducible with calendar (most obvious), kanban, and any object once it gets near the 40x30 hard floor.

Root cause

In crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js (the transformer's boundBoxFunc):

boundBoxFunc: function(oldBox, newBox) {
    if (newBox.width < 40 || newBox.height < 30) return oldBox;
    ...
    if (minW > 0 && (newBox.width < minW || newBox.height < minH)) return oldBox;
}

Konva builds newBox in __getNodeShape as width: rect.width * absoluteScale.x. getAbsoluteScale() walks up to the stage and includes stage.scaleX(). So newBox.width = world_width * stage.scaleX() — i.e., screen pixels.

But the constants in this function (40, 30, calendar getMinSize() returns 280/260/etc., kanban cols * 108 + ...) are world units. Comparing world-unit constants against screen-pixel box widths means: the smaller the zoom, the larger the effective world-unit floor.

  • At zoom 1.0: floor = 280 world units (works correctly).
  • At zoom 0.33: floor = 280 / 0.33 ≈ 849 world units.
  • At zoom 0.17: floor = 280 / 0.17 ≈ 1647 world units (most calendars can't be shrunk at all).

Scope

Affects every resizable object — every selection routes through the same transformer's boundBoxFunc. So the fix needs to cover sticky, text, shape, frame, document, image, webframe, drawing, calendar, kanban, mindmap.

Expected behavior

The minimum-size limit should be evaluated in world units, independent of zoom. The user can shrink a calendar to its content-readability minimum (e.g., 280 world units for month view) at any zoom level, just as they can at 100% zoom.

Affected file

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js — the boundBoxFunc inside the Konva.Transformer config (around lines 51-71).

Only the live boundBoxFunc is affected. The post-resize per-type floors in objects.js (Math.max(60, Math.round(bg.width() * scaleX)), etc.) already work in world units because bg.width() is the unscaled property and scaleX is the relative transform factor — no fix needed there.

Fix sketch

Divide newBox.width and newBox.height by the stage's scale before comparing against the world-unit minimums:

var stage = WhiteboardCanvas.getStage && WhiteboardCanvas.getStage();
var sx = (stage && stage.scaleX && stage.scaleX()) || 1;
var sy = (stage && stage.scaleY && stage.scaleY()) || sx;
var worldW = newBox.width / sx;
var worldH = newBox.height / sy;
if (worldW < 40 || worldH < 30) return oldBox;
... if (minW > 0 && (worldW < minW || worldH < minH)) return oldBox;

Acceptance criteria

  • At any zoom level (10% -> 400%), every resizable object can be shrunk to the same world-unit minimum it could be shrunk to at 100% zoom.
  • Calendar can be shrunk to its getMinSize() floor (280x260 month, 320x220 week, 200x280 day) at any zoom.
  • Kanban can be shrunk to its formula floor at any zoom.
  • All other objects (sticky, text, shape, frame, document, image, webframe, drawing, mindmap) can be shrunk to the 40x30 hard floor at any zoom.
  • The hard floor still prevents zero/negative dimensions during drag.
  • No regression at 100% zoom — behavior is identical there.
  • Multi-select resize behaves correctly when the selected nodes are at different getAbsoluteScale values (the stage scale is what matters).
## Bug The live resize floor enforced by the Konva Transformer's `boundBoxFunc` is interpreted as world units, but Konva supplies the box dimensions in absolute (stage-scaled) coordinates. As a result, the effective world-unit minimum *grows* as the user zooms out — at low zoom levels objects become impossible to shrink at all, even though their on-screen size is already small. ## Reproduction 1. Open the same board in two browser windows. 2. Set window A to ~33% zoom (Ctrl+wheel out a few times) and window B to ~17% zoom. 3. Place a calendar (or any object) in both windows. 4. In window A, drag a corner handle inward — the calendar shrinks. 5. In window B, drag a corner handle inward — the calendar refuses to shrink (or only shrinks a tiny amount). Reproducible with calendar (most obvious), kanban, and any object once it gets near the 40x30 hard floor. ## Root cause In `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` (the transformer's `boundBoxFunc`): ```js boundBoxFunc: function(oldBox, newBox) { if (newBox.width < 40 || newBox.height < 30) return oldBox; ... if (minW > 0 && (newBox.width < minW || newBox.height < minH)) return oldBox; } ``` Konva builds `newBox` in `__getNodeShape` as `width: rect.width * absoluteScale.x`. `getAbsoluteScale()` walks up to the stage and includes `stage.scaleX()`. So `newBox.width = world_width * stage.scaleX()` — i.e., screen pixels. But the constants in this function (`40`, `30`, calendar `getMinSize()` returns 280/260/etc., kanban `cols * 108 + ...`) are world units. Comparing world-unit constants against screen-pixel box widths means: the smaller the zoom, the larger the *effective* world-unit floor. - At zoom 1.0: floor = 280 world units (works correctly). - At zoom 0.33: floor = 280 / 0.33 ≈ 849 world units. - At zoom 0.17: floor = 280 / 0.17 ≈ 1647 world units (most calendars can't be shrunk at all). ## Scope Affects every resizable object — every selection routes through the same transformer's `boundBoxFunc`. So the fix needs to cover sticky, text, shape, frame, document, image, webframe, drawing, calendar, kanban, mindmap. ## Expected behavior The minimum-size limit should be evaluated in **world units**, independent of zoom. The user can shrink a calendar to its content-readability minimum (e.g., 280 world units for month view) at any zoom level, just as they can at 100% zoom. ## Affected file - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` — the `boundBoxFunc` inside the `Konva.Transformer` config (around lines 51-71). Only the live `boundBoxFunc` is affected. The post-resize per-type floors in `objects.js` (`Math.max(60, Math.round(bg.width() * scaleX))`, etc.) already work in world units because `bg.width()` is the unscaled property and `scaleX` is the relative transform factor — no fix needed there. ## Fix sketch Divide `newBox.width` and `newBox.height` by the stage's scale before comparing against the world-unit minimums: ```js var stage = WhiteboardCanvas.getStage && WhiteboardCanvas.getStage(); var sx = (stage && stage.scaleX && stage.scaleX()) || 1; var sy = (stage && stage.scaleY && stage.scaleY()) || sx; var worldW = newBox.width / sx; var worldH = newBox.height / sy; if (worldW < 40 || worldH < 30) return oldBox; ... if (minW > 0 && (worldW < minW || worldH < minH)) return oldBox; ``` ## Acceptance criteria - [ ] At any zoom level (10% -> 400%), every resizable object can be shrunk to the same world-unit minimum it could be shrunk to at 100% zoom. - [ ] Calendar can be shrunk to its `getMinSize()` floor (280x260 month, 320x220 week, 200x280 day) at any zoom. - [ ] Kanban can be shrunk to its formula floor at any zoom. - [ ] All other objects (sticky, text, shape, frame, document, image, webframe, drawing, mindmap) can be shrunk to the 40x30 hard floor at any zoom. - [ ] The hard floor still prevents zero/negative dimensions during drag. - [ ] No regression at 100% zoom — behavior is identical there. - [ ] Multi-select resize behaves correctly when the selected nodes are at different `getAbsoluteScale` values (the stage scale is what matters).
Author
Member

Implementation Spec for Issue #77

Objective

Make the live resize floor zoom-independent by evaluating the transformer's boundBoxFunc thresholds in world units, so every object can be shrunk to the same minimum size regardless of the stage's current zoom level.

Requirements

  • The hard 40x30 floor and the per-type minimums (calendar, kanban) are compared in world units.
  • Behavior at 100% zoom is unchanged.
  • The fix covers every resizable object — sticky, text, shape, frame, document, image, webframe, drawing, calendar, kanban, mindmap — because all selections route through the same transformer.
  • Negative/zero box dimensions are still rejected (the safety guard that the original 40x30 floor provides).

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js — the boundBoxFunc defined on the Konva.Transformer (around lines 51-71).

No other file needs changes. The post-resize per-type floors in objects.js (Math.max(60, Math.round(bg.width() * scaleX)), etc.) already operate in world units (bg.width() is the unscaled property, scaleX is the relative transform factor) and continue to act as a back-stop after the user releases.

Implementation Plan

Step 1: Convert boundBoxFunc thresholds to world units

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js

Inside the existing boundBoxFunc(oldBox, newBox):

  1. Read the stage's current scale at the start of the function:
    var stage = (typeof WhiteboardCanvas !== 'undefined' && WhiteboardCanvas.getStage) ? WhiteboardCanvas.getStage() : null;
    var sx = (stage && stage.scaleX && stage.scaleX()) || 1;
    var sy = (stage && stage.scaleY && stage.scaleY()) || sx;
    
  2. Derive world-unit dimensions:
    var worldW = newBox.width / sx;
    var worldH = newBox.height / sy;
    
  3. Replace newBox.width / newBox.height with worldW / worldH in the two existing comparisons:
    • if (worldW < 40 || worldH < 30) return oldBox;
    • if (minW > 0 && (worldW < minW || worldH < minH)) return oldBox;
  4. Update the leading comment to explain that Konva reports newBox in stage-scaled coords and we divide by stage scale to compare against world-unit floors.

Dependencies: none.

Acceptance Criteria

  • At any zoom level (10% to 400%), every resizable object can be shrunk to the same world-unit minimum it can be shrunk to at 100% zoom.
  • Calendar can be shrunk to its getMinSize() floor (month 280x260, week 320x220, day 200x280) at any zoom.
  • Kanban can be shrunk to its formula floor at any zoom.
  • All other objects can be shrunk to the 40x30 hard floor at any zoom.
  • At 100% zoom (stage.scaleX() === 1), worldW === newBox.width and worldH === newBox.height — no behavior change there.
  • Negative/zero dimensions during drag are still rejected (the < 40 / < 30 guard catches them after division by a positive stage scale).
  • No JS console errors at any zoom.

Notes

  • Konva builds newBox in Transformer.__getNodeShape as width: rect.width * absoluteScale.x, where getAbsoluteScale() walks up to the stage and includes stage.scaleX(). So newBox.width = world_width * stage.scaleX(). Dividing by stage.scaleX() recovers the world-unit width.
  • Multi-select resize is unaffected — the conversion uses the stage scale, which is identical for every node (no node has getAbsoluteScale that diverges from the stage's, except mindmap which keeps a per-node group scale; mindmap doesn't have a min in boundBoxFunc so it's not part of the comparison).
  • This is a JS-static-asset-only change. No Rust code is touched. No new dependencies. No DB / SDK / openrpc impact.
## Implementation Spec for Issue #77 ### Objective Make the live resize floor zoom-independent by evaluating the transformer's `boundBoxFunc` thresholds in world units, so every object can be shrunk to the same minimum size regardless of the stage's current zoom level. ### Requirements - The hard 40x30 floor and the per-type minimums (calendar, kanban) are compared in world units. - Behavior at 100% zoom is unchanged. - The fix covers every resizable object — sticky, text, shape, frame, document, image, webframe, drawing, calendar, kanban, mindmap — because all selections route through the same transformer. - Negative/zero box dimensions are still rejected (the safety guard that the original 40x30 floor provides). ### Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` — the `boundBoxFunc` defined on the `Konva.Transformer` (around lines 51-71). No other file needs changes. The post-resize per-type floors in `objects.js` (`Math.max(60, Math.round(bg.width() * scaleX))`, etc.) already operate in world units (`bg.width()` is the unscaled property, `scaleX` is the relative transform factor) and continue to act as a back-stop after the user releases. ### Implementation Plan #### Step 1: Convert boundBoxFunc thresholds to world units Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` Inside the existing `boundBoxFunc(oldBox, newBox)`: 1. Read the stage's current scale at the start of the function: ```js var stage = (typeof WhiteboardCanvas !== 'undefined' && WhiteboardCanvas.getStage) ? WhiteboardCanvas.getStage() : null; var sx = (stage && stage.scaleX && stage.scaleX()) || 1; var sy = (stage && stage.scaleY && stage.scaleY()) || sx; ``` 2. Derive world-unit dimensions: ```js var worldW = newBox.width / sx; var worldH = newBox.height / sy; ``` 3. Replace `newBox.width` / `newBox.height` with `worldW` / `worldH` in the two existing comparisons: - `if (worldW < 40 || worldH < 30) return oldBox;` - `if (minW > 0 && (worldW < minW || worldH < minH)) return oldBox;` 4. Update the leading comment to explain that Konva reports `newBox` in stage-scaled coords and we divide by stage scale to compare against world-unit floors. Dependencies: none. ### Acceptance Criteria - [ ] At any zoom level (10% to 400%), every resizable object can be shrunk to the same world-unit minimum it can be shrunk to at 100% zoom. - [ ] Calendar can be shrunk to its `getMinSize()` floor (month 280x260, week 320x220, day 200x280) at any zoom. - [ ] Kanban can be shrunk to its formula floor at any zoom. - [ ] All other objects can be shrunk to the 40x30 hard floor at any zoom. - [ ] At 100% zoom (`stage.scaleX() === 1`), `worldW === newBox.width` and `worldH === newBox.height` — no behavior change there. - [ ] Negative/zero dimensions during drag are still rejected (the `< 40` / `< 30` guard catches them after division by a positive stage scale). - [ ] No JS console errors at any zoom. ### Notes - Konva builds `newBox` in `Transformer.__getNodeShape` as `width: rect.width * absoluteScale.x`, where `getAbsoluteScale()` walks up to the stage and includes `stage.scaleX()`. So `newBox.width = world_width * stage.scaleX()`. Dividing by `stage.scaleX()` recovers the world-unit width. - Multi-select resize is unaffected — the conversion uses the stage scale, which is identical for every node (no node has `getAbsoluteScale` that diverges from the stage's, except mindmap which keeps a per-node group scale; mindmap doesn't have a min in `boundBoxFunc` so it's not part of the comparison). - This is a JS-static-asset-only change. No Rust code is touched. No new dependencies. No DB / SDK / openrpc impact.
Author
Member

Test Results

  • cargo test --workspace --lib: 4 lib targets, 0 tests / 0 passed / 0 failed each (no Rust unit tests for these crates).
  • cargo clippy --workspace -- -D warnings: clean.
  • node --check tools.js: parses cleanly.

This is a JS-static-asset-only change; there are no JS unit tests in the repo. Recommended manual verification: open the same board in two windows at very different zoom levels (e.g., 100% and 17%) and confirm the same object can be shrunk to the same world-unit minimum in both.

## Test Results - `cargo test --workspace --lib`: 4 lib targets, 0 tests / 0 passed / 0 failed each (no Rust unit tests for these crates). - `cargo clippy --workspace -- -D warnings`: clean. - `node --check tools.js`: parses cleanly. This is a JS-static-asset-only change; there are no JS unit tests in the repo. Recommended manual verification: open the same board in two windows at very different zoom levels (e.g., 100% and 17%) and confirm the same object can be shrunk to the same world-unit minimum in both.
Author
Member

Implementation Summary

Changes

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js (+13 / -3 lines)
    • In the Konva.Transformer's boundBoxFunc, read the stage's current scale (stage.scaleX() / scaleY()).
    • Convert newBox.width / newBox.height to world units by dividing by the stage scale.
    • Compare the world-unit dimensions against the existing world-unit floors (40x30 hard floor, calendar getMinSize() per view, kanban formula).
    • Updated the leading comment to explain why the conversion is needed.

Why this fixes it for every object

Every resizable object's selection routes through this single transformer's boundBoxFunc — sticky, text, shape, frame, document, image, webframe, drawing, calendar, kanban, mindmap. Fixing the comparison once covers them all.

Verification

  • cargo test --workspace --lib passes (4 lib targets, 0 tests).
  • cargo clippy --workspace -- -D warnings clean.
  • node --check tools.js parses cleanly.

Notes / caveats

  • No Rust code touched; JS static-asset-only fix.
  • At 100% zoom (stage.scaleX() === 1), worldW === newBox.width and behavior is identical to before.
  • The post-resize per-type floors in objects.js (sticky 60, frame 80, document 140x100, webframe 180x120, image 40, etc.) already work in world units (bg.width() is unscaled, scaleX is the relative transform factor) and continue to act as a back-stop after release.
  • Manual visual check recommended: open the same board in two windows at very different zoom levels (100% and 17%) and confirm the same calendar / sticky / kanban / etc. can be shrunk to the same world-unit minimum in both.
## Implementation Summary ### Changes - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` (+13 / -3 lines) - In the `Konva.Transformer`'s `boundBoxFunc`, read the stage's current scale (`stage.scaleX()` / `scaleY()`). - Convert `newBox.width` / `newBox.height` to world units by dividing by the stage scale. - Compare the world-unit dimensions against the existing world-unit floors (40x30 hard floor, calendar `getMinSize()` per view, kanban formula). - Updated the leading comment to explain why the conversion is needed. ### Why this fixes it for every object Every resizable object's selection routes through this single transformer's `boundBoxFunc` — sticky, text, shape, frame, document, image, webframe, drawing, calendar, kanban, mindmap. Fixing the comparison once covers them all. ### Verification - `cargo test --workspace --lib` passes (4 lib targets, 0 tests). - `cargo clippy --workspace -- -D warnings` clean. - `node --check tools.js` parses cleanly. ### Notes / caveats - No Rust code touched; JS static-asset-only fix. - At 100% zoom (`stage.scaleX() === 1`), `worldW === newBox.width` and behavior is identical to before. - The post-resize per-type floors in `objects.js` (sticky 60, frame 80, document 140x100, webframe 180x120, image 40, etc.) already work in world units (`bg.width()` is unscaled, `scaleX` is the relative transform factor) and continue to act as a back-stop after release. - Manual visual check recommended: open the same board in two windows at very different zoom levels (100% and 17%) and confirm the same calendar / sticky / kanban / etc. can be shrunk to the same world-unit minimum in both.
Sign in to join this conversation.
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#77
No description provided.