From 6f4e8274d691d7e976f71dc564b4a9f409c7da2e Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 17 May 2026 00:21:34 +0300 Subject: [PATCH] fix(canvas): use screen-space handles for multi-selection bounds Single-node selection draws handles inside withNodeBounds() which applies canvas.scale(zoom), so handle size / zoom is correct there. Multi-selection drawGroupBounds() computes screen-space coordinates directly (x * zoom + pan) without a canvas zoom transform, so handles need fixed screen-space sizes. Added drawHandleScreenSpace() and drawBoundsHandlesScreenSpace() for the multi-select path. --- .../core/src/canvas/overlays/selection.ts | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/core/src/canvas/overlays/selection.ts b/packages/core/src/canvas/overlays/selection.ts index 580e19076..04dafad92 100644 --- a/packages/core/src/canvas/overlays/selection.ts +++ b/packages/core/src/canvas/overlays/selection.ts @@ -306,7 +306,7 @@ export function drawGroupBounds( canvas.drawRect(r.ck.LTRBRect(minX, minY, maxX, maxY), r.auxStroke) - drawBoundsHandles(r, canvas, minX, minY, maxX, maxY) + drawBoundsHandlesScreenSpace(r, canvas, minX, minY, maxX, maxY) } export function getRotatedCorners(r: SkiaRenderer, n: SceneNode, abs: Vector): Vector[] { @@ -318,13 +318,44 @@ export function getRotatedCorners(r: SkiaRenderer, n: SceneNode, abs: Vector): V } export function drawHandle(r: SkiaRenderer, canvas: Canvas, x: number, y: number): void { + r.auxFill.setColor(r.ck.WHITE) + const s = HANDLE_HALF_SIZE / r.zoom + const rect = r.ck.LTRBRect(x - s, y - s, x + s, y + s) + canvas.drawRect(rect, r.auxFill) + canvas.drawRect(rect, r.selectionPaint) +} + +function drawHandleScreenSpace(r: SkiaRenderer, canvas: Canvas, x: number, y: number): void { r.auxFill.setColor(r.ck.WHITE) const rect = r.ck.LTRBRect( - x - HANDLE_HALF_SIZE / r.zoom, - y - HANDLE_HALF_SIZE / r.zoom, - x + HANDLE_HALF_SIZE / r.zoom, - y + HANDLE_HALF_SIZE / r.zoom + x - HANDLE_HALF_SIZE, + y - HANDLE_HALF_SIZE, + x + HANDLE_HALF_SIZE, + y + HANDLE_HALF_SIZE ) canvas.drawRect(rect, r.auxFill) canvas.drawRect(rect, r.selectionPaint) } + +function drawBoundsHandlesScreenSpace( + r: SkiaRenderer, + canvas: Canvas, + minX: number, + minY: number, + maxX: number, + maxY: number +): void { + drawHandleScreenSpace(r, canvas, minX, minY) + drawHandleScreenSpace(r, canvas, maxX, minY) + drawHandleScreenSpace(r, canvas, minX, maxY) + drawHandleScreenSpace(r, canvas, maxX, maxY) + const midX = (minX + maxX) / 2 + const midY = (minY + maxY) / 2 + const rotationHandleY = minY - 24 + canvas.drawLine(midX, minY, midX, rotationHandleY, r.selectionPaint) + drawHandleScreenSpace(r, canvas, midX, rotationHandleY) + drawHandleScreenSpace(r, canvas, midX, minY) + drawHandleScreenSpace(r, canvas, midX, maxY) + drawHandleScreenSpace(r, canvas, minX, midY) + drawHandleScreenSpace(r, canvas, maxX, midY) +}