Skip to content

feat(2d): support rotated sprites in atlas#2990

Draft
cptbtptpbcptdtptp wants to merge 1 commit into
galacean:dev/2.0from
cptbtptpbcptdtptp:feat/atlas-support-rotate
Draft

feat(2d): support rotated sprites in atlas#2990
cptbtptpbcptdtptp wants to merge 1 commit into
galacean:dev/2.0from
cptbtptpbcptdtptp:feat/atlas-support-rotate

Conversation

@cptbtptpbcptdtptp

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds support for rotated sprites in SpriteAtlas — TexturePacker-style atlases that pack sprites with 90° rotation to optimize space.

Changes

  • Sprite.ts — extends UV calculation to handle rotated source rect (~97 lines updated)
  • SimpleSpriteAssembler / SlicedSpriteAssembler / TiledSpriteAssembler — vertex/UV generation respects rotation flag
  • SpriteAtlasLoader.ts — propagates rotation flag from atlas metadata to Sprite

Test Plan

  • Existing sprite atlas e2e tests pass with non-rotated sprites
  • Atlas with rotated sprites loads and renders correctly (rotation flag from JSON propagates through)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed sprite atlas loader to properly default atlas rotation configuration when not specified.
  • Refactor

    • Improved UV coordinate handling and calculation for sprites to support 90° rotated atlases and enhanced grid-based sprite rendering.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Walkthrough

The PR expands sprite UV handling to support a full 16-vertex (4×4) grid with atlas 90° rotation awareness. Sprite's UV storage and computation are rewritten; size calculation accounts for rotated atlas dimensions; the loader explicitly initializes the rotation flag; and three assemblers adapt their UV reading to use column-major indexing into the new grid.

Changes

UV Grid Expansion and Atlas Rotation Support

Layer / File(s) Summary
UV Data Model Definition
packages/core/src/2d/sprite/Sprite.ts
_uvs expands to 16 elements in column-major (4×4) layout with documentation describing vertex indexing for simple and sliced/tiled assembler corner usage.
UV Computation with Rotation
packages/core/src/2d/sprite/Sprite.ts
_updateUVs() now computes all 16 UV vertices: calculates real atlas dimensions from region offsets, derives outer and 9-slice inner boundaries, and populates vertices with rotation-aware assignment logic (different order for rotated atlases).
Size Calculation with Atlas Rotation
packages/core/src/2d/sprite/Sprite.ts
_calDefaultSize() adds rotation support by swapping origin atlas width/height when _atlasRotated is true, affecting automatic dimension derivation.
Loader Rotation Flag Initialization
packages/loader/src/SpriteAtlasLoader.ts
_makeSprite now explicitly sets sprite.atlasRotated to config.atlasRotated ?? false, ensuring the flag is always initialized even when omitted from config.
Simple Sprite Assembler UV Reading
packages/core/src/2d/assembler/SimpleSpriteAssembler.ts
updateUVs derives corner UVs from the 16-element grid using fixed indices (0/3/12/15 for LB/LT/RB/RT) and assigns them into vertex UV slots.
Sliced Sprite Assembler UV Reading
packages/core/src/2d/assembler/SlicedSpriteAssembler.ts
updateUVs uses column-major indexing (i * 4 + j) to read UV pairs from the grid and write uv.x/uv.y directly into each vertex position for 9-slice rendering.
Tiled Sprite Assembler UV Reading
packages/core/src/2d/assembler/TiledSpriteAssembler.ts
_calculateDividing extracts four corner UV vectors using explicit indices (0/5/10/15) into the full UV array with column-major layout documentation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A grid of UVs, four by four,
Spins and tilts like never before,
Assemblers read with indices bright,
Corners and columns mapped just right!
rotation is in the sprite's delight.

