Skip to content

Add characterSpacing and change spacing units to em#2908

Merged
cptbtptpbcptdtptp merged 25 commits intogalacean:dev/2.0from
GuoLei1990:feat/text-letter-spacing
Mar 1, 2026
Merged

Add characterSpacing and change spacing units to em#2908
cptbtptpbcptdtptp merged 25 commits intogalacean:dev/2.0from
GuoLei1990:feat/text-letter-spacing

Conversation

@GuoLei1990
Copy link
Member

@GuoLei1990 GuoLei1990 commented Feb 28, 2026

Summary

  • Add characterSpacing property to TextRenderer and UI Text for controlling space between characters
  • Use em units (ratio of fontSize) for both characterSpacing and lineSpacing, matching Unity TMP / Figma / CSS best practices
  • Fix text layout bug: remove charInfo.offsetX from pen advance (both core TextRenderer and UI Text), offsetX is already consumed during atlas baking
  • Fix long word wrap bug: deduplicate trailing characterSpacing before pushing word line width
  • Fix UICanvas _canDispatchEvent: fallback to all cameras when assigned camera is inactive (_phasedActiveInScene)
  • Sync UI Text (packages/ui): add characterSpacing property, change lineSpacing to em units, fix offsetX pen advance bug
  • Add e2e test case with 5 spacing variations (0, 0.1, 0.3, 1, -0.05 em)
  • Fix unit tests for new characterSpacing parameter

Breaking Changes

  • lineSpacing unit changed from world units to em (ratio of fontSize) in both TextRenderer and UI Text

Test plan

  • e2e visual regression: text-character-spacing case
  • Unit tests pass (TextUtils, UICanvas)
  • Verify spacing scales proportionally with fontSize
  • Verify negative characterSpacing tightens text correctly
  • Verify text wrapping works correctly with characterSpacing
  • Verify UI Text characterSpacing and lineSpacing work correctly

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Walkthrough

Adds character spacing support across text rendering: new public characterSpacing accessors in core and UI text classes, measurement utilities updated to accept characterSpacing, tests and an E2E visual case added, and a small UI event-dispatch guard change.

Changes

Cohort / File(s) Summary
End-to-End Test & Config
e2e/case/text-character-spacing.ts, e2e/config.ts
Adds an E2E test that renders multiple texts with varying characterSpacing values and registers CharacterSpacing in E2E_CONFIG.
Core Text Renderer
packages/core/src/2d/text/TextRenderer.ts
Adds public characterSpacing getter/setter, internal _characterSpacing, marks positional dirty flags, integrates character spacing (scaled by fontSize) into measurement and per-character x-advance, converts some spacing/line units to em-based, and uses internal _width/_height in calculations.
Text Measurement Utilities
packages/core/src/2d/text/TextUtils.ts
Updates signatures of measureTextWithWrap and measureTextWithoutWrap to include characterSpacing and incorporates inter-character spacing into wrap and no-wrap width/line accumulation logic.
UI Text Component
packages/ui/src/component/advanced/Text.ts
Adds private _characterSpacing and public getter/setter for characterSpacing; passes character spacing (computed as _characterSpacing * fontSize) into measurement and rendering loops, replacing the prior per-character offset usage.
Unit Tests
tests/src/core/2d/text/TextUtils.test.ts
Updates test call sites to pass the new characterSpacing argument (uses 0 in tests) for the updated TextUtils measurement functions.
UI Canvas Event Guard
packages/ui/src/component/UICanvas.ts
Adds a guard to _canDispatchEvent to prevent dispatch when the assigned camera is phased-active in the scene (checks !assignedCamera._phasedActiveInScene).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I nibble kerning, count gaps with glee,

I stretch and squeeze each letter's spree.
From tight to airy, negative to wide,
I hop through lines where tiny spaces hide.
A joyful rabbit, spacing side by side.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately summarizes the two main changes: adding a characterSpacing feature and converting spacing units to em-based measurements.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@GuoLei1990 GuoLei1990 added ignore for release ignore for release text 2D text system labels Feb 28, 2026
@GuoLei1990 GuoLei1990 self-assigned this Feb 28, 2026
@GuoLei1990 GuoLei1990 changed the title Sup Add characterSpacing and change spacing units to em Feb 28, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/src/core/2d/text/TextUtils.test.ts (1)

