@nimbus2d/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/index.mjs +18433 -0
  2. package/package.json +39 -0
  3. package/src/base/EventEmitter.js +187 -0
  4. package/src/base/Subsystem.js +183 -0
  5. package/src/base/index.js +6 -0
  6. package/src/core/Application/AppState.js +27 -0
  7. package/src/core/Application/Application.js +480 -0
  8. package/src/core/Application/DefaultConfig.js +63 -0
  9. package/src/core/Application/index.js +7 -0
  10. package/src/core/AssetManager/AssetManager.js +711 -0
  11. package/src/core/AssetManager/AssetTypes.js +108 -0
  12. package/src/core/AssetManager/adapters/IResourceAdapter.js +50 -0
  13. package/src/core/AssetManager/adapters/PixiAdapter.js +90 -0
  14. package/src/core/AssetManager/core/RefCounter.js +167 -0
  15. package/src/core/AssetManager/core/ResourceCache.js +281 -0
  16. package/src/core/AssetManager/core/ResourceEntry.js +191 -0
  17. package/src/core/AssetManager/core/ResourceEventBus.js +127 -0
  18. package/src/core/AssetManager/handlers/IResourceHandler.js +45 -0
  19. package/src/core/AssetManager/index.js +26 -0
  20. package/src/core/AssetManager/plugins/IResourcePlugin.js +56 -0
  21. package/src/core/Camera/Camera.js +534 -0
  22. package/src/core/Camera/CameraTypes.js +59 -0
  23. package/src/core/Camera/index.js +13 -0
  24. package/src/core/ConfigSystem/ConfigLayer.js +230 -0
  25. package/src/core/ConfigSystem/ConfigLayerType.js +31 -0
  26. package/src/core/ConfigSystem/ConfigMerger.js +50 -0
  27. package/src/core/ConfigSystem/ConfigPath.js +96 -0
  28. package/src/core/ConfigSystem/ConfigSystem.js +378 -0
  29. package/src/core/ConfigSystem/ConfigTypes.js +29 -0
  30. package/src/core/ConfigSystem/ConfigWatcher.js +84 -0
  31. package/src/core/ConfigSystem/index.js +19 -0
  32. package/src/core/EventBus/EventBus.js +797 -0
  33. package/src/core/EventBus/Interceptor.js +26 -0
  34. package/src/core/EventBus/PerformanceStats.js +105 -0
  35. package/src/core/EventBus/index.js +7 -0
  36. package/src/core/Lifecycle/Lifecycle.js +265 -0
  37. package/src/core/Lifecycle/LifecycleState.js +31 -0
  38. package/src/core/Lifecycle/index.js +6 -0
  39. package/src/core/Logger/ConsoleHandler.js +55 -0
  40. package/src/core/Logger/LogHandler.js +34 -0
  41. package/src/core/Logger/LogLevel.js +38 -0
  42. package/src/core/Logger/Logger.js +234 -0
  43. package/src/core/Logger/index.js +8 -0
  44. package/src/core/PluginSystem/Plugin.js +156 -0
  45. package/src/core/PluginSystem/PluginEntry.js +102 -0
  46. package/src/core/PluginSystem/PluginState.js +30 -0
  47. package/src/core/PluginSystem/PluginSystem.js +530 -0
  48. package/src/core/PluginSystem/index.js +8 -0
  49. package/src/core/PoolManager/DefaultConfig.js +19 -0
  50. package/src/core/PoolManager/ObjectPool.js +326 -0
  51. package/src/core/PoolManager/PoolManager.js +253 -0
  52. package/src/core/PoolManager/PoolTypes.js +92 -0
  53. package/src/core/PoolManager/index.js +15 -0
  54. package/src/core/SceneManager/FadeTransition.js +64 -0
  55. package/src/core/SceneManager/MaskTransition.js +234 -0
  56. package/src/core/SceneManager/Scene.js +221 -0
  57. package/src/core/SceneManager/SceneManager.js +658 -0
  58. package/src/core/SceneManager/SceneState.js +29 -0
  59. package/src/core/SceneManager/SlideTransition.js +113 -0
  60. package/src/core/SceneManager/Transition.js +102 -0
  61. package/src/core/SceneManager/ZoomTransition.js +119 -0
  62. package/src/core/SceneManager/index.js +12 -0
  63. package/src/core/SpriteGPULayer/SpriteGPULayer.js +321 -0
  64. package/src/core/SpriteGPULayer/SpriteGPULayerTypes.js +49 -0
  65. package/src/core/SpriteGPULayer/index.js +11 -0
  66. package/src/core/Timeline/Clip.js +101 -0
  67. package/src/core/Timeline/DefaultConfig.js +41 -0
  68. package/src/core/Timeline/Easing.js +176 -0
  69. package/src/core/Timeline/Timeline.js +458 -0
  70. package/src/core/Timeline/TimelineTypes.js +89 -0
  71. package/src/core/Timeline/Track.js +202 -0
  72. package/src/core/Timeline/index.js +22 -0
  73. package/src/errors/ConfigError.js +47 -0
  74. package/src/errors/LifecycleError.js +58 -0
  75. package/src/errors/NimbusError.js +43 -0
  76. package/src/errors/PluginError.js +138 -0
  77. package/src/errors/ResourceError.js +166 -0
  78. package/src/errors/SceneError.js +124 -0
  79. package/src/errors/SubsystemError.js +68 -0
  80. package/src/errors/index.js +53 -0
  81. package/src/index.js +86 -0
  82. package/src/types/index.js +141 -0
  83. package/src/utils/MemoryChecker.js +374 -0
  84. package/src/utils/Validator.js +60 -0
  85. package/src/utils/index.js +84 -0
