From 317c0a7e0c63acf32c6f6717bd9afd05e752cb03 Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Wed, 23 Sep 2026 14:45:54 +0100 Subject: [PATCH] feat(ui): add panelResize geometry for edge and corner resizing Pure box arithmetic for resizing a floating panel from any of the eight compass handles: the named edges follow the pointer, the opposite edge stays pinned, and minimum size plus the viewport margin win. Owned by the same boundary groups as PanelPositionControls, which wires it next. Co-Authored-By: Claude Fable 5.1 --- scripts/format-scope.json | 1 + scripts/package-boundaries.json | 3 ++ src/ui/panelResize.js | 65 +++++++++++++++++++++++++++++++++ src/ui/panelResize.test.mjs | 64 ++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 src/ui/panelResize.js create mode 100644 src/ui/panelResize.test.mjs diff --git a/scripts/format-scope.json b/scripts/format-scope.json index caffd41d2..cc248f38d 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -147,6 +147,7 @@ "src/ui/panelRails.js", "src/ui/leftPanelRail.js", "src/ui/rightPanelRail.js", + "src/ui/panelResize.test.mjs", "src/ui/panelMeasurement.js", "src/ui/panelRailGeometry.js", "src/ui/panelRails.test.mjs", diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 433f5f36a..8e6cda2cd 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -553,6 +553,7 @@ "src/ui/panelLayoutController.js", "src/ui/panelMeasurement.js", "src/ui/panelPositionControls.js", + "src/ui/panelResize.js", "src/ui/panelRailGeometry.js", "src/ui/panelRails.js", "src/ui/radio.js", @@ -1670,6 +1671,7 @@ "src/ui/panelLayoutController.js", "src/ui/panelMeasurement.js", "src/ui/panelPositionControls.js", + "src/ui/panelResize.js", "src/ui/panelRailGeometry.js", "src/ui/panelRails.js", "src/ui/radio.js", @@ -2327,6 +2329,7 @@ "src/ui/panelLayoutController.js", "src/ui/panelMeasurement.js", "src/ui/panelPositionControls.js", + "src/ui/panelResize.js", "src/ui/panelRailGeometry.js", "src/ui/panelRails.js", "src/ui/rightPanelRail.js", diff --git a/src/ui/panelResize.js b/src/ui/panelResize.js new file mode 100644 index 000000000..4472e7a01 --- /dev/null +++ b/src/ui/panelResize.js @@ -0,0 +1,65 @@ +/** + * Pure geometry for resizing a floating panel from any edge or corner. The + * DOM wiring lives in PanelPositionControls; this module only decides boxes. + */ + +/** Viewport margin a resized panel keeps clear on every side. */ +const EDGE_MARGIN_PX = 6; +/** Resize directions: compass edges first, then corners. */ +export const RESIZE_DIRECTIONS = ['n', 's', 'e', 'w', 'ne', 'nw', 'sw', 'se']; + +function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} + +/** + * Work out the box a resize gesture produces. Edges named in `dir` move with + * the pointer; the opposite edge stays put, so growing from the left keeps + * the right side pinned. Minimum size and the viewport margin win over the + * pointer. + * @param {{left: number, top: number, width: number, height: number}} box Box at gesture start. + * @param {string} dir One of RESIZE_DIRECTIONS. + * @param {number} dx Pointer travel since gesture start. + * @param {number} dy Pointer travel since gesture start. + * @param {object} [limits] + * @param {number} limits.minWidth + * @param {number} limits.minHeight + * @param {number} [limits.viewportWidth=Infinity] + * @param {number} [limits.viewportHeight=Infinity] + * @param {number} [limits.margin=6] + * @returns {{left: number, top: number, width: number, height: number}} + */ +export function resizeBox( + box, + dir, + dx, + dy, + { + minWidth, + minHeight, + viewportWidth = Infinity, + viewportHeight = Infinity, + margin = EDGE_MARGIN_PX, + } = {}, +) { + let { left, top, width, height } = box; + const right = box.left + box.width; + const bottom = box.top + box.height; + if (dir.includes('e')) + width = clamp(box.width + dx, minWidth, viewportWidth - margin - box.left); + if (dir.includes('s')) + height = clamp( + box.height + dy, + minHeight, + viewportHeight - margin - box.top, + ); + if (dir.includes('w')) { + width = clamp(box.width - dx, minWidth, right - margin); + left = right - width; + } + if (dir.includes('n')) { + height = clamp(box.height - dy, minHeight, bottom - margin); + top = bottom - height; + } + return { left, top, width, height }; +} diff --git a/src/ui/panelResize.test.mjs b/src/ui/panelResize.test.mjs new file mode 100644 index 000000000..3e1577665 --- /dev/null +++ b/src/ui/panelResize.test.mjs @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { resizeBox } from './panelResize.js'; + +const box = { left: 100, top: 100, width: 400, height: 300 }; +const limits = { + minWidth: 200, + minHeight: 150, + viewportWidth: 1000, + viewportHeight: 800, +}; + +test('east and south edges grow away from the anchored corner', () => { + assert.deepEqual(resizeBox(box, 'se', 50, 20, limits), { + left: 100, + top: 100, + width: 450, + height: 320, + }); + assert.deepEqual(resizeBox(box, 'e', -30, 999, limits), { + left: 100, + top: 100, + width: 370, + height: 300, + }); +}); + +test('west and north edges keep the opposite edge pinned', () => { + assert.deepEqual(resizeBox(box, 'w', -40, 0, limits), { + left: 60, + top: 100, + width: 440, + height: 300, + }); + assert.deepEqual(resizeBox(box, 'nw', 40, 30, limits), { + left: 140, + top: 130, + width: 360, + height: 270, + }); + assert.deepEqual( + resizeBox(box, 'n', -50, 0, limits).top, + 100, + 'horizontal motion does not move a north edge', + ); +}); + +test('minimum size stops the moving edge, not the anchored one', () => { + const small = resizeBox(box, 'w', 350, 0, limits); + assert.deepEqual(small, { left: 300, top: 100, width: 200, height: 300 }); + assert.equal(resizeBox(box, 'ne', 0, 400, limits).height, 150); + assert.equal(resizeBox(box, 'ne', 0, 400, limits).top, 250); +}); + +test('the viewport margin caps growth in every direction', () => { + assert.equal(resizeBox(box, 'e', 2000, 0, limits).width, 1000 - 6 - 100); + assert.equal(resizeBox(box, 's', 0, 2000, limits).height, 800 - 6 - 100); + const west = resizeBox(box, 'w', -2000, 0, limits); + assert.equal(west.left, 6); + assert.equal(west.left + west.width, 500); + const north = resizeBox(box, 'n', 0, -2000, limits); + assert.equal(north.top, 6); + assert.equal(north.top + north.height, 400); +});