Fix/rotation recalc compound transforms#1083
Merged
jfhenon merged 6 commits intoMar 15, 2026
Merged
Conversation
Two bugs in the rotation center recalculation triggered by attribute changes on rotated elements: 1. The recalculation fires for ALL attribute changes (stroke-width, fill, opacity, etc.) even though only geometric attributes (x, y, width, height, d, points, etc.) can change the bbox center. This causes unnecessary and incorrect rotation center updates. 2. In undo.js, the center is computed through ALL remaining transforms via transformListToTransform(tlist).matrix, but for compound transform lists like [translate(tx,ty), rotate(angle)], the pre-rotation translate leaks into the center calculation, producing rotate(angle, bcx+tx, bcy+ty) instead of the correct rotate(angle, bcx, bcy). 3. In history.js (undo/redo), the code replaces the ENTIRE transform attribute with just rotate(...), completely destroying any other transforms (translate, scale, etc.) in the list. Fixes: - Guard recalculation with a BBOX_AFFECTING_ATTRS set so non-geometric attribute changes skip the rotation block entirely. - In undo.js, use transformListToTransform(tlist, n, max) to compute the center through only post-rotation transforms. - In history.js, replace string-based transform replacement with transform list API that finds and updates only the rotation entry, preserving all other transforms. Extract shared logic into relocateRotationCenter() helper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests demonstrate and verify fixes for three bugs: 1. Non-geometric attributes (stroke-width, fill, opacity) no longer trigger rotation center recalculation — the transform list is left untouched. 2. Geometric attribute changes on elements with compound transforms (e.g. translate + rotate) compute the rotation center using only post-rotation transforms, preventing the translate from leaking into the center calculation. 3. ChangeElementCommand.apply/unapply preserve compound transform structure instead of replacing the entire transform attribute with just rotate(...). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Includes an HTML demo showing both bugs side-by-side: - Bug 1: Non-geometric attribute changes (stroke-width) on compound transforms corrupt character positions (e.g., word art on curves) - Bug 2: Rotation center computed through ALL transforms instead of only post-rotation transforms, causing translate to leak into center Each panel shows Original / Buggy / Fixed comparisons with the actual SVG transform math applied. A Playwright script generates screenshots. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
A pinwheel of 6 colored rectangles using translate(200,200) rotate(N). Loading this into SVG-Edit and changing stroke-width on the group triggers the rotation recalculation bug on unfixed builds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reviewer's GuideRefactors rotation-center recalculation so it only runs for geometry-affecting attribute changes and correctly preserves compound transform lists, and adds visual/demo assets and supporting test scaffolding for the bug. Sequence diagram for attribute change with safe rotation-center recalculationsequenceDiagram
actor User
participant EditorUI
participant SvgCanvas
participant ChangeElementCommand
participant relocateRotationCenter
participant SVGElement as SvgElement
User->>EditorUI: Change attribute (e.g. stroke-width, x, width)
EditorUI->>SvgCanvas: changeSelectedAttributeNoUndoMethod(attr, newValue, elems)
SvgCanvas->>SvgElement: Update attribute
SvgCanvas->>SvgElement: getRotationAngle(elem)
alt angle is zero or attr is not bbox-affecting
SvgCanvas-->>EditorUI: No rotation recalc
else angle nonzero and attr bbox-affecting
SvgCanvas->>SvgElement: getTransformList(elem)
SvgCanvas->>SvgElement: Remove existing rotate transform
SvgCanvas->>SvgElement: utilsGetBBox(elem)
SvgCanvas->>SvgElement: transformListToTransform(tlist, n, end)
SvgCanvas->>SvgElement: transformPoint(bboxCenter, centerMatrix)
SvgCanvas->>SvgElement: createSVGTransform().setRotate(angle, center.x, center.y)
SvgCanvas->>SvgElement: insertItemBefore(newRotate, n)
end
User->>EditorUI: Perform undo or redo
EditorUI->>ChangeElementCommand: apply() or unapply()
ChangeElementCommand->>SvgElement: Apply stored attribute values
ChangeElementCommand->>relocateRotationCenter: relocateRotationCenter(elem, changedAttrNames)
relocateRotationCenter->>SvgElement: getRotationAngle(elem)
alt no bbox-affecting attrs or angle is zero
relocateRotationCenter-->>ChangeElementCommand: Return without changes
else
relocateRotationCenter->>SvgElement: getTransformList(elem)
relocateRotationCenter->>SvgElement: Remove rotate transform at index n
relocateRotationCenter->>SvgElement: getBBox(elem)
relocateRotationCenter->>SvgElement: transformListToTransform(tlist, n, end)
relocateRotationCenter->>SvgElement: transformPoint(bboxCenter, centerMatrix)
relocateRotationCenter->>SvgElement: createSVGTransform().setRotate(angle, center.x, center.y)
relocateRotationCenter->>SvgElement: insertItemBefore(newRotate, n)
end
Class diagram for ChangeElementCommand and rotation recalc helperclassDiagram
class ChangeElementCommand {
- elem : Element
- oldValues : Object
- newValues : Object
+ constructor(elem, attrs, values)
+ apply()
+ unapply()
}
class relocateRotationCenterHelper {
+ BBOX_AFFECTING_ATTRS : Set~string~
+ relocateRotationCenter(elem, changedAttrs) void
}
class SvgUtilities {
+ getRotationAngle(elem) number
+ getBBox(elem) DOMRect
}
class SvgMath {
+ getTransformList(elem) SVGTransformList
+ transformListToTransform(tlist, fromIndex, toIndex) SVGTransform
+ transformPoint(x, y, matrix) SVGPoint
}
ChangeElementCommand --> relocateRotationCenterHelper : uses
relocateRotationCenterHelper --> SvgUtilities : uses
relocateRotationCenterHelper --> SvgMath : uses
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The
BBOX_AFFECTING_ATTRSset is now defined twice (inhistory.jsand insidechangeSelectedAttributeNoUndoMethod); consider centralizing this in a shared module/constant so the attribute list stays consistent and isn’t duplicated. - In
relocateRotationCenter,tlist.removeItem(n)is called beforegetBBox(elem)and an earlyreturnon a falsy bbox would leave the rotation permanently removed; it would be safer to compute/validate the bbox before mutating the transform list or otherwise ensure the rotation is restored on failure paths.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `BBOX_AFFECTING_ATTRS` set is now defined twice (in `history.js` and inside `changeSelectedAttributeNoUndoMethod`); consider centralizing this in a shared module/constant so the attribute list stays consistent and isn’t duplicated.
- In `relocateRotationCenter`, `tlist.removeItem(n)` is called before `getBBox(elem)` and an early `return` on a falsy bbox would leave the rotation permanently removed; it would be safer to compute/validate the bbox before mutating the transform list or otherwise ensure the rotation is restored on failure paths.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Collaborator
|
plz fix the linter errors so I can merge your (very useful) PR |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address two code review comments: 1. Export BBOX_AFFECTING_ATTRS from history.js and import it in undo.js so the attribute list is defined in one place. 2. Compute getBBox() before calling tlist.removeItem() so that a falsy bbox causes an early return/break without having already destroyed the rotation transform. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
Author
|
Apologies - I should have checked that before submitting. SVG-Edit is an excellent and mature tool - I was both surprised and excited to find a legitimate bug! |
jfhenon
approved these changes
Mar 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR description
Fix rotation centre recalculation corrupting compound transforms
When changing attributes on rotated elements, the rotation center recalculation had two bugs:
Both bugs existed in undo.js (changeSelectedAttributeNoUndoMethod) and history.js (ChangeElementCommand.apply/unapply). The history.js version was worse — it replaced the entire transform attribute with just rotate(...), destroying any other transforms.
To replicate the bug:
Use this sample SVG:
This branch is deployed to: https://geoffyoungs.github.io/svgedit/
Compare with the upstream demo at https://svgedit.netlify.app/editor/index.html
To test the bug:
Checklist
Note that we require UI tests to ensure that the added feature will not be
nixed by some future fix and that there is at least some test-as-documentation
to indicate how the fix or enhancement is expected to behave.
npm test, ensuring linting passes and that Cypress UI tests keepcoverage to at least the same percent (reflected in the coverage badge
that should be updated after the tests run)
help both for future users and for the PR reviewer.
NB: Claude Code was used to identify and fix this issue.
Summary by Sourcery
Fix rotation center recalculation so that transforms on rotated elements are preserved and compound transforms remain stable when attributes change or history operations are applied.
Bug Fixes:
Tests: