Skip to content

feat(svg-editor): Shift + side-edge resize aspect-locks about the opposite-edge center#865

Merged
softmarshmallow merged 4 commits into
mainfrom
feat/svg-editor-shift-edge-aspect-resize
Jun 18, 2026
Merged

feat(svg-editor): Shift + side-edge resize aspect-locks about the opposite-edge center#865
softmarshmallow merged 4 commits into
mainfrom
feat/svg-editor-shift-edge-aspect-resize

Conversation

@softmarshmallow

@softmarshmallow softmarshmallow commented Jun 18, 2026

Copy link
Copy Markdown
Member

What

Holding Shift while dragging a side-edge handle (N/S/E/W) in @grida/svg-editor now resizes uniformly, preserving the aspect ratio. The perpendicular axis scales by the same factor as the dragged axis, about the opposite side's center as the anchor — matching every mainstream graphics tool and the long-standing corner behavior. Composes with Alt (from-center) → Shift+Alt is uniform-about-center.

Before: Shift on a side edge did nothing. After: drag the E handle with Shift → width and height grow proportionally, left edge pinned, top/bottom move symmetrically.

Why

The modifier already flowed end-to-end — dom.ts maps Shift → aspect_lock: "uniform" for all directions, with a mid-drag redrive — but it dead-ended because compute_factors masked the off-axis on edge handles. This unmasks it.

How

All feature logic lives in the headless math core, mirroring the Alt resize-from-center feature (#859):

Core (@grida/svg-editor)

  • resize-pipeline.ts: ResizePlan gains aspect_lock; compute_factors' Shift block gains edge arms (the perpendicular follows the driven factor about the opposite-edge center); apply passes Shift to compute_factors only for edges.
  • Corners are untouched — they keep carrying the lock as rewritten deltas via the existing aspect_lock stage, so corner gestures stay byte-identical.
  • orchestrator.ts: threads aspect_lock onto the plan.

Key design point: an edge handle's tracked midpoint is invariant under perpendicular center-scaling, so edge-uniform cannot be encoded as a delta — it must be carried as a factor (threaded on the plan like from_center). That's also why the corner delta-rewrite path could stay completely unchanged.

HUD (@grida/hud) — dashed-preview parity so the overlay squares up under Shift (also fixes a pre-existing latent corner-preview lag):

  • gesture.ts: applyResize/applyResizeRect gain an aspect opt mirroring the core math (corner = max-magnitude; edge = perpendicular follows). The rect rebuild reuses cmath.rect.scale.
  • state.ts: resizePreviewShape forwards Shift; the mid-drag modifier toggle already refreshes the preview.

Tests & docs

  • New resize-aspect-edge.test.tscompute_factors edge arms, per-primitive apply (rect/ellipse/line/polygon/path), Shift+Alt about center, multi-member union, orchestrator end-to-end, byte-exact round-trip revert.
  • New apply-resize-aspect.test.ts (HUD) — edge/corner/Shift+Alt preview geometry.
  • Updated the edge-stage assertion comment in resize-pipeline.test.ts.
  • docs/keybindings.md updated; manual TC test/svg-editor-resize-shift-edge-aspect.md added.

Verification

  • ✅ Full @grida/svg-editor (1451) + @grida/hud suites green, incl. 24 new tests.
  • ✅ Build (dist rebuilt) + repo typecheck + oxlint + oxfmt clean.
  • ⚠️ A faithful Shift+drag on a HUD handle isn't scriptable with the available preview tooling (no modifier-drag primitive); the logic is the spec and is fully covered by the deterministic core/orchestrator tests, with the interactive pass captured as a manual TC.

Reviewer notes

  • Corner resize behavior is intentionally byte-identical (Shift is gated to edges in apply); the split is principled, not a bandaid.
  • The corner/edge collapse rule is duplicated across the package boundary (core emits (sx, sy, origin); HUD operates on a Rect) — forced by the one-way layering (HUD must not import the editor core) and pinned by mirrored tests on both sides.

Summary by CodeRabbit

  • New Features
    • Added Shift-based aspect-ratio lock for resize handles on both edges and corners.
    • For side edges, the perpendicular dimension now resizes uniformly while remaining anchored around the opposite edge’s center.
    • Shift-based resize now shows a dashed aspect-ratio guide during previews.
    • Shift + Alt combines aspect-lock with symmetric, opposite-edge movement.
  • Bug Fixes
    • Resize previews now update instantly when toggling Shift/Alt mid-drag.
  • Documentation
    • Updated Shift keybinding help to clarify edge behavior.
  • Tests
    • Expanded HUD/SVG resize aspect-lock and integration coverage (including preview→cancel and multi-member cases).

…osite-edge center

Holding Shift while dragging a side-edge handle (N/S/E/W) now resizes
uniformly: the perpendicular axis scales by the same factor as the dragged
axis, about the opposite side's center, preserving the aspect ratio. This
matches every mainstream graphics tool and the long-standing corner behavior.

The modifier already flowed end-to-end (dom.ts maps Shift -> aspect_lock:
"uniform", with a mid-drag redrive); it dead-ended because compute_factors
masked the off-axis on edge handles. The fix is entirely in the headless math
core, mirroring the from-center (Alt) feature:

- resize-pipeline.ts: ResizePlan gains `aspect_lock`; compute_factors' Shift
  block gains edge arms (perpendicular follows the driven factor about the
  opposite-edge center); `apply` passes Shift to compute_factors only for
  edges. Corners are untouched — they keep carrying the lock as rewritten
  deltas via the aspect_lock stage, so corner gestures stay byte-identical.
  (An edge handle's tracked midpoint is invariant under perpendicular
  center-scaling, so edge-uniform can't be a delta; it must be a factor.)
- orchestrator.ts: threads aspect_lock onto the plan.

HUD dashed-preview parity (so the overlay squares up under Shift; also fixes a
pre-existing latent corner-preview lag):

- gesture.ts: applyResize/applyResizeRect gain an `aspect` opt mirroring the
  core math (corner = max-magnitude; edge = perpendicular follows). Rebuild
  reuses cmath.rect.scale.
- state.ts: resizePreviewShape forwards Shift; mid-drag toggle refreshes.

Tests: new resize-aspect-edge.test.ts (core: compute_factors arms,
per-primitive apply, multi-member union, orchestrator end-to-end, round-trip
revert) and apply-resize-aspect.test.ts (HUD). Docs + manual TC added.
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
grida Ready Ready Preview, Comment Jun 18, 2026 9:08am
6 Skipped Deployments
Project Deployment Actions Updated (UTC)
code Ignored Ignored Jun 18, 2026 9:08am
docs Ignored Ignored Preview Jun 18, 2026 9:08am
legacy Ignored Ignored Jun 18, 2026 9:08am
backgrounds Skipped Skipped Jun 18, 2026 9:08am
blog Skipped Skipped Jun 18, 2026 9:08am
viewer Skipped Skipped Jun 18, 2026 9:08am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 794592b9-f017-414a-bf39-97c5e46d7848

📥 Commits

Reviewing files that changed from the base of the PR and between c5d0fcd and 8276932.

📒 Files selected for processing (2)
  • packages/grida-canvas-hud/__tests__/chrome-aspect-guide.test.ts
  • packages/grida-canvas-hud/surface/chrome.ts

Walkthrough

Adds Shift-based aspect-lock resize for side-edge handles in both the SVG editor resize pipeline and the canvas HUD. ResizePlan gains an aspect_lock flag; compute_factors couples the perpendicular scale to the driven axis for edge handles; applyResizeRect implements the geometric anchor-and-scale math; the HUD preview state integrates both Alt and Shift together and renders an aspect-ratio diagonal guide. New tests cover factor computation, multi-primitive apply, orchestrator integration, round-trip cancel, and HUD geometry rendering.

Changes

Shift aspect-lock for side-edge resize handles

Layer / File(s) Summary
ResizePlan type + edge factor computation + apply wiring
packages/grida-svg-editor/src/core/resize-pipeline/resize-pipeline.ts
ResizePlan gains optional aspect_lock?: boolean. compute_factors couples the perpendicular axis scale factor to the driven axis for edge handles when Shift is active, leaving corner max-magnitude logic unchanged. apply derives a shift boolean from the plan and skips corners to prevent double-application.
Orchestrator passes aspect_lock into ResizePlan
packages/grida-svg-editor/src/core/resize-pipeline/orchestrator.ts
run_pass sets aspect_lock on the constructed ResizePlan from modifiers.aspect_lock === "uniform".
HUD applyResizeRect aspect-lock math
packages/grida-canvas-hud/event/gesture.ts
applyResizeRect accepts a new aspect parameter; when enabled, computes per-axis scale factors, locks both axes to the max-magnitude factor, selects an anchor from pinned edges or bbox center, and returns a scaled rect via cmath.rect.scale. applyResize forwards aspect for both rect and transformed selection kinds.
HUD preview state wires Shift into resizePreviewShape
packages/grida-canvas-hud/event/state.ts
resizePreviewShape calls applyResize with { fromCenter: alt, aspect: shift } whenever either modifier is held; dispatch comment updated to reflect both Alt and Shift trigger an immediate dashed-preview refresh without pointer movement.
HUD chrome aspect-ratio guide rendering
packages/grida-canvas-hud/surface/chrome.ts
buildChrome renders a dashed diagonal guide within the preview shape when Shift is active. For transformed shapes, the diagonal is computed in local frame and projected through the matrix; for axis-aligned shapes, it uses the bounds diagonal.
SVG pipeline and orchestrator tests
packages/grida-svg-editor/__tests__/resize-aspect-edge.test.ts, packages/grida-svg-editor/__tests__/resize-pipeline.test.ts
New suite covers compute_factors edge anchoring and coupling, apply assertions across rect/ellipse/line/polygon/path, multi-member union scaling, orchestrator integration, and round-trip preview/cancel for byte-exact SVG restoration. Pipeline test clarifies the aspect_lock stage is a delta no-op for edge handles.
HUD applyResize aspect-lock tests
packages/grida-canvas-hud/__tests__/apply-resize-aspect.test.ts
New suite covers: no-aspect regression guard for side edges, side-edge pinned-edge and perpendicular-center preservation (for e and n directions), crossing behavior with clamping, corner origin-pinned scaling, and Shift+Alt bbox-center uniform scaling.
HUD aspect-ratio guide tests
packages/grida-canvas-hud/__tests__/chrome-aspect-guide.test.ts
Tests validate diagonal guide rendering: absence when Shift is false, presence with correct endpoints for multiple directions, correctness under transformation matrices via projected coordinates, and coexistence with dashed preview rectangle.
Docs: keybindings update and test-case specification
packages/grida-svg-editor/docs/keybindings.md, test/svg-editor-resize-shift-edge-aspect.md
Keybindings doc broadened from corner-only to corner-or-edge Shift behavior with perpendicular-axis anchoring description. New test-case doc specifies expected behavior, manual verification steps for East/North/Shift+Alt/mid-drag toggle scenarios, and links to automated test suites.

Sequence Diagram(s)

sequenceDiagram
  participant User as User (Shift+drag edge)
  participant SurfaceState
  participant applyResize
  participant applyResizeRect
  participant ResizeOrchestrator
  participant resize_pipeline_apply

  User->>SurfaceState: pointer move (dx, dy) with Shift held
  SurfaceState->>applyResize: initial, direction, dx, dy, { fromCenter: alt, aspect: shift }
  applyResize->>applyResizeRect: rect, direction, dx, dy, fromCenter, aspect=true
  applyResizeRect->>applyResizeRect: compute driven/perpendicular scale factors
  applyResizeRect->>applyResizeRect: select anchor (pinned edge or bbox center)
  applyResizeRect-->>applyResize: scaled SelectionShape
  applyResize-->>SurfaceState: dashed preview shape with aspect guide

  User->>ResizeOrchestrator: drive(dx, dy) with aspect_lock="uniform"
  ResizeOrchestrator->>resize_pipeline_apply: ResizePlan { direction, dx, dy, aspect_lock: true }
  resize_pipeline_apply->>resize_pipeline_apply: compute shift = aspect_lock && !is_corner
  resize_pipeline_apply->>resize_pipeline_apply: compute_factors(shift=true) → sy=sx or sx=sy
  resize_pipeline_apply-->>ResizeOrchestrator: updated DOM attributes
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • gridaco/grida#859: Extends the same applyResize/applyResizeRect functions and HUD dashed-preview refresh logic with Alt-derived fromCenter behavior, which this PR directly composes with by adding the Shift/aspect-lock path alongside it.

Suggested labels

enhancement, canvas, packages, svg, vector

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(svg-editor): Shift + side-edge resize aspect-locks about the opposite-edge center' is clear, specific, and directly describes the main feature—aspect-ratio locking for side-edge resizing with Shift modifier. It matches the primary objective of the PR and uses conventional commit formatting.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/svg-editor-shift-edge-aspect-resize

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef9927f3c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/grida-svg-editor/src/core/resize-pipeline/resize-pipeline.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/grida-canvas-hud/event/gesture.ts`:
- Around line 531-532: The scale factor calculations for sxRaw and syRaw at
lines 531-532 can produce negative values when dragging crosses the opposite
edge, causing the HUD preview to diverge from the core resize behavior which
clamps factors to a positive floor. Apply Math.max clamping to both sxRaw and
syRaw calculations to ensure they remain positive, and apply the same fix to the
corresponding calculations at lines 547-548 to maintain consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2436dde-f66d-4c99-bfee-e91a3cd13057

📥 Commits

Reviewing files that changed from the base of the PR and between d389f0c and ef9927f.

📒 Files selected for processing (9)
  • packages/grida-canvas-hud/__tests__/apply-resize-aspect.test.ts
  • packages/grida-canvas-hud/event/gesture.ts
  • packages/grida-canvas-hud/event/state.ts
  • packages/grida-svg-editor/__tests__/resize-aspect-edge.test.ts
  • packages/grida-svg-editor/__tests__/resize-pipeline.test.ts
  • packages/grida-svg-editor/docs/keybindings.md
  • packages/grida-svg-editor/src/core/resize-pipeline/orchestrator.ts
  • packages/grida-svg-editor/src/core/resize-pipeline/resize-pipeline.ts
  • test/svg-editor-resize-shift-edge-aspect.md

Comment thread packages/grida-canvas-hud/event/gesture.ts
`resize_text` infers corner-ness from the factors (`sx`/`sy !== 1`), so the
new edge aspect-lock — which makes both factors non-1 — fooled it into
treating a `<text>` edge drag as a corner drag and scaling x/y/font-size,
breaking the established text-edge no-op contract (and diverging from snap /
pixel-grid, which already gate on `resize_capability.constraint`).

`apply` now computes free factors first and only re-locks for edges when the
element's `constraint` is not a no-op — routing the decision through the same
authority snap uses, so apply and snap stay consistent. The synthesized group
baseline is a free rect, so group members (text included) still scale
uniformly, matching corner-drag.

Adds a regression test driving `resize_pipeline.apply` (which exercises the
gate) on a text edge+Shift gesture.

Addresses Codex review on #865.
The HUD aspect preview derived sx/sy from the free box dimensions, which can go
negative when the drag crosses the opposite edge — mirroring the dashed box.
The core `compute_factors` clamps factors to a positive floor (0.001), so it
commits a degenerate-thin box pinned at the anchor instead. Clamp the preview
factors the same way so the dashed box tracks committed geometry on crossover
instead of flipping.

Adds a crossover test. Addresses CodeRabbit review on #865.
While Shift aspect-locks a resize, draw the diagonal of the preview box
opposite the dragged handle (the locked ratio the user is holding) — mirroring
the main editor's `AspectRatioGuide`. Rendered as a dashed `HUDLine` in the
resize chrome's `decoration_lines` band, reusing the `transformPreview` group.

Geometry comes from the shared `cmath.ui.diagonalForDirection` (+ `transformLine`
to project a rotated/sheared box's diagonal through its matrix), so no new
geometry lives in the HUD. Gated on `state.modifiers.shift` + an active resize
gesture; the HUD can't see per-element refusals (e.g. text-on-edge no-ops), so
the guide tracks the gesture, not the committed write. No svg-editor change —
the HUD already tracks Shift and draws its own chrome.
@vercel vercel Bot temporarily deployed to Preview – backgrounds June 18, 2026 09:05 Inactive
@vercel vercel Bot temporarily deployed to Preview – viewer June 18, 2026 09:05 Inactive
@vercel vercel Bot temporarily deployed to Preview – blog June 18, 2026 09:05 Inactive
@softmarshmallow softmarshmallow merged commit 1c822db into main Jun 18, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant