Skip to content

feat(spine): build Spine in as a 2D skeletal animation solution (4.2 backend)#3057

Draft
cptbtptpbcptdtptp wants to merge 8 commits into
dev/2.0from
feat/builtin-spine
Draft

feat(spine): build Spine in as a 2D skeletal animation solution (4.2 backend)#3057
cptbtptpbcptdtptp wants to merge 8 commits into
dev/2.0from
feat/builtin-spine

Conversation

@cptbtptpbcptdtptp

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #3050 (same branch, pushed directly to this repo instead of a fork so CI/collaborators have direct access).

See #3050 for the full RFC and discussion.

Summary by CodeRabbit

  • New Features
    • Added Spine animation support, including new loaders, runtime handling, rendering, and material setup.
    • Added support for Spine 4.2 assets and two-color tint rendering.
    • Added new sample/E2E coverage for Spine Boy and a tank animation scene.
  • Bug Fixes
    • Improved asset loading and path handling for Spine files and texture atlases.
  • Tests
    • Added unit and E2E tests for loaders, pools, materials, runtime registry, and Spine rendering behavior.

cptbtptpbcptdtptp and others added 5 commits June 24, 2026 14:20
Move engine-spine into the engine monorepo following the physics provider pattern: a
version-agnostic facade plus a pluggable version backend. Public API matches the
engine-spine 4.2 branch.

- @galacean/engine-spine: user-facing SpineAnimationRenderer + loaders, owns the
  Galacean-side buffers/material, with zero spine-core runtime dependency. ISpineRuntime and
  ISpineRenderTarget are the seam; the backend self-registers on import, mirroring IPhysics
  with physics-physx / physics-lite.
- @galacean/engine-spine-core-4.2: bundles @esotericsoftware/spine-core 4.2, the mesh
  generator and Spine42Runtime.
- 2D/Spine ShaderLab shader, precompiled and registered in ShaderPool.
- e2e: spineboy case.

The 3.8 backend (@galacean/engine-spine-core-3.8) follows in a separate PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
e2e: spineboy basic render, plus a tint-black two-color case using the real tank-pro
asset sampled at the muzzle-smoke frame where tintBlack on/off differs ~7% (registered
in config.ts with 0.05 tolerance).

unit (tests/src/spine): pool reuse, runtime registry, vertex stride and blend ordinal
contracts, loader url parsing, SpineMaterial blend factors, SpineAnimationRenderer
tintBlack setter and material cache.

fix: SpineAnimationRenderer._getMaterial read its cache via Map[key] (always undefined)
instead of .get(key), so every sub-mesh allocated a fresh SpineMaterial every frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	packages/shader/compiledShaders/index.ts
The packages/spine-core-3.8 directory was never committed, but a leftover
lockfile importer entry for it remained, which pnpm install prunes.
The merge's pre-commit hook auto-fixed Object -> object in this file per
the newly-merged ESLint config, which made the implementation signature's
AssetPromise.all(promises) return type no longer assignable (T is
unconstrained here since it must satisfy both the single-item and
collection overloads).
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0cc3c586-820e-4318-b870-71bf344a6c32

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR adds Spine 2D skeletal animation support to the engine: a runtime contract (ISpineRuntime/ISpineRenderTarget), a spine-core-4.2 backend package, a spine loader/renderer/material package, shader registration, E2E test cases and assets, plus minor unrelated Object-type cleanups and a monorepo-wide experimental version bump.

Changes

Spine Animation Feature

Layer / File(s) Summary
Runtime contracts and constants
packages/spine/src/runtime/ISpineRuntime.ts, ISpineRenderTarget.ts, SpineRuntimeRegistry.ts, enums/SpineBlendMode.ts, SpineConstant.ts, tests/src/spine/{SpineRuntimeRegistry,SpineConstant}.test.ts
Defines the version-neutral spine runtime backend interface, render target contract, a global runtime registry, blend mode enum, and vertex stride constants, with tests.
Object pooling utilities
packages/spine-core-4.2/src/util/{ClearablePool,ReturnablePool}.ts, tests/src/spine/Pool.test.ts
Adds ClearablePool and ReturnablePool generic instance pools used by the mesh generator, with tests.
Spine 4.2 runtime backend
packages/spine-core-4.2/src/{Spine42Runtime,SpineGenerator,SpineTexture,index}.ts, package.json, tsconfig.json
Implements the 4.2 backend that parses atlases/skeletons, steps animation state, and builds sub-primitive geometry with clipping, tinting, and blend-mode batching; self-registers via the runtime registry.
Spine loader pipeline
packages/spine/src/loader/{LoaderUtils,SpineAtlasLoader,SpineLoader,SpineResource,index}.ts, tests/src/spine/{LoaderUtils,SpineLoader}.test.ts
Adds loaders that resolve atlas/skeleton/texture URLs, load them via the resource manager, build TextureAtlas/SkeletonData, and wrap results in a SpineResource with a reusable entity template.
Spine renderer and material
packages/spine/src/renderer/{SpineAnimationRenderer,SpineMaterial,index}.ts, tests/src/spine/{SpineAnimationRenderer,SpineMaterial}.test.ts
Implements the renderer component managing buffers, sub-primitives, and materials, and SpineMaterial for blend/tint/premultiplied-alpha shader configuration.
Package entrypoint and shader registration
packages/spine/src/index.ts, packages/galacean/src/ShaderPool.ts, packages/shader/src/Shaders/index.ts, packages/spine/{package.json,tsconfig.json}
Registers loader/renderer classes, re-exports the public spine API, and registers the built-in 2D/Spine shader.
E2E Spine test cases
e2e/case/spine-*.ts, e2e/.dev/public/{spineboy,tank-pro}.atlas, e2e/config.ts, e2e/package.json
Adds Spineboy idle and tank-pro tintBlack/shoot E2E scenarios with atlas fixtures and config entries.

Core Type Cleanups and Build Config