🚥 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 'feat(2d): support rotated sprites in atlas' directly and accurately summarizes the main change: adding support for rotated sprites in sprite atlases, which is the core feature across all modified files.
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 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/sprite/Sprite.ts`:
- Around line 299-310: The sprite's cached size and UVs must be invalidated when
_atlasRotated changes: in the atlasRotated setter (the code that flips
this._atlasRotated), after toggling the boolean clear the cached computed size
and UVs by setting this._automaticWidth and this._automaticHeight to undefined
(or null) and force UV recompute/clear by invoking or resetting whatever
_getUVs() cache (e.g., call this._getUVs() or set the UV cache to null) and mark
the sprite dirty so width/height/_getUVs() will be recalculated; apply the same
invalidation logic wherever _atlasRotated can change.
- Around line 352-366: The rotated branch misapplies trim offsets—when
atlasRotated is true you must rotate the offsets and border axes to match the
packed axes mapping (original region/offset left/top/right/bottom →
bottom/left/top/right); update the computations that set left/top/right/bottom
and bLeft/bTop/bRight/bBottom to use the swapped offsets and border components:
for the X-side math (calculations that use atlasRegionW/realWidth and
regionBottom/regionTop) use offsetBottom/offsetTop and border.y/w where
appropriate, and for the Y-side math (calculations that use
atlasRegionH/realHeight and regionLeft/regionRight) use offsetLeft/offsetRight
and border.x/z accordingly so trimmed rotated sprites and 9-slice boundaries
align correctly (references: atlasRotated, realWidth, realHeight,
atlasRegionX/Y/W/H, regionLeft/Top/Right/Bottom, offsetLeft/Top/Right/Bottom,
border.x/y/w/z, regionW/regionH).
🪄 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: c7a666d6-7c92-41c5-b8a9-6232f629a763

📥 Commits

Reviewing files that changed from the base of the PR and between 1bc2b10 and 0a0c4a7.

📒 Files selected for processing (5)
  • packages/core/src/2d/assembler/SimpleSpriteAssembler.ts
  • packages/core/src/2d/assembler/SlicedSpriteAssembler.ts
  • packages/core/src/2d/assembler/TiledSpriteAssembler.ts
  • packages/core/src/2d/sprite/Sprite.ts
  • packages/loader/src/SpriteAtlasLoader.ts

Comment on lines +299 to +310
const { _texture, _atlasRegion, _atlasRegionOffset, _region, _atlasRotated } = this;
const ppuReciprocal = 1.0 / Engine._pixelsPerUnit;
// 先算 atlas 中绝对像素(texture 不一定是方形,必须各自乘对应维度)
const atlasPxW = _texture.width * _atlasRegion.width;
const atlasPxH = _texture.height * _atlasRegion.height;
// atlas 顺时针 pack 90°:原图 W×H 在 atlas 中占 H×W 区域,仅交换 atlasPx 的 W/H
const originWidth = _atlasRotated ? atlasPxH : atlasPxW;
const originHeight = _atlasRotated ? atlasPxW : atlasPxH;
this._automaticWidth =
((_texture.width * _atlasRegion.width) / (1 - _atlasRegionOffset.x - _atlasRegionOffset.z)) *
_region.width *
pixelsPerUnitReciprocal;
(originWidth / (1 - _atlasRegionOffset.x - _atlasRegionOffset.z)) * _region.width * ppuReciprocal;
this._automaticHeight =
((_texture.height * _atlasRegion.height) / (1 - _atlasRegionOffset.y - _atlasRegionOffset.w)) *
_region.height *
pixelsPerUnitReciprocal;
(originHeight / (1 - _atlasRegionOffset.y - _atlasRegionOffset.w)) * _region.height * ppuReciprocal;

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

Invalidate cached size and UVs when atlasRotated changes.

These paths now depend on _atlasRotated, but the setter at Lines 121-124 still only flips the boolean. If width, height, or _getUVs() has already been evaluated, changing sprite.atlasRotated leaves stale cached results until some other property dirties the sprite.

Suggested fix
  set atlasRotated(value: boolean) {
-    if (this._atlasRotated != value) {
+    if (this._atlasRotated !== value) {
       this._atlasRotated = value;
+      this._dispatchSpriteChange(SpriteModifyFlags.atlasRegion);
+      if (this._customWidth === undefined || this._customHeight === undefined) {
+        this._dispatchSpriteChange(SpriteModifyFlags.size);
+      }
     }
   }

Also applies to: 345-392

🤖 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/Sprite.ts` around lines 299 - 310, The sprite's
cached size and UVs must be invalidated when _atlasRotated changes: in the
atlasRotated setter (the code that flips this._atlasRotated), after toggling the
boolean clear the cached computed size and UVs by setting this._automaticWidth
and this._automaticHeight to undefined (or null) and force UV recompute/clear by
invoking or resetting whatever _getUVs() cache (e.g., call this._getUVs() or set
the UV cache to null) and mark the sprite dirty so width/height/_getUVs() will
be recalculated; apply the same invalidation logic wherever _atlasRotated can
change.