142-143: Use a named constant for the new characterSpacing argument instead of repeating literal 0.

This improves readability and makes future API/unit adjustments less error-prone.

Refactor sketch
+ const DEFAULT_CHARACTER_SPACING_EM = 0;

  let result = TextUtils.measureTextWithWrap(
    textRendererTruncate,
    textRendererTruncate.width * _pixelsPerUnit,
    textRendererTruncate.height * _pixelsPerUnit,
    textRendererTruncate.lineSpacing * _pixelsPerUnit,
-   0
+   DEFAULT_CHARACTER_SPACING_EM
  );

Also applies to: 176-177, 205-206, 239-240, 260-261, 294-295, 323-324, 357-358, 374-375, 386-387, 405-406, 417-418, 429-430, 441-442, 461-462, 474-475, 487-488, 499-500, 511-512, 523-524

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/core/2d/text/TextUtils.test.ts` around lines 142 - 143, Replace the
repeated literal 0 used for the trailing characterSpacing argument with a named
constant (e.g., DEFAULT_CHARACTER_SPACING = 0) to improve readability and
maintainability; update the calls that compute height using
textRendererTruncate.lineSpacing * _pixelsPerUnit, 0 (and the other listed
occurrences) to pass DEFAULT_CHARACTER_SPACING instead of 0, and add the
constant near the top of TextUtils.test.ts so all tests reference the same named
value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/src/core/2d/text/TextUtils.test.ts`:
- Around line 142-143: The tests use textRendererTruncate.lineSpacing *
_pixelsPerUnit which is incorrect; update all calls to convert em-based
lineSpacing using the renderer's font size instead (multiply lineSpacing by
textRendererTruncate.fontSize), e.g. wherever TextUtils.measureTextWithWrap or
measureTextWithoutWrap is invoked with a pixel lineSpacing argument, replace
uses of _pixelsPerUnit with the renderer instance's fontSize property; ensure
references are to TextRenderer (textRendererTruncate) and its fontSize so
lineHeight calculations match the implementation (this._lineSpacing *
this._fontSize).

---

Nitpick comments:
In `@tests/src/core/2d/text/TextUtils.test.ts`:
- Around line 142-143: Replace the repeated literal 0 used for the trailing
characterSpacing argument with a named constant (e.g., DEFAULT_CHARACTER_SPACING
= 0) to improve readability and maintainability; update the calls that compute
height using textRendererTruncate.lineSpacing * _pixelsPerUnit, 0 (and the other
listed occurrences) to pass DEFAULT_CHARACTER_SPACING instead of 0, and add the
constant near the top of TextUtils.test.ts so all tests reference the same named
value.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be60ced and b651bcc.

📒 Files selected for processing (1)
  • tests/src/core/2d/text/TextUtils.test.ts

Comment on lines +142 to +143
textRendererTruncate.lineSpacing * _pixelsPerUnit,
0
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== 1) Inspect unit semantics in core text APIs ==="
echo "--- TextUtils.ts method signatures and lineSpacing handling ---"
rg -n 'measureTextWithWrap|measureTextWithoutWrap|lineSpacing|characterSpacing' \
  packages/core/src/2d/text/TextUtils.ts -C3 | head -200

echo ""
echo "--- TextRenderer.ts lineSpacing/characterSpacing context ---"
rg -n 'lineSpacing|characterSpacing' \
  packages/core/src/2d/text/TextRenderer.ts -C3 | head -200

echo ""
echo "=== 2) Inspect current test conversions and call arguments ==="
echo "--- Test lineSpacing conversions ---"
rg -n 'lineSpacing\s*\*\s*_pixelsPerUnit' \
  tests/src/core/2d/text/TextUtils.test.ts -C1

echo ""
echo "--- Test method calls with arguments ---"
rg -n 'measureTextWithWrap\(|measureTextWithoutWrap\(' \
  tests/src/core/2d/text/TextUtils.test.ts -C2 | head -150

