Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions packages/core/src/2d/assembler/SimpleSpriteAssembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,25 @@ export class SimpleSpriteAssembler {
}

static updateUVs(renderer: ISpriteRenderer): void {
// sprite._uvs 16 个网格点 (column-major, index=column*4+row),4 corner: [0]=LB / [3]=LT / [12]=RB / [15]=RT
// atlasRotated 由 sprite._updateUVs 内部处理,此处直接用结果,无需关心朝向
const spriteUVs = renderer.sprite._getUVs();
const { x: left, y: bottom } = spriteUVs[0];
const { x: right, y: top } = spriteUVs[3];
const subChunk = renderer._subChunk;
const vertices = subChunk.chunk.vertices;
const offset = subChunk.vertexArea.start + 3;
vertices[offset] = left;
vertices[offset + 1] = bottom;
vertices[offset + 9] = right;
vertices[offset + 10] = bottom;
vertices[offset + 18] = left;
vertices[offset + 19] = top;
vertices[offset + 27] = right;
vertices[offset + 28] = top;
const uvLB = spriteUVs[0];
const uvLT = spriteUVs[3];
const uvRB = spriteUVs[12];
const uvRT = spriteUVs[15];
// SimpleAssembler 的 vertex 顺序:0=LB, 1=RB, 2=LT, 3=RT (按 _rectangleTriangles 的拓扑)
vertices[offset] = uvLB.x;
vertices[offset + 1] = uvLB.y;
vertices[offset + 9] = uvRB.x;
vertices[offset + 10] = uvRB.y;
vertices[offset + 18] = uvLT.x;
vertices[offset + 19] = uvLT.y;
vertices[offset + 27] = uvRT.x;
vertices[offset + 28] = uvRT.y;
}

