Skip to content

fix(studio): a resized corner stays on the pointer when the size rounds to whole pixels - #5213

Draft
miguel-heygen wants to merge 6 commits into
mainfrom
fix/studio-resize-corner-on-pointer
Draft

miguel-heygen wants to merge 6 commits into
mainfrom
fix/studio-resize-corner-on-pointer

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

What

During a corner resize, the grabbed corner now stays on the pointer when the element's size rounds to whole pixels. The written width and height stay whole, as before.

Why

Studio writes resized sizes in whole CSS pixels (#4766): GSAP works out a -50% centring from the element's integer offset size, and a fractional size once saved a centred element 113 px off. A corner resize keeps the element's centre planted, so rounding the size moved the grabbed corner off the pointer by up to about 0.35 px times the element's rendered scale.

On a layer that GSAP scales to 1.5x, that is up to 0.53 px. The edit accuracy bench's two remaining misses, resize-scale-px-r30-root-z50-after (0.53) and resize-scale-px-r30-nested-z50-after (0.52), are exactly this, and the whole scale family's tracking climbs with the scale (about 0.27 / 0.36 / 0.45 px at 1x / 1.25x / 1.5x).

Related work

Refs #4766 (whole-pixel sizes stay).

How

  • resizeRemainderShift (domEditResizeLocal.ts): the shift of the pinned centre that puts the grabbed corner where the wanted size would. It is half the rounding remainder, measured along the element's own rendered edges, so rotation and GSAP scale are included.
  • The resize draft (resizeDraft.ts) pins the centre at its gesture-start point plus that shift. The release already commits the draft's anchor as the offset, so the saved file matches what the drag showed.
  • applyStudioBoxSizeDraft returns the size it wrote; the whole-pixel rule moved into one helper, roundToLayoutSize (utils/rounding.ts), used by every size write in manualEditsDom.ts.
  • Trade-off: the centre now moves by at most that remainder (under 0.36 px times the scale) instead of the corner.
  • On a GSAP-scaled layer the resize saves as a new scale, and finalizeScaleResizeCommit then moves the box onto the drop point. It skipped that correction whenever the box was within 0.5 px of the drop point on each axis, so the first CI run of this PR showed the saved box up to 0.51 px off the drop (drop/reload). It now skips only when the correction rounds to nothing at the precision the file is written in (3 decimals). A scale resize can therefore save a sub-pixel position correction when the drop sits a fraction of a pixel off the committed scale's box.
  • That correction lined the boxes up by their top-left corners. With the playhead on a keyframe the tween takes one scale for both axes, which cannot reproduce the draft's separately rounded width and height, so the second CI run put the whole gap on the far edges (drop 0.46-0.55 on the -on cases). The resize is centre-anchored, so the correction now lines up centres and the gap splits evenly, as it did on main when the correction was skipped.

Test plan

  • anchoredResizeCommitFeedsOffset.test.ts: a drag that asks for 300.6 x 150.3 writes 301 x 150 and commits the grabbed corner exactly where 300.6 x 150.3 puts it. Fails without the fix (corner off).
  • domEditResizeLocal.test.ts: on a 30 deg, 1.5x box, each of the four corners lands on its wanted position.
  • gsapResizeIntercept.test.ts: a scale resize leaves a static position alone when the drop is centred on the committed box, and moves it by exactly 0.3 / -0.2 px when the drop sits that far off. The second case fails on main (0.5 px skip); both fail with top-left alignment.
  • First CI grid: every resize-scale-* case's tracking fell from 0.25-0.53 px to 0.02-0.19 px; drop and reload on four -after cases were the remaining misses.
  • Second CI grid: both target cases pass; four -on cases missed on drop (0.46-0.55), fixed by the centre alignment above.
  • Studio components/editor, hooks and utils suites: 4339 passed. tsc --noEmit, oxlint, oxfmt, comment ratchet and Fallow clean.
  • Edit accuracy: the scale family's tracking before and after, and the full grid with no case losing accuracy
  • Comments follow CONTRIBUTING.md "Comments"

Size

9 files, about +125 / -20, about half of it tests.

…ds to whole pixels

Resized sizes stay on whole CSS pixels (GSAP centres a -50% translate from the
integer offset size), so the centre-anchored resize left the grabbed corner up to
a third of a pixel times the element's scale off the pointer: 0.53 px on a 1.5x
scaled layer. The centre pin now takes half the rounding remainder along the
element's rendered edges, so the grabbed corner lands where the wanted size puts
it while the written size stays whole.
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 2061 (base branch 2059), smooth 1652 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Newly passing (2)

  • resize-scale-px-r30-root-z50-after
  • resize-scale-px-r30-nested-z50-after

Quarantined, measured but not gated (0)

finalizeScaleResizeCommit skipped its position correction whenever the box sat
within 0.5 px of the drop point on each axis, so a sub-pixel drop (now the
norm, with the grabbed corner on the pointer) saved up to half a pixel off.
It now skips only when the correction rounds to nothing at the precision the
file is written in.
finalizeScaleResizeCommit lined the committed box up with the drop by its
top-left corner. When one scale cannot reproduce the dragged width and height
(the draft rounds each to whole pixels), that put the whole gap on the far
edges, up to half a pixel. The resize is centre-anchored, so it now lines up
centres and the gap splits evenly.

This branch has not been deployed

No deployments
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