EGR-15: Rotate теряет качество изображения - #53
Conversation
A turn used to resample the pixels the previous turn produced: dragging the angle handle fires one per pointermove, so a slow drag smeared the art while a single 60° turn stayed sharp. The pre-rotation bitmap is now pinned in the cache (ImageCache.rot0, next to the resize/adjust `original`) together with the angle it already carries, and every turn renders that pin at the running total instead — twelve 5° steps land on the same pixels as one 60° turn, and coming back to the pinned angle restores the art exactly. The pin is per session and is dropped wherever the art is re-baked or rescaled (resize, glyph re-render, replace, clear, invert), since it would then be pixels of the wrong size. An asset's `adjust` is re-applied when drawing from an older pin, because the turned pixels bake it in the way they always did. Also round the layer's re-centring half-pixel symmetrically about zero: growing by 45px moved it 23 but shrinking back moved it 22, so turning an angle back and forth walked the layer across the dial. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P69VpBDQA4fmgwEq8yFNE1
| const cf = a.cf === 4 && deg % 90 !== 0 ? 5 : a.cf; | ||
| const sw = rot0.src.width, | ||
| sh = rot0.src.height; | ||
| const w = size(sw * cos + sh * sin), |
There was a problem hiding this comment.
The output size is now derived from the cached bitmap's dimensions (rot0.src.width/height) instead of the asset's a.w/a.h. $cache is not part of the undo history, so the two can legitimately disagree, and then a rotation silently resizes the widget.
Scenario: resize a 100x40 widget to 200x80 (cache.bitmap is now 200x80, rot0 cleared), press Ctrl+Z (the doc goes back to 100x40, the cache is untouched — undo in doc.model only swaps $doc), then turn it 15°. The fresh pin is the stale 200x80 bitmap, so w = size(200·cos15 + 80·sin15) = 214, h = 129 — the widget jumps to more than double its size on the canvas. The old code computed the box from a.w/a.h and drew the stale bitmap scaled into it, so it stayed put. Same path via replace-image + undo.
Recording the asset size at pin time (w: a.w, h: a.h next to deg) and drawing rot0.src into that box keeps the pin lossless and keeps the box tied to the document.
| // rounded symmetrically about zero: Math.round(-22.5) is -22 but Math.round(22.5) is 23, and | ||
| // that half pixel would walk the layer one step every time an angle is turned and turned back | ||
| const half = (v: number) => Math.sign(v) * Math.round(Math.abs(v) / 2); | ||
| const dx = grown ? half(grown.w - first.w) : 0; |
There was a problem hiding this comment.
half() fixes the asymmetry for one turn and its exact inverse, but the offset is still computed per step against the previous asset size (grown.w - first.w), so the position still accumulates rounding across a stepped turn — which is exactly the case this PR is about. The pixels now match a single turn; x/y do not.
Measured for a 100x40 frame turned to 60°:
| step | stepped (dx, dy) | one 60° turn |
|---|---|---|
15° (the angle field's step) |
-7, 34 | -8, 34 |
| 5° | -9, 37 | -8, 34 |
| 1° | -15, 52 | -8, 34 |
So four clicks of the angle spinner leave the art 1px off where typing 60 puts it, and a 200x20 hand nudged a degree at a time ends up 16px off. editor-rotate.browser.test.ts compares w/h and pixels between the stepped and one-shot runs but not x/y, which is why it passes.
Since rot0 already pins the geometry, the offset can be made exact by measuring both sides against the pin rather than against the previous step: dx = half(grown.w - pinW) - half(first.w - pinW) (with pinW = the asset width at pin time). Then the per-step offsets telescope and any path to the same angle lands on the same pixel.
| cache: { | ||
| bitmap: fresh.bitmap ?? (await bitmapOf(fresh)), | ||
| original: undefined, | ||
| rot0: undefined, |
There was a problem hiding this comment.
This branch clears original and now rot0, but still not accent — unlike the JPEG branch three lines up. bmpOf in render/canvas.ts returns c?.accent ?? c?.bitmap, so for an accent-flagged widget (meta flags === 4) replacing a frame's PNG keeps drawing the previous image's tinted silhouette, at the previous size, until the accent color changes and accentFx reruns.
Pre-existing, but this hunk rewrites the literal, so it's a one-word fix.
| rot0: undefined, | |
| rot0: undefined, | |
| accent: undefined, |
Task: https://linear.app/freethinkel/issue/EGR-15/rotate-teryaet-kachestvo-izobrazheniya
Надо сделать также как и с resize сохранять оригинал изображение для сессии
Generated by shepherd: run
run_60e6c63655, agentclaude, workspacewD.