Comment on lines 352 to +366
const realWidth = atlasRegionW / (1 - offsetLeft - offsetRight);
const realHeight = atlasRegionH / (1 - offsetTop - offsetBottom);
// Coordinates of the four boundaries.
const left = Math.max(regionX - offsetLeft, 0) * realWidth + atlasRegionX;
const top = Math.max(regionBottom - offsetTop, 0) * realHeight + atlasRegionY;
const right = atlasRegionW + atlasRegionX - Math.max(regionRight - offsetRight, 0) * realWidth;
const bottom = atlasRegionH + atlasRegionY - Math.max(regionY - offsetBottom, 0) * realHeight;
const { x: borderLeft, y: borderBottom, z: borderRight, w: borderTop } = this._border;
// Left-Bottom
uv[0].set(left, bottom);
// Border ( Left-Bottom )
uv[1].set(
(regionX - offsetLeft + borderLeft * regionW) * realWidth + atlasRegionX,
atlasRegionH + atlasRegionY - (regionY - offsetBottom + borderBottom * regionH) * realHeight
);
// Border ( Right-Top )
uv[2].set(
atlasRegionW + atlasRegionX - (regionRight - offsetRight + borderRight * regionW) * realWidth,
(regionBottom - offsetTop + borderTop * regionH) * realHeight + atlasRegionY
);
// Right-Top
uv[3].set(right, top);
// 4 个外边界 + 4 个 9-slice 内边界
let left: number, top: number, right: number, bottom: number;
let bLeft: number, bTop: number, bRight: number, bBottom: number;
if (atlasRotated) {
// 原图 region/offset (left/top/right/bottom) 在 atlas 中映射为 (bottom/left/top/right)
left = Math.max(regionBottom - offsetLeft, 0) * realWidth + atlasRegionX;
top = Math.max(regionLeft - offsetTop, 0) * realHeight + atlasRegionY;
right = atlasRegionW + atlasRegionX - Math.max(regionTop - offsetRight, 0) * realWidth;
bottom = atlasRegionH + atlasRegionY - Math.max(regionRight - offsetBottom, 0) * realHeight;
bLeft = (regionBottom - offsetLeft + border.y * regionH) * realWidth + atlasRegionX;
bTop = (regionLeft - offsetTop + border.x * regionW) * realHeight + atlasRegionY;
bRight = atlasRegionW + atlasRegionX - (regionTop - offsetRight + border.w * regionH) * realWidth;
bBottom = atlasRegionH + atlasRegionY - (regionRight - offsetBottom + border.z * regionW) * realHeight;

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

Rotate the trim offsets with the packed axes.

In the rotated branch, atlas-X is derived from the sprite’s vertical span and atlas-Y from the horizontal span, but the code still feeds offsetLeft/offsetRight into the X-side math and offsetTop/offsetBottom into the Y-side math. That breaks trimmed rotated sprites when horizontal and vertical trims differ, and the 9-slice boundaries drift with them.

Suggested fix
-    const realWidth = atlasRegionW / (1 - offsetLeft - offsetRight);
-    const realHeight = atlasRegionH / (1 - offsetTop - offsetBottom);
+    const realWidth = atlasRotated
+      ? atlasRegionW / (1 - offsetTop - offsetBottom)
+      : atlasRegionW / (1 - offsetLeft - offsetRight);
+    const realHeight = atlasRotated
+      ? atlasRegionH / (1 - offsetLeft - offsetRight)
+      : atlasRegionH / (1 - offsetTop - offsetBottom);
@@
     if (atlasRotated) {
-      left = Math.max(regionBottom - offsetLeft, 0) * realWidth + atlasRegionX;
-      top = Math.max(regionLeft - offsetTop, 0) * realHeight + atlasRegionY;
-      right = atlasRegionW + atlasRegionX - Math.max(regionTop - offsetRight, 0) * realWidth;
-      bottom = atlasRegionH + atlasRegionY - Math.max(regionRight - offsetBottom, 0) * realHeight;
-      bLeft = (regionBottom - offsetLeft + border.y * regionH) * realWidth + atlasRegionX;
-      bTop = (regionLeft - offsetTop + border.x * regionW) * realHeight + atlasRegionY;
-      bRight = atlasRegionW + atlasRegionX - (regionTop - offsetRight + border.w * regionH) * realWidth;
-      bBottom = atlasRegionH + atlasRegionY - (regionRight - offsetBottom + border.z * regionW) * realHeight;
+      left = Math.max(regionBottom - offsetBottom, 0) * realWidth + atlasRegionX;
+      top = Math.max(regionLeft - offsetLeft, 0) * realHeight + atlasRegionY;
+      right = atlasRegionW + atlasRegionX - Math.max(regionTop - offsetTop, 0) * realWidth;
+      bottom = atlasRegionH + atlasRegionY - Math.max(regionRight - offsetRight, 0) * realHeight;
+      bLeft = (regionBottom - offsetBottom + border.y * regionH) * realWidth + atlasRegionX;
+      bTop = (regionLeft - offsetLeft + border.x * regionW) * realHeight + atlasRegionY;
+      bRight = atlasRegionW + atlasRegionX - (regionTop - offsetTop + border.w * regionH) * realWidth;
+      bBottom = atlasRegionH + atlasRegionY - (regionRight - offsetRight + border.z * regionW) * realHeight;
     } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const realWidth = atlasRegionW / (1 - offsetLeft - offsetRight);
const realHeight = atlasRegionH / (1 - offsetTop - offsetBottom);
// Coordinates of the four boundaries.
const left = Math.max(regionX - offsetLeft, 0) * realWidth + atlasRegionX;
const top = Math.max(regionBottom - offsetTop, 0) * realHeight + atlasRegionY;
const right = atlasRegionW + atlasRegionX - Math.max(regionRight - offsetRight, 0) * realWidth;
const bottom = atlasRegionH + atlasRegionY - Math.max(regionY - offsetBottom, 0) * realHeight;
const { x: borderLeft, y: borderBottom, z: borderRight, w: borderTop } = this._border;
// Left-Bottom
uv[0].set(left, bottom);
// Border ( Left-Bottom )
uv[1].set(
(regionX - offsetLeft + borderLeft * regionW) * realWidth + atlasRegionX,
atlasRegionH + atlasRegionY - (regionY - offsetBottom + borderBottom * regionH) * realHeight
);
// Border ( Right-Top )
uv[2].set(
atlasRegionW + atlasRegionX - (regionRight - offsetRight + borderRight * regionW) * realWidth,
(regionBottom - offsetTop + borderTop * regionH) * realHeight + atlasRegionY
);
// Right-Top
uv[3].set(right, top);
// 4 个外边界 + 4 个 9-slice 内边界
let left: number, top: number, right: number, bottom: number;
let bLeft: number, bTop: number, bRight: number, bBottom: number;
if (atlasRotated) {
// 原图 region/offset (left/top/right/bottom) 在 atlas 中映射为 (bottom/left/top/right)
left = Math.max(regionBottom - offsetLeft, 0) * realWidth + atlasRegionX;
top = Math.max(regionLeft - offsetTop, 0) * realHeight + atlasRegionY;
right = atlasRegionW + atlasRegionX - Math.max(regionTop - offsetRight, 0) * realWidth;
bottom = atlasRegionH + atlasRegionY - Math.max(regionRight - offsetBottom, 0) * realHeight;
bLeft = (regionBottom - offsetLeft + border.y * regionH) * realWidth + atlasRegionX;
bTop = (regionLeft - offsetTop + border.x * regionW) * realHeight + atlasRegionY;
bRight = atlasRegionW + atlasRegionX - (regionTop - offsetRight + border.w * regionH) * realWidth;
bBottom = atlasRegionH + atlasRegionY - (regionRight - offsetBottom + border.z * regionW) * realHeight;
const realWidth = atlasRotated
? atlasRegionW / (1 - offsetTop - offsetBottom)
: atlasRegionW / (1 - offsetLeft - offsetRight);
const realHeight = atlasRotated
? atlasRegionH / (1 - offsetLeft - offsetRight)
: atlasRegionH / (1 - offsetTop - offsetBottom);
// 4 个外边界 + 4 个 9-slice 内边界
let left: number, top: number, right: number, bottom: number;
let bLeft: number, bTop: number, bRight: number, bBottom: number;
if (atlasRotated) {
// 原图 region/offset (left/top/right/bottom) 在 atlas 中映射为 (bottom/left/top/right)
left = Math.max(regionBottom - offsetBottom, 0) * realWidth + atlasRegionX;
top = Math.max(regionLeft - offsetLeft, 0) * realHeight + atlasRegionY;
right = atlasRegionW + atlasRegionX - Math.max(regionTop - offsetTop, 0) * realWidth;
bottom = atlasRegionH + atlasRegionY - Math.max(regionRight - offsetRight, 0) * realHeight;
bLeft = (regionBottom - offsetBottom + border.y * regionH) * realWidth + atlasRegionX;
bTop = (regionLeft - offsetLeft + border.x * regionW) * realHeight + atlasRegionY;
bRight = atlasRegionW + atlasRegionX - (regionTop - offsetTop + border.w * regionH) * realWidth;
bBottom = atlasRegionH + atlasRegionY - (regionRight - offsetRight + border.z * regionW) * realHeight;
🤖 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/Sprite.ts` around lines 352 - 366, The rotated
branch misapplies trim offsets—when atlasRotated is true you must rotate the
offsets and border axes to match the packed axes mapping (original region/offset
left/top/right/bottom → bottom/left/top/right); update the computations that set
left/top/right/bottom and bLeft/bTop/bRight/bBottom to use the swapped offsets
and border components: for the X-side math (calculations that use
atlasRegionW/realWidth and regionBottom/regionTop) use offsetBottom/offsetTop
and border.y/w where appropriate, and for the Y-side math (calculations that use
atlasRegionH/realHeight and regionLeft/regionRight) use offsetLeft/offsetRight
and border.x/z accordingly so trimmed rotated sprites and 9-slice boundaries
align correctly (references: atlasRotated, realWidth, realHeight,
atlasRegionX/Y/W/H, regionLeft/Top/Right/Bottom, offsetLeft/Top/Right/Bottom,
border.x/y/w/z, regionW/regionH).

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.

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 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

增量审查(2026-06-14,commit 0a0c4a72

代码自首次审查以来未变更,作者未回复也未推送修复。本轮在第一性原理下重新推导了旋转分支的 trim-offset 轴向映射,结论与之前几轮"公式正确"的关闭判断相反——下方 [P1] trim-offset 是实锤 bug,并给出证明。

已关闭问题清单

问题 状态
rotated 分支变量命名 atlas 语义翻转 ✅ 关闭(纯命名/可读性,逻辑无误)
UV 网格填充注释 / Y 轴方向说明 ✅ 关闭(已处理)
rotated 分支 trim/border 映射"公式推导正确" 撤销关闭——早期是我自己的推导失误,下方重新证明为真 bug

总结

Sprite._uvs 从 4 点扩到 16 点 column-major 网格,旋转完全烘焙进 _updateUVsif (atlasRotated) 分支,三个 assembler 保持 rotation-agnostic(读固定索引 [0]/[3]/[12]/[15]i*4+j[0]/[5]/[10]/[15]),热路径无 per-vertex 分支,分层干净,方向正确。但旋转分支的 trim-offset 轴向配错,且 setter 漏脏标记两年级 P1 始终未修。

问题

[P1] Sprite.ts:_updateUVs 旋转分支 — trim offset 的轴向未随 90° 旋转交换(实锤,非"待测试坐实")

证明(用同 PR 内的 _calDefaultSize 做对照,两者必须用同一套约定):

TexturePacker 的 spriteSourceSize(即 atlasRegionOffset)始终是原图坐标系的 trim,与 rotated 无关;frame(即 atlasRegion)是 atlas 坐标系,旋转时 W/H 已交换。所以旋转后:atlasRegionW(atlas-X 跨度)= 原图高度方向的 packed 跨度,atlasRegionH(atlas-Y 跨度)= 原图宽度方向。

_calDefaultSize 正确地遵守了这个约定:

const originWidth  = _atlasRotated ? atlasPxH : atlasPxW;   // 旋转→atlasPxH=原图宽 ✓
const originHeight = _atlasRotated ? atlasPxW : atlasPxH;   // 旋转→atlasPxW=原图高 ✓
this._automaticWidth  = (originWidth  / (1 - offset.x - offset.z)) * ...  // 原图宽 ÷ (1-左trim-右trim) ✓
this._automaticHeight = (originHeight / (1 - offset.y - offset.w)) * ...  // 原图高 ÷ (1-上trim-下trim) ✓

即"原图宽方向的尺寸配原图左右 trim",轴向自洽。

_updateUVs 旋转分支违反了同一约定:

const realWidth  = atlasRegionW / (1 - offsetLeft - offsetRight);  // ✗ atlasRegionW=原图高方向,却除以左右(宽)trim
const realHeight = atlasRegionH / (1 - offsetTop  - offsetBottom); // ✗ atlasRegionH=原图宽方向,却除以上下(高)trim
...
left = Math.max(regionBottom - offsetLeft, 0) * realWidth + atlasRegionX; // ✗ atlas-left(=原图bottom) 却配 offsetLeft

atlasRegionW 是原图高方向,重建满跨度应除以 (1 - offsetTop - offsetBottom)(原图上下 trim),而不是 (1 - offsetLeft - offsetRight)。region/offset 配对同理:CW 90° 下 atlas-left 对应原图 bottom,应配 regionBottom - offsetBottom,而非 regionBottom - offsetLeft

后果:带非对称 trim 的旋转 spriteoffsetLeft+offsetRight ≠ offsetTop+offsetBottom)UV 边界算错,渲染错位。而"裁剪 + 旋转打包"正是 TexturePacker rotated 的典型产物,不是罕见组合。border(9-slice 内边界)沿用同一错配,同样受影响。

修复即 CodeRabbit inline 建议的轴向交换(realWidth/realHeight 的分母互换 + 旋转分支内 offsetLeft↔offsetBottomoffsetTop↔offsetLeft 等按 CW 90° 重新配对)。注意修复后必须配一个反向证伪测试:构造 atlasRotated=true + 四边 trim 不等的 sprite,断言四角及内边界 UV 落在期望 atlas 坐标;revert 修复后该测试须 fail。当前 tests/src/core/Sprite.test.ts:58atlasRotated 用例只测 getter/setter 往返,完全没覆盖 UV 数学。

[P1] Sprite.ts:111-115atlasRotated setter 不触发 UV/Size 脏标记

set atlasRotated(value: boolean) {
  if (this._atlasRotated != value) {
    this._atlasRotated = value;   // 只翻 bool,不 dispatch 任何脏标记
  }
}

_updateUVs(受 SpriteUpdateFlags.uvs 门控)和 _calDefaultSize(受 automaticSize 门控)现在都读 _atlasRotated,但运行时 sprite.atlasRotated = x 后两个脏标记都不置位 → UV/size 保持陈旧。atlasRotated 是公开 setter(无 @internal),编辑器 Inspector 改值、序列化回写都会静默失效。修复模式就在同文件下方 atlasRegion setter 现成:

set atlasRotated(value: boolean) {
  if (this._atlasRotated !== value) {        // 顺手 != 改 !==
    this._atlasRotated = value;
    this._dispatchSpriteChange(SpriteModifyFlags.atlasRegion);  // 触发 uvs + automaticSize
    if (this._customWidth === undefined || this._customHeight === undefined) {
      this._dispatchSpriteChange(SpriteModifyFlags.size);
    }
  }
}

SpriteAtlasLoader 在 Sprite 刚创建全脏时赋值,故 loader 路径不暴露此 bug,但这是 setter 语义契约的缺失,与 loader 是否安全无关。)

简化建议

无新增。两分支的 16 点填充已有注释辅助,可读性足够,不强求抽 helper。

自检

  • trim-offset 一条:已用同 PR 的 _calDefaultSize(被各轮一致认可为正确)做对照证明 _updateUVs 旋转分支轴向不自洽,是纯静态可证的 bug,不再是之前几轮"需测试坐实"的 P2,升回 P1。早期"公式正确"的关闭是我的推导失误,已在清单中显式撤销。
  • setter 漏脏标记:对照 HEAD 0a0c4a72 源码 L111-115 与下方 atlasRegion setter 实际确认。
  • assembler rotation-agnostic、热路径无回归:已确认(读固定 corner 索引,旋转烘焙在 _updateUVs)。

@GuoLei1990 GuoLei1990 marked this pull request as draft June 15, 2026 02:46
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