Skip to content

fix(joint-core): guard against non-invertible screen CTM#3342

Open
kumilingus wants to merge 2 commits into
clientIO:masterfrom
kumilingus:fix/vectorizer-non-invertible-ctm-master
Open

fix(joint-core): guard against non-invertible screen CTM#3342
kumilingus wants to merge 2 commits into
clientIO:masterfrom
kumilingus:fix/vectorizer-non-invertible-ctm-master

Conversation

@kumilingus
Copy link
Copy Markdown
Contributor

@kumilingus kumilingus commented Jun 2, 2026

Summary

If an SVG element sits inside a zero-sized container (e.g. an <svg> with width=0 / height=0, a hidden <foreignObject>, or anything else the browser renders with no extent), its getScreenCTM() returns a zero matrix (a=b=c=d=0, det=0). We then call .inverse() on it inside getRelativeTransformation(), which either throws or yields a NaN/zero matrix — and getTransformToElement() breaks for everything in that subtree.

This PR adds a determinant check in getRelativeTransformation(). If the target's screen CTM isn't invertible, it returns null and getTransformToElement() falls back to the identity matrix, same as it does for any other failure case.

Regression test in vectorizer.js:

  • baseline with an invertible ancestor proves the identity result isn't just trivial CTMs lining up,
  • then forces the target CTM to be singular, asserts a=b=c=d=0 and det=0,
  • finally checks getTransformToElement() doesn't throw and returns identity.

Companion PR against dev: #3341

Test plan

  • yarn grunt karma:vectorizer — 453/453 pass
  • Manual check in DevTools with a zero-sized ancestor

🤖 Generated with Claude Code

…veTransformation

When a target SVG element is nested under an ancestor with a singular
transform (e.g. scale(0)), its getScreenCTM() returns a matrix with
det = 0. Calling .inverse() on such a matrix throws or produces a
zero/NaN matrix, breaking getTransformToElement for any caller in that
subtree.

Detect this case and return null so getTransformToElement falls back to
the identity matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Guards V.getTransformToElement()’s underlying getRelativeTransformation() against non-invertible getScreenCTM() results (e.g., when an ancestor has scale(0)), preventing exceptions and allowing the API to gracefully fall back to the identity matrix.

Changes:

  • Added an invertibility check for the target’s screen CTM before calling .inverse() in getRelativeTransformation().
  • Added a regression test that reproduces a singular screen CTM and asserts getTransformToElement() does not throw and returns identity.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/joint-core/src/V/transform.mjs Adds a determinant-based invertibility guard to avoid inverting singular screen CTMs.
packages/joint-core/test/vectorizer/vectorizer.js Adds a QUnit regression test covering the singular-CTM fallback behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/joint-core/src/V/transform.mjs
QUnit fails the test automatically when an exception bubbles up,
so the manual try/catch was redundant — and the unused `e` binding
tripped the `no-unused-vars` rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants