Skip to content

feat(2d): add FilledSpriteAssembler and sprite filled mode support#2989

Open
cptbtptpbcptdtptp wants to merge 2 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:feat/sprite-filled-mode
Open

feat(2d): add FilledSpriteAssembler and sprite filled mode support#2989
cptbtptpbcptdtptp wants to merge 2 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:feat/sprite-filled-mode

Conversation

@cptbtptpbcptdtptp
Copy link
Copy Markdown
Collaborator

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented May 11, 2026

Summary

Adds support for Sprite Filled draw mode — render only a portion of a sprite based on a fill ratio along a configurable axis/origin. Useful for progress bars, cooldown indicators, etc.

Changes

  • New assembler FilledSpriteAssembler (~613 lines) handles vertex generation for filled mode
  • New enums:
    • SpriteFilledMode — direction (Horizontal / Vertical / Radial90 / Radial180 / Radial360)
    • SpriteFilledOrigin — anchor point for the fill direction
  • Extends SpriteDrawMode with Filled
  • Extends ISpriteRenderer interface
  • SpriteRenderer integrates filled assembler

Test Plan

  • Existing sprite e2e tests pass
  • Visual verification: progress-bar-style filled sprite renders correctly across all SpriteFilledMode × SpriteFilledOrigin combinations

Summary by CodeRabbit

Release Notes

  • New Features
    • Added filled sprite drawing mode with multiple fill types: linear (horizontal/vertical) and radial (90°, 180°, 360°)
    • Sprite fills are now fully configurable with adjustable amount, origin position (8 directional options), and direction control

Review Change Stack

cptbtptpbcptdtptp and others added 2 commits May 11, 2026 19:19
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Walkthrough

This PR adds filled-mode sprite rendering to the Galacean 2D engine. It introduces new enums (SpriteFilledMode, SpriteFilledOrigin), extends the SpriteRenderer interface and class with filled configuration properties, implements a new FilledSpriteAssembler that generates optimized geometry for horizontal, vertical, and radial fills (90°/180°/360°), and updates the barrel exports to expose the new components.

Changes

Filled Sprite Rendering

Layer / File(s) Summary
Draw Mode & Fill Enums
packages/core/src/2d/enums/SpriteDrawMode.ts, packages/core/src/2d/enums/SpriteFilledMode.ts, packages/core/src/2d/enums/SpriteFilledOrigin.ts
Added Filled enum member to SpriteDrawMode; introduced SpriteFilledMode with five variants (Horizontal, Vertical, Radial90/180/360); introduced SpriteFilledOrigin with eight directional origins (Right, TopRight, Top, TopLeft, Left, BottomLeft, Bottom, BottomRight).
Renderer Interface Contract
packages/core/src/2d/assembler/ISpriteRenderer.ts
Extended ISpriteRenderer with optional properties: filledMode, filledAmount, filledOrigin, filledClockWise; added imports for SpriteFilledMode and SpriteFilledOrigin.
Filled Geometry Assembler
packages/core/src/2d/assembler/FilledSpriteAssembler.ts
Implemented FilledSpriteAssembler class with static methods: resetData allocates sub-chunk geometry; updatePositions transforms vertices and dispatches to fill-mode handlers (linear, radial); updateColor writes per-vertex RGBA; internal methods _filledLinear, _filledRadial90/180/360, _processRadialGrid, _radialCut compute clipped rectangles and radial sectors using angle-based triangulation; _addTriangle/_addQuad append vertices and indices to sub-chunk buffer.
SpriteRenderer Integration
packages/core/src/2d/sprite/SpriteRenderer.ts
Updated imports; added internal state for _filledMode, _filledAmount, _filledOrigin, _filledClockWise; extended drawMode setter to assign FilledSpriteAssembler for SpriteDrawMode.Filled; exposed public getters/setters for filled properties with clamping (filledAmount [0,1]), automatic filledOrigin reset on filledMode change, and dirty flag updates when in filled draw mode.
Barrel Exports
packages/core/src/2d/index.ts
Added exports for FilledSpriteAssembler, SpriteFilledMode, and SpriteFilledOrigin alongside existing sprite assembler and enum re-exports.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A sprite now fills with grace,
From corner to radiant space,
Ninety, one-eighty, three-sixty glow—
Math-woven geometry show!
Triangles dance, quads align,
The filled new mode is fine.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding FilledSpriteAssembler class and sprite filled mode support, which are the primary focus of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

❌ Patch coverage is 20.35278% with 587 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.62%. Comparing base (1bc2b10) to head (44702de).

Files with missing lines Patch % Lines
...ges/core/src/2d/assembler/FilledSpriteAssembler.ts 11.41% 543 Missing ⚠️
packages/core/src/2d/sprite/SpriteRenderer.ts 48.23% 44 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #2989      +/-   ##
===========================================
- Coverage    78.25%   77.62%   -0.64%     
===========================================
  Files          900      903       +3     
  Lines        99234    99970     +736     
  Branches     10172    10178       +6     
===========================================
- Hits         77657    77599      -58     
- Misses       21406    22200     +794     
  Partials       171      171              
Flag Coverage Δ
unittests 77.62% <20.35%> (-0.64%) ⬇️

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
Copy Markdown

@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: 3

🧹 Nitpick comments (5)
packages/core/src/2d/assembler/FilledSpriteAssembler.ts (3)

113-115: 💤 Low value

@ts-ignore hides an interface gap on renderer._bounds.

The _bounds field isn't declared on ISpriteRenderer, so this access requires @ts-ignore. Other assemblers presumably need the same hatch. Consider widening ISpriteRenderer to include _bounds: BoundingBox (or expose a small _setBounds(box) method) so the suppression can be removed and the interface accurately reflects what the assemblers depend on.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/2d/assembler/FilledSpriteAssembler.ts` around lines 113 -
115, The `@ts-ignore` exists because assemblers access renderer._bounds which
isn't declared on ISpriteRenderer; update the renderer interface
(ISpriteRenderer) to include a private/internal _bounds: BoundingBox (or add a
small method like _setBounds(box: BoundingBox) and use that) so
FilledSpriteAssembler (where BoundingBox.transform(renderer._getBounds(),
modelMatrix, renderer._bounds) is called) no longer needs `@ts-ignore`; change the
interface accordingly and remove the suppression from FilledSpriteAssembler (and
any other assemblers using _bounds).

137-140: 💤 Low value

Extract repeated amount <= 0.001 short-circuit and the magic threshold.

