From 0be0c3916c0ed453a64916f35f12cd453f890605 Mon Sep 17 00:00:00 2001 From: Namhoon Lee Date: Wed, 4 Feb 2026 22:57:35 +0900 Subject: [PATCH] fix(text-actions): Calculate accumulated transform matrix for text inside groups (#1082) When editing text inside a group element, the text cursor was appearing in the wrong position because only the text element's own transform was being considered, ignoring transforms from parent groups. This change: - Adds a new #getAccumulatedMatrix() method that traverses up the DOM tree from the text element to the SVG content element, collecting and multiplying all transform matrices along the way - Updates the init() method to use this accumulated matrix instead of just the text element's transform - Updates test mock to include getSvgContent() method Fixes the issue where editing text inside a transformed group would show the cursor at the wrong position. Co-authored-by: Claude Opus 4.5 --- packages/svgcanvas/core/text-actions.js | 43 ++++++++++++++++++++++--- tests/unit/text-actions.test.js | 8 ++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/svgcanvas/core/text-actions.js b/packages/svgcanvas/core/text-actions.js index b1846ff8..cbb7382d 100644 --- a/packages/svgcanvas/core/text-actions.js +++ b/packages/svgcanvas/core/text-actions.js @@ -6,7 +6,7 @@ */ import { NS } from './namespaces.js' -import { transformPoint, getMatrix } from './math.js' +import { transformPoint, matrixMultiply, getTransformList, transformListToTransform } from './math.js' import { assignAttributes, getElement, @@ -44,6 +44,40 @@ class TextActions { #lastY = null #allowDbl = false + /** + * Get the accumulated transformation matrix from the element up to the SVG content element. + * This includes transforms from all parent groups, fixing the issue where text cursor + * appears in the wrong position when editing text inside a transformed group. + * @param {Element} elem - The element to get the accumulated matrix for + * @returns {SVGMatrix|null} The accumulated transformation matrix, or null if none + * @private + */ + #getAccumulatedMatrix = (elem) => { + const svgContent = svgCanvas.getSvgContent() + const matrices = [] + + let current = elem + while (current && current !== svgContent && current.nodeType === 1) { + const tlist = getTransformList(current) + if (tlist && tlist.numberOfItems > 0) { + const matrix = transformListToTransform(tlist).matrix + matrices.unshift(matrix) // Add to beginning to maintain correct order + } + current = current.parentNode + } + + if (matrices.length === 0) { + return null + } + + if (matrices.length === 1) { + return matrices[0] + } + + // Multiply all matrices together + return matrixMultiply(...matrices) + } + /** * * @param {Integer} index @@ -526,11 +560,12 @@ class TextActions { const str = this.#curtext.textContent const len = str.length - const xform = this.#curtext.getAttribute('transform') - this.#textbb = utilsGetBBox(this.#curtext) - this.#matrix = xform ? getMatrix(this.#curtext) : null + // Calculate accumulated transform matrix including all parent groups + // This fixes the issue where text cursor appears in wrong position + // when editing text inside a group with transforms + this.#matrix = this.#getAccumulatedMatrix(this.#curtext) this.#chardata = [] this.#chardata.length = len diff --git a/tests/unit/text-actions.test.js b/tests/unit/text-actions.test.js index 3bf8b8e2..ee6be15d 100644 --- a/tests/unit/text-actions.test.js +++ b/tests/unit/text-actions.test.js @@ -18,12 +18,17 @@ describe('TextActions', () => { svgRoot.setAttribute('height', '480') document.body.append(svgRoot) + // Create svgContent element (container for SVG content) + const svgContent = document.createElementNS(NS.SVG, 'svg') + svgContent.id = 'svgcontent' + svgRoot.append(svgContent) + textElement = document.createElementNS(NS.SVG, 'text') textElement.setAttribute('x', '100') textElement.setAttribute('y', '100') textElement.setAttribute('id', 'text1') textElement.textContent = 'Test' - svgRoot.append(textElement) + svgContent.append(textElement) // Mock text measurement methods textElement.getStartPositionOfChar = vi.fn((i) => ({ x: 100 + i * 10, y: 100 })) @@ -55,6 +60,7 @@ describe('TextActions', () => { // Mock svgCanvas svgCanvas = { getSvgRoot: () => svgRoot, + getSvgContent: () => svgContent, getZoom: () => 1, setCurrentMode: vi.fn(), clearSelection: vi.fn(),