Layer / File(s) Summary
Object type and loop variable cleanups
packages/core/src/{Engine,Entity,Polyfill}.ts, packages/core/src/asset/ResourceManager.ts, packages/core/src/particle/modules/CustomDataModule.ts
Replaces Object/Object,Object generic usages with lowercase object, and switches some let loop variables to const.
Build config and version bumps
rollup.config.js, tests/package.json, package.json, and all workspace packages/*/package.json, examples/package.json
Adjusts SWC transpile excludes for @esotericsoftware packages, adds spine workspace dependencies, and bumps versions to 0.0.0-experimental-2.0-spine.0 across the monorepo.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SpineAnimationRenderer
  participant Spine42Runtime
  participant SpineGenerator
  participant Skeleton
  SpineAnimationRenderer->>Spine42Runtime: updateState(skeleton, state, delta)
  Spine42Runtime->>Skeleton: update, apply, Physics.update
  SpineAnimationRenderer->>Spine42Runtime: buildPrimitive(skeleton, target)
  Spine42Runtime->>SpineGenerator: buildPrimitive(skeleton, target)
  SpineGenerator->>SpineGenerator: compute vertices/indices, group sub-primitives
  SpineGenerator-->>SpineAnimationRenderer: publish sub-primitives and materials
Loading
sequenceDiagram
  participant SpineLoader
  participant ResourceManager
  participant SpineAtlasLoader
  participant LoaderUtils
  participant SpineResource
  SpineLoader->>ResourceManager: request skeleton data
  SpineLoader->>ResourceManager: load atlas (single/multiple URLs)
  ResourceManager->>SpineAtlasLoader: load(item)
  SpineAtlasLoader->>LoaderUtils: loadTexturesByPaths / createTextureAtlas
  LoaderUtils-->>SpineAtlasLoader: TextureAtlas
  SpineAtlasLoader-->>SpineLoader: TextureAtlas
  SpineLoader->>LoaderUtils: createSkeletonData(rawData, atlas, scale)
  LoaderUtils-->>SpineLoader: SkeletonData
  SpineLoader->>SpineResource: new SpineResource(engine, skeletonData, url)
  SpineResource-->>SpineLoader: instantiate() entity template
Loading

Suggested labels: enhancement

Suggested reviewers: GuoLei1990

Poem

A rabbit hopped through skeleton bones,
Wired up atlases, textures, and tones,
🐇 Spine now dances, tinted so black,
Pooled little hops down a well-batched track,
Version bumped high — hop on, release the show! 🎉

🚥 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 clearly matches the PR’s main change: adding Spine as a built-in 2D skeletal animation solution with a 4.2 backend.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/builtin-spine

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.

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

总结

把 Spine 从独立仓库收进 engine,按物理包的 provider 模式拆成版本无关门面 @galacean/engine-spine + 可替换版本内核 engine-spine-core-4.2。方向非常正确:接缝切在 ISpineRuntime(解析/步进/生成)+ ISpineRenderTarget(缓冲/材质/包围盒的窄契约),门面运行时零 spine-core 引用、内核经副作用自注册,与 IPhysics 抽象同构——比 Cocos 的"一工程锁死单版本"更灵活,又不像 Three.js/Babylon 那样把 Spine 完全甩给 userland。RFC(#3050)写得很清楚,_render 忠实对标 MeshRenderer 2.0 模型、update()_overrideUpdate 自动探测正确挂帧、tint-black shader 是全工程唯一非直译处且有专门 e2e(采样 darkColor 可见帧、~7% 差异)守住。绝大多数文件是 engine-spine 4.2 的忠实移植,SpineConstant.test.ts 显式锁 SpineVertexStride/SpineBlendMode 数值防跨包漂移是亮点。build 三平台 + e2e 4/4 全绿。

先澄清一处我一度怀疑的 P0(已排除):ShaderPool.registerShaders()Shader._createFromPrecompiled(SpineSource),而 tip 上既无 compiledShaders/2D/Spine.shaderc 也无 compiledShaders/index.ts 的 Spine 导出——但 packages/shader/.gitignorecompiledShaders/,该目录是 precompile 脚本从 src/Shaders(已含 Spine 源注册)构建期生成的产物,非提交物。build+e2e 绿即证明生成正确、2D/Spine 运行时可 Shader.find无 P0。

整体建议合入,以下均为 P2/P3,不阻塞。

问题

  • [P2] SpineAnimationRenderer.ts — 跨包硬编码 engine 内部 enum 值
    文件末尾重声明 enum RendererUpdateFlags { WorldVolume = 0x1 },update()this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume 依赖这个 0x1 与 engine Renderer.ts:507RendererUpdateFlags.WorldVolume = 0x1 数值一致。卫星包看不到 @internal enum 只能重定义我理解,但这是无编译期护栏的跨包耦合:engine 一旦给 RendererUpdateFlags 重新编号,spine 会静默用错 bit(可能标错脏标记 → 包围盒/剔除异常),而不会有任何报错或测试失败。SpineConstant.test.ts 已经用"锁数值"手法守住了 SpineBlendMode/SpineVertexStride,建议同样加一条断言把 spine 侧的 RendererUpdateFlags.WorldVolume 与 engine 导出值对齐(engine 侧 RendererUpdateFlags 是 export 的,可直接 import 比对),让漂移在 CI 红而非线上白。

  • [P3] SpineGenerator.ts:122 / SpineTexture.ts:23 — 移植带进两处上游既有小瑕疵
    if (numFloats > _vertices.length) { tempVerts = new Array(numFloats) }:预扩的是 scratch tempVerts,判据却拿目标 GPU 缓冲 _vertices.length 比,应比 tempVerts.length。实际无害(JS Array 自动增长,且首帧 _vertices 长度 0 会强制扩一次自愈),但判据对象是错的。② SpineTexture.setFiltersmagFilter === MipMapLinearLinear 判 trilinear——mipmap 是 minification 概念,放大过滤不会用 mipmap,常规 atlas 路径(minFilter=MipMapLinearLinear/magFilter=Linear)下 trilinear 永远选不到。两处都与上游 galacean/engine-spine 逐字一致(非本 PR 引入),但既然是全新落地到本仓,顺手修掉比让它长期潜伏好。

  • [P3] SpineAnimationRenderer.ts:_materialCacheMap 缓存键不含 tintBlack
    静态材质缓存键 ${texture.instanceId}_${blendMode}_${pma} 不含 tintBlack,而 _setTintBlack 每帧 mutate 共享材质。理论上两个共享同一 texture 但 tintBlack 不同的 renderer 会撞同一材质、后跑的 buildPrimitive 覆盖前者。实践中 tintBlack 是 per-asset 的编辑器导出标记,共享 atlas 的实例几乎必然同 tintBlack,故基本不可达;且与上游一致。仅记录,可不动。

  • [P3] 范围蔓延 — core 无关清理混进 feature PR
    Engine.ts/Entity.ts/Polyfill.ts/ResourceManager.ts/CustomDataModule.ts 里的 Objectobjectlet..inconst..inAssetPromise.all 的 cast,都是与 spine 无关的类型/语法卫生清理。混进 11k 行的 feature PR 会稀释 review 注意力、粗化回滚粒度。建议这类纯 hygiene 单独一个 chore PR(不阻塞本 PR)。附带一致性:新 packages/spine/src/index.ts 自己却仍用 for (let key in ...),与本 PR 给 core 改的 const 风格相反。

  • [P3] SpineAnimationRenderer._render:if (!_subPrimitives) return 是死守卫
    _subPrimitives 初始化为 [] 且从不置 null/undefined,该判永假。真正想跳过空绘制应判 _subPrimitives.length === 0(虽然 for 循环 n=0 时也自然空转,当前无害)。

简化建议

架构层面无需简化——facade/kernel 的切分、副作用自注册、ISpineRenderTarget 窄契约都恰到好处,没有过度抽象。唯一可收敛的是上面 [P2]:把 spine 侧对 engine 内部 enum 的"重声明 + 隐式数值同步"用一条 CI 断言钉死,是这次跨包耦合里投入产出比最高的一处硬化(与你已经在 SpineConstant.test.ts 用的手法完全一致,顺手就能补)。

竞品对标:Cocos 内置 Spine 但锁死一工程一版本;Three.js/Babylon 不内置、交给 userland(类 pixi-spine)。Galacean 复用已有的 IPhysics provider 心智做 spine 版本后端,既内置又可换版本内核,是比二者都更自洽的选择——值得肯定。

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 2, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.18083% with 934 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.73%. Comparing base (721b42a) to head (72d440f).

Files with missing lines Patch % Lines
packages/spine-core-4.2/src/SpineGenerator.ts 0.00% 302 Missing and 1 partial ⚠️
...kages/spine/src/renderer/SpineAnimationRenderer.ts 51.42% 119 Missing ⚠️
packages/spine/src/loader/SpineLoader.ts 14.81% 92 Missing ⚠️
packages/spine/src/loader/LoaderUtils.ts 26.19% 62 Missing ⚠️
packages/spine-core-4.2/src/Spine42Runtime.ts 0.00% 49 Missing and 1 partial ⚠️
examples/src/spine-keli-4.2.ts 0.00% 47 Missing and 1 partial ⚠️
packages/spine-core-3.8/src/spine-core/index.ts 0.00% 43 Missing and 1 partial ⚠️
packages/spine/src/loader/SpineResource.ts 42.46% 42 Missing ⚠️
examples/src/spine-otakugirl-3.8.ts 0.00% 39 Missing and 1 partial ⚠️
e2e/case/spine-tint-black.ts 0.00% 29 Missing and 1 partial ⚠️
... and 10 more
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3057      +/-   ##
===========================================
+ Coverage    79.37%   80.73%   +1.35%     
===========================================
  Files          903      977      +74     
  Lines       100632   112568   +11936     
  Branches     11260    11330      +70     
===========================================
+ Hits         79879    90878   +10999     
- Misses       20569    21494     +925     
- Partials       184      196      +12     
Flag Coverage Δ
unittests 80.73% <92.18%> (+1.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 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.

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

增量审查(tip `b53a03e98`)

自上轮审查(`369102e3e`,我 `--comment` LGTM)以来,唯一新增 commit 是 `b53a03e98 chore: release v0.0.0-experimental-2.0-spine.0`——纯版本号发布 commit,零代码改动。已逐项核实:

  • 只改 18 个 package.jsonversion 字段(每文件 +1/-1),无任何 src/ 或 test 文件变动。
  • 18 个包统一 bump 到 0.0.0-experimental-2.0-spine.0,版本一致。
  • 未触碰任何 dependencies/peerDependencies 版本 pin——@galacean/engine-spinepeerDependencies: { "@galacean/engine": "workspace:*" },workspace 协议在 publish 时解析,故无需同步跨包版本引用(这是正确做法,非遗漏)。
  • CI:build×3 / e2e×4 / lint / codecov(主) / release×2 全绿。codecov/patch+codecov/project fail = 发布 commit 覆盖率中性的既知 artifact,非测试失败。

无新问题。发布 commit 本身干净可合。

上轮的 P2/P3(跨包硬编码 RendererUpdateFlags.WorldVolume=0x1 建议加 CI 断言、SpineGenerator/SpineTexture 两处上游移植小瑕疵、_materialCacheMap key 缺 tintBlack、core hygiene 范围蔓延、_subPrimitives 死守卫)均为不阻塞项,本次无代码变更故不重复展开——详见上一条 review。整体仍建议合入。

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

🧹 Nitpick comments (8)
packages/spine/src/runtime/SpineRuntimeRegistry.ts (1)

9-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider warning on silent re-registration.

registerSpineRuntime silently overwrites any previously registered backend. If a consumer accidentally imports two different core packages (e.g. spine-core-4.2 and a future spine-core-3.8), the second import silently wins with no indication anything went wrong, which could be confusing to debug given how side-effect-driven this registration is.

♻️ Optional: warn on overwrite
 export function registerSpineRuntime(runtime: ISpineRuntime): void {
+  if (_runtime && _runtime !== runtime) {
+    console.warn("`@galacean/engine-spine`: overwriting a previously registered spine runtime backend.");
+  }
   _runtime = runtime;
 }
🤖 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/spine/src/runtime/SpineRuntimeRegistry.ts` around lines 9 - 11,
registerSpineRuntime currently overwrites the existing _runtime without any
notice, so add a guard in registerSpineRuntime to detect when a runtime is
already registered and emit a warning before replacing it. Use the existing
registerSpineRuntime and _runtime symbols in SpineRuntimeRegistry to keep the
change localized, and make the warning descriptive enough to indicate a second
registration occurred and that the previous backend is being replaced.
tests/src/spine/SpineRuntimeRegistry.test.ts (1)

6-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Order-dependent test relying on module singleton state.

This test only passes if it executes before the other two tests in the file, since there's no way to reset the module-scoped _runtime singleton. The comment flags this, but it's still fragile if the test file is later refactored (e.g., new it blocks added before this one, or test.concurrent/shuffle mode is enabled).

🤖 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 `@tests/src/spine/SpineRuntimeRegistry.test.ts` around lines 6 - 9, The test in
SpineRuntimeRegistry.test.ts is order-dependent because getSpineRuntime() reads
a module-scoped singleton with no reset path. Refactor the registry or test
setup so each test can start from a clean state, ideally by adding a
reset/helper around the _runtime singleton (or equivalent API) and using it in
the test suite instead of relying on this “must run first” case. Keep the
existing getSpineRuntime and registry symbols in place, but make the “throws
when no runtime is registered” assertion independent of execution order.
packages/spine/src/index.ts (1)

20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unconditional console.log on every import.

Logging the package version on module load pollutes consumer console output unconditionally, especially for a library entrypoint loaded in production apps.

🤖 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/spine/src/index.ts` around lines 20 - 21, The module entrypoint is
logging the package version unconditionally on import, which pollutes consumer
output. Remove or guard the console.log in the index.ts module initialization
around the version export so it does not run on every import; keep the version
constant available via version without side effects.
e2e/case/spine-spineboy.ts (1)

22-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use SpineResource type instead of any.

resource is typed as any, losing type checking on .instantiate() and any other member access. SpineResource is exported from @galacean/engine-spine.

♻️ Proposed fix
-import { SpineAnimationRenderer } from "`@galacean/engine-spine`";
+import { SpineAnimationRenderer, SpineResource } from "`@galacean/engine-spine`";
...
   engine.resourceManager
     .load({
       urls: ["/spineboy.json", "/spineboy.atlas", "/spineboy.png"],
       type: "Spine"
     })
-    .then((resource: any) => {
+    .then((resource: SpineResource) => {
🤖 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 `@e2e/case/spine-spineboy.ts` around lines 22 - 27, The load callback in
spine-spineboy currently types `resource` as `any`, which removes type safety
for `.instantiate()` and other member access. Update the `.then(...)` handler to
use the exported `SpineResource` type from `@galacean/engine-spine` for the
`resource` parameter, and keep the rest of the
`engine.resourceManager.load(...)` flow unchanged.
packages/spine-core-4.2/src/SpineTexture.ts (1)

18-26: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Filter mapping loses information for several mipmap combinations.

spine-core's TextureFilter has 7 values (Nearest, Linear, MipMap, MipMapNearestNearest, MipMapLinearNearest, MipMapNearestLinear, MipMapLinearLinear), but this mapping only distinguishes Nearest (checked via minFilter) and MipMapLinearLinear (checked via magFilter) — every other mipmap variant (MipMap, MipMapNearestNearest, MipMapLinearNearest, MipMapNearestLinear) falls through to Bilinear, losing the mipmap/trilinear behavior even though Galacean's engine does support Trilinear. Mixing the check field (minFilter for one branch, magFilter for another) is also inconsistent and may not match spine atlas authoring conventions where min/mag filters can differ.

Consider checking whichever of minFilter/magFilter indicates mipmap usage consistently (e.g., treat any MipMap* value as Trilinear, matching Galacean's simplified 3-mode model).

🤖 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/spine-core-4.2/src/SpineTexture.ts` around lines 18 - 26, The
setFilters mapping in SpineTexture is dropping most mipmap cases by only
checking minFilter for Nearest and magFilter for MipMapLinearLinear, so the
other MipMap* TextureFilter values incorrectly fall back to Bilinear. Update
setFilters to handle all mipmap-related TextureFilter variants consistently,
using the same filter field for the decision and mapping any mipmap mode to
TextureFilterMode.Trilinear while keeping Nearest as Point and Linear as
Bilinear.
packages/spine-core-4.2/src/Spine42Runtime.ts (1)

28-40: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Atlas text parsed twice per load.

parseAtlasPageNames and createTextureAtlas each construct a fresh new TextureAtlas(atlasText), so the same atlas text is parsed twice for every atlas load (per the LoaderUtils.loadTextureAtlas flow: parseAtlasPageNames → load textures → createTextureAtlas). Consider caching the first parse result and reusing it (e.g., have createTextureAtlas reuse the already-parsed pages instead of re-parsing).

🤖 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/spine-core-4.2/src/Spine42Runtime.ts` around lines 28 - 40,
`Spine42Runtime` is parsing the same atlas text twice because
`parseAtlasPageNames` and `createTextureAtlas` each create a new `TextureAtlas`.
Update the atlas-loading flow so the first parsed `TextureAtlas` (or its pages)
is reused by `createTextureAtlas` instead of constructing a second one, and
adjust the `LoaderUtils.loadTextureAtlas` path accordingly.
packages/spine-core-4.2/src/index.ts (1)

14-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unconditional console.log on module import.

Every import of this package prints a version log line to the console; if this diverges from other Galacean packages' logging convention, consider gating it (e.g., only in dev builds) to avoid noisy console output in production consumer apps. Skip if this matches an existing repo-wide pattern.

#!/bin/bash
# Description: Check whether other Galacean packages follow the same version-log-on-import pattern.
rg -n '__buildVersion' --type=ts -g '!**/dist/**'
🤖 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/spine-core-4.2/src/index.ts` around lines 14 - 15, The module
entrypoint in index.ts unconditionally logs the package version on import, which
can create noisy output in consumer apps. Update the version logging around the
version constant and top-level console.log so it follows the repo’s existing
convention, gating it behind a dev-only or debug-only check if that is the
established pattern; use the version export in index.ts as the reference point
and keep the import side effects minimal.
tests/src/spine/SpineAnimationRenderer.test.ts (1)

28-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a regression test for the tintBlack cache-key gap.

Current caching test only varies blendMode; it doesn't catch the missing tintBlack dimension in the cache key (see comment on SpineAnimationRenderer.ts Lines 294-324). Once that fix lands, a test asserting two renderers with the same texture/blendMode but different tintBlack get distinct materials would guard against regression.

🤖 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 `@tests/src/spine/SpineAnimationRenderer.test.ts` around lines 28 - 36, Add a
regression test around SpineAnimationRenderer._getMaterial to cover the missing
tintBlack cache-key dimension. Extend the existing caching test in
SpineAnimationRenderer.test.ts by creating two renderer instances with the same
Texture2D and SpineBlendMode but different tintBlack settings, then assert that
each call to _getMaterial returns a different material. Use the existing
_getMaterial and SpineAnimationRenderer symbols so the test stays tied to the
cache behavior and protects the fix from regressing.
🤖 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/spine-core-4.2/src/SpineGenerator.ts`:
- Line 51: The tempVerts preallocation in SpineGenerator is ineffective because
buildPrimitive compares the needed float count against _vertices.length instead
of tempVerts.length, and the destructured const tempVerts keeps pointing to the
old array after reassignment. Update the resize check to use tempVerts length,
and change the local tempVerts binding in buildPrimitive so it can be reassigned
from SpineGenerator.tempVerts when growing, ensuring later writes use the new
buffer immediately.
- Around line 108-144: The switch in SpineGenerator’s attachment handling
violates noSwitchDeclarations because `const` and `let` are declared directly in
`case` clauses. Wrap the bodies for the `RegionAttachment`, `MeshAttachment`,
and `ClippingAttachment` cases in their own blocks so `regionAttachment`,
`meshAttachment`, and `clip` are scoped per case. Keep the existing logic in
`SpineGenerator` unchanged while moving those declarations inside block braces.

In `@packages/spine/src/loader/LoaderUtils.ts`:
- Around line 46-49: Propagate failures from the loader helpers instead of
converting them into successful fallback results. In LoaderUtils, the
Promise.all(texturePromises) and the matching loader path that currently call
reject(...) and then return [] or null should either re-throw the caught error
or stop using the side-effect reject parameter so callers of these helpers can
observe the failure. Update the affected helper methods in LoaderUtils to keep
rejected loads rejected, and preserve the original error when bubbling it up.
- Around line 64-69: In loadTextureAtlas, atlas page URLs are always loaded
through AssetType.Texture, which skips compressed texture handling for .ktx and
.ktx2 pages. Update the page-loading logic to detect each page name’s extension
and choose the compressed texture asset type when appropriate, while keeping the
existing texture loading path for standard image formats. Use the
loadTextureAtlas function and the loadTexturePromises mapping in LoaderUtils as
the place to apply the fix.

In `@packages/spine/src/loader/SpineAtlasLoader.ts`:
- Around line 21-23: The texture path collection in SpineAtlasLoader is out of
sync because imagePaths is populated for ktx/ktx2 but imageExtensions is not, so
LoaderUtils.loadTexturesByPaths cannot select the compressed texture loader.
Update the SpineAtlasLoader logic that builds assetPath (and any related
atlas/image parsing in the same loader) so every pushed URL has a matching entry
in imageExtensions, preserving index alignment for loadTexturesByPaths.
- Around line 68-72: The atlasPath variable in the SpineAtlasLoader is being set
to item.url, but according to the LoadItem contract, the url property is absent
when urls is provided instead. When a load request uses urls (like { urls:
["foo.atlas"], type: "SpineAtlas" }), item.url will be undefined. Replace the
assignment of atlasPath to use the actual resolved atlas path from the urls
array (likely the first element or the one being processed in the current
iteration) instead of item.url, so that loadTextureAtlas receives a valid path
string.

In `@packages/spine/src/loader/SpineLoader.ts`:
- Around line 105-107: The JSON skeleton detection in
SpineLoader._decoder.decode currently only checks
skeletonString.startsWith("{"), so BOM-prefixed or whitespace-prefixed JSON gets
misrouted to the binary parser. Update the SpineLoader logic around
skeletonString and spineLoadContext.skeletonRawData to normalize the decoded
string first (for example, trim leading whitespace and strip any BOM) before
checking whether it is JSON, while keeping the existing decode-and-assign flow
intact.

In `@packages/spine/src/loader/SpineResource.ts`:
- Around line 83-88: The skin attachment scan in SpineResource currently only
checks one placeholder per slot via slot.name, so it can miss textures from
other attachments in the same skin. Update the attachment enumeration in
SpineResource to iterate all skin attachments using skin.getAttachments() or
getAttachmentsForSlot(slot.index), then resolve each attachment’s texture before
adding it to the tracked set.

In `@packages/spine/src/renderer/SpineAnimationRenderer.ts`:
- Around line 294-324: The shared material cache in SpineAnimationRenderer is
missing tintBlack in its cache key, so different renderers can reuse and mutate
the same SpineMaterial with conflicting tint state. Update both _getMaterial and
_clearMaterialCache to include tintBlack in the key alongside
texture.instanceId, blendMode, and premultipliedAlpha, and keep the cache
lookup/deletion logic consistent so each tintBlack variant gets its own cached
material.

---

Nitpick comments:
In `@e2e/case/spine-spineboy.ts`:
- Around line 22-27: The load callback in spine-spineboy currently types
`resource` as `any`, which removes type safety for `.instantiate()` and other
member access. Update the `.then(...)` handler to use the exported
`SpineResource` type from `@galacean/engine-spine` for the `resource` parameter,
and keep the rest of the `engine.resourceManager.load(...)` flow unchanged.

In `@packages/spine-core-4.2/src/index.ts`:
- Around line 14-15: The module entrypoint in index.ts unconditionally logs the
package version on import, which can create noisy output in consumer apps.
Update the version logging around the version constant and top-level console.log
so it follows the repo’s existing convention, gating it behind a dev-only or
debug-only check if that is the established pattern; use the version export in
index.ts as the reference point and keep the import side effects minimal.

In `@packages/spine-core-4.2/src/Spine42Runtime.ts`:
- Around line 28-40: `Spine42Runtime` is parsing the same atlas text twice
because `parseAtlasPageNames` and `createTextureAtlas` each create a new
`TextureAtlas`. Update the atlas-loading flow so the first parsed `TextureAtlas`
(or its pages) is reused by `createTextureAtlas` instead of constructing a
second one, and adjust the `LoaderUtils.loadTextureAtlas` path accordingly.

In `@packages/spine-core-4.2/src/SpineTexture.ts`:
- Around line 18-26: The setFilters mapping in SpineTexture is dropping most
mipmap cases by only checking minFilter for Nearest and magFilter for
MipMapLinearLinear, so the other MipMap* TextureFilter values incorrectly fall
back to Bilinear. Update setFilters to handle all mipmap-related TextureFilter
variants consistently, using the same filter field for the decision and mapping
any mipmap mode to TextureFilterMode.Trilinear while keeping Nearest as Point
and Linear as Bilinear.

In `@packages/spine/src/index.ts`:
- Around line 20-21: The module entrypoint is logging the package version
unconditionally on import, which pollutes consumer output. Remove or guard the
console.log in the index.ts module initialization around the version export so
it does not run on every import; keep the version constant available via version
without side effects.

In `@packages/spine/src/runtime/SpineRuntimeRegistry.ts`:
- Around line 9-11: registerSpineRuntime currently overwrites the existing
_runtime without any notice, so add a guard in registerSpineRuntime to detect
when a runtime is already registered and emit a warning before replacing it. Use
the existing registerSpineRuntime and _runtime symbols in SpineRuntimeRegistry
to keep the change localized, and make the warning descriptive enough to
indicate a second registration occurred and that the previous backend is being
replaced.

In `@tests/src/spine/SpineAnimationRenderer.test.ts`:
- Around line 28-36: Add a regression test around
SpineAnimationRenderer._getMaterial to cover the missing tintBlack cache-key
dimension. Extend the existing caching test in SpineAnimationRenderer.test.ts by
creating two renderer instances with the same Texture2D and SpineBlendMode but
different tintBlack settings, then assert that each call to _getMaterial returns
a different material. Use the existing _getMaterial and SpineAnimationRenderer
symbols so the test stays tied to the cache behavior and protects the fix from
regressing.

In `@tests/src/spine/SpineRuntimeRegistry.test.ts`:
- Around line 6-9: The test in SpineRuntimeRegistry.test.ts is order-dependent
because getSpineRuntime() reads a module-scoped singleton with no reset path.
Refactor the registry or test setup so each test can start from a clean state,
ideally by adding a reset/helper around the _runtime singleton (or equivalent
API) and using it in the test suite instead of relying on this “must run first”
case. Keep the existing getSpineRuntime and registry symbols in place, but make
the “throws when no runtime is registered” assertion independent of execution
order.
🪄 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: 3c2b84b5-f3ca-4cf1-9fb7-24b8115838dc

📥 Commits

Reviewing files that changed from the base of the PR and between 721b42a and b53a03e.

⛔ Files ignored due to path filters (6)
  • e2e/.dev/public/spineboy.png is excluded by !**/*.png
  • e2e/.dev/public/tank-pro.png is excluded by !**/*.png
  • e2e/fixtures/originImage/Spine_spine-spineboy.jpg is excluded by !**/*.jpg
  • e2e/fixtures/originImage/Spine_spine-tint-black.jpg is excluded by !**/*.jpg
  • packages/shader/src/Shaders/2D/Spine.shader is excluded by !**/*.shader
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (62)
  • e2e/.dev/public/spineboy.atlas
  • e2e/.dev/public/spineboy.json
  • e2e/.dev/public/tank-pro.atlas
  • e2e/.dev/public/tank-pro.json
  • e2e/case/spine-spineboy.ts
  • e2e/case/spine-tint-black.ts
  • e2e/config.ts
  • e2e/package.json
  • examples/package.json
  • package.json
  • packages/core/package.json
  • packages/core/src/Engine.ts
  • packages/core/src/Entity.ts
  • packages/core/src/Polyfill.ts
  • packages/core/src/asset/ResourceManager.ts
  • packages/core/src/particle/modules/CustomDataModule.ts
  • packages/design/package.json
  • packages/galacean/package.json
  • packages/galacean/src/ShaderPool.ts
  • packages/loader/package.json
  • packages/math/package.json
  • packages/physics-physx/package.json
  • packages/rhi-webgl/package.json
  • packages/shader-compiler/package.json
  • packages/shader/package.json
  • packages/shader/src/Shaders/index.ts
  • packages/spine-core-4.2/package.json
  • packages/spine-core-4.2/src/Spine42Runtime.ts
  • packages/spine-core-4.2/src/SpineGenerator.ts
  • packages/spine-core-4.2/src/SpineTexture.ts
  • packages/spine-core-4.2/src/index.ts
  • packages/spine-core-4.2/src/util/ClearablePool.ts
  • packages/spine-core-4.2/src/util/ReturnablePool.ts
  • packages/spine-core-4.2/tsconfig.json
  • packages/spine/package.json
  • packages/spine/src/SpineConstant.ts
  • packages/spine/src/enums/SpineBlendMode.ts
  • packages/spine/src/index.ts
  • packages/spine/src/loader/LoaderUtils.ts
  • packages/spine/src/loader/SpineAtlasLoader.ts
  • packages/spine/src/loader/SpineLoader.ts
  • packages/spine/src/loader/SpineResource.ts
  • packages/spine/src/loader/index.ts
  • packages/spine/src/renderer/SpineAnimationRenderer.ts
  • packages/spine/src/renderer/SpineMaterial.ts
  • packages/spine/src/renderer/index.ts
  • packages/spine/src/runtime/ISpineRenderTarget.ts
  • packages/spine/src/runtime/ISpineRuntime.ts
  • packages/spine/src/runtime/SpineRuntimeRegistry.ts
  • packages/spine/tsconfig.json
  • packages/ui/package.json
  • packages/xr-webxr/package.json
  • packages/xr/package.json
  • rollup.config.js
  • tests/package.json
  • tests/src/spine/LoaderUtils.test.ts
  • tests/src/spine/Pool.test.ts
  • tests/src/spine/SpineAnimationRenderer.test.ts
  • tests/src/spine/SpineConstant.test.ts
  • tests/src/spine/SpineLoader.test.ts
  • tests/src/spine/SpineMaterial.test.ts
  • tests/src/spine/SpineRuntimeRegistry.test.ts