The same if (amount <= 0.001) { renderer._subChunk.indices.length = 0; return; } guard is repeated in 4 _filled* methods, and the threshold itself is a magic number. Consider hoisting both into a single named constant + helper (or moving the early-out up into updatePositions so the per-mode functions don't have to duplicate it).

Also applies to: 222-225, 288-291, 383-386

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/2d/assembler/FilledSpriteAssembler.ts` around lines 137 -
140, Extract the repeated small-amount guard into a named constant and single
helper (or move the early-out into updatePositions) to remove duplication:
define a descriptive constant (e.g. MIN_FILL_AMOUNT = 0.001) in
FilledSpriteAssembler and replace the four occurrences of the literal check (the
lines in the _filled* methods that do `if (amount <= 0.001) {
renderer._subChunk.indices.length = 0; return; }`) with a call to a new helper
like shouldSkipFill(amount, renderer) or performSkipFillIfNeeded(amount,
renderer) so the threshold is centralized and the index-clear/return logic is
not duplicated across _filled* methods. Ensure you update all four locations
mentioned (the guards at the shown snippet and the ones around lines 222-225,
288-291, 383-386) and keep behavior identical.

244-271: ⚡ Quick win

Radial handlers silently no-op on unexpected origins.

_filledRadial90 only handles the 4 corner origins, _filledRadial180 and _filledRadial360 only handle the 4 edge origins. The default: break; branches leave _inPositions / _inUVs populated from a previous invocation (radial 90/180) or startAngle = 0 (radial 360), producing visually wrong geometry without any indication of misuse.

Since SpriteRenderer.filledOrigin setter does not validate against filledMode, a user can put the renderer in a bad state. Consider either:

  • Validating in SpriteRenderer.filledOrigin setter (reject/coerce origins that don't match the current mode), or
  • Adding an explicit warning/Logger.warn in the default branch here.

Also applies to: 320-371, 389-404

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/2d/assembler/FilledSpriteAssembler.ts` around lines 244 -
271, The switch default branches in _filledRadial90, _filledRadial180 and
_filledRadial360 silently leave inPositions/inUVs from prior calls causing wrong
geometry; update each default case to log a warning (use Logger.warn or the
project logger) mentioning the function name and the invalid origin, and then
clear/reset the arrays (inPositions and inUVs) or set them to safe defaults so
stale data isn't reused; alternatively, add validation/coercion in the
SpriteRenderer.filledOrigin setter to reject or coerce origins incompatible with
the current filledMode, but if you choose the logger approach make sure to
reference the origin value and the method name in the message.
packages/core/src/2d/sprite/SpriteRenderer.ts (1)

179-186: ⚡ Quick win

Consider validating filledOrigin against the active filledMode.

The setter accepts any SpriteFilledOrigin, but each filledMode only renders correctly for a subset of origins (Horizontal: Left/Right; Vertical: Top/Bottom; Radial90: corners; Radial180/360: edge midpoints). Combined with the silent default: break; branches in FilledSpriteAssembler, an invalid combination produces broken geometry with no diagnostic.

Validating here (e.g., Logger.warn and coercing to a valid origin, or clamping based on _filledMode) would help users catch misuse early. Same applies to the filledMode setter when the previously-set origin is no longer valid for the new mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/2d/sprite/SpriteRenderer.ts` around lines 179 - 186, The
filledOrigin setter currently accepts any SpriteFilledOrigin even when
incompatible with the active _filledMode, which can produce broken geometry;
update the set filledOrigin(value: SpriteFilledOrigin) to validate value against
the current _filledMode (e.g., allowed origins for Horizontal = Left/Right,
Vertical = Top/Bottom, Radial90 = corners, Radial180/360 = edge midpoints), and
if invalid call Logger.warn with a clear message and coerce/clamp to a valid
origin before assigning _filledOrigin and setting the dirty flag
(_dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV) for
SpriteDrawMode.Filled; also mirror this validation in the filledMode setter so
when changing _filledMode you adjust or coerce the existing _filledOrigin (and
warn) to a compatible value to avoid relying on the silent default branches in
FilledSpriteAssembler.
packages/core/src/2d/assembler/ISpriteRenderer.ts (1)

20-20: ⚡ Quick win

Consider renaming filledClockWisefilledClockwise.

"Clockwise" is a single English word; splitting the W creates an unusual public API name. Since this property is also exposed via SpriteRenderer and is part of a 2.0 release, normalizing it now avoids a breaking rename later. Same rename should be applied in SpriteRenderer.ts (private field, getter/setter) and FilledSpriteAssembler.ts (read site).

♻️ Suggested rename
-  filledClockWise?: boolean;
+  filledClockwise?: boolean;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/2d/assembler/ISpriteRenderer.ts` at line 20, Rename the
public property and all usages from filledClockWise to filledClockwise: update
the ISpriteRenderer interface property (filledClockWise → filledClockwise),
rename the private field and its getter/setter in SpriteRenderer (ensure method
names and internal references use filledClockwise), and update the read site in
FilledSpriteAssembler to read the new property name; keep behavior unchanged and
run tests to catch any missed references.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/2d/assembler/FilledSpriteAssembler.ts`:
- Around line 117-119: The FilledSpriteAssembler.updateUVs is a no-op which
leaves UVs stale when atlasRegion changes because SpriteRenderer._onSpriteChange
sets only SpriteRendererUpdateFlags.UV for SpriteModifyFlags.atlasRegion and
_render will call updateUVs (skipped updatePositions); fix this by changing
SpriteRenderer._onSpriteChange to treat atlasRegion the same as other
UV-affecting changes for filled mode: when atlasRegion is detected, set the
update flags to WorldVolumeAndUV (or include WorldVolume) instead of only UV so
FilledSpriteAssembler.updatePositions runs and recomputes baked UVs; update
references to SpriteModifyFlags.atlasRegion and SpriteRendererUpdateFlags.*
accordingly.

In `@packages/core/src/2d/sprite/SpriteRenderer.ts`:
- Around line 160-170: The filledMode setter currently sets _filledOrigin to
Bottom for all non-Radial90 modes which makes Horizontal default to an invalid
Bottom origin; update the logic in the set filledMode(value: SpriteFilledMode)
to choose a mode-appropriate default: when value === SpriteFilledMode.Horizontal
set _filledOrigin to SpriteFilledOrigin.Left (so
FilledSpriteAssembler._filledLinear sees originIsStart true), when value ===
SpriteFilledMode.Radial90 set it to SpriteFilledOrigin.BottomLeft, otherwise
keep SpriteFilledOrigin.Bottom; keep the existing branch that sets
_dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV when
this._drawMode === SpriteDrawMode.Filled.

---

Nitpick comments:
In `@packages/core/src/2d/assembler/FilledSpriteAssembler.ts`:
- Around line 113-115: The `@ts-ignore` exists because assemblers access
renderer._bounds which isn't declared on ISpriteRenderer; update the renderer
interface (ISpriteRenderer) to include a private/internal _bounds: BoundingBox
(or add a small method like _setBounds(box: BoundingBox) and use that) so
FilledSpriteAssembler (where BoundingBox.transform(renderer._getBounds(),
modelMatrix, renderer._bounds) is called) no longer needs `@ts-ignore`; change the
interface accordingly and remove the suppression from FilledSpriteAssembler (and
any other assemblers using _bounds).
- Around line 137-140: Extract the repeated small-amount guard into a named
constant and single helper (or move the early-out into updatePositions) to
remove duplication: define a descriptive constant (e.g. MIN_FILL_AMOUNT = 0.001)
in FilledSpriteAssembler and replace the four occurrences of the literal check
(the lines in the _filled* methods that do `if (amount <= 0.001) {
renderer._subChunk.indices.length = 0; return; }`) with a call to a new helper
like shouldSkipFill(amount, renderer) or performSkipFillIfNeeded(amount,
renderer) so the threshold is centralized and the index-clear/return logic is
not duplicated across _filled* methods. Ensure you update all four locations
mentioned (the guards at the shown snippet and the ones around lines 222-225,
288-291, 383-386) and keep behavior identical.
- Around line 244-271: The switch default branches in _filledRadial90,
_filledRadial180 and _filledRadial360 silently leave inPositions/inUVs from
prior calls causing wrong geometry; update each default case to log a warning
(use Logger.warn or the project logger) mentioning the function name and the
invalid origin, and then clear/reset the arrays (inPositions and inUVs) or set
them to safe defaults so stale data isn't reused; alternatively, add
validation/coercion in the SpriteRenderer.filledOrigin setter to reject or
coerce origins incompatible with the current filledMode, but if you choose the
logger approach make sure to reference the origin value and the method name in
the message.

In `@packages/core/src/2d/assembler/ISpriteRenderer.ts`:
- Line 20: Rename the public property and all usages from filledClockWise to
filledClockwise: update the ISpriteRenderer interface property (filledClockWise
→ filledClockwise), rename the private field and its getter/setter in
SpriteRenderer (ensure method names and internal references use
filledClockwise), and update the read site in FilledSpriteAssembler to read the
new property name; keep behavior unchanged and run tests to catch any missed
references.

In `@packages/core/src/2d/sprite/SpriteRenderer.ts`:
- Around line 179-186: The filledOrigin setter currently accepts any
SpriteFilledOrigin even when incompatible with the active _filledMode, which can
produce broken geometry; update the set filledOrigin(value: SpriteFilledOrigin)
to validate value against the current _filledMode (e.g., allowed origins for
Horizontal = Left/Right, Vertical = Top/Bottom, Radial90 = corners,
Radial180/360 = edge midpoints), and if invalid call Logger.warn with a clear
message and coerce/clamp to a valid origin before assigning _filledOrigin and
setting the dirty flag (_dirtyUpdateFlag |=
SpriteRendererUpdateFlags.WorldVolumeAndUV) for SpriteDrawMode.Filled; also
mirror this validation in the filledMode setter so when changing _filledMode you
adjust or coerce the existing _filledOrigin (and warn) to a compatible value to
avoid relying on the silent default branches in FilledSpriteAssembler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 24f0fb05-7587-4d34-9703-6bb15d3ffab5

📥 Commits

Reviewing files that changed from the base of the PR and between 1bc2b10 and 44702de.

📒 Files selected for processing (7)
  • packages/core/src/2d/assembler/FilledSpriteAssembler.ts
  • packages/core/src/2d/assembler/ISpriteRenderer.ts
  • packages/core/src/2d/enums/SpriteDrawMode.ts
  • packages/core/src/2d/enums/SpriteFilledMode.ts
  • packages/core/src/2d/enums/SpriteFilledOrigin.ts
  • packages/core/src/2d/index.ts
  • packages/core/src/2d/sprite/SpriteRenderer.ts

Comment on lines +117 to +119
static updateUVs(renderer: ISpriteRenderer): void {
// UVs are computed in updatePositions.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Empty updateUVs leaves UVs stale on atlas-region changes.

updateUVs is a no-op because UVs are baked inside updatePositions. However, SpriteRenderer._onSpriteChange handles SpriteModifyFlags.atlasRegion by setting only SpriteRendererUpdateFlags.UV (no WorldVolume). In _render, this triggers updateUVs while updatePositions is skipped — so for filled mode the UVs are never refreshed when the sprite's atlas region changes (e.g., dynamic atlas repacking). Other assemblers don't have this regression because they have a real updateUVs.

Two reasonable fixes:

  1. In SpriteRenderer._onSpriteChange, treat atlasRegion like other UV-affecting events for filled mode (set WorldVolumeAndUV), or
  2. Implement a real UV-only path here (extract the UV computation from each _filled* function so it can be invoked independently).

Option 1 is the minimal fix.

🛠 Minimal fix in SpriteRenderer._onSpriteChange
       case SpriteModifyFlags.atlasRegion:
-        this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.UV;
+        this._dirtyUpdateFlag |=
+          this._drawMode === SpriteDrawMode.Filled
+            ? SpriteRendererUpdateFlags.WorldVolumeAndUV
+            : SpriteRendererUpdateFlags.UV;
         break;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/2d/assembler/FilledSpriteAssembler.ts` around lines 117 -
119, The FilledSpriteAssembler.updateUVs is a no-op which leaves UVs stale when
atlasRegion changes because SpriteRenderer._onSpriteChange sets only
SpriteRendererUpdateFlags.UV for SpriteModifyFlags.atlasRegion and _render will
call updateUVs (skipped updatePositions); fix this by changing
SpriteRenderer._onSpriteChange to treat atlasRegion the same as other
UV-affecting changes for filled mode: when atlasRegion is detected, set the
update flags to WorldVolumeAndUV (or include WorldVolume) instead of only UV so
FilledSpriteAssembler.updatePositions runs and recomputes baked UVs; update
references to SpriteModifyFlags.atlasRegion and SpriteRendererUpdateFlags.*
accordingly.

Comment on lines +156 to +176
if (isHorizontal) {
const originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Left;
const startX = originIsStart ? lPosLB.x : lPosRB.x - (lPosRB.x - lPosLB.x) * amount;
const endX = originIsStart ? lPosLB.x + (lPosRB.x - lPosLB.x) * amount : lPosRB.x;
const startU = originIsStart ? left : right - (right - left) * amount;
const endU = originIsStart ? left + (right - left) * amount : right;
(x0 = startX), (y0 = lPosLB.y), (u0 = startU), (v0 = bottom);
(x1 = endX), (y1 = lPosRB.y), (u1 = endU), (v1 = bottom);
(x2 = startX), (y2 = lPosLT.y), (u2 = startU), (v2 = top);
(x3 = endX), (y3 = lPosRT.y), (u3 = endU), (v3 = top);
} else {
const originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Bottom;
const startY = originIsStart ? lPosLB.y : lPosLT.y - (lPosLT.y - lPosLB.y) * amount;
const endY = originIsStart ? lPosLB.y + (lPosLT.y - lPosLB.y) * amount : lPosLT.y;
const startV = originIsStart ? bottom : top - (top - bottom) * amount;
const endV = originIsStart ? bottom + (top - bottom) * amount : top;
(x0 = lPosLB.x), (y0 = startY), (u0 = left), (v0 = startV);
(x1 = lPosRB.x), (y1 = startY), (u1 = right), (v1 = startV);
(x2 = lPosLT.x), (y2 = endY), (u2 = left), (v2 = endV);
(x3 = lPosRT.x), (y3 = endY), (u3 = right), (v3 = endV);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

_filledLinear silently treats any non-Left/non-Bottom origin as the opposite end.

originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Left (resp. Bottom) means any other origin value — including corner origins or an unrelated default — quietly behaves as Right (resp. Top). Combined with the default-origin choice in SpriteRenderer.filledMode setter (see comment there), filledMode = Horizontal currently fills from the right side by default, which is almost certainly not intended.

Once the default-origin issue in SpriteRenderer is fixed, consider tightening the check here to be explicit (e.g., assert/validate the origin matches the mode, or compare against both valid values and warn otherwise). At minimum, an explicit comparison documents intent better than a single-sided equality.

Comment on lines +160 to +170
set filledMode(value: SpriteFilledMode) {
if (this._filledMode !== value) {
this._filledMode = value;
// Reset origin to a valid default for the new mode
this._filledOrigin =
value === SpriteFilledMode.Radial90 ? SpriteFilledOrigin.BottomLeft : SpriteFilledOrigin.Bottom;
if (this._drawMode === SpriteDrawMode.Filled) {
this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV;
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

filledMode setter picks an invalid default origin for Horizontal.

The expression value === SpriteFilledMode.Radial90 ? BottomLeft : Bottom assigns SpriteFilledOrigin.Bottom to every non-Radial90 mode, including Horizontal. For horizontal fills, only Left/Right are meaningful origins, and in FilledSpriteAssembler._filledLinear:

const originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Left;

So when a user writes renderer.filledMode = SpriteFilledMode.Horizontal, the renderer ends up with origin = BottomoriginIsStart = false → fills from the right side. That's almost certainly not the intended default.

🛠 Suggested fix
   set filledMode(value: SpriteFilledMode) {
     if (this._filledMode !== value) {
       this._filledMode = value;
       // Reset origin to a valid default for the new mode
-      this._filledOrigin =
-        value === SpriteFilledMode.Radial90 ? SpriteFilledOrigin.BottomLeft : SpriteFilledOrigin.Bottom;
+      switch (value) {
+        case SpriteFilledMode.Horizontal:
+          this._filledOrigin = SpriteFilledOrigin.Left;
+          break;
+        case SpriteFilledMode.Radial90:
+          this._filledOrigin = SpriteFilledOrigin.BottomLeft;
+          break;
+        default:
+          this._filledOrigin = SpriteFilledOrigin.Bottom;
+          break;
+      }
       if (this._drawMode === SpriteDrawMode.Filled) {
         this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV;
       }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/2d/sprite/SpriteRenderer.ts` around lines 160 - 170, The
filledMode setter currently sets _filledOrigin to Bottom for all non-Radial90
modes which makes Horizontal default to an invalid Bottom origin; update the
logic in the set filledMode(value: SpriteFilledMode) to choose a
mode-appropriate default: when value === SpriteFilledMode.Horizontal set
_filledOrigin to SpriteFilledOrigin.Left (so FilledSpriteAssembler._filledLinear
sees originIsStart true), when value === SpriteFilledMode.Radial90 set it to
SpriteFilledOrigin.BottomLeft, otherwise keep SpriteFilledOrigin.Bottom; keep
the existing branch that sets _dirtyUpdateFlag |=
SpriteRendererUpdateFlags.WorldVolumeAndUV when this._drawMode ===
SpriteDrawMode.Filled.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

@GuoLei1990 GuoLei1990 mentioned this pull request May 11, 2026
3 tasks
GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

已关闭问题清单

问题 状态
spriteUVs[0]/[15] 依赖 PR #2990 的 16-point UV ✅ 已关闭(依赖 #2990,合并顺序保证)
updateColor 遍历全 16 slot 写颜色 ✅ 已关闭(未使用顶点无 indices,不影响渲染)

总结

新增 FilledSpriteAssembler,支持 Horizontal/Vertical/Radial90/Radial180/Radial360 五种填充模式,对应 Unity Image.fillMethod + Image.fillAmount 语义。Radial 模式通过参数化四边形原点旋转统一 4 种 corner origin,设计简洁。613 行实现量可观但逻辑清晰。

[P2] _filledLinear 和所有 Radial 方法在 atlasRotated=true 时 UV 命名语义颠倒

_filledLinear 中:

const { x: left, y: bottom } = spriteUVs[0];
const { x: right, y: top } = spriteUVs[15];

根据 PR #2990_updateUVs

  • non-rotateduvs[0] = (left, bottom)uvs[15] = (right, top) — 变量命名正确
  • atlasRotateduvs[0] = (left, top)uvs[15] = (right, bottom)bottom/top 实际含义颠倒

当 atlas 旋转时,_filledLinearstartV/endV 插值方向会反转(bottom 和 top 互换),导致 vertical fill 显示倒序。Radial 系列同理(uvLB/uvLT 命名在 rotated 时错误)。

修复方向:在 _filledLinear/_filledRadial* 的 UV 取点处,不假设 uvs[0].y = bottom,而是从 spriteUVs 读取具体的 x/y 值并命名为 uLeft/uRight/vBottom/vTop(不加 atlas 语义假设),或在每个方法开头区分 atlasRotated

P2,不阻塞合并(当前代码在 non-rotated 场景正确),但建议在合入前修复以避免 atlasRotated + filledMode 组合场景出现视觉 bug。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

已关闭问题清单

问题 状态
spriteUVs[0]/[15] 依赖 PR #2990 的 16-point UV ✅ 已关闭(依赖 #2990,合并顺序保证)
updateColor 遍历全 16 slot 写颜色 ✅ 已关闭(未使用顶点无 indices,不影响渲染)

总结

新增 FilledSpriteAssembler,支持 Horizontal/Vertical/Radial90/Radial180/Radial360 五种填充模式,对应 Unity Image.fillMethod + Image.fillAmount 语义。Radial 模式通过参数化四边形原点旋转统一 4 种 corner origin,设计简洁。

问题

[P2] _filledLinear 和所有 Radial 方法在 atlasRotated=true 时 UV 命名语义颠倒

_filledLinear 中:

const { x: left, y: bottom } = spriteUVs[0];
const { x: right, y: top } = spriteUVs[15];

根据 PR #2990_updateUVs

  • non-rotateduvs[0] = (left, bottom)uvs[15] = (right, top) — 变量命名正确
  • atlasRotateduvs[0] = (left, top)uvs[15] = (right, bottom)bottom/top 实际含义颠倒

当 atlas 旋转时,_filledLinearstartV/endV 插值方向会反转,导致 vertical fill 显示倒序。Radial 系列同理(uvLB/uvLT 命名在 rotated 时错误)。

修复方向:在 _filledLinear/_filledRadial* 的 UV 取点处,不假设 uvs[0].y = bottom,而是直接使用 uLeft/uRight/vBottom/vTop(从 non-rotated 语义基准取值),或在每个方法开头区分 atlasRotated

P2,当前代码在 non-rotated 场景正确,atlasRotated + filledMode 组合场景会有视觉 bug。此问题上轮 review 已提出,尚未修复。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

增量审查(2026-05-15)

已关闭问题清单

问题 状态
spriteUVs[0]/[15] 依赖 PR #2990 的 16-point UV ✅ 已关闭(依赖 #2990,合并顺序保证)
updateColor 遍历全 16 slot 写颜色 ✅ 已关闭(未使用顶点无 indices,不影响渲染)

总结

新增 FilledSpriteAssembler,支持 Horizontal/Vertical/Radial90/Radial180/Radial360 五种填充模式,与 Unity Image.FillMethod 语义对齐。Radial 模式通过参数化四边形原点旋转统一四种 corner origin,设计简洁。功能完整,依赖 PR #2990 先合入。

问题

[P2] _filledLinear 和所有 Radial 方法在 atlasRotated=true 时 UV 命名语义颠倒

_filledLinear 中:

const { x: left, y: bottom } = spriteUVs[0];
const { x: right, y: top } = spriteUVs[15];

根据 PR #2990_updateUVs

  • non-rotateduvs[0] = (left, bottom)uvs[15] = (right, top) — 变量命名正确
  • atlasRotateduvs[0] = (left, top)uvs[15] = (right, bottom)bottom/top 实际含义颠倒

当 atlas 旋转时,_filledLinearstartV/endV 插值方向会反转,导致 vertical fill 显示倒序。Radial 系列同理(uvLB/uvLT 命名在 rotated 时语义错误)。

此问题上轮 review 已提出,不阻塞当前 non-rotated 场景。atlasRotated + filledMode 组合场景会有视觉 bug,建议后续修复或在文档中注明限制。P2,不阻塞合入(需先合 #2990)。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

总结

新增 FilledSpriteAssembler,支持 Horizontal/Vertical/Radial90/Radial180/Radial360 五种填充模式,对应 Unity SpriteRenderer.drawMode = Filled 的语义。这是 2D 进度条/冷却圈等常见 UI 需求的标配功能。

架构设计合理:静态分配的 scratch buffer(_worldPositions/_uvs/_outPositions/_outUVs)避免 per-frame GC,_radialCut 作为核心几何裁切原语被 Radial90/180/360 复用。

问题

[P2] FilledSpriteAssembler.ts:56resetData)— allocate 16 vertices,但 linear 模式只用 4 个

const subChunk = manager.allocateSubChunk(16);

Linear(Horizontal/Vertical)填充只需 4 顶点(1 quad),但每次 resetData 都分配了 16 个顶点槽。对于频繁变更 filledAmount 的进度条场景,这意味着顶点 buffer 利用率只有 25%。Radial360 最多 4 个 quadrant × 4 顶点 = 16,但 Radial90 最多只需 4 顶点。

建议:如果顶点数可以在 resetData 时已知(filledMode 在组件上是可枚举的),按 mode 分配精确数量;如果 mode 动态可变,至少可以按 min(mode 最大顶点数, 16) 分配。不过这属于优化,不阻塞合入。

[P2] FilledSpriteAssembler.ts:142_filledLinear)— 使用 spriteUVs[0]spriteUVs[15] 的 x/y 作为 UV 的左右上下

const { x: left, y: bottom } = spriteUVs[0];
const { x: right, y: top } = spriteUVs[15];

atlasRotated = true 时,_updateUVs 填充的是旋转后的 UV 坐标,此时 spriteUVs[0]spriteUVs[15] 代表的是 atlas 中实际的 UV 角点,但这两个点的 x/y 分量在旋转 atlas 下不再分别对应"水平""垂直"方向。_filledLinearleft/right 驱动 U 插值、top/bottom 驱动 V 插值,在旋转 atlas 下可能产生错误的 UV 映射。

此问题属于"rotated atlas + filled mode"的组合场景,当前 atlas 支持尚在 PR #2990 中引入,两个 PR 之间存在潜在交叉问题。建议在文档或代码中明确此组合的支持状态。

[P3] FilledSpriteAssembler.tsSpriteFilledOrigin.Bottom 在 Radial360 的 startAngle 为 270°

Radial360Right = 0° 为基准,Top = 90°Left = 180°Bottom = 270°。这与 Unity 的 SpriteRenderer.fillOrigin 起始角度枚举一致,正确。

可合入

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

增量审查(2026-05-15)

已关闭问题清单

问题 状态
spriteUVs[0]/[15] 依赖 PR #2990 的 16-point UV ✅ 已关闭(依赖 #2990,合并顺序保证)
updateColor 遍历全 16 slot 写颜色 ✅ 已关闭(未使用顶点无 indices,不影响渲染)

总结

新增 FilledSpriteAssembler,支持 Horizontal/Vertical/Radial90/Radial180/Radial360 五种填充模式,对标 Unity Image.fillMethod。613 行新代码覆盖了所有模式——radial 填充的扇形裁切几何本身就需要这个复杂度,没有冗余。_radialCut 作为核心几何裁切原语被 Radial90/180/360 复用,设计清晰。静态分配 scratch buffer(_worldPositions/_uvs/_outPositions/_outUVs)避免 per-frame GC,对热路径有利。

问题

[P2] FilledSpriteAssembler.ts:_filledLinear:130-168 — 本地坐标系下的 UV 与 Sprite._uvs[0]/[15] 的对应关系依赖 #2990 的坐标轴方向

_filledLinear 中取 spriteUVs[0].xleftspriteUVs[15].ytop,这只在 #2990 的 non-rotated 分支中成立(uvs[0] = LB, uvs[15] = RT)。当 atlasRotated = true 时,uvs[0]uvs[15] 的语义在 #2990 中仍是 LB/RT(因为 _updateUVs 已在 UV 层面处理了旋转映射,Assembler 无需感知),所以实际上是正确的。建议在代码中加一句注释说明:"Assembler-visible UV grid is always in display-space (rotated atlas handled inside _updateUVs), so [0]=LB / [15]=RT regardless of atlasRotated."

[P2] updatePositionsBoundingBox.transform@ts-ignore

renderer._bounds 存在于 SpriteRenderer 实现上但未在 ISpriteRenderer 接口中声明,导致需要 // @ts-ignore。这是接口不完整的信号——要么在 ISpriteRenderer 中声明 _bounds,要么让 Assembler 接受 SpriteRenderer 类型(只有 sprite 相关组件才用这个 assembler)。

无阻塞问题,此 PR 依赖 #2990 先合入,LGTM。

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