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.

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.

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.

总结

新增 FilledSpriteAssembler,支持 sprite filled 模式(Horizontal/Vertical/Radial90/180/360),对标 Unity Image.Type.Filled。径向裁剪 _radialCut 的 tan 插值 + 象限分解结构清晰,静态 scratch 复用得当。但有两个 code-confirmed 的 P1 仍未修,作者自上轮 review 后未推新提交。

问题

P1

  • SpriteRenderer._onSpriteChange 的 atlasRegion 分支在 Filled 模式下不刷新 UV

    atlasRegion case 只置 UV 脏标记 → 调 updateUVs,而 FilledSpriteAssembler.updateUVs 是 no-op(UV 烘焙在 updatePositions 里),且 updatePositions 不会因 UV 标记运行。结果:sprite atlas region 变了,filled 渲染的 UV 不更新。最小修法是让 Filled 模式下 atlasRegion 变更置 WorldVolumeAndUV(参考已有的 size 变更 → WorldVolumeUVAndColor 分支)。

  • SpriteRenderer.filledMode setter 为 Horizontal 选了错误的默认 origin

    setter 里非 Radial90 一律 reset 成 Bottom;但 _filledLinear 的 horizontal 分支判 originIsStart = origin === LeftBottom 不等于 Left → 从右往左填。Unity 默认:Horizontal→Left、Vertical→Bottom、Radial180/360→Bottom、Radial90→BottomLeft。建议按 mode 给默认 origin,而不是统一 Bottom

    顺带 _filledLinear 对非 Left/Bottom 的 origin 是静默 fallthrough 当成反向端,建议对非法 origin 至少有个明确处理(而非静默走反向)。

P2

  • FilledSpriteAssembler.resetData 每帧 realloc subChunk + indices = [] 是为可变顶点数服务的有意设计(径向象限数不定),但没注释,容易被误读成浪费。补一句说明。

  • ISpriteRenderer_bounds 声明,导致 assembler 里 BoundingBox.transform(..., renderer._bounds)@ts-ignore。建议在接口上补 _bounds,消掉 ts-ignore。

P3

  • 测试覆盖缺失(codecov patch 20%)。filled 模式的 fillAmount 边界(0/1)、各 origin、cw/ccw 至少要有链路级用例。

撤回之前的两条 P1(自查后判定非缺陷,不再坚持)

  • "静态 scratch _vertexOffset/_indicesOffset 跨 renderer 污染"——实际每个径向入口(_filledRadial*/_processRadialGrid)在用之前都 reset 成 0,linear 路径不用它们,单线程顺序渲染下不构成 bug。撤回。
  • "fillAmount=0 残留脏顶点"——所有 _filled*amount<=0.001indices.length=0 提前返回,0 索引画不出东西(不是 garbage,是 no-draw)。撤回。

自检

atlasRegion UV-staleness 与 Horizontal default-origin 两条 P1 已对照 base SpriteRenderer._onSpriteChange(atlasRegion 仅置 UV)与 _filledLinearorigin === Left 判据)实际代码确认。之前提的 scratch 并发 / fillAmount=0 两条经复查不成立,已主动撤回,不重复坚持。已关闭的 spriteUVs[0]/[15]#2990 依赖)、updateColor 16-slot(P3)不重提。

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