const { _clipper, _separateSlots, _subRenderItems, _separateSlotTextureMap } = this;

const { tempVerts, subRenderItemPool, subPrimitivePool } = SpineGenerator;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

tempVerts resize logic is broken: wrong length comparison and stale local reference.

Two issues combine to make this "grow if needed" logic ineffective:

  1. Line 122 compares numFloats (size needed for a single attachment) against _vertices.length (the full render target vertex buffer, unrelated in scale) instead of tempVerts.length. In practice _vertices.length is almost always far larger than numFloats, so the resize branch essentially never fires.
  2. Even when it does fire, SpineGenerator.tempVerts is reassigned to a new array (line 123), but the local tempVerts used at lines 113/129 was already destructured from the static field at the top of the function (line 51) and still points to the old array — the resize doesn't take effect until the next buildPrimitive call.

Since tempVerts is a plain JS array it auto-grows on out-of-bounds writes, so there's no crash, but the intended pre-allocation never actually happens for large mesh attachments, causing repeated implicit array growth every frame.

🔧 Proposed fix
-        case MeshAttachment:
+        case MeshAttachment: {
           const meshAttachment = <MeshAttachment>attachment;
           attachmentColor = meshAttachment.color;
           numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize;
-          if (numFloats > _vertices.length) {
-            SpineGenerator.tempVerts = new Array(numFloats);
+          if (numFloats > tempVerts.length) {
+            tempVerts = SpineGenerator.tempVerts = new Array(numFloats);
           }

(requires tempVerts to be declared with let instead of destructured const so the local reference can be reassigned)

Also applies to: 121-132

🤖 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/spine-core-4.2/src/SpineGenerator.ts` at line 51, The tempVerts
preallocation in SpineGenerator is ineffective because buildPrimitive compares
the needed float count against _vertices.length instead of tempVerts.length, and
the destructured const tempVerts keeps pointing to the old array after
reassignment. Update the resize check to use tempVerts length, and change the
local tempVerts binding in buildPrimitive so it can be reassigned from
SpineGenerator.tempVerts when growing, ensuring later writes use the new buffer
immediately.

Comment on lines +108 to +144
switch (attachment.constructor) {
case RegionAttachment:
const regionAttachment = <RegionAttachment>attachment;
attachmentColor = regionAttachment.color;
numFloats = clippedVertexSize << 2;
regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize);
triangles = SpineGenerator.QUAD_TRIANGLES;
uvs = regionAttachment.uvs;
texture = regionAttachment.region.texture;
break;
case MeshAttachment:
const meshAttachment = <MeshAttachment>attachment;
attachmentColor = meshAttachment.color;
numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize;
if (numFloats > _vertices.length) {
SpineGenerator.tempVerts = new Array(numFloats);
}
meshAttachment.computeWorldVertices(
slot,
0,
meshAttachment.worldVerticesLength,
tempVerts,
0,
clippedVertexSize
);
triangles = meshAttachment.triangles;
uvs = meshAttachment.uvs;
texture = meshAttachment.region.texture;
break;
case ClippingAttachment:
let clip = <ClippingAttachment>attachment;
_clipper.clipStart(slot, clip);
continue;
default:
_clipper.clipEndWithSlot(slot);
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Wrap switch-case declarations in blocks (Biome error).

Static analysis flags noSwitchDeclarations at lines 110, 119, and 138: const/let declared directly in case clauses are visible to sibling clauses. Wrapping each case body in { } resolves the lint error without changing behavior.

🔧 Proposed fix
       switch (attachment.constructor) {
-        case RegionAttachment:
-          const regionAttachment = <RegionAttachment>attachment;
-          attachmentColor = regionAttachment.color;
-          numFloats = clippedVertexSize << 2;
-          regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize);
-          triangles = SpineGenerator.QUAD_TRIANGLES;
-          uvs = regionAttachment.uvs;
-          texture = regionAttachment.region.texture;
-          break;
-        case MeshAttachment:
-          const meshAttachment = <MeshAttachment>attachment;
-          attachmentColor = meshAttachment.color;
-          numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize;
-          if (numFloats > _vertices.length) {
-            SpineGenerator.tempVerts = new Array(numFloats);
-          }
-          meshAttachment.computeWorldVertices(
-            slot,
-            0,
-            meshAttachment.worldVerticesLength,
-            tempVerts,
-            0,
-            clippedVertexSize
-          );
-          triangles = meshAttachment.triangles;
-          uvs = meshAttachment.uvs;
-          texture = meshAttachment.region.texture;
-          break;
-        case ClippingAttachment:
-          let clip = <ClippingAttachment>attachment;
-          _clipper.clipStart(slot, clip);
-          continue;
+        case RegionAttachment: {
+          const regionAttachment = <RegionAttachment>attachment;
+          attachmentColor = regionAttachment.color;
+          numFloats = clippedVertexSize << 2;
+          regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize);
+          triangles = SpineGenerator.QUAD_TRIANGLES;
+          uvs = regionAttachment.uvs;
+          texture = regionAttachment.region.texture;
+          break;
+        }
+        case MeshAttachment: {
+          const meshAttachment = <MeshAttachment>attachment;
+          attachmentColor = meshAttachment.color;
+          numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize;
+          if (numFloats > _vertices.length) {
+            SpineGenerator.tempVerts = new Array(numFloats);
+          }
+          meshAttachment.computeWorldVertices(
+            slot,
+            0,
+            meshAttachment.worldVerticesLength,
+            tempVerts,
+            0,
+            clippedVertexSize
+          );
+          triangles = meshAttachment.triangles;
+          uvs = meshAttachment.uvs;
+          texture = meshAttachment.region.texture;
+          break;
+        }
+        case ClippingAttachment: {
+          let clip = <ClippingAttachment>attachment;
+          _clipper.clipStart(slot, clip);
+          continue;
+        }
📝 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
switch (attachment.constructor) {
case RegionAttachment:
const regionAttachment = <RegionAttachment>attachment;
attachmentColor = regionAttachment.color;
numFloats = clippedVertexSize << 2;
regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize);
triangles = SpineGenerator.QUAD_TRIANGLES;
uvs = regionAttachment.uvs;
texture = regionAttachment.region.texture;
break;
case MeshAttachment:
const meshAttachment = <MeshAttachment>attachment;
attachmentColor = meshAttachment.color;
numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize;
if (numFloats > _vertices.length) {
SpineGenerator.tempVerts = new Array(numFloats);
}
meshAttachment.computeWorldVertices(
slot,
0,
meshAttachment.worldVerticesLength,
tempVerts,
0,
clippedVertexSize
);
triangles = meshAttachment.triangles;
uvs = meshAttachment.uvs;
texture = meshAttachment.region.texture;
break;
case ClippingAttachment:
let clip = <ClippingAttachment>attachment;
_clipper.clipStart(slot, clip);
continue;
default:
_clipper.clipEndWithSlot(slot);
continue;
}
switch (attachment.constructor) {
case RegionAttachment: {
const regionAttachment = <RegionAttachment>attachment;
attachmentColor = regionAttachment.color;
numFloats = clippedVertexSize << 2;
regionAttachment.computeWorldVertices(slot, tempVerts, 0, clippedVertexSize);
triangles = SpineGenerator.QUAD_TRIANGLES;
uvs = regionAttachment.uvs;
texture = regionAttachment.region.texture;
break;
}
case MeshAttachment: {
const meshAttachment = <MeshAttachment>attachment;
attachmentColor = meshAttachment.color;
numFloats = (meshAttachment.worldVerticesLength >> 1) * clippedVertexSize;
if (numFloats > _vertices.length) {
SpineGenerator.tempVerts = new Array(numFloats);
}
meshAttachment.computeWorldVertices(
slot,
0,
meshAttachment.worldVerticesLength,
tempVerts,
0,
clippedVertexSize
);
triangles = meshAttachment.triangles;
uvs = meshAttachment.uvs;
texture = meshAttachment.region.texture;
break;
}
case ClippingAttachment: {
let clip = <ClippingAttachment>attachment;
_clipper.clipStart(slot, clip);
continue;
}
default:
_clipper.clipEndWithSlot(slot);
continue;
}
🧰 Tools
🪛 Biome (2.5.1)

[error] 110-110: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)


[error] 119-119: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)


[error] 138-138: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 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/spine-core-4.2/src/SpineGenerator.ts` around lines 108 - 144, The
switch in SpineGenerator’s attachment handling violates noSwitchDeclarations
because `const` and `let` are declared directly in `case` clauses. Wrap the
bodies for the `RegionAttachment`, `MeshAttachment`, and `ClippingAttachment`
cases in their own blocks so `regionAttachment`, `meshAttachment`, and `clip`
are scoped per case. Keep the existing logic in `SpineGenerator` unchanged while
moving those declarations inside block braces.

Source: Linters/SAST tools

Comment on lines +46 to +49
return Promise.all(texturePromises).catch((error) => {
reject(error);
return [];
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Propagate loader failures instead of fulfilling fallback values.

Line 48 and Line 77 convert failed loads into fulfilled []/null promises after calling reject, so any direct caller of these helpers can observe a successful result for a failed asset load. Re-throw after wrapping/rejecting, or remove the side-effect reject parameter.

Proposed fix
     return Promise.all(texturePromises).catch((error) => {
       reject(error);
-      return [];
+      throw error;
     });
         .catch((err) => {
-          reject(new Error(`Spine Atlas: ${atlasPath} load error: ${err}`));
-          return null;
+          const error = new Error(`Spine Atlas: ${atlasPath} load error: ${err}`);
+          reject(error);
+          throw error;
         })

Also applies to: 75-78

🤖 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/spine/src/loader/LoaderUtils.ts` around lines 46 - 49, Propagate
failures from the loader helpers instead of converting them into successful
fallback results. In LoaderUtils, the Promise.all(texturePromises) and the
matching loader path that currently call reject(...) and then return [] or null
should either re-throw the caught error or stop using the side-effect reject
parameter so callers of these helpers can observe the failure. Update the
affected helper methods in LoaderUtils to keep rejected loads rejected, and
preserve the original error when bubbling it up.

Comment on lines +64 to +69
const loadTexturePromises = pageNames.map((name) => {
const textureUrl = baseUrl + name;
return resourceManager.load({
url: textureUrl,
type: AssetType.Texture
}) as unknown as Promise<Texture2D>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use compressed texture loaders for parsed atlas pages.

loadTextureAtlas parses page names from the atlas but always loads them as AssetType.Texture, so atlas pages ending in .ktx or .ktx2 will bypass the KTX/KTX2 loaders.

Proposed fix
           const loadTexturePromises = pageNames.map((name) => {
             const textureUrl = baseUrl + name;
+            const ext = name.match(/\.(\w+)(\?|$)/)?.[1]?.toLowerCase();
+            const textureType = ext === "ktx" ? AssetType.KTX : ext === "ktx2" ? AssetType.KTX2 : AssetType.Texture;
             return resourceManager.load({
               url: textureUrl,
-              type: AssetType.Texture
+              type: textureType
             }) as unknown as Promise<Texture2D>;
           });
📝 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 loadTexturePromises = pageNames.map((name) => {
const textureUrl = baseUrl + name;
return resourceManager.load({
url: textureUrl,
type: AssetType.Texture
}) as unknown as Promise<Texture2D>;
const loadTexturePromises = pageNames.map((name) => {
const textureUrl = baseUrl + name;
const ext = name.match(/\.(\w+)(\?|$)/)?.[1]?.toLowerCase();
const textureType = ext === "ktx" ? AssetType.KTX : ext === "ktx2" ? AssetType.KTX2 : AssetType.Texture;
return resourceManager.load({
url: textureUrl,
type: textureType
}) as unknown as Promise<Texture2D>;
🤖 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/spine/src/loader/LoaderUtils.ts` around lines 64 - 69, In
loadTextureAtlas, atlas page URLs are always loaded through AssetType.Texture,
which skips compressed texture handling for .ktx and .ktx2 pages. Update the
page-loading logic to detect each page name’s extension and choose the
compressed texture asset type when appropriate, while keeping the existing
texture loading path for standard image formats. Use the loadTextureAtlas
function and the loadTexturePromises mapping in LoaderUtils as the place to
apply the fix.

Comment on lines +21 to +23
if (["png", "jpg", "webp", "jpeg", "ktx", "ktx2"].includes(ext)) {
assetPath.imagePaths.push(url);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep imageExtensions aligned with imagePaths.

LoaderUtils.loadTexturesByPaths reads imageExtensions[index] to choose KTX/KTX2 loaders, but these branches only push paths. Explicit compressed texture URLs therefore fall back to AssetType.Texture.

Proposed fix
-    let ext = SpineLoader._getUrlExtension(url);
+    const ext = SpineLoader._getUrlExtension(url)?.toLowerCase();
     if (!ext) return;
 
     if (ext === "atlas") {
       assetPath.atlasPath = url;
     }
     if (["png", "jpg", "webp", "jpeg", "ktx", "ktx2"].includes(ext)) {
       assetPath.imagePaths.push(url);
+      assetPath.imageExtensions.push(ext);
     }
         for (let key in atlasDependency) {
           const imageVirtualPath = atlasDependency[key];
           assetPath.imagePaths.push(imageVirtualPath);
+          assetPath.imageExtensions.push(SpineLoader._getUrlExtension(imageVirtualPath)?.toLowerCase() ?? "");
         }

Also applies to: 33-36

🤖 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/spine/src/loader/SpineAtlasLoader.ts` around lines 21 - 23, The
texture path collection in SpineAtlasLoader is out of sync because imagePaths is
populated for ktx/ktx2 but imageExtensions is not, so
LoaderUtils.loadTexturesByPaths cannot select the compressed texture loader.
Update the SpineAtlasLoader logic that builds assetPath (and any related
atlas/image parsing in the same loader) so every pushed URL has a matching entry
in imageExtensions, preserving index alignment for loadTexturesByPaths.

Comment on lines +68 to +72
const imagePaths = spineAtlasAsset.imagePaths;
if (imagePaths.length === 0) {
const atlasPath = item.url;
LoaderUtils.loadTextureAtlas(atlasPath, engine, reject)
.then((textureAtlas) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use the resolved atlas path when loading from urls.

Line 70 reads item.url, but the LoadItem contract makes url absent when urls is used. A { urls: ["foo.atlas"], type: "SpineAtlas" } load will pass undefined into loadTextureAtlas.

Proposed fix
       const imagePaths = spineAtlasAsset.imagePaths;
       if (imagePaths.length === 0) {
-        const atlasPath = item.url;
         LoaderUtils.loadTextureAtlas(atlasPath, engine, reject)
🤖 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/spine/src/loader/SpineAtlasLoader.ts` around lines 68 - 72, The
atlasPath variable in the SpineAtlasLoader is being set to item.url, but
according to the LoadItem contract, the url property is absent when urls is
provided instead. When a load request uses urls (like { urls: ["foo.atlas"],
type: "SpineAtlas" }), item.url will be undefined. Replace the assignment of
atlasPath to use the actual resolved atlas path from the urls array (likely the
first element or the one being processed in the current iteration) instead of
item.url, so that loadTextureAtlas receives a valid path string.

Comment on lines +105 to +107
const skeletonString = SpineLoader._decoder.decode(skeletonRawData);
if (skeletonString.startsWith("{")) {
spineLoadContext.skeletonRawData = skeletonString;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle BOM/leading whitespace when detecting JSON skeletons.

Line 106 treats JSON as text only when the decoded buffer starts exactly with {; valid JSON with a BOM or leading whitespace will be passed to the binary parser.

Proposed fix
-          const skeletonString = SpineLoader._decoder.decode(skeletonRawData);
-          if (skeletonString.startsWith("{")) {
+          const skeletonString = SpineLoader._decoder.decode(skeletonRawData).replace(/^\uFEFF/, "");
+          if (skeletonString.trimStart().startsWith("{")) {
             spineLoadContext.skeletonRawData = skeletonString;
           }
📝 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 skeletonString = SpineLoader._decoder.decode(skeletonRawData);
if (skeletonString.startsWith("{")) {
spineLoadContext.skeletonRawData = skeletonString;
const skeletonString = SpineLoader._decoder.decode(skeletonRawData).replace(/^\uFEFF/, "");
if (skeletonString.trimStart().startsWith("{")) {
spineLoadContext.skeletonRawData = skeletonString;
🤖 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/spine/src/loader/SpineLoader.ts` around lines 105 - 107, The JSON
skeleton detection in SpineLoader._decoder.decode currently only checks
skeletonString.startsWith("{"), so BOM-prefixed or whitespace-prefixed JSON gets
misrouted to the binary parser. Update the SpineLoader logic around
skeletonString and spineLoadContext.skeletonRawData to normalize the decoded
string first (for example, trim leading whitespace and strip any BOM) before
checking whether it is JSON, while keeping the existing decode-and-assign flow
intact.

Comment on lines +83 to +88
for (let i = 0, n = skins.length; i < n; i++) {
for (let j = 0, m = slots.length; j < m; j++) {
const slot = slots[j];
const attachment = skins[i].getAttachment(slot.index, slot.name);
const texture = <Texture2D>(<RegionAttachment | MeshAttachment>attachment)?.region?.texture.texture;
if (texture) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect available Spine Skin attachment enumeration APIs in the checked-out sources.
rg -n -C 4 'class\s+Skin|interface\s+Skin|getAttachments\(|attachments\s*:' packages

Repository: galacean/engine

Length of output: 1312


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== packages/spine/src/loader/SpineResource.ts (relevant range) ==="
cat -n packages/spine/src/loader/SpineResource.ts | sed -n '1,180p'

echo
echo "=== Spine-related API definitions in repository ==="
rg -n -C 3 'getAttachments\(|class\s+Skin|interface\s+Skin|attachments\s*:' packages/spine packages -g '!**/dist/**' -g '!**/build/**'

Repository: galacean/engine

Length of output: 5560


🌐 Web query:

Spine 4.2 TypeScript Skin API getAttachments getAttachment enumerate skin entries attachments

💡 Result:

In Spine 4.2 TypeScript (spine-ts), the Skin API provides methods to access and enumerate attachments stored within a skin. The Skin class stores attachments using slot indices and names as keys [1][2]. Key methods in the Skin class include: getAttachment(slotIndex: number, name: string): Attachment | null This method returns the attachment for a specific slot index and attachment name within the skin [1]. getAttachments: Array This method returns an array containing all attachments currently stored in the skin [1]. Each SkinEntry contains the slotIndex, the name (skin placeholder name), and the Attachment object itself [1]. getAttachmentsForSlot(slotIndex: number, attachments: Array): void This is a utility method to populate a provided array with all attachments specifically for a given slot index [1]. For skeletons, the Skeleton class provides high-level access to attachments, which automatically checks the current skin and falls back to the default skin if necessary [3]: getAttachment(slotIndex: number, attachmentName: string): Attachment | null This method searches the current skin for the attachment; if not found, it subsequently checks the SkeletonData's default skin [3][2]. getAttachmentByName(slotName: string, attachmentName: string): Attachment | null A convenience method that looks up an attachment using string-based slot and attachment names by first resolving the slot index [3]. These APIs allow you to programmatically inspect, modify, or create skins at runtime, decoupling animations and code from specific attachments [4]. To enumerate all attachments in a skin, you typically call skin.getAttachments and iterate over the resulting array of SkinEntry objects [1].

Citations:


Enumerate all skin attachments here. slot.name only checks a single placeholder per slot, so textures referenced by other entries in the same skin can be missed and end up untracked. Iterate skin.getAttachments() or getAttachmentsForSlot(slot.index) instead.

🤖 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/spine/src/loader/SpineResource.ts` around lines 83 - 88, The skin
attachment scan in SpineResource currently only checks one placeholder per slot
via slot.name, so it can miss textures from other attachments in the same skin.
Update the attachment enumeration in SpineResource to iterate all skin
attachments using skin.getAttachments() or getAttachmentsForSlot(slot.index),
then resolve each attachment’s texture before adding it to the tracked set.

Comment on lines +294 to +324
_getMaterial(texture: Texture2D, blendMode: SpineBlendMode): Material {
const engine = this.engine;
const premultipliedAlpha = this.premultipliedAlpha;
const tintBlack = this.tintBlack;

const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`;
let cached = SpineAnimationRenderer._materialCacheMap.get(key);
if (!cached) {
cached = new SpineMaterial(engine);
cached.isGCIgnored = true;
SpineAnimationRenderer._materialCacheMap.set(key, cached);
}
cached._setBlendMode(blendMode, premultipliedAlpha);
cached._setTexture(texture);
cached._setTintBlack(tintBlack);
cached._setPremultipliedAlpha(premultipliedAlpha);
return cached;
}

private _clearMaterialCache(): void {
const materialCache = SpineAnimationRenderer._materialCacheMap;
const premultipliedAlpha = this.premultipliedAlpha;
const materials = this._materials;
for (let i = 0, len = materials.length; i < len; i += 1) {
const material = materials[i] as SpineMaterial;
const texture = material.shaderData.getTexture("material_SpineTexture");
const blendMode = material._getBlendMode();
const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`;
materialCache.delete(key);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Material cache key omits tintBlack, causing shared-material corruption across renderers.

_materialCacheMap is a static map shared by every SpineAnimationRenderer instance, but the cache key at Line 299 (_getMaterial) and Line 321 (_clearMaterialCache) is built from texture.instanceId, blendMode, and premultipliedAlpha only — tintBlack is never part of it. Since tintBlack is a per-material macro (RENDERER_TINT_BLACK), two renderers that share the same texture/blendMode/premultipliedAlpha combo but differ in tintBlack will resolve to the same cached SpineMaterial instance, and each _getMaterial() call mutates it in place (Lines 306-309). Whichever renderer updates last in a frame silently overrides the tint state for every other renderer sharing that key — a very likely scenario since multiple instances of the same skeleton/atlas are common in Spine scenes.

🐛 Proposed fix
-    const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`;
+    const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}_${tintBlack ? 1 : 0}`;
     let cached = SpineAnimationRenderer._materialCacheMap.get(key);
-      const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`;
+      const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}_${this.tintBlack ? 1 : 0}`;
       materialCache.delete(key);
📝 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
_getMaterial(texture: Texture2D, blendMode: SpineBlendMode): Material {
const engine = this.engine;
const premultipliedAlpha = this.premultipliedAlpha;
const tintBlack = this.tintBlack;
const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`;
let cached = SpineAnimationRenderer._materialCacheMap.get(key);
if (!cached) {
cached = new SpineMaterial(engine);
cached.isGCIgnored = true;
SpineAnimationRenderer._materialCacheMap.set(key, cached);
}
cached._setBlendMode(blendMode, premultipliedAlpha);
cached._setTexture(texture);
cached._setTintBlack(tintBlack);
cached._setPremultipliedAlpha(premultipliedAlpha);
return cached;
}
private _clearMaterialCache(): void {
const materialCache = SpineAnimationRenderer._materialCacheMap;
const premultipliedAlpha = this.premultipliedAlpha;
const materials = this._materials;
for (let i = 0, len = materials.length; i < len; i += 1) {
const material = materials[i] as SpineMaterial;
const texture = material.shaderData.getTexture("material_SpineTexture");
const blendMode = material._getBlendMode();
const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}`;
materialCache.delete(key);
}
}
_getMaterial(texture: Texture2D, blendMode: SpineBlendMode): Material {
const engine = this.engine;
const premultipliedAlpha = this.premultipliedAlpha;
const tintBlack = this.tintBlack;
const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}_${tintBlack ? 1 : 0}`;
let cached = SpineAnimationRenderer._materialCacheMap.get(key);
if (!cached) {
cached = new SpineMaterial(engine);
cached.isGCIgnored = true;
SpineAnimationRenderer._materialCacheMap.set(key, cached);
}
cached._setBlendMode(blendMode, premultipliedAlpha);
cached._setTexture(texture);
cached._setTintBlack(tintBlack);
cached._setPremultipliedAlpha(premultipliedAlpha);
return cached;
}
private _clearMaterialCache(): void {
const materialCache = SpineAnimationRenderer._materialCacheMap;
const premultipliedAlpha = this.premultipliedAlpha;
const materials = this._materials;
for (let i = 0, len = materials.length; i < len; i += 1) {
const material = materials[i] as SpineMaterial;
const texture = material.shaderData.getTexture("material_SpineTexture");
const blendMode = material._getBlendMode();
const key = `${texture.instanceId}_${blendMode}_${premultipliedAlpha ? 1 : 0}_${this.tintBlack ? 1 : 0}`;
materialCache.delete(key);
}
}
🤖 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/spine/src/renderer/SpineAnimationRenderer.ts` around lines 294 -
324, The shared material cache in SpineAnimationRenderer is missing tintBlack in
its cache key, so different renderers can reuse and mutate the same
SpineMaterial with conflicting tint state. Update both _getMaterial and
_clearMaterialCache to include tintBlack in the key alongside
texture.instanceId, blendMode, and premultipliedAlpha, and keep the cache
lookup/deletion logic consistent so each tintBlack variant gets its own cached
material.

@cptbtptpbcptdtptp cptbtptpbcptdtptp marked this pull request as draft July 2, 2026 08:46
cptbtptpbcptdtptp and others added 2 commits July 2, 2026 18:00
Vendors the spine 3.8 runtime (no npm package exists for that line;
@esotericsoftware/spine-core starts at 4.0.1) and wires it into the existing
ISpineRuntime/SpineRuntimeRegistry architecture alongside spine-core-4.2, so
`import "@galacean/engine-spine-core-3.8"` registers a working 3.8 backend.

Adapts the 4.2-derived SpineGenerator/Spine38Runtime for real 3.8 API
differences: no-arg updateWorldTransform (no Physics enum), Bone- instead of
Slot-based RegionAttachment.computeWorldVertices, a synchronous
TextureAtlas(text, textureLoader) constructor, and an extra unused parameter
on SkeletonClipping.clipTriangles. Spine38Runtime intentionally doesn't
`implements ISpineRuntime`, since the 4.2 npm package has drifted from 3.8's
shape (added TextureAtlas.setTextures, dropped Skeleton/SkeletonData's
updateCacheReset/findBoneIndex/findSlotIndex/findPathConstraintIndex) enough
that TypeScript's structural check fails despite every member the interface
actually calls lining up; it's registered via a single cast instead.

Adds two examples (spine-keli-4.2.ts, spine-otakugirl-3.8.ts) that verify
both backends end-to-end against real assets, including a genuine spine
3.8.99 binary skeleton exercising the SkeletonBinary parse path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ture property

SpineResource._associationTextureInSkeletonData read
`region.texture.texture`, but `region.texture` is already the SpineTexture
instance - getting its backing Texture2D needs `.getImage()`, same as every
other texture read site in SpineGenerator. Because TextureRegion.texture is
typed `any` in the npm 4.2 package, the extra `.texture` compiled fine but
always resolved to undefined at runtime, so textures were never associated
as a super-resource of their SpineResource and lost that GC protection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants