Skip to content

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

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

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

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented May 11, 2026

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

coderabbitai Bot commented May 11, 2026

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.

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: 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.

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.

已关闭问题清单

问题 状态
rotated 分支变量命名 atlas 语义翻转 ✅ 已关闭(逻辑正确)
rotated 分支 border 映射公式 ✅ 已关闭(公式推导正确)
UV 网格填充注释 / Y 轴方向说明 ✅ 已处理

总结

Sprite._uvs 从 4 点扩展为 4×4 共 16 点网格(column-major),_updateUVs 完整处理 atlasRotated/non-rotated 的 UV 坐标变换和 9-slice border 映射。SimpleSpriteAssembler/SlicedSpriteAssembler/TiledSpriteAssembler 均已更新索引方式,Assembler 侧不感知 rotation,层次清晰。_calDefaultSize 中交换 atlas 宽高也是必要修正。整体方向正确。

问题

[P1] Sprite.ts:112-115atlasRotated setter 未触发 UV 和 Size dirty,运行时动态修改无效(第 7 轮提出,仍未修复)

已通过读取代码确认,当前 HEAD(commit 0a0c4a7)的 setter 实现:

set atlasRotated(value: boolean) {
  if (this._atlasRotated != value) {
    this._atlasRotated = value;
    // ← 无任何 dirty 通知
  }
}

此 PR 的 _updateUVs_calDefaultSize 新增了对 _atlasRotated 的依赖,但 setter 从未补充对应的 dirty 通知。对比同文件 atlasRegion setter,后者修改时调用 _dispatchSpriteChange(SpriteModifyFlags.atlasRegion),触发 SpriteUpdateFlags.automaticSize | SpriteUpdateFlags.uvs

修复只需两行,类比 atlasRegion setter:

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

当前 SpriteAtlasLoader 在 Sprite 刚创建时赋值(全脏状态),所以 loader 路径不暴露此 bug。但编辑器 Inspector 动态切换 atlasRotated、序列化回写等场景会静默失效——UV 和尺寸保持旧值,无任何报错。这是 setter 语义契约的根本缺失,和 loader 路径是否安全无关。

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)

已关闭问题清单

问题 状态
rotated 分支变量命名 atlas 语义翻转 ✅ 已关闭(逻辑正确)
rotated 分支 border 映射公式 ✅ 已关闭(公式推导正确)
UV 网格填充注释 / Y 轴方向说明 ✅ 已处理

以下问题仍未修复


[P1] Sprite.tsatlasRotated setter 未触发 UV 和 Size dirty

当前 HEAD 的 setter 实现:

set atlasRotated(value: boolean) {
  if (this._atlasRotated != value) {
    this._atlasRotated = value;
    // ← 无任何 dirty 通知
  }
}

此 PR 的 _updateUVs_calDefaultSize 新增了对 _atlasRotated 的依赖,但 setter 从未补充对应的 dirty 通知。

对比同文件 atlasRegion setter——修改时调用 _dispatchSpriteChange(SpriteModifyFlags.atlasRegion),触发 SpriteUpdateFlags.automaticSize | SpriteUpdateFlags.uvs

当前 SpriteAtlasLoader 在 Sprite 刚创建时赋值(全脏状态),所以 loader 路径不暴露此 bug。但编辑器 Inspector 动态切换 atlasRotated、序列化回写等场景会静默失效——UV 和尺寸保持旧值,无任何报错。

修复(类比 atlasRegion setter):

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

此问题已在第 7 轮 review 指出,当前 HEAD 仍未修复。修复后可合入。

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.

总结

SpriteAtlas 添加 TexturePacker 风格的 90° 旋转 Sprite 支持。实现覆盖了完整的链路:Sprite._updateUVs 中区分 atlasRotated 分支重新计算 16 个 UV 点,三个 Assembler 的 updateUVs 方法适配新的 16-UV 布局,_calDefaultSize 修正旋转后宽高互换的计算,SpriteAtlasLoader 补上 atlasRotated ?? false 默认值。

核心修改正确:atlasRotated 时 column 对应 atlas Y 轴(倒序)、row 对应 atlas X 轴,UV 网格填充的两个分支在注释辅助下可验证。

问题

[P2] Sprite.ts:350+_updateUVs rotated 分支)— regionBottom 语义需验证

非旋转分支中:

const regionBottom = 1 - regionTop - regionH;

旋转分支中对 left/top/right/bottom 的映射为:

left = regionBottom → offset left
top  = regionLeft  → offset top
...

这里 regionBottom = 1 - regionTop - regionH 实际上是原图的"下边距",在 atlas 顺时针旋转 90° 后它对应 atlas 的左边。border 分量(border.x/.y/.z/.w)在旋转分支中做了 x↔y、z↔w 的交换,符合旋转语义。建议在分支顶部加一行注释说明坐标轴映射关系(如"CW 90°: display-left ↔ atlas-bottom,display-top ↔ atlas-left"),方便后续维护者验证。不阻塞合入。

[P2] TiledSpriteAssembler.tsupdateUVs)— rotated atlas 的 tiling 行为未验证

TiledSpriteAssembler 取的是 allUVs[0]/[5]/[10]/[15](4 个边界点),用于平铺计算。当 atlasRotated = true 时,_updateUVs 填充了正确的旋转后 UV,但 TiledAssembler 在平铺过程中会插值生成中间 UV 点——这些中间点的插值方向(U or V)在旋转后是否与视觉方向一致?不影响此 PR 合入(tiling + rotated atlas 是罕见组合),但建议在后续版本中补充相关 e2e 用例。

可合入

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)

已关闭问题清单

问题 状态
rotated 分支变量命名 atlas 语义翻转 ✅ 已关闭(逻辑正确)
rotated 分支 border 映射公式 ✅ 已关闭(公式推导正确)
UV 网格填充注释 / Y 轴方向说明 ✅ 已处理

以下问题仍未修复


[P1] Sprite.tsatlasRotated setter 未触发 UV 和 Size dirty

当前 atlasRotated setter(根据 diff,已有 _atlasRotated 字段):

set atlasRotated(value: boolean) {
  if (this._atlasRotated != value) {
    this._atlasRotated = value;
    // ← 无任何 dirty 通知
  }
}

此 PR 的 _updateUVs_calDefaultSize 都新增了对 _atlasRotated 的分支处理,说明这个字段会影响 UV 和 Size 的计算结果。但 setter 变更后没有触发对应 dirty flag,导致:

  • 运行时动态修改 sprite.atlasRotated 时,UV 和 Size 不会重新计算
  • 通过 SpriteAtlasLoader 加载时会先 new Sprite() 后 set atlasRotated = true,如果 UV/Size 在 set 之前已经计算过,结果是错的

对照同文件中其他影响 UV/Size 的 setter(如 atlasRegionborderregion),它们均调用了 this._dirtyUpdateFlag |= SpriteUpdateFlags.uvs 或相关 dirty 通知。atlasRotated 的 setter 应做同样处理:

set atlasRotated(value: boolean) {
  if (this._atlasRotated != value) {
    this._atlasRotated = value;
    this._dirtyUpdateFlag |= SpriteUpdateFlags.uvs;
    // 如果 _calDefaultSize 依赖 atlasRotated 且 size 是 auto,还需触发 size dirty
  }
}

请修复后重新提交。

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