{"version":3,"file":"canvasTextSplit.mjs","sources":["../../../../src/scene/text/utils/canvasTextSplit.ts"],"sourcesContent":["import { Matrix } from '../../../maths/matrix/Matrix';\nimport { Container } from '../../container/Container';\nimport { FillGradient } from '../../graphics/shared/fill/FillGradient';\nimport { type SplitOptions } from '../../text-split/SplitText';\nimport { type TextSplitOutput } from '../../text-split/types';\nimport { CanvasTextGenerator } from '../canvas/CanvasTextGenerator';\nimport { CanvasTextMetrics } from '../canvas/CanvasTextMetrics';\nimport { Text } from '../Text';\nimport { type TextStyle } from '../TextStyle';\n\ninterface Segment\n{\n    char: string;\n    metric: CanvasTextMetrics;\n}\n\ninterface GroupedSegment\n{\n    line: string;\n    chars: Segment[];\n    width: number;\n}\n\nfunction getAlignmentOffset(alignment: string, lineWidth: number, largestLine: number): number\n{\n    switch (alignment)\n    {\n        case 'center':\n            return (largestLine - lineWidth) / 2;\n        case 'right':\n            return largestLine - lineWidth;\n        case 'left':\n        default:\n            return 0;\n    }\n}\n\nfunction isNewlineCharacter(char: string): boolean\n{\n    return char === '\\r' || char === '\\n' || char === '\\r\\n';\n}\n\n/**\n * Groups text segments into lines based on measured text metrics\n * @param segments - Array of text segments to group\n * @param measuredText - The pre-measured text metrics\n * @param measuredText.lines\n * @param textStyle - The text style to use for measurements\n * @returns Array of grouped segments containing line information\n */\nfunction groupTextSegments(\n    segments: string[],\n    measuredText: { lines: string[] },\n    textStyle: TextStyle,\n): GroupedSegment[]\n{\n    const groupedSegments: GroupedSegment[] = [];\n    let currentLine = measuredText.lines[0];\n    let matchedLine = '';\n    let chars: Segment[] = [];\n    let lineCount = 0;\n\n    // Disable word wrap for individual character measurements\n    textStyle.wordWrap = false;\n\n    segments.forEach((segment) =>\n    {\n        const isWhitespace = (/^\\s*$/).test(segment);\n        const isNewline = isNewlineCharacter(segment);\n        const isSpaceAtStart = matchedLine.length === 0 && isWhitespace;\n\n        if (isWhitespace && !isNewline && isSpaceAtStart)\n        {\n            return;\n        }\n\n        if (!isNewline) matchedLine += segment;\n\n        const metric = CanvasTextMetrics.measureText(segment, textStyle);\n\n        chars.push({ char: segment, metric });\n\n        if (matchedLine.length >= currentLine.length)\n        {\n            groupedSegments.push({\n                line: matchedLine,\n                chars,\n                width: chars.reduce((acc, seg) => acc + seg.metric.width, 0),\n            });\n            chars = [];\n            matchedLine = '';\n            lineCount++;\n            currentLine = measuredText.lines[lineCount];\n        }\n    });\n\n    return groupedSegments;\n}\n\n/**\n * Splits a Text object into segments based on the text's layout and style,\n * and adds these segments as individual Text objects to a specified container.\n *\n * This function handles word wrapping, alignment, and letter spacing,\n * ensuring that each segment is rendered correctly according to the original text's style.\n * It uses the CanvasTextMetrics to measure text dimensions and segment the text into lines.\n * @param options - Configuration options for the text split operation.\n * @returns An array of Text objects representing the split segments.\n * @internal\n */\nexport function canvasTextSplit(\n    options: Pick<SplitOptions, 'text' | 'style'> & { chars: Text[] },\n): TextSplitOutput<Text>\n{\n    const { text, style, chars: existingChars } = options;\n    const textStyle = style as TextStyle;\n\n    // measure the entire text to get the layout\n    const measuredText = CanvasTextMetrics.measureText(text, textStyle);\n    // split the text into segments\n    const segments = CanvasTextMetrics.graphemeSegmenter(text);\n    // now group the segments into lines based on measured lines\n    const groupedSegments: GroupedSegment[] = groupTextSegments(segments, measuredText, textStyle.clone());\n\n    const alignment = textStyle.align;\n    const maxLineWidth = measuredText.lineWidths.reduce((max, line) => Math.max(max, line), 0);\n    const isSingleLine = measuredText.lines.length === 1;\n    // For single-line text, alignment has no effect (nothing to align relative to)\n    // Multi-line text uses wordWrapWidth when word wrap is enabled\n    const useWordWrapWidth = !isSingleLine && textStyle.wordWrap;\n    const alignWidth = useWordWrapWidth ? textStyle.wordWrapWidth : maxLineWidth;\n\n    // Check if fill or stroke contains a gradient that needs offset/bounds\n    const fillGradient = textStyle._fill?.fill;\n    const strokeGradient = textStyle._stroke?.fill;\n\n    const hasFillGradient = fillGradient instanceof FillGradient;\n    const hasStrokeGradient = strokeGradient instanceof FillGradient;\n    const hasGradient = hasFillGradient || hasStrokeGradient;\n    const hasLocalGradient = (hasFillGradient && fillGradient.textureSpace === 'local')\n        || (hasStrokeGradient && strokeGradient.textureSpace === 'local');\n\n    // Store full text dimensions for gradient calculation\n    const fullTextWidth = measuredText.width;\n    const fullTextHeight = measuredText.height;\n\n    // Clone style for individual characters with left alignment.\n    // Container-level positioning handles alignment via getAlignmentOffset().\n    // Without this, each character applies its own alignment offset within its measurement area.\n    const baseCharStyle = textStyle.clone();\n\n    baseCharStyle.align = 'left';\n\n    // When trim is enabled on the style, calculate the trim offset for the whole text block once,\n    // then disable trim on individual characters and offset all characters to compensate\n    let trimOffsetX = 0;\n    let trimOffsetY = 0;\n\n    if (baseCharStyle.trim)\n    {\n        const { frame, canvasAndContext } = CanvasTextGenerator.getCanvasAndContext({\n            text,\n            style: textStyle,\n            resolution: 1,\n        });\n\n        CanvasTextGenerator.returnCanvasAndContext(canvasAndContext);\n\n        trimOffsetX = -frame.x;\n        trimOffsetY = -frame.y;\n\n        // Disable trim for individual characters; we'll use the whole-text trim offset instead\n        baseCharStyle.trim = false;\n    }\n\n    // now create Text objects for each segment and add them to the container\n    const chars: Text[] = [];\n    const lineContainers: Container[] = [];\n    const wordContainers: Container[] = [];\n    let yOffset = 0;\n    const strokeWidth = textStyle._stroke?.width || 0;\n    const dropShadowDistance = textStyle.dropShadow?.distance || 0;\n\n    groupedSegments.forEach((group, lineIndex) =>\n    {\n        const lineContainer = new Container({ label: `line-${lineIndex}` });\n\n        lineContainer.y = yOffset + trimOffsetY;\n        lineContainers.push(lineContainer);\n\n        const lineWidth = measuredText.lineWidths[lineIndex];\n        let xOffset = getAlignmentOffset(alignment, lineWidth, alignWidth);\n\n        let currentWordContainer = new Container({ label: 'word' });\n\n        currentWordContainer.x = xOffset + trimOffsetX;\n\n        group.chars.forEach((segment, charIndex) =>\n        {\n            if (segment.metric.width === 0)\n            {\n                return; // skip zero-width segments\n            }\n\n            if (isNewlineCharacter(segment.char))\n            {\n                xOffset += segment.metric.width - strokeWidth;\n\n                return; // Skip newline characters\n            }\n\n            if (segment.char === ' ')\n            {\n                // Add current word container if it has content\n                if (currentWordContainer.children.length > 0)\n                {\n                    wordContainers.push(currentWordContainer);\n                    lineContainer.addChild(currentWordContainer);\n                }\n\n                // Start new word container\n                xOffset += segment.metric.width + textStyle.letterSpacing - strokeWidth;\n                currentWordContainer = new Container({ label: 'word' });\n                currentWordContainer.x = xOffset + trimOffsetX;\n            }\n            else\n            {\n                // Create style for this character\n                let charStyle = baseCharStyle;\n\n                if (hasGradient)\n                {\n                    charStyle = baseCharStyle.clone();\n                    // All gradients need offset to position correctly within split text\n                    charStyle._gradientOffset = { x: -xOffset, y: -yOffset };\n                    // Local gradients also need full text bounds for proper scaling\n                    if (hasLocalGradient)\n                    {\n                        charStyle._gradientBounds = { width: fullTextWidth, height: fullTextHeight };\n                    }\n                }\n\n                // if there are existing characters, reuse them\n                let char: Text;\n\n                if (existingChars.length > 0)\n                {\n                    char = existingChars.shift();\n\n                    char.text = segment.char;\n                    char.style = charStyle;\n                    char.setFromMatrix(Matrix.IDENTITY);\n                    char.x = xOffset - currentWordContainer.x + trimOffsetX - (dropShadowDistance * charIndex);\n                }\n                else\n                {\n                    char = new Text({\n                        text: segment.char,\n                        style: charStyle,\n                        x: xOffset - currentWordContainer.x + trimOffsetX - (dropShadowDistance * charIndex),\n                    });\n                }\n\n                chars.push(char);\n                currentWordContainer.addChild(char);\n                xOffset += segment.metric.width + textStyle.letterSpacing - strokeWidth;\n            }\n        });\n\n        // Add the last word container of the line if it has children\n        if (currentWordContainer.children.length > 0)\n        {\n            wordContainers.push(currentWordContainer);\n            lineContainer.addChild(currentWordContainer);\n        }\n\n        yOffset += measuredText.lineHeight;\n    });\n\n    return { chars, lines: lineContainers, words: wordContainers };\n}\n"],"names":[],"mappings":"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"}