static updateColor(renderer: ISpriteRenderer, alpha: number): void {
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/2d/assembler/SlicedSpriteAssembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,15 @@ export class SlicedSpriteAssembler {
}

static updateUVs(renderer: ISpriteRenderer): void {
// 16 UV 网格 (column-major: index = i*4+j, i=column, j=row),与 vertex 索引一一对应
const subChunk = renderer._subChunk;
const vertices = subChunk.chunk.vertices;
const spriteUVs = renderer.sprite._getUVs();
for (let i = 0, o = subChunk.vertexArea.start + 3; i < 4; i++) {
const rowU = spriteUVs[i].x;
for (let j = 0; j < 4; j++, o += 9) {
vertices[o] = rowU;
vertices[o + 1] = spriteUVs[j].y;
const uv = spriteUVs[i * 4 + j];
vertices[o] = uv.x;
vertices[o + 1] = uv.y;
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/2d/assembler/TiledSpriteAssembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,12 @@ export class TiledSpriteAssembler {
const spritePositions = sprite._getPositions();
const { x: left, y: bottom } = spritePositions[0];
const { x: right, y: top } = spritePositions[3];
const [spriteUV0, spriteUV1, spriteUV2, spriteUV3] = sprite._getUVs();
// 16 UV column-major: [0]=LB(left,bottom), [5]=border-LB(bLeft,bBottom), [10]=border-RT(bRight,bTop), [15]=RT(right,top)
const allUVs = sprite._getUVs();
const spriteUV0 = allUVs[0];
const spriteUV1 = allUVs[5];
const spriteUV2 = allUVs[10];
const spriteUV3 = allUVs[15];
const expectWidth = sprite.width * referenceResolutionPerUnit;
const expectHeight = sprite.height * referenceResolutionPerUnit;
const fixedL = expectWidth * border.x;
Expand Down
97 changes: 63 additions & 34 deletions packages/core/src/2d/sprite/Sprite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,16 @@ export class Sprite extends ReferResource {
private _customHeight: number = undefined;

private _positions: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()];
private _uvs: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()];
// 16 UV 顶点构成 4×4 网格(含 9-slice 内边界)。column-major:index = i*4 + j,i=column(0=left..3=right),j=row(0=bottom..3=top)。
// 与 SlicedAssembler/TiledAssembler 的 vertex 索引图一致:[0]=LB, [3]=LT, [12]=RB, [15]=RT。
// SimpleAssembler 取 [0]/[3]/[12]/[15] 4 个 corner;Sliced/Tiled 用全 16。
// prettier-ignore
private _uvs: Vector2[] = [
new Vector2(), new Vector2(), new Vector2(), new Vector2(),
new Vector2(), new Vector2(), new Vector2(), new Vector2(),
new Vector2(), new Vector2(), new Vector2(), new Vector2(),
new Vector2(), new Vector2(), new Vector2(), new Vector2(),
];
private _bounds: BoundingBox = new BoundingBox();

private _texture: Texture2D = null;
Expand Down Expand Up @@ -287,16 +296,18 @@ export class Sprite extends ReferResource {

private _calDefaultSize(): void {
if (this._texture) {
const { _texture, _atlasRegion, _atlasRegionOffset, _region } = this;
const pixelsPerUnitReciprocal = 1.0 / Engine._pixelsPerUnit;
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;
Comment on lines +299 to +310
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.

} else {
this._automaticWidth = this._automaticHeight = 0;
}
Expand Down Expand Up @@ -332,34 +343,52 @@ export class Sprite extends ReferResource {
}

private _updateUVs(): void {
const { _uvs: uv, _atlasRegionOffset: atlasRegionOffset } = this;
const { x: regionX, y: regionY, width: regionW, height: regionH } = this._region;
const regionRight = 1 - regionX - regionW;
const regionBottom = 1 - regionY - regionH;
const { _uvs: uvs, _atlasRotated: atlasRotated, _border: border } = this;
const { x: regionLeft, y: regionTop, width: regionW, height: regionH } = this._region;
const regionRight = 1 - regionLeft - regionW;
const regionBottom = 1 - regionTop - regionH;
const { x: atlasRegionX, y: atlasRegionY, width: atlasRegionW, height: atlasRegionH } = this._atlasRegion;
const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = atlasRegionOffset;
const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = this._atlasRegionOffset;
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;
Comment on lines 352 to +366
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).

} else {
left = Math.max(regionLeft - offsetLeft, 0) * realWidth + atlasRegionX;
top = Math.max(regionBottom - offsetTop, 0) * realHeight + atlasRegionY;
right = atlasRegionW + atlasRegionX - Math.max(regionRight - offsetRight, 0) * realWidth;
bottom = atlasRegionH + atlasRegionY - Math.max(regionTop - offsetBottom, 0) * realHeight;
bLeft = (regionLeft - offsetLeft + border.x * regionW) * realWidth + atlasRegionX;
bTop = (regionBottom - offsetTop + border.w * regionH) * realHeight + atlasRegionY;
bRight = atlasRegionW + atlasRegionX - (regionRight - offsetRight + border.z * regionW) * realWidth;
bBottom = atlasRegionH + atlasRegionY - (regionTop - offsetBottom + border.y * regionH) * realHeight;
}

// 16 UV 网格填充(column-major:index=i*4+j,i=column[0=left..3=right],j=row[0=bottom..3=top])
// - 非 rotated:column 决定 atlas X (left/bLeft/bRight/right),row 决定 atlas Y (bottom/bBottom/bTop/top)
// - rotated 90° 顺时针 packed:display column 对应 atlas Y 的反向(顶→底),display row 对应 atlas X
if (atlasRotated) {
uvs[0].set(left, top), uvs[1].set(bLeft, top), uvs[2].set(bRight, top), uvs[3].set(right, top);
uvs[4].set(left, bTop), uvs[5].set(bLeft, bTop), uvs[6].set(bRight, bTop), uvs[7].set(right, bTop);
uvs[8].set(left, bBottom), uvs[9].set(bLeft, bBottom), uvs[10].set(bRight, bBottom), uvs[11].set(right, bBottom);
uvs[12].set(left, bottom), uvs[13].set(bLeft, bottom), uvs[14].set(bRight, bottom), uvs[15].set(right, bottom);
} else {
uvs[0].set(left, bottom), uvs[1].set(left, bBottom), uvs[2].set(left, bTop), uvs[3].set(left, top);
uvs[4].set(bLeft, bottom), uvs[5].set(bLeft, bBottom), uvs[6].set(bLeft, bTop), uvs[7].set(bLeft, top);
uvs[8].set(bRight, bottom), uvs[9].set(bRight, bBottom), uvs[10].set(bRight, bTop), uvs[11].set(bRight, top);
uvs[12].set(right, bottom), uvs[13].set(right, bBottom), uvs[14].set(right, bTop), uvs[15].set(right, top);
}
this._dirtyUpdateFlag &= ~SpriteUpdateFlags.uvs;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/loader/src/SpriteAtlasLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class SpriteAtlasLoader extends Loader<SpriteAtlas> {
const { x: offsetLeft, y: offsetTop, z: offsetRight, w: offsetBottom } = atlasRegionOffset;
sprite.atlasRegionOffset.set(offsetLeft * invW, offsetTop * invH, offsetRight * invW, offsetBottom * invH);
}
config.atlasRotated && (sprite.atlasRotated = true);
sprite.atlasRotated = config.atlasRotated ?? false;
}
width === undefined || (sprite.width = width);
height === undefined || (sprite.height = height);
Expand Down
Loading