@@ -0,0 +1,113 @@
1
+ // ============================================================================
2
+ // Nimbus2D - 滑动过渡效果
3
+ // ----------------------------------------------------------------------------
4
+ // 源场景滑出,目标场景滑入。支持四个方向:left/right/up/down。
5
+ //
6
+ // 继承关系:
7
+ // Transition → SlideTransition
8
+ // ============================================================================
9
+
10
+ import { Transition } from './Transition.js'
11
+
12
+ class SlideTransition extends Transition {
13
+ /**
14
+ * 构造函数
15
+ * @param {Object} [options={}] - 选项
16
+ * @param {string} [options.direction='left'] - 滑动方向(left/right/up/down)
17
+ */
18
+ constructor(options = {}) {
19
+ super(options)
20
+
21
+ /** @type {string} 滑动方向 */
22
+ this._direction = options.direction || 'left'
23
+ }
24
+
25
+ /**
26
+ * 执行滑动过渡
27
+ * @param {import('./Scene.js').Scene} fromScene - 源场景
28
+ * @param {import('./Scene.js').Scene} toScene - 目标场景
29
+ * @returns {Promise<void>}
30
+ */
31
+ async execute(fromScene, toScene) {
32
+ // 触发开始回调
33
+ if (this._onTransitionStart) this._onTransitionStart()
34
+
35
+ // 获取画布尺寸
36
+ const width = (fromScene && fromScene.stage && fromScene.stage.parent) ? fromScene.stage.parent.width : 800
37
+ const height = (fromScene && fromScene.stage && fromScene.stage.parent) ? fromScene.stage.parent.height : 600
38
+
39
+ // 设置目标场景初始位置
40
+ if (toScene && toScene.stage) {
41
+ switch (this._direction) {
42
+ case 'left':
43
+ toScene.stage.x = width
44
+ toScene.stage.y = 0
45
+ break
46
+ case 'right':
47
+ toScene.stage.x = -width
48
+ toScene.stage.y = 0
49
+ break
50
+ case 'up':
51
+ toScene.stage.x = 0
52
+ toScene.stage.y = height
53
+ break
54
+ case 'down':
55
+ toScene.stage.x = 0
56
+ toScene.stage.y = -height
57
+ break
58
+ }
59
+ }
60
+
61
+ // 同时滑动:源场景滑出,目标场景滑入
62
+ await this._animate(this._duration, (progress) => {
63
+ if (fromScene && fromScene.stage) {
64
+ switch (this._direction) {
65
+ case 'left':
66
+ fromScene.stage.x = -progress * width
67
+ break
68
+ case 'right':
69
+ fromScene.stage.x = progress * width
70
+ break
71
+ case 'up':
72
+ fromScene.stage.y = -progress * height
73
+ break
74
+ case 'down':
75
+ fromScene.stage.y = progress * height
76
+ break
77
+ }
78
+ }
79
+
80
+ if (toScene && toScene.stage) {
81
+ switch (this._direction) {
82
+ case 'left':
83
+ toScene.stage.x = width * (1 - progress)
84
+ break
85
+ case 'right':
86
+ toScene.stage.x = -width * (1 - progress)
87
+ break
88
+ case 'up':
89
+ toScene.stage.y = height * (1 - progress)
90
+ break
91
+ case 'down':
92
+ toScene.stage.y = -height * (1 - progress)
93
+ break
94
+ }
95
+ }
96
+ })
97
+
98
+ // 确保最终位置
99
+ if (fromScene && fromScene.stage) {
100
+ fromScene.stage.x = 0
101
+ fromScene.stage.y = 0
102
+ }
103
+ if (toScene && toScene.stage) {
104
+ toScene.stage.x = 0
105
+ toScene.stage.y = 0
106
+ }
107
+
108
+ // 触发结束回调
109
+ if (this._onTransitionEnd) this._onTransitionEnd()
110
+ }
111
+ }
112
+
113
+ export { SlideTransition }
@@ -0,0 +1,102 @@
1
+ // ============================================================================
2
+ // Nimbus2D - 过渡效果基类
3
+ // ----------------------------------------------------------------------------
4
+ // 所有过渡效果必须继承此基类,提供 execute 方法契约。
5
+ //
6
+ // 设计决策:
7
+ // - 独立设计(不继承任何类)
8
+ // - 支持 4 种缓动函数:linear/easeIn/easeOut/easeInOut
9
+ // - 子类重写 execute(fromScene, toScene) 实现过渡逻辑
10
+ // ============================================================================
11
+
12
+ /**
13
+ * 缓动函数映射表
14
+ * @type {Object<string, Function>}
15
+ */
16
+ const EASING_FUNCTIONS = {
17
+ /** 线性(匀速) */
18
+ linear: (t) => t,
19
+ /** 先慢后快 */
20
+ easeIn: (t) => t * t,
21
+ /** 先快后慢 */
22
+ easeOut: (t) => t * (2 - t),
23
+ /** 先慢后快再慢(默认) */
24
+ easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)
25
+ }
26
+
27
+ class Transition {
28
+ /**
29
+ * 构造函数
30
+ * @param {Object} [options={}] - 选项
31
+ * @param {number} [options.duration=500] - 持续时间(ms)
32
+ * @param {string} [options.easing='easeInOut'] - 缓动函数
33
+ * @param {Function} [options.onTransitionStart] - 开始回调
34
+ * @param {Function} [options.onTransitionEnd] - 结束回调
35
+ */
36
+ constructor(options = {}) {
37
+ /** @type {number} 持续时间(ms) */
38
+ this._duration = options.duration || 500
39
+
40
+ /** @type {string} 缓动函数名称 */
41
+ this._easing = EASING_FUNCTIONS[options.easing] ? options.easing : 'easeInOut'
42
+
43
+ /** @type {Function|null} 开始回调 */
44
+ this._onTransitionStart = options.onTransitionStart || null
45
+
46
+ /** @type {Function|null} 结束回调 */
47
+ this._onTransitionEnd = options.onTransitionEnd || null
48
+ }
49
+
50
+ /**
51
+ * 执行过渡(子类重写)
52
+ * @param {import('./Scene.js').Scene} fromScene - 源场景
53
+ * @param {import('./Scene.js').Scene} toScene - 目标场景
54
+ * @returns {Promise<void>}
55
+ */
56
+ async execute(fromScene, toScene) {
57
+ // 基类空实现,子类必须重写
58
+ }
59
+
60
+ /**
61
+ * 应用缓动函数
62
+ * @param {number} t - 进度值 [0, 1]
63
+ * @param {string} [easing] - 缓动函数名
64
+ * @returns {number} 缓动后的值
65
+ * @protected
66
+ */
67
+ _applyEasing(t, easing) {
68
+ const fn = EASING_FUNCTIONS[easing || this._easing]
69
+ return fn ? fn(t) : EASING_FUNCTIONS.easeInOut(t)
70
+ }
71
+
72
+ /**
73
+ * 等待指定时长(带缓动动画辅助)
74
+ * @param {number} duration - 时长(ms)
75
+ * @param {Function} onFrame - 每帧回调 (progress: number) => void
76
+ * @returns {Promise<void>}
77
+ * @protected
78
+ */
79
+ _animate(duration, onFrame) {
80
+ return new Promise((resolve) => {
81
+ const startTime = performance.now()
82
+
83
+ const tick = () => {
84
+ const elapsed = performance.now() - startTime
85
+ const rawProgress = Math.min(elapsed / duration, 1)
86
+ const easedProgress = this._applyEasing(rawProgress)
87
+
88
+ onFrame(easedProgress)
89
+
90
+ if (rawProgress < 1) {
91
+ requestAnimationFrame(tick)
92
+ } else {
93
+ resolve()
94
+ }
95
+ }
96
+
97
+ requestAnimationFrame(tick)
98
+ })
99
+ }
100
+ }
101
+
102
+ export { Transition }
@@ -0,0 +1,119 @@
1
+ // ============================================================================
2
+ // Nimbus2D - ZoomTransition 缩放转场效果
3
+ // ----------------------------------------------------------------------------
4
+ // 职责:
5
+ // 1. 实现缩放转场动画(源场景缩小消失 + 目标场景放大出现)
6
+ // 2. 支持 scaleFrom/scaleTo 配置源场景缩放范围
7
+ // 3. 目标场景从 0 缩放到 1,并同步淡入
8
+ // 4. 复用 _animate/_applyEasing 提供缓动效果
9
+ //
10
+ // 继承关系:
11
+ // Transition → ZoomTransition
12
+ //
13
+ // 设计决策:
14
+ // - 继承现有 Transition 基类(execute 契约),复用 _animate/_applyEasing
15
+ // - 源场景缩放范围可配置(scaleFrom→scaleTo),目标场景固定 0→1
16
+ // - 同步淡入淡出 alpha 增强转场视觉效果
17
+ // - 完成后恢复 stage 的 scale/alpha 到正常值(避免状态残留)
18
+ //
19
+ // 四期新增(任务 1.5/3.2):对标 Phaser 的 ZoomTransition
20
+ // ============================================================================
21
+
22
+ import { Transition } from './Transition.js'
23
+
24
+ class ZoomTransition extends Transition {
25
+ /**
26
+ * 构造函数
27
+ * @param {Object} [options={}] - 选项(继承自 Transition)
28
+ * @param {number} [options.scaleFrom=1] - 源场景起始缩放(默认 1,正常大小)
29
+ * @param {number} [options.scaleTo=0] - 源场景结束缩放(默认 0,完全缩小)
30
+ */
31
+ constructor(options = {}) {
32
+ super(options)
33
+
34
+ /** @type {number} 源场景起始缩放 */
35
+ this._scaleFrom = options.scaleFrom ?? 1
36
+
37
+ /** @type {number} 源场景结束缩放 */
38
+ this._scaleTo = options.scaleTo ?? 0
39
+ }
40
+
41
+ /**
42
+ * 执行缩放转场
43
+ * @param {import('./Scene.js').Scene} fromScene - 源场景
44
+ * @param {import('./Scene.js').Scene} toScene - 目标场景
45
+ * @returns {Promise<void>}
46
+ */
47
+ async execute(fromScene, toScene) {
48
+ // 触发开始回调
49
+ if (this._onTransitionStart) this._onTransitionStart()
50
+
51
+ /** @type {import('pixi.js').Container|null} 源场景 stage */
52
+ const fromStage = fromScene ? fromScene.stage : null
53
+ /** @type {import('pixi.js').Container|null} 目标场景 stage */
54
+ const toStage = toScene ? toScene.stage : null
55
+
56
+ // 设置目标场景初始状态(缩放为 0,alpha 为 0,完全不可见)
57
+ if (toStage) {
58
+ toStage.scale.set(0, 0)
59
+ toStage.alpha = 0
60
+ }
61
+
62
+ // 设置源场景初始状态(缩放为 scaleFrom,alpha 为 1,完全可见)
63
+ if (fromStage) {
64
+ fromStage.scale.set(this._scaleFrom, this._scaleFrom)
65
+ fromStage.alpha = 1
66
+ }
67
+
68
+ try {
69
+ // 执行缩放动画
70
+ await this._animate(this._duration, (progress) => {
71
+ // 源场景:从 scaleFrom 缩放到 scaleTo,同步淡出
72
+ if (fromStage) {
73
+ /** @type {number} 当前源场景缩放值(线性插值) */
74
+ const fromScale = this._scaleFrom + (this._scaleTo - this._scaleFrom) * progress
75
+ fromStage.scale.set(fromScale, fromScale)
76
+ fromStage.alpha = 1 - progress
77
+ }
78
+
79
+ // 目标场景:从 0 放大到 1,同步淡入
80
+ if (toStage) {
81
+ toStage.scale.set(progress, progress)
82
+ toStage.alpha = progress
83
+ }
84
+ })
85
+ } finally {
86
+ // 无论动画成功或失败,都恢复 stage 到正常状态
87
+ this._restoreStages(fromStage, toStage)
88
+ }
89
+
90
+ // 触发结束回调
91
+ if (this._onTransitionEnd) this._onTransitionEnd()
92
+ }
93
+
94
+ // --------------------------------------------------------------------------
95
+ // 私有方法:状态恢复
96
+ // --------------------------------------------------------------------------
97
+
98
+ /**
99
+ * 恢复 stage 的 scale/alpha 到正常值
100
+ * 避免转场异常时 stage 状态残留(scale=0 或 alpha=0 导致场景不可见)
101
+ * @param {import('pixi.js').Container|null} fromStage - 源场景 stage
102
+ * @param {import('pixi.js').Container|null} toStage - 目标场景 stage
103
+ * @private
104
+ */
105
+ _restoreStages(fromStage, toStage) {
106
+ // 源场景:恢复到正常显示状态(转场结束后源场景会被 stop,但状态仍需恢复)
107
+ if (fromStage) {
108
+ fromStage.scale.set(1, 1)
109
+ fromStage.alpha = 1
110
+ }
111
+ // 目标场景:恢复到正常显示状态(scale=1, alpha=1)
112
+ if (toStage) {
113
+ toStage.scale.set(1, 1)
114
+ toStage.alpha = 1
115
+ }
116
+ }
117
+ }
118
+
119
+ export { ZoomTransition }
@@ -0,0 +1,12 @@
1
+ // ============================================================================
2
+ // Nimbus2D - SceneManager 模块统一导出
3
+ // ============================================================================
4
+
5
+ export { SceneManager } from './SceneManager.js'
6
+ export { Scene } from './Scene.js'
7
+ export { SceneState } from './SceneState.js'
8
+ export { Transition } from './Transition.js'
9
+ export { FadeTransition } from './FadeTransition.js'
10
+ export { SlideTransition } from './SlideTransition.js'
11
+ export { MaskTransition } from './MaskTransition.js'
12
+ export { ZoomTransition } from './ZoomTransition.js'
@@ -0,0 +1,321 @@
1
+ // ============================================================================
2
+ // Nimbus2D - SpriteGPULayer GPU 批量精灵容器
3
+ // ----------------------------------------------------------------------------
4
+ // 职责:
5
+ // 1. 继承 PIXI.Container,将添加到其中的 Sprite 自动合批 GPU 渲染
6
+ // 2. 按纹理分组管理 Sprite,便于统计 batch 数量
7
+ // 3. 通过 EventBus 广播渲染和容量事件
8
+ // 4. 提供 setTexture 批量替换纹理的能力
9
+ //
10
+ // 设计决策:
11
+ // - 继承 PIXI.Container:利用 PixiJS v8 BatchRenderer 自动合批,无需手动实现
12
+ // - 按纹理分组:以 Texture 实例为 Map key(Map 支持对象 key,O(1) 查找)
13
+ // - 渲染事件:重写 render(),先调用 super.render() 后发射事件(确保渲染完成)
14
+ // - 容量事件:addSprite 时检测纹理组容量达上限立即发射
15
+ // - EventBus 注入:通过 setEventBus 注入,避免构造函数耦合
16
+ //
17
+ // 四期新增(任务 1.1):对标 Phaser 的 SpriteGPULayer
18
+ // ============================================================================
19
+
20
+ import { Container, Sprite } from 'pixi.js'
21
+ import { SpriteGPULayerEventType, SpriteGPULayerError } from './SpriteGPULayerTypes.js'
22
+
23
+ class SpriteGPULayer extends Container {
24
+ /**
25
+ * 构造函数
26
+ * @param {Object} [options={}] - 配置
27
+ * @param {number} [options.maxBatchSize=4096] - 单 batch 最大 Sprite 数
28
+ * 超过此值时发射 gpu:spriteBatch:full 事件(仅提示,不阻止添加)
29
+ * @param {string} [options.label] - 容器标签(用于事件识别)
30
+ */
31
+ constructor(options = {}) {
32
+ super()
33
+
34
+ /**
35
+ * 单 batch 最大 Sprite 数
36
+ * @type {number}
37
+ * @private
38
+ */
39
+ this._maxBatchSize = options.maxBatchSize ?? 4096
40
+
41
+ /**
42
+ * 容器标签(用于事件识别,优先使用 options.label,其次 PIXI.label)
43
+ */
44
+ if (options.label) {
45
+ this.label = options.label
46
+ }
47
+
48
+ /**
49
+ * 管理的 Sprite 列表
50
+ * @type {Sprite[]}
51
+ * @private
52
+ */
53
+ this._sprites = []
54
+
55
+ /**
56
+ * 按纹理分组的 Sprite 集合
57
+ * key: Texture 实例(Map 支持对象 key)
58
+ * value: Set<Sprite>
59
+ * @type {Map<import('pixi.js').Texture, Set<Sprite>>}
60
+ * @private
61
+ */
62
+ this._textureGroups = new Map()
63
+
64
+ /**
65
+ * 事件总线(通过 setEventBus 注入)
66
+ * @type {import('../../core/EventBus/EventBus.js').EventBus|null}
67
+ * @private
68
+ */
69
+ this._eventBus = null
70
+
71
+ /**
72
+ * 是否已销毁(与 PIXI 的 destroyed 字段独立,用于我们的销毁逻辑)
73
+ * @type {boolean}
74
+ * @private
75
+ */
76
+ this._isDestroyed = false
77
+ }
78
+
79
+ // --------------------------------------------------------------------------
80
+ // Sprite 管理
81
+ // --------------------------------------------------------------------------
82
+
83
+ /**
84
+ * 添加 Sprite 到 GPU 层
85
+ * 同纹理 Sprite 由 PixiJS BatchRenderer 自动合批为单个 draw call
86
+ *
87
+ * @param {Sprite} sprite - Sprite 实例
88
+ * @returns {SpriteGPULayer} this(链式调用)
89
+ * @throws {SpriteGPULayerError} sprite 不是 PIXI.Sprite 实例
90
+ */
91
+ addSprite(sprite) {
92
+ if (this._isDestroyed) return this
93
+
94
+ // 参数校验:必须是 PIXI.Sprite 实例
95
+ if (!(sprite instanceof Sprite)) {
96
+ throw new SpriteGPULayerError('addSprite 参数必须是 PIXI.Sprite 实例')
97
+ }
98
+
99
+ // 防止重复添加:若已存在则直接返回(幂等)
100
+ if (this._sprites.indexOf(sprite) !== -1) {
101
+ return this
102
+ }
103
+
104
+ /** @type {import('pixi.js').Texture|null} Sprite 的纹理 */
105
+ const texture = sprite.texture
106
+ if (!texture) {
107
+ throw new SpriteGPULayerError('Sprite 必须有纹理')
108
+ }
109
+
110
+ // 1. 添加到管理列表
111
+ this._sprites.push(sprite)
112
+
113
+ // 2. 按纹理分组(以 Texture 实例为 key)
114
+ let group = this._textureGroups.get(texture)
115
+ if (!group) {
116
+ group = new Set()
117
+ this._textureGroups.set(texture, group)
118
+ }
119
+ group.add(sprite)
120
+
121
+ // 3. 添加到 PIXI.Container(实际合批由 PixiJS BatchRenderer 自动处理)
122
+ this.addChild(sprite)
123
+
124
+ // 4. 检测纹理组是否达到上限,发射容量事件
125
+ if (group.size >= this._maxBatchSize) {
126
+ this._emitEvent(SpriteGPULayerEventType.BATCH_FULL, {
127
+ name: this.label,
128
+ textureGroupSize: group.size,
129
+ maxBatchSize: this._maxBatchSize
130
+ })
131
+ }
132
+
133
+ return this
134
+ }
135
+
136
+ /**
137
+ * 从 GPU 层移除 Sprite
138
+ * @param {Sprite} sprite - Sprite 实例
139
+ * @returns {boolean} 是否移除成功
140
+ */
141
+ removeSprite(sprite) {
142
+ if (this._isDestroyed) return false
143
+ if (!(sprite instanceof Sprite)) return false
144
+
145
+ // 1. 从管理列表移除
146
+ const index = this._sprites.indexOf(sprite)
147
+ if (index === -1) return false
148
+ this._sprites.splice(index, 1)
149
+
150
+ // 2. 从纹理分组移除(处理 texture 被外部修改的情况)
151
+ this._removeSpriteFromGroups(sprite)
152
+
153
+ // 3. 从 PIXI.Container 移除
154
+ this.removeChild(sprite)
155
+
156
+ return true
157
+ }
158
+
159
+ /**
160
+ * 批量替换所有 Sprite 的纹理
161
+ * 替换后所有 Sprite 共享同一纹理,重新分组为单个纹理组
162
+ *
163
+ * @param {import('pixi.js').Texture} texture - 新纹理
164
+ */
165
+ setTexture(texture) {
166
+ if (this._isDestroyed) return
167
+ if (!texture) return
168
+
169
+ // 遍历设置新纹理
170
+ for (let i = 0; i < this._sprites.length; i++) {
171
+ this._sprites[i].texture = texture
172
+ }
173
+
174
+ // 重新分组:纹理统一后所有 Sprite 在同一组
175
+ this._textureGroups.clear()
176
+ if (this._sprites.length > 0) {
177
+ const newGroup = new Set(this._sprites)
178
+ this._textureGroups.set(texture, newGroup)
179
+ }
180
+ }
181
+
182
+ // --------------------------------------------------------------------------
183
+ // 统计与查询
184
+ // --------------------------------------------------------------------------
185
+
186
+ /**
187
+ * 获取 GPU 层统计信息
188
+ * @returns {{ spriteCount: number, batchCount: number, maxBatchSize: number, textureGroupCount: number }}
189
+ */
190
+ getStats() {
191
+ return {
192
+ spriteCount: this._sprites.length,
193
+ batchCount: this._textureGroups.size,
194
+ maxBatchSize: this._maxBatchSize,
195
+ textureGroupCount: this._textureGroups.size
196
+ }
197
+ }
198
+
199
+ /** @type {boolean} 是否已销毁 */
200
+ get isDestroyed() {
201
+ return this._isDestroyed
202
+ }
203
+
204
+ // --------------------------------------------------------------------------
205
+ // EventBus 注入
206
+ // --------------------------------------------------------------------------
207
+
208
+ /**
209
+ * 注入事件总线
210
+ * 由 Scene 或用户调用,注入后才能发射 gpu:spriteBatch:* 事件
211
+ *
212
+ * @param {import('../../core/EventBus/EventBus.js').EventBus} eventBus - 事件总线实例
213
+ */
214
+ setEventBus(eventBus) {
215
+ this._eventBus = eventBus
216
+ }
217
+
218
+ // --------------------------------------------------------------------------
219
+ // 渲染钩子(重写 PIXI.Container.render)
220
+ // --------------------------------------------------------------------------
221
+
222
+ /**
223
+ * 重写 PIXI.Container.render
224
+ * 先调用 super.render(renderer) 完成 PIXI 标准渲染流程,
225
+ * 然后发射 gpu:spriteBatch:rendered 事件(仅在 EventBus 已注入时)
226
+ *
227
+ * @param {import('pixi.js').Renderer} renderer - 渲染器
228
+ */
229
+ render(renderer) {
230
+ // 先完成 PIXI 标准渲染(含可见性检查、变换、子节点渲染等)
231
+ super.render(renderer)
232
+
233
+ // 渲染完成后发射事件(仅当 EventBus 已注入)
234
+ if (this._eventBus) {
235
+ this._emitEvent(SpriteGPULayerEventType.RENDERED, {
236
+ name: this.label,
237
+ spriteCount: this._sprites.length,
238
+ batchCount: this._textureGroups.size
239
+ })
240
+ }
241
+ }
242
+
243
+ // --------------------------------------------------------------------------
244
+ // 私有方法
245
+ // --------------------------------------------------------------------------
246
+
247
+ /**
248
+ * 发射事件(内部方法)
249
+ * @param {string} eventType - 事件类型
250
+ * @param {Object} payload - 事件数据
251
+ * @private
252
+ */
253
+ _emitEvent(eventType, payload) {
254
+ if (this._eventBus) {
255
+ this._eventBus.emit(eventType, payload)
256
+ }
257
+ }
258
+
259
+ /**
260
+ * 从纹理分组中移除 Sprite(内部方法)
261
+ *
262
+ * 处理 sprite.texture 被外部修改导致用当前 texture 找不到原组的情况:
263
+ * 1. 先用 sprite.texture 快速查找(O(1))—— 覆盖正常场景
264
+ * 2. 快速查找失败时遍历所有组(O(n))—— 覆盖 texture 被外部修改场景
265
+ *
266
+ * 这样保证 _textureGroups 中不会残留 sprite 引用,避免内存泄漏
267
+ *
268
+ * @param {Sprite} sprite - Sprite 实例
269
+ * @private
270
+ */
271
+ _removeSpriteFromGroups(sprite) {
272
+ // 1. 先用当前 texture 快速查找(正常场景,O(1))
273
+ const texture = sprite.texture
274
+ if (texture) {
275
+ const group = this._textureGroups.get(texture)
276
+ if (group && group.has(sprite)) {
277
+ group.delete(sprite)
278
+ // 纹理组为空时清理,避免 Map 无限增长
279
+ if (group.size === 0) {
280
+ this._textureGroups.delete(texture)
281
+ }
282
+ return
283
+ }
284
+ }
285
+
286
+ // 2. 快速查找失败,遍历所有组查找(texture 被外部修改场景,O(n))
287
+ for (const [key, group] of this._textureGroups) {
288
+ if (group.has(sprite)) {
289
+ group.delete(sprite)
290
+ if (group.size === 0) {
291
+ this._textureGroups.delete(key)
292
+ }
293
+ return
294
+ }
295
+ }
296
+ }
297
+
298
+ // --------------------------------------------------------------------------
299
+ // 销毁
300
+ // --------------------------------------------------------------------------
301
+
302
+ /**
303
+ * 销毁 GPU 层
304
+ * 清理所有内部引用和 PIXI 资源
305
+ * @param {import('pixi.js').DestroyOptions|boolean} [options] - PIXI 销毁选项
306
+ */
307
+ destroy(options) {
308
+ if (this._isDestroyed) return
309
+ this._isDestroyed = true
310
+
311
+ // 清理内部引用
312
+ this._sprites.length = 0
313
+ this._textureGroups.clear()
314
+ this._eventBus = null
315
+
316
+ // 调用 PIXI.Container.destroy 销毁子节点和资源
317
+ super.destroy(options)
318
+ }
319
+ }
320
+
321
+ export { SpriteGPULayer }