Repository: galacean/engine

Length of output: 14405


Correct lineSpacing unit conversion in test calls — use fontSize instead of _pixelsPerUnit.

The tests incorrectly convert lineSpacing by multiplying by _pixelsPerUnit. According to the TextRenderer.ts implementation (lines 535, 541), em-based lineSpacing should be converted using _fontSize, not _pixelsPerUnit:

// Correct conversion in TextRenderer.ts:
this._lineSpacing * this._fontSize

// Incorrect in tests:
textRendererTruncate.lineSpacing * _pixelsPerUnit

Since TextUtils.measureTextWithWrap/WithoutWrap expect pixel-based lineSpacing (used as lineHeight = fontSizeInfo.size + lineSpacing), update all test calls to multiply by the renderer's fontSize property.

Affected lines: 142-143, 176-177, 205-206, 239-240, 260-261, 294-295, 323-324, 357-358, 374-375, 386-387, 405-406, 417-418, 429-430, 441-442, 461-462, 474-475, 487-488, 499-500, 511-512, 523-524.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/core/2d/text/TextUtils.test.ts` around lines 142 - 143, The tests
use textRendererTruncate.lineSpacing * _pixelsPerUnit which is incorrect; update
all calls to convert em-based lineSpacing using the renderer's font size instead
(multiply lineSpacing by textRendererTruncate.fontSize), e.g. wherever
TextUtils.measureTextWithWrap or measureTextWithoutWrap is invoked with a pixel
lineSpacing argument, replace uses of _pixelsPerUnit with the renderer
instance's fontSize property; ensure references are to TextRenderer
(textRendererTruncate) and its fontSize so lineHeight calculations match the
implementation (this._lineSpacing * this._fontSize).

@codecov
Copy link

codecov bot commented Feb 28, 2026

Codecov Report

❌ Patch coverage is 43.24324% with 84 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.35%. Comparing base (e42150a) to head (9711a64).
⚠️ Report is 5 commits behind head on dev/2.0.

Files with missing lines Patch % Lines
e2e/case/text-character-spacing.ts 0.00% 52 Missing and 1 partial ⚠️
packages/ui/src/component/advanced/Text.ts 43.47% 13 Missing ⚠️
packages/core/src/2d/text/TextRenderer.ts 71.42% 12 Missing ⚠️
e2e/config.ts 0.00% 6 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #2908      +/-   ##
===========================================
- Coverage    78.36%   78.35%   -0.01%     
===========================================
  Files          880      881       +1     
  Lines        96111    96304     +193     
  Branches      9602     9626      +24     
===========================================
+ Hits         75315    75459     +144     
- Misses       20634    20682      +48     
- Partials       162      163       +1     
Flag Coverage Δ
unittests 78.35% <43.24%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/2d/text/TextUtils.ts`:
- Around line 184-185: The wrap check is using glyph width variable `w` while
the line accumulation logic uses `xAdvance` (+ `characterSpacing`), causing
incorrect wrapping; update the conditional(s) that currently compare `lineWidth
+ w > rendererWidth` (appearing near the checks at the locations referencing
lines 184 and 205-206) to use the same metric used for accumulation (i.e., use
`xAdvance` and include `characterSpacing` where appropriate) so the wrap
decision matches how `lineWidth` is incremented.
- Around line 303-305: The current logic computes curWidth using
characterSpacing and then only adds the line when curWidth > 0, which causes
non-empty lines to be dropped if characterSpacing is large negative; change the
push condition to check for a non-empty line (use lineLength > 0 or line.length
> 0) rather than curWidth > 0 so lines are preserved regardless of spacing, and
apply the same fix to the analogous block handling lines at the 319-322 region;
reference symbols: curWidth, lineLength, characterSpacing and the code path that
pushes the line into the result.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b651bcc and 9711a64.

📒 Files selected for processing (3)
  • packages/core/src/2d/text/TextUtils.ts
  • packages/ui/src/component/UICanvas.ts
  • packages/ui/src/component/advanced/Text.ts

Comment on lines 184 to +185
if (lineWidth + w > rendererWidth && lineWidth > 0) {
lineWidth -= characterSpacing;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wrap checks use w while line accumulation uses xAdvance; this can mis-wrap lines.

Line 184 and Line 205 compare against glyph width (w) but widths are accumulated with xAdvance (plus spacing), so wrap boundaries can be off for many fonts/kerning cases.

🛠️ Suggested fix
-          if (lineWidth + w > rendererWidth && lineWidth > 0) {
+          if (lineWidth + charInfo.xAdvance > rendererWidth && lineWidth > 0) {
             lineWidth -= characterSpacing;
             this._pushLine(lines, lineWidths, lineMaxSizes, line, lineWidth, lineMaxAscent, lineMaxDescent);
             textWidth = Math.max(textWidth, lineWidth);
             notFirstLine = true;
             if (isSpace) {
@@
-          if (wordWidth + charInfo.w > rendererWidth) {
+          if (wordWidth + charInfo.xAdvance > rendererWidth) {
             if (lineWidth > 0) {
               lineWidth -= characterSpacing;
               this._pushLine(lines, lineWidths, lineMaxSizes, line, lineWidth, lineMaxAscent, lineMaxDescent);

Also applies to: 205-206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/2d/text/TextUtils.ts` around lines 184 - 185, The wrap
check is using glyph width variable `w` while the line accumulation logic uses
`xAdvance` (+ `characterSpacing`), causing incorrect wrapping; update the
conditional(s) that currently compare `lineWidth + w > rendererWidth` (appearing
near the checks at the locations referencing lines 184 and 205-206) to use the
same metric used for accumulation (i.e., use `xAdvance` and include
`characterSpacing` where appropriate) so the wrap decision matches how
`lineWidth` is incremented.

Comment on lines +303 to 305
const lineLength = line.length;
let curWidth = lineLength > 1 ? characterSpacing * (lineLength - 1) : 0;
let maxAscent = 0;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Non-empty lines can disappear with large negative characterSpacing.

Line 319 only pushes when curWidth > 0; with sufficiently negative spacing, a non-empty line can be skipped entirely.

🛠️ Suggested fix
-      if (curWidth > 0) {
-        this._pushLine(lines, lineWidths, lineMaxSizes, line, curWidth, maxAscent, maxDescent);
-        width = Math.max(width, curWidth);
+      if (lineLength > 0) {
+        const finalWidth = Math.max(0, curWidth);
+        this._pushLine(lines, lineWidths, lineMaxSizes, line, finalWidth, maxAscent, maxDescent);
+        width = Math.max(width, finalWidth);
       }

Also applies to: 319-322

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/2d/text/TextUtils.ts` around lines 303 - 305, The current
logic computes curWidth using characterSpacing and then only adds the line when
curWidth > 0, which causes non-empty lines to be dropped if characterSpacing is
large negative; change the push condition to check for a non-empty line (use
lineLength > 0 or line.length > 0) rather than curWidth > 0 so lines are
preserved regardless of spacing, and apply the same fix to the analogous block
handling lines at the 319-322 region; reference symbols: curWidth, lineLength,
characterSpacing and the code path that pushes the line into the result.

@GuoLei1990 GuoLei1990 changed the title Add characterSpacing and change spacing units to em 【大】Add characterSpacing and change spacing units to em Feb 28, 2026
Copy link
Collaborator

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Choose a reason for hiding this comment

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

+1

Copy link
Collaborator

@cptbtptpbcptdtptp cptbtptpbcptdtptp left a comment

Choose a reason for hiding this comment

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

+1

@cptbtptpbcptdtptp cptbtptpbcptdtptp merged commit 70453db into galacean:dev/2.0 Mar 1, 2026
10 of 12 checks passed
@github-project-automation github-project-automation bot moved this from In progress to Done in 2D Mar 1, 2026
@github-project-automation github-project-automation bot moved this to In progress in 2D Mar 1, 2026
@cptbtptpbcptdtptp cptbtptpbcptdtptp changed the title 【大】Add characterSpacing and change spacing units to em Add characterSpacing and change spacing units to em Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2D ignore for release ignore for release text 2D text system

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants