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 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NS } from './namespaces.js'
|
import { NS } from './namespaces.js'
|
||||||
import { transformPoint, getMatrix } from './math.js'
|
import { transformPoint, matrixMultiply, getTransformList, transformListToTransform } from './math.js'
|
||||||
import {
|
import {
|
||||||
assignAttributes,
|
assignAttributes,
|
||||||
getElement,
|
getElement,
|
||||||
@@ -44,6 +44,40 @@ class TextActions {
|
|||||||
#lastY = null
|
#lastY = null
|
||||||
#allowDbl = false
|
#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
|
* @param {Integer} index
|
||||||
@@ -526,11 +560,12 @@ class TextActions {
|
|||||||
const str = this.#curtext.textContent
|
const str = this.#curtext.textContent
|
||||||
const len = str.length
|
const len = str.length
|
||||||
|
|
||||||
const xform = this.#curtext.getAttribute('transform')
|
|
||||||
|
|
||||||
this.#textbb = utilsGetBBox(this.#curtext)
|
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 = []
|
||||||
this.#chardata.length = len
|
this.#chardata.length = len
|
||||||
|
|||||||
@@ -18,12 +18,17 @@ describe('TextActions', () => {
|
|||||||
svgRoot.setAttribute('height', '480')
|
svgRoot.setAttribute('height', '480')
|
||||||
document.body.append(svgRoot)
|
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 = document.createElementNS(NS.SVG, 'text')
|
||||||
textElement.setAttribute('x', '100')
|
textElement.setAttribute('x', '100')
|
||||||
textElement.setAttribute('y', '100')
|
textElement.setAttribute('y', '100')
|
||||||
textElement.setAttribute('id', 'text1')
|
textElement.setAttribute('id', 'text1')
|
||||||
textElement.textContent = 'Test'
|
textElement.textContent = 'Test'
|
||||||
svgRoot.append(textElement)
|
svgContent.append(textElement)
|
||||||
|
|
||||||
// Mock text measurement methods
|
// Mock text measurement methods
|
||||||
textElement.getStartPositionOfChar = vi.fn((i) => ({ x: 100 + i * 10, y: 100 }))
|
textElement.getStartPositionOfChar = vi.fn((i) => ({ x: 100 + i * 10, y: 100 }))
|
||||||
@@ -55,6 +60,7 @@ describe('TextActions', () => {
|
|||||||
// Mock svgCanvas
|
// Mock svgCanvas
|
||||||
svgCanvas = {
|
svgCanvas = {
|
||||||
getSvgRoot: () => svgRoot,
|
getSvgRoot: () => svgRoot,
|
||||||
|
getSvgContent: () => svgContent,
|
||||||
getZoom: () => 1,
|
getZoom: () => 1,
|
||||||
setCurrentMode: vi.fn(),
|
setCurrentMode: vi.fn(),
|
||||||
clearSelection: vi.fn(),
|
clearSelection: vi.fn(),
|
||||||
|
|||||||
Reference in New Issue
Block a user