From 9cb39203669abfe5e4dbcf8f57194bdba4332749 Mon Sep 17 00:00:00 2001 From: luzhuang Date: Wed, 1 Jul 2026 18:14:43 +0800 Subject: [PATCH 01/23] feat: limit physics delta time per scene --- packages/core/src/physics/PhysicsScene.ts | 14 +++++++++++- tests/src/core/physics/PhysicsScene.test.ts | 24 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/core/src/physics/PhysicsScene.ts b/packages/core/src/physics/PhysicsScene.ts index ce6bf2e4e8..1357b8ba90 100644 --- a/packages/core/src/physics/PhysicsScene.ts +++ b/packages/core/src/physics/PhysicsScene.ts @@ -24,6 +24,7 @@ export class PhysicsScene { private _scene: Scene; private _restTime: number = 0; private _fixedTimeStep: number = 1 / 60; + private _maximumDeltaTime: number = Infinity; private _colliders: DisorderedArray = new DisorderedArray(); @@ -55,6 +56,17 @@ export class PhysicsScene { this._fixedTimeStep = Math.max(value, MathUtil.zeroTolerance); } + /** + * Maximum delta time in seconds allowed per frame for physics simulation. + */ + get maximumDeltaTime(): number { + return this._maximumDeltaTime; + } + + set maximumDeltaTime(value: number) { + this._maximumDeltaTime = Math.max(value, MathUtil.zeroTolerance); + } + constructor(scene: Scene) { this._scene = scene; @@ -640,7 +652,7 @@ export class PhysicsScene { const { _fixedTimeStep: fixedTimeStep, _nativePhysicsScene: nativePhysicsManager } = this; const componentsManager = this._scene._componentsManager; - const simulateTime = this._restTime + deltaTime; + const simulateTime = this._restTime + Math.min(deltaTime, this._maximumDeltaTime); const step = Math.floor(simulateTime / fixedTimeStep); this._restTime = simulateTime - step * fixedTimeStep; for (let i = 0; i < step; i++) { diff --git a/tests/src/core/physics/PhysicsScene.test.ts b/tests/src/core/physics/PhysicsScene.test.ts index fae4d817c2..a93028a1e6 100644 --- a/tests/src/core/physics/PhysicsScene.test.ts +++ b/tests/src/core/physics/PhysicsScene.test.ts @@ -142,6 +142,30 @@ describe("Physics Test", () => { expect(enginePhysX.sceneManager.scenes[0].physics.fixedTimeStep).to.eq(fixedTimeStep); }); + it("maximumDeltaTime", () => { + const physics = enginePhysX.sceneManager.scenes[0].physics; + expect(physics.maximumDeltaTime).to.eq(Infinity); + + physics.fixedTimeStep = 1 / 60; + physics.maximumDeltaTime = 1 / 60; + (physics as any)._restTime = 0; + + const nativePhysicsScene = (physics as any)._nativePhysicsScene; + const update = vi.fn(); + (physics as any)._nativePhysicsScene = { + update, + updateEvents: () => ({ contactEvents: [], contactEventCount: 0, triggerEvents: [] }) + }; + + try { + physics._update(1); + expect(update).toHaveBeenCalledTimes(1); + } finally { + (physics as any)._nativePhysicsScene = nativePhysicsScene; + physics.maximumDeltaTime = Infinity; + } + }); + it("raycast", () => { const scene = enginePhysX.sceneManager.activeScene; const physicsScene = scene.physics; From 099b63744baf28a4ed8c9017853ea615f9ba82e4 Mon Sep 17 00:00:00 2001 From: luzhuang Date: Sat, 11 Jul 2026 05:55:39 +0800 Subject: [PATCH 02/23] fix: instantiate source-v2 glTF scene references --- notes/2026-07-11-gltf-scene-subasset-key.md | 39 +++++++++++++++ packages/galacean/src/index.ts | 5 ++ packages/loader/src/gltf/GLTFResource.ts | 8 ++++ .../src/gltf/parser/GLTFParserContext.ts | 12 +++-- .../resources/parser/HierarchyParser.ts | 47 ++++++++++++++++--- tests/src/loader/GLTFLoader.test.ts | 13 +++++ tests/src/loader/SceneFormatV2.test.ts | 42 +++++++++++++++++ 7 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 notes/2026-07-11-gltf-scene-subasset-key.md diff --git a/notes/2026-07-11-gltf-scene-subasset-key.md b/notes/2026-07-11-gltf-scene-subasset-key.md new file mode 100644 index 0000000000..cbcf93c660 --- /dev/null +++ b/notes/2026-07-11-gltf-scene-subasset-key.md @@ -0,0 +1,39 @@ +# glTF scene sub-asset key alignment + +## Symptom + +Editor source-v2 uses the glTF schema path `scenes[0]` for an instance asset key. The Engine loaded and cached the main GLTFResource, but a concurrent `?q=scenes[0]` request never resolved, so Scene and Prefab loading remained pending without a network or parser error. + +## Root cause + +`GLTFParserContext` stored scene roots in the internal `_sceneRoots` field and only published `_sceneRoots[index]` plus `defaultSceneRoot` to ResourceManager. Editor metadata and source-v2 use `scenes[index]`, matching the glTF schema rather than the Engine's private storage field. Cached query traversal also failed because GLTFResource exposed no `scenes` property. + +## Decision + +- `scenes[index]` is the canonical public sub-asset query key. +- GLTFResource exposes a read-only `scenes` view for cached query traversal. +- The parser publishes `scenes[index]` while retaining `_sceneRoots[index]` and `defaultSceneRoot` as compatibility aliases. +- The fix belongs in Engine, not in Editor CLI lowering or migration-specific key rewriting, because Editor already uses the glTF schema key consistently. + +## Verification + +- Loader test covers both first-load callback resolution and cached lookup for `?q=scenes[0]`. +- The migration runtime reproduction uses two source-v2 glTF instances and previously left both query promises pending while their main GLTFResource objects were cached. + +## Source-v2 instance consumption + +Editor source-v2 instance refs can select a glTF scene with key scenes[n]. Generic resource-ref resolution must not consume that key first, because it returns the scene Entity while HierarchyParser needs the owning GLTFResource to clone the selected scene. + +HierarchyParser now: + +- parses scenes[n] without regex and keeps defaultSceneRoot compatibility; +- loads the main glTF resource by URL; +- passes the selected index to instantiateSceneRoot(index); +- keeps ordinary Prefab refs on the existing path; +- rejects resources that are neither PrefabResource nor GLTFResource with a boundary-specific error. + +Verification: + +- pnpm run b:module: passed. +- HEADLESS=true pnpm exec vitest run tests/src/loader/GLTFLoader.test.ts tests/src/loader/SceneFormatV2.test.ts: 60 passed. +- A locally linked 3DCube source-v2 build loaded, entered gameplay, instantiated repeated glTF-backed dice, and produced no console errors. diff --git a/packages/galacean/src/index.ts b/packages/galacean/src/index.ts index de3768419f..fe03bb1da9 100644 --- a/packages/galacean/src/index.ts +++ b/packages/galacean/src/index.ts @@ -1,4 +1,5 @@ import * as CoreObjects from "@galacean/engine-core"; +import * as MathObjects from "@galacean/engine-math"; import { Loader, Polyfill, SystemInfo } from "@galacean/engine-core"; import { ShaderPool } from "./ShaderPool"; //@ts-ignore @@ -15,6 +16,10 @@ for (let key in CoreObjects) { Loader.registerClass(key, CoreObjects[key]); } +for (let key in MathObjects) { + Loader.registerClass(key, MathObjects[key]); +} + // Bootstrap the Galacean engine flavor: browser polyfills first (must // patch globals before anything else runs), then platform detection // (other modules may read `SystemInfo.platform`), then built-in shader diff --git a/packages/loader/src/gltf/GLTFResource.ts b/packages/loader/src/gltf/GLTFResource.ts index 26c7768708..7c58958195 100644 --- a/packages/loader/src/gltf/GLTFResource.ts +++ b/packages/loader/src/gltf/GLTFResource.ts @@ -63,6 +63,14 @@ export class GLTFResource extends ReferResource { return sceneRoot.clone(); } + /** + * Scene root templates indexed by the glTF `scenes` array. + * @remarks This is also the canonical sub-asset query surface, for example `?q=scenes[0]`. + */ + get scenes(): ReadonlyArray { + return this._sceneRoots; + } + protected override _onDestroy(): void { super._onDestroy(); diff --git a/packages/loader/src/gltf/parser/GLTFParserContext.ts b/packages/loader/src/gltf/parser/GLTFParserContext.ts index b549da5ca2..bf99dcf09f 100644 --- a/packages/loader/src/gltf/parser/GLTFParserContext.ts +++ b/packages/loader/src/gltf/parser/GLTFParserContext.ts @@ -194,16 +194,22 @@ export class GLTFParserContext { this.resourceManager._onSubAssetSuccess(url, `${glTFResourceKey}[${index}][${i}]`, mesh); } } else { + const subAssetResourceKey = type === GLTFParserType.Scene ? "scenes" : glTFResourceKey; // @ts-ignore this.resourceManager._onSubAssetSuccess( url, - `${glTFResourceKey}${index === undefined ? "" : `[${index}]`}`, + `${subAssetResourceKey}${index === undefined ? "" : `[${index}]`}`, item ); - if (type === GLTFParserType.Scene && (this.glTF.scene ?? 0) === index) { + if (type === GLTFParserType.Scene) { + // Keep the historical internal query key while exposing the canonical glTF schema key above. // @ts-ignore - this.resourceManager._onSubAssetSuccess(url, `defaultSceneRoot`, item as Entity); + this.resourceManager._onSubAssetSuccess(url, `_sceneRoots[${index}]`, item as Entity); + if ((this.glTF.scene ?? 0) === index) { + // @ts-ignore + this.resourceManager._onSubAssetSuccess(url, `defaultSceneRoot`, item as Entity); + } } } }) diff --git a/packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts b/packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts index 2216ea3b42..3512ca3813 100644 --- a/packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts +++ b/packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts @@ -17,6 +17,31 @@ import type { HierarchyFile } from "../../../schema/HierarchySchema"; import { ParserContext, ParserType } from "./ParserContext"; import { ReflectionParser } from "./ReflectionParser"; +interface GLTFSceneSelection { + sceneIndex?: number; +} + +function parseGLTFSceneSelection(key: string | undefined): GLTFSceneSelection | undefined { + if (key === "defaultSceneRoot") { + return {}; + } + + const prefix = "scenes["; + if (!key?.startsWith(prefix)) { + return undefined; + } + if (!key.endsWith("]")) { + throw new Error(`HierarchyParser: invalid glTF scene key "${key}". Expected "scenes[n]".`); + } + + const indexText = key.slice(prefix.length, -1); + const sceneIndex = Number(indexText); + if (!Number.isSafeInteger(sceneIndex) || sceneIndex < 0 || String(sceneIndex) !== indexText) { + throw new Error(`HierarchyParser: invalid glTF scene key "${key}". Expected "scenes[n]".`); + } + return { sceneIndex }; +} + /** @Internal */ export abstract class HierarchyParser { readonly promise: Promise; @@ -265,21 +290,31 @@ export abstract class HierarchyParser { const instance = entityConfig.instance; let refItem: RefItem; + let glTFSceneSelection: GLTFSceneSelection | undefined; try { refItem = resolveRefItem(this.data.refs, instance.asset, "HierarchyParser", "instance.asset"); + glTFSceneSelection = parseGLTFSceneSelection(refItem.key); } catch (error) { return Promise.reject(error); } + const resourceRef = glTFSceneSelection ? { url: refItem.url } : refItem; return ( engine.resourceManager // @ts-ignore - .getResourceByRef(refItem) - .then((prefabResource: PrefabResource | GLTFResource) => { - const entity = - prefabResource instanceof PrefabResource - ? prefabResource.instantiate() - : prefabResource.instantiateSceneRoot(); + .getResourceByRef(resourceRef) + .then((resource: PrefabResource | GLTFResource) => { + let entity: Entity; + if (resource instanceof PrefabResource) { + if (glTFSceneSelection) { + throw new Error(`HierarchyParser: glTF scene key "${refItem.key}" resolved to a prefab resource.`); + } + entity = resource.instantiate(); + } else if (resource instanceof GLTFResource) { + entity = resource.instantiateSceneRoot(glTFSceneSelection?.sceneIndex); + } else { + throw new Error(`HierarchyParser: instance asset "${refItem.url}" is not a prefab or glTF resource.`); + } this._onEntityCreated(entity); return entity; }) diff --git a/tests/src/loader/GLTFLoader.test.ts b/tests/src/loader/GLTFLoader.test.ts index 85cceaa5d7..acdc6a69af 100644 --- a/tests/src/loader/GLTFLoader.test.ts +++ b/tests/src/loader/GLTFLoader.test.ts @@ -602,6 +602,19 @@ afterAll(() => { }); describe("glTF Loader test", function () { + it("resolves scene sub-assets by the canonical glTF schema key", async () => { + const glTFResource = await engine.resourceManager.load({ + type: AssetType.GLTF, + url: "mock/path/testRoot.gltf" + }); + const sceneRoot = await engine.resourceManager.load({ + type: AssetType.GLTF, + url: "mock/path/testRoot.gltf?q=scenes[0]" + }); + + expect(sceneRoot).to.equal(glTFResource.scenes[0]); + }); + it("Pipeline Parser", async () => { const glTFResource: GLTFResource = await engine.resourceManager.load({ type: AssetType.GLTF, diff --git a/tests/src/loader/SceneFormatV2.test.ts b/tests/src/loader/SceneFormatV2.test.ts index 95cdc3a693..ceb4ce57ec 100644 --- a/tests/src/loader/SceneFormatV2.test.ts +++ b/tests/src/loader/SceneFormatV2.test.ts @@ -23,7 +23,9 @@ import { } from "../../../packages/loader/src/resource-deserialize/resources/parser/ParserContext"; import { ReflectionParser } from "../../../packages/loader/src/resource-deserialize/resources/parser/ReflectionParser"; import { SceneParser } from "../../../packages/loader/src/resource-deserialize/resources/scene/SceneParser"; +import { GLTFResource } from "../../../packages/loader/src/gltf/GLTFResource"; import { WebGLEngine } from "@galacean/engine"; +import { Vector2 } from "@galacean/engine-math"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; Loader.registerClass("Transform", Transform); @@ -387,6 +389,31 @@ describe("SceneParser v2 entity tree", () => { }; } + it("loads the main glTF resource and instantiates the selected scene", async () => { + const data = createSceneData([{ instance: { asset: 0 } }], [], [0], [{ url: "model.glb", key: "scenes[1]" }]); + const scene = new Scene(engine); + const context = new ParserContext(engine, ParserType.Scene, scene); + const glTFResource = new GLTFResource(engine, "model.glb"); + const instanceRoot = new Entity(engine, "Scene 1"); + const instantiateSceneRoot = vi.spyOn(glTFResource, "instantiateSceneRoot").mockReturnValue(instanceRoot); + const getResourceByRef = vi + .spyOn(engine.resourceManager as any, "getResourceByRef") + .mockResolvedValue(glTFResource); + + try { + const parser = new SceneParser(data, context, scene); + parser.start(); + await parser.promise; + + expect(getResourceByRef).toHaveBeenCalledWith({ url: "model.glb" }); + expect(instantiateSceneRoot).toHaveBeenCalledWith(1); + expect(scene.rootEntities[0]).to.equal(instanceRoot); + } finally { + getResourceByRef.mockRestore(); + instantiateSceneRoot.mockRestore(); + } + }); + it("should reject scene data without the v2 version marker", () => { const data = { entities: [{ name: "Entity" }], @@ -773,6 +800,21 @@ describe("ReflectionParser $signal resolution", () => { // --------------------------------------------------------------------------- describe("ReflectionParser $type resolution", () => { + it("should construct built-in math types registered by the engine package", async () => { + const scene = new Scene(engine); + const context = new ParserContext(engine, ParserType.Scene, scene); + const parser = new ReflectionParser(context, []); + const target: any = {}; + + await parser.parseProps(target, { + value: { $type: "Vector2", x: 10, y: 20 } + }); + + expect(target.value).to.be.instanceOf(Vector2); + expect(target.value.x).to.equal(10); + expect(target.value.y).to.equal(20); + }); + it("should construct $type instance and apply remaining props", async () => { const scene = new Scene(engine); const context = new ParserContext(engine, ParserType.Scene, scene); From 4b65762a8daeed853c94e6e15ead6328e064177d Mon Sep 17 00:00:00 2001 From: luzhuang Date: Wed, 17 Jun 2026 20:27:24 +0800 Subject: [PATCH 03/23] fix(physics): rebuild mesh shapes and scaled defaults --- packages/core/src/physics/Collider.ts | 7 + packages/core/src/physics/DynamicCollider.ts | 9 +- packages/core/src/physics/PhysicsMaterial.ts | 25 +++- .../core/src/physics/shape/ColliderShape.ts | 19 ++- .../src/physics/shape/MeshColliderShape.ts | 62 ++++++++- packages/design/src/physics/IPhysics.ts | 10 ++ .../src/PhysXCharacterController.ts | 2 +- packages/physics-physx/src/PhysXPhysics.ts | 67 +++++++++- packages/physics-physx/src/index.ts | 1 + .../src/shape/PhysXColliderShape.ts | 5 +- .../src/shape/PhysXMeshColliderShape.ts | 81 +++++++----- .../src/core/physics/DynamicCollider.test.ts | 7 + .../core/physics/MeshColliderShape.test.ts | 124 +++++++++++++++++- .../src/core/physics/PhysicsMaterial.test.ts | 52 +++++++- 14 files changed, 418 insertions(+), 53 deletions(-) diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index a8a591fe56..c7ca7dac36 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -122,6 +122,13 @@ export class Collider extends Component implements ICustomClone { } this._updateFlag.flag = false; } + + // Drive per-shape physics update. MeshColliderShape uses this to retry + // native shape creation after PhysX cooking returned null despite valid + // extracted mesh data. + for (let i = 0, n = shapes.length; i < n; i++) { + shapes[i]._onPhysicsUpdate(); + } } /** diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 5c8725c604..7483b6ffe1 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -13,6 +13,7 @@ import { MeshColliderShape } from "./shape/MeshColliderShape"; */ export class DynamicCollider extends Collider { private static _tempVector3 = new Vector3(); + private static _tempVector3_1 = new Vector3(); private static _tempQuat = new Quaternion(); private _linearDamping = 0; @@ -33,7 +34,7 @@ export class DynamicCollider extends Collider { private _isKinematic = false; private _constraints: DynamicColliderConstraints = 0; private _collisionDetectionMode: CollisionDetectionMode = CollisionDetectionMode.Discrete; - private _sleepThreshold = 5e-3; + private _sleepThreshold: number | undefined; private _automaticCenterOfMass = true; private _automaticInertiaTensor = true; @@ -223,7 +224,7 @@ export class DynamicCollider extends Collider { * The mass-normalized energy threshold, below which objects start going to sleep. */ get sleepThreshold(): number { - return this._sleepThreshold; + return this._sleepThreshold ?? Engine._nativePhysics?.getDefaultSleepThreshold?.() ?? 5e-3; } set sleepThreshold(value: number) { @@ -498,7 +499,9 @@ export class DynamicCollider extends Collider { } (this._nativeCollider).setMaxAngularVelocity(this._maxAngularVelocity); (this._nativeCollider).setMaxDepenetrationVelocity(this._maxDepenetrationVelocity); - (this._nativeCollider).setSleepThreshold(this._sleepThreshold); + if (this._sleepThreshold !== undefined) { + (this._nativeCollider).setSleepThreshold(this._sleepThreshold); + } (this._nativeCollider).setSolverIterations(this._solverIterations); (this._nativeCollider).setUseGravity(this._useGravity); (this._nativeCollider).setIsKinematic(this._isKinematic); diff --git a/packages/core/src/physics/PhysicsMaterial.ts b/packages/core/src/physics/PhysicsMaterial.ts index 3ac16e66d8..aa22bc8c24 100644 --- a/packages/core/src/physics/PhysicsMaterial.ts +++ b/packages/core/src/physics/PhysicsMaterial.ts @@ -1,6 +1,7 @@ import { IPhysicsMaterial } from "@galacean/engine-design"; import { Engine } from "../Engine"; import { PhysicsMaterialCombineMode } from "./enums/PhysicsMaterialCombineMode"; +import { ignoreClone } from "../clone/CloneManager"; /** * Material class to represent a set of surface properties. @@ -14,6 +15,7 @@ export class PhysicsMaterial { private _destroyed: boolean; /** @internal */ + @ignoreClone _nativeMaterial: IPhysicsMaterial; constructor() { @@ -21,8 +23,8 @@ export class PhysicsMaterial { this._staticFriction, this._dynamicFriction, this._bounciness, - this._bounceCombine, - this._frictionCombine + this._frictionCombine, + this._bounceCombine ); } @@ -103,4 +105,23 @@ export class PhysicsMaterial { !this._destroyed && this._nativeMaterial.destroy(); this._destroyed = true; } + + /** + * @internal + */ + _cloneTo(target: PhysicsMaterial): void { + target._syncNative(); + } + + /** + * @internal + */ + _syncNative(): void { + const nativeMaterial = this._nativeMaterial; + nativeMaterial.setStaticFriction(this._staticFriction); + nativeMaterial.setDynamicFriction(this._dynamicFriction); + nativeMaterial.setBounciness(this._bounciness); + nativeMaterial.setFrictionCombine(this._frictionCombine); + nativeMaterial.setBounceCombine(this._bounceCombine); + } } diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index 2ac82319fd..28eb87ba0e 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -27,7 +27,7 @@ export abstract class ColliderShape implements ICustomClone { private _rotation: Vector3 = new Vector3(); @deepClone private _position: Vector3 = new Vector3(); - private _contactOffset: number = 0.02; + private _contactOffset: number | undefined; /** * @internal @@ -55,7 +55,7 @@ export abstract class ColliderShape implements ICustomClone { * @defaultValue 0.02 */ get contactOffset(): number { - return this._contactOffset; + return this._contactOffset ?? Engine._nativePhysics?.getDefaultContactOffset?.() ?? 0.02; } set contactOffset(value: number) { @@ -168,6 +168,17 @@ export abstract class ColliderShape implements ICustomClone { target._syncNative(); } + /** + * @internal + * + * Called once per physics update tick by `Collider._onUpdate`. Base no-op. + * + * Subclasses can override for frame-driven maintenance. Currently used by + * `MeshColliderShape` to retry native shape creation after PhysX cooking + * returned null despite valid extracted mesh data. + */ + _onPhysicsUpdate(): void {} + /** * @internal */ @@ -183,7 +194,9 @@ export abstract class ColliderShape implements ICustomClone { if (!this._nativeShape) return; this._nativeShape.setPosition(this._position); this._nativeShape.setRotation(this._rotation); - this._nativeShape.setContactOffset(this._contactOffset); + if (this._contactOffset !== undefined) { + this._nativeShape.setContactOffset(this._contactOffset); + } this._nativeShape.setIsTrigger(this._isTrigger); this._nativeShape.setMaterial(this._material._nativeMaterial); diff --git a/packages/core/src/physics/shape/MeshColliderShape.ts b/packages/core/src/physics/shape/MeshColliderShape.ts index 51a46ab0e0..aba038ce1c 100644 --- a/packages/core/src/physics/shape/MeshColliderShape.ts +++ b/packages/core/src/physics/shape/MeshColliderShape.ts @@ -16,6 +16,11 @@ export class MeshColliderShape extends ColliderShape { private _indices: Uint8Array | Uint16Array | Uint32Array | null = null; private _cookingFlags = MeshColliderShapeCookingFlag.Cleaning | MeshColliderShapeCookingFlag.VertexWelding; private _isShapeAttached = false; + /** + * `true` if PhysX cooking returned null after valid mesh data was extracted. + * Unsupported collider/mesh combinations are terminal and must not enter retry. + */ + private _pendingNativeShapeCreation = false; /** * Cooking flags for this mesh collider shape. @@ -56,6 +61,7 @@ export class MeshColliderShape extends ColliderShape { this._createNativeShape(); } else { this._clearMeshData(); + this._pendingNativeShapeCreation = false; } } } @@ -74,15 +80,23 @@ export class MeshColliderShape extends ColliderShape { this._mesh?._addReferCount(-1); value?._addReferCount(1); this._mesh = value; - if (value && this._extractMeshData(value)) { - if (this._nativeShape) { - this._updateNativeShapeData(); + if (value) { + if (this._extractMeshData(value)) { + if (this._nativeShape) { + this._updateNativeShapeData(); + } else { + this._createNativeShape(); + } } else { - this._createNativeShape(); + // Mesh data extraction cannot recover without assigning a new mesh. + this._destroyNativeShape(); + this._clearMeshData(); + this._pendingNativeShapeCreation = false; } } else { this._destroyNativeShape(); this._clearMeshData(); + this._pendingNativeShapeCreation = false; } } } @@ -165,8 +179,6 @@ export class MeshColliderShape extends ColliderShape { if (this._collider && !this._isShapeAttached) { this._attachToCollider(); } - } else if (this._isShapeAttached) { - this._detachFromCollider(); } } @@ -183,6 +195,7 @@ export class MeshColliderShape extends ColliderShape { private _createNativeShape(): void { // Non-convex MeshColliderShape is only supported on StaticCollider or kinematic DynamicCollider if (!this._isConvex && this._collider instanceof DynamicCollider && !this._collider.isKinematic) { + this._pendingNativeShapeCreation = false; console.error("MeshColliderShape: Non-convex mesh is not supported on non-kinematic DynamicCollider."); return; } @@ -197,10 +210,13 @@ export class MeshColliderShape extends ColliderShape { ); if (!nativeShape) { + // Cook failed — `_onPhysicsUpdate` will retry next frame + this._pendingNativeShapeCreation = true; return; } this._nativeShape = nativeShape; + this._pendingNativeShapeCreation = false; // Sync base class properties (position, rotation, contactOffset, isTrigger, material) super._syncNative(); @@ -211,4 +227,38 @@ export class MeshColliderShape extends ColliderShape { this._attachToCollider(); } } + + /** + * @internal + * Retry hook: keep attempting `_createNativeShape` until it succeeds. + * + * Triggered every physics update tick by `Collider._onUpdate`. Handles the case + * where `_cookMesh` fails on first attempt due to PhysX cooking pipeline timing + * (the mesh data was extracted successfully at `set mesh` time, but the native + * cook call returned null). No-op once a valid native shape exists. + * + * We DO NOT re-call `_extractMeshData` here — once `set mesh` finished, either: + * - extraction succeeded → `_positions` is populated and we reuse it + * - extraction failed → `_clearMeshData` cleared `_positions`, and `mesh.accessible` + * won't recover (GPU upload is one-way), so re-extracting would fail again + */ + override _onPhysicsUpdate(): void { + if (!this._pendingNativeShapeCreation || !this._mesh || !this._positions) return; + this._createNativeShape(); + } + + /** + * @internal + * After CloneManager deep-copies `_positions` / `_indices` / `_mesh` and remaps `_collider`, + * the cloned shape still has no native PhysX shape because `_nativeShape` is `@ignoreClone`. + * Cook a fresh native shape now using the already-cloned vertex/index buffers; on transient + * cook failure `_createNativeShape` sets `_pendingNativeShapeCreation = true` and + * `_onPhysicsUpdate` will retry next tick. + */ + override _cloneTo(target: MeshColliderShape): void { + super._cloneTo(target); + if (target._positions) { + target._createNativeShape(); + } + } } diff --git a/packages/design/src/physics/IPhysics.ts b/packages/design/src/physics/IPhysics.ts index 17c9b6b466..91a6c1c7c6 100644 --- a/packages/design/src/physics/IPhysics.ts +++ b/packages/design/src/physics/IPhysics.ts @@ -36,6 +36,16 @@ export interface IPhysics { */ createPhysicsScene(physicsManager: IPhysicsManager): IPhysicsScene; + /** + * Get the default contact offset for collider shapes. + */ + getDefaultContactOffset?(): number; + + /** + * Get the default sleep threshold for dynamic colliders. + */ + getDefaultSleepThreshold?(): number; + /** * Create dynamic collider. * @param position - The global position diff --git a/packages/physics-physx/src/PhysXCharacterController.ts b/packages/physics-physx/src/PhysXCharacterController.ts index 38b5228062..a56077b596 100644 --- a/packages/physics-physx/src/PhysXCharacterController.ts +++ b/packages/physics-physx/src/PhysXCharacterController.ts @@ -94,7 +94,7 @@ export class PhysXCharacterController implements ICharacterController { this._pxManager && this._createPXController(this._pxManager, shape); this._shape = shape; shape._controllers.add(this); - this._pxController?.setContactOffset(shape._contractOffset); + this._pxController?.setContactOffset(shape._contactOffset); this._scene?._addColliderShape(shape._id); } diff --git a/packages/physics-physx/src/PhysXPhysics.ts b/packages/physics-physx/src/PhysXPhysics.ts index 8bcf8d510a..56a2704113 100644 --- a/packages/physics-physx/src/PhysXPhysics.ts +++ b/packages/physics-physx/src/PhysXPhysics.ts @@ -57,13 +57,26 @@ export class PhysXPhysics implements IPhysics { private _tolerancesScale: any; private _wasmSIMDModeUrl: string; private _wasmModeUrl: string; + private _tolerancesScaleOptions: PhysXTolerancesScale | undefined; + private _defaultContactOffset = 0.02; + private _defaultSleepThreshold = 5e-3; /** * Create a PhysXPhysics instance. * @param runtimeMode - Runtime mode, `Auto` prefers WebAssembly SIMD if supported @see {@link PhysXRuntimeMode} * @param runtimeUrls - Manually specify the runtime URLs + * @param options - PhysX options. */ - constructor(runtimeMode: PhysXRuntimeMode = PhysXRuntimeMode.Auto, runtimeUrls?: PhysXRuntimeUrls) { + constructor(runtimeMode?: PhysXRuntimeMode, runtimeUrls?: PhysXRuntimeUrls, options?: PhysXPhysicsOptions); + constructor(options?: PhysXPhysicsOptions); + constructor( + runtimeModeOrOptions: PhysXRuntimeMode | PhysXPhysicsOptions = PhysXRuntimeMode.Auto, + runtimeUrls?: PhysXRuntimeUrls, + options?: PhysXPhysicsOptions + ) { + const isOptionsObject = typeof runtimeModeOrOptions === "object"; + const runtimeMode = isOptionsObject ? PhysXRuntimeMode.Auto : (runtimeModeOrOptions ?? PhysXRuntimeMode.Auto); + const resolvedOptions = isOptionsObject ? runtimeModeOrOptions : options; this._runTimeMode = runtimeMode; this._wasmSIMDModeUrl = runtimeUrls?.wasmSIMDModeUrl ?? @@ -71,6 +84,8 @@ export class PhysXPhysics implements IPhysics { this._wasmModeUrl = runtimeUrls?.wasmModeUrl ?? "https://mdn.alipayobjects.com/rms/uri/file/as/apwallet/1781696156399/suyi/physx.release.js"; + this._tolerancesScaleOptions = resolvedOptions?.tolerancesScale; + this._updateScaledDefaults(this._tolerancesScaleOptions?.length ?? 1, this._tolerancesScaleOptions?.speed ?? 10); } /** @@ -156,6 +171,20 @@ export class PhysXPhysics implements IPhysics { return scene; } + /** + * {@inheritDoc IPhysics.getDefaultContactOffset } + */ + getDefaultContactOffset(): number { + return this._defaultContactOffset; + } + + /** + * {@inheritDoc IPhysics.getDefaultSleepThreshold } + */ + getDefaultSleepThreshold(): number { + return this._defaultSleepThreshold; + } + /** * {@inheritDoc IPhysics.createStaticCollider } */ @@ -279,6 +308,7 @@ export class PhysXPhysics implements IPhysics { const allocator = new physX.PxDefaultAllocator(); const pxFoundation = physX.PxCreateFoundation(version, allocator, defaultErrorCallback); const tolerancesScale = new physX.PxTolerancesScale(); + this._applyTolerancesScale(tolerancesScale); const pxPhysics = physX.PxCreatePhysics(version, pxFoundation, tolerancesScale, false, null); physX.PxInitExtensions(pxPhysics, null); @@ -302,6 +332,29 @@ export class PhysXPhysics implements IPhysics { this._allocator = allocator; this._tolerancesScale = tolerancesScale; } + + private _applyTolerancesScale(tolerancesScale: any): void { + const length = this._tolerancesScaleOptions?.length ?? tolerancesScale.length; + const speed = this._tolerancesScaleOptions?.speed ?? tolerancesScale.speed; + + this._assertPositiveFinite(length, "tolerancesScale.length"); + this._assertPositiveFinite(speed, "tolerancesScale.speed"); + + tolerancesScale.length = length; + tolerancesScale.speed = speed; + this._updateScaledDefaults(length, speed); + } + + private _assertPositiveFinite(value: number, name: string): void { + if (!Number.isFinite(value) || value <= 0) { + throw new Error(`PhysXPhysics ${name} must be a positive finite number.`); + } + } + + private _updateScaledDefaults(length: number, speed: number): void { + this._defaultContactOffset = 0.02 * length; + this._defaultSleepThreshold = 5e-5 * speed * speed; + } } enum InitializeState { @@ -316,3 +369,15 @@ interface PhysXRuntimeUrls { /*** The URL of `PhysXRuntimeMode.WebAssemblySIMD` mode. */ wasmSIMDModeUrl?: string; } + +export interface PhysXTolerancesScale { + /** Approximate object length in the simulation unit. PhysX default is 1. */ + length?: number; + /** Typical object speed in the simulation unit. PhysX default is 10. */ + speed?: number; +} + +export interface PhysXPhysicsOptions { + /** PhysX world unit scale used before PxPhysics, PxSceneDesc and PxCookingParams are created. */ + tolerancesScale?: PhysXTolerancesScale; +} diff --git a/packages/physics-physx/src/index.ts b/packages/physics-physx/src/index.ts index aa3dd875c3..1798c64b70 100644 --- a/packages/physics-physx/src/index.ts +++ b/packages/physics-physx/src/index.ts @@ -1,4 +1,5 @@ export { PhysXPhysics } from "./PhysXPhysics"; +export type { PhysXPhysicsOptions, PhysXTolerancesScale } from "./PhysXPhysics"; export { PhysXRuntimeMode } from "./enum/PhysXRuntimeMode"; //@ts-ignore diff --git a/packages/physics-physx/src/shape/PhysXColliderShape.ts b/packages/physics-physx/src/shape/PhysXColliderShape.ts index ea5a3df8b4..9d782f73fb 100644 --- a/packages/physics-physx/src/shape/PhysXColliderShape.ts +++ b/packages/physics-physx/src/shape/PhysXColliderShape.ts @@ -31,7 +31,7 @@ export abstract class PhysXColliderShape implements IColliderShape { /** @internal */ _controllers: DisorderedArray = new DisorderedArray(); /** @internal */ - _contractOffset: number = 0.02; + _contactOffset: number = 0.02; /** @internal */ _worldScale: Vector3 = new Vector3(1, 1, 1); @@ -56,6 +56,7 @@ export abstract class PhysXColliderShape implements IColliderShape { constructor(physXPhysics: PhysXPhysics) { this._physXPhysics = physXPhysics; + this._contactOffset = physXPhysics.getDefaultContactOffset(); } /** @@ -106,7 +107,7 @@ export abstract class PhysXColliderShape implements IColliderShape { * @default 0.02f * PxTolerancesScale::length */ setContactOffset(offset: number): void { - this._contractOffset = offset; + this._contactOffset = offset; const controllers = this._controllers; if (controllers.length) { for (let i = 0, n = controllers.length; i < n; i++) { diff --git a/packages/physics-physx/src/shape/PhysXMeshColliderShape.ts b/packages/physics-physx/src/shape/PhysXMeshColliderShape.ts index 9a25c2fd89..835c331cb0 100644 --- a/packages/physics-physx/src/shape/PhysXMeshColliderShape.ts +++ b/packages/physics-physx/src/shape/PhysXMeshColliderShape.ts @@ -25,7 +25,8 @@ export class PhysXMeshColliderShape extends PhysXColliderShape implements IMeshC super(physXPhysics); this._isConvex = isConvex; - if (!this._cookMesh(positions, indices, cookingFlags)) { + const cooked = this._cookMesh(positions, indices, isConvex, cookingFlags); + if (!cooked) { return; } @@ -36,7 +37,7 @@ export class PhysXMeshColliderShape extends PhysXColliderShape implements IMeshC const createShapeFn = isConvex ? physX.createConvexMeshShape : physX.createTriMeshShape; this._pxShape = createShapeFn( - this._pxMesh, + cooked.mesh, scaleX, scaleY, scaleZ, @@ -48,6 +49,8 @@ export class PhysXMeshColliderShape extends PhysXColliderShape implements IMeshC this._id = uniqueID; this._pxMaterial = material._pxMaterial; + this._pxMesh = cooked.mesh; + this._pxGeometry = cooked.geometry; this._pxShape.setUUID(uniqueID); this._setLocalPose(); } @@ -61,17 +64,28 @@ export class PhysXMeshColliderShape extends PhysXColliderShape implements IMeshC isConvex: boolean, cookingFlags: number ): boolean { - this._pxMesh?.release(); - this._pxGeometry?.delete(); - this._pxMesh = null; - this._pxGeometry = null; - this._isConvex = isConvex; - - if (!this._cookMesh(positions, indices, cookingFlags)) { + const cooked = this._cookMesh(positions, indices, isConvex, cookingFlags); + if (!cooked) { return false; } - this._pxShape.setGeometry(this._pxGeometry); + const oldMesh = this._pxMesh; + const oldGeometry = this._pxGeometry; + + try { + this._pxShape.setGeometry(cooked.geometry); + } catch (error) { + cooked.mesh?.release(); + cooked.geometry?.delete(); + throw error; + } + + this._pxMesh = cooked.mesh; + this._pxGeometry = cooked.geometry; + this._isConvex = isConvex; + + oldMesh?.release(); + oldGeometry?.delete(); return true; } @@ -94,8 +108,9 @@ export class PhysXMeshColliderShape extends PhysXColliderShape implements IMeshC private _cookMesh( positions: Vector3[], indices: Uint8Array | Uint16Array | Uint32Array | null, + isConvex: boolean, cookingFlags: number - ): boolean { + ): { mesh: any; geometry: any } | null { const { _physX: physX, _pxPhysics: physics, @@ -115,48 +130,42 @@ export class PhysXMeshColliderShape extends PhysXColliderShape implements IMeshC cooking.setParams(cookingParams); const verticesPtr = this._allocatePositions(positions); + let pxMesh: any = null; - if (this._isConvex) { - this._pxMesh = cooking.createConvexMesh(verticesPtr, positions.length, physics); + if (isConvex) { + pxMesh = cooking.createConvexMesh(verticesPtr, positions.length, physics); physX._free(verticesPtr); - if (!this._pxMesh) { + if (!pxMesh) { this._logConvexCookingError(physX); - return false; + return null; } } else { if (!indices) { physX._free(verticesPtr); console.error("PhysXMeshColliderShape: Triangle mesh requires indices."); - return false; + return null; } const isU32 = indices instanceof Uint32Array; const indicesPtr = this._allocateIndices(indices, isU32); - this._pxMesh = cooking.createTriMesh( - verticesPtr, - positions.length, - indicesPtr, - indices.length / 3, - !isU32, - physics - ); + pxMesh = cooking.createTriMesh(verticesPtr, positions.length, indicesPtr, indices.length / 3, !isU32, physics); physX._free(verticesPtr); physX._free(indicesPtr); - if (!this._pxMesh) { + if (!pxMesh) { this._logTriMeshCookingError(physX); - return false; + return null; } } const { x: scaleX, y: scaleY, z: scaleZ } = this._worldScale; - const meshFlag = this._isConvex ? PhysXMeshColliderShape._tightBoundsFlag : 0; - this._pxGeometry = this._isConvex - ? physX.createConvexMeshGeometry(this._pxMesh, scaleX, scaleY, scaleZ, meshFlag) - : physX.createTriMeshGeometry(this._pxMesh, scaleX, scaleY, scaleZ, meshFlag); + const meshFlag = isConvex ? PhysXMeshColliderShape._tightBoundsFlag : 0; + const geometry = isConvex + ? physX.createConvexMeshGeometry(pxMesh, scaleX, scaleY, scaleZ, meshFlag) + : physX.createTriMeshGeometry(pxMesh, scaleX, scaleY, scaleZ, meshFlag); - return true; + return { mesh: pxMesh, geometry }; } private _logConvexCookingError(physX: any): void { @@ -221,8 +230,14 @@ export class PhysXMeshColliderShape extends PhysXColliderShape implements IMeshC ? physX.createConvexMeshGeometry(this._pxMesh, scaleX, scaleY, scaleZ, meshFlag) : physX.createTriMeshGeometry(this._pxMesh, scaleX, scaleY, scaleZ, meshFlag); - this._pxGeometry.delete(); + const oldGeometry = this._pxGeometry; + try { + this._pxShape.setGeometry(newGeometry); + } catch (error) { + newGeometry?.delete(); + throw error; + } this._pxGeometry = newGeometry; - this._pxShape.setGeometry(this._pxGeometry); + oldGeometry?.delete(); } } diff --git a/tests/src/core/physics/DynamicCollider.test.ts b/tests/src/core/physics/DynamicCollider.test.ts index 0ed815b9ca..1e4274da55 100644 --- a/tests/src/core/physics/DynamicCollider.test.ts +++ b/tests/src/core/physics/DynamicCollider.test.ts @@ -72,6 +72,13 @@ describe("DynamicCollider", function () { rootEntity.clearChildren(); }); + it("tolerancesScale drives PhysX default contact offset and sleep threshold", function () { + const physics = new PhysXPhysics({ tolerancesScale: { length: 2, speed: 20 } }); + + expect(physics.getDefaultContactOffset()).to.closeTo(0.04, 1e-6); + expect(physics.getDefaultSleepThreshold()).to.closeTo(0.02, 1e-6); + }); + it("addShape and removeShape", function () { const collider = rootEntity.createChild("entity").addComponent(DynamicCollider); const boxCollider = new BoxColliderShape(); diff --git a/tests/src/core/physics/MeshColliderShape.test.ts b/tests/src/core/physics/MeshColliderShape.test.ts index 9506569526..76c9227f0c 100644 --- a/tests/src/core/physics/MeshColliderShape.test.ts +++ b/tests/src/core/physics/MeshColliderShape.test.ts @@ -11,7 +11,7 @@ import { ModelMesh } from "@galacean/engine-core"; import { Vector3 } from "@galacean/engine-math"; -import { WebGLEngine } from "@galacean/engine"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; import { PhysXPhysics } from "@galacean/engine-physics-physx"; import { describe, beforeAll, beforeEach, expect, it, vi } from "vitest"; @@ -182,6 +182,46 @@ describe("MeshColliderShape PhysX", () => { defaultMaterial?.destroy(); material?.destroy(); }); + + it("cloned MeshColliderShape rebuilds its native PhysX shape", async () => { + const groundEntity = root.createChild("meshGroundForClone"); + groundEntity.transform.setPosition(0, 0, 0); + const groundCollider = groundEntity.addComponent(StaticCollider); + const meshShape = new MeshColliderShape(); + const meshMaterial = meshShape.material; + const mesh = createModelMesh(engine, [-10, 0, -10, 10, 0, -10, -10, 0, 10, 10, 0, 10], [0, 2, 1, 1, 2, 3]); + meshShape.mesh = mesh; + groundCollider.addShape(meshShape); + + const clonedGround = groundEntity.clone(); + // Move the original aside so the cloned ground is the only surface below the sphere. + groundEntity.transform.setPosition(1000, 0, 0); + root.addChild(clonedGround); + clonedGround.transform.setPosition(0, 0, 0); + + const sphereEntity = root.createChild("sphereForClone"); + sphereEntity.transform.setPosition(0, 2, 0); + const dynamicCollider = sphereEntity.addComponent(DynamicCollider); + const sphereShape = new SphereColliderShape(); + const sphereMaterial = sphereShape.material; + sphereShape.radius = 0.5; + dynamicCollider.addShape(sphereShape); + + for (let i = 0; i < 60; i++) { + physicsScene._update(1 / 60); + } + + // Sphere lands on cloned ground (y > -1), not falls forever (y < -10). + const sphereY = sphereEntity.transform.position.y; + expect(sphereY).toBeGreaterThan(-1); + expect(sphereY).toBeLessThan(2); + + groundEntity.destroy(); + clonedGround.destroy(); + sphereEntity.destroy(); + meshMaterial?.destroy(); + sphereMaterial?.destroy(); + }); }); describe("Convex Mesh (Dynamic)", () => { @@ -205,6 +245,42 @@ describe("MeshColliderShape PhysX", () => { defaultMaterial?.destroy(); }); + it("does not retry non-convex mesh creation on non-kinematic dynamic colliders", () => { + const entity = root.createChild("unsupportedDynamicMesh"); + const dynamicCollider = entity.addComponent(DynamicCollider); + + const meshShape = new MeshColliderShape(); + const meshMaterial = meshShape.material; + const convexMesh = createModelMesh( + engine, + [0, 1, 0, -1, 0, -1, 1, 0, -1, 0, 0, 1], + [0, 1, 2, 0, 2, 3, 0, 3, 1, 1, 3, 2] + ); + meshShape.isConvex = true; + meshShape.mesh = convexMesh; + dynamicCollider.addShape(meshShape); + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + meshShape.isConvex = false; + consoleErrorSpy.mockClear(); + + const triangleMesh = createModelMesh(engine, [-1, 0, -1, 1, 0, -1, 0, 0, 1], [0, 1, 2]); + meshShape.mesh = triangleMesh; + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + consoleErrorSpy.mockClear(); + + for (let i = 0; i < 3; i++) { + physicsScene._update(1 / 60); + } + expect(consoleErrorSpy).not.toHaveBeenCalled(); + } finally { + consoleErrorSpy.mockRestore(); + entity.destroy(); + meshMaterial?.destroy(); + } + }); + it("should allow convex mesh on dynamic collider", async () => { // Create ground const groundEntity = root.createChild("ground2"); @@ -418,6 +494,52 @@ describe("MeshColliderShape PhysX", () => { entity.destroy(); defaultMaterial?.destroy(); }); + + it("keeps the existing native mesh when runtime mesh recooking fails", () => { + const groundEntity = root.createChild("transactionalMeshUpdateGround"); + const staticCollider = groundEntity.addComponent(StaticCollider); + const meshShape = new MeshColliderShape(); + const meshMaterial = meshShape.material; + const groundMesh = createModelMesh(engine, [-10, 0, -10, 10, 0, -10, -10, 0, 10, 10, 0, 10], [0, 2, 1, 1, 2, 3]); + meshShape.mesh = groundMesh; + staticCollider.addShape(meshShape); + + const nativeShape = (meshShape as any)._nativeShape; + const cooking = nativeShape._physXPhysics._pxCooking; + const originalCreateTriMesh = cooking.createTriMesh; + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + let sphereEntity: Entity | undefined; + let sphereMaterial: PhysicsMaterial | undefined; + + try { + cooking.createTriMesh = () => null; + const replacementMesh = createModelMesh(engine, [-2, 0, -2, 2, 0, -2, -2, 0, 2, 2, 0, 2], [0, 2, 1, 1, 2, 3]); + meshShape.mesh = replacementMesh; + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to create triangle mesh")); + + sphereEntity = root.createChild("transactionalMeshUpdateSphere"); + sphereEntity.transform.setPosition(0, 2, 0); + const dynamicCollider = sphereEntity.addComponent(DynamicCollider); + const sphereShape = new SphereColliderShape(); + sphereMaterial = sphereShape.material; + sphereShape.radius = 0.5; + dynamicCollider.addShape(sphereShape); + + for (let i = 0; i < 60; i++) { + physicsScene._update(1 / 60); + } + + expect(sphereEntity.transform.position.y).toBeGreaterThan(-1); + } finally { + cooking.createTriMesh = originalCreateTriMesh; + consoleErrorSpy.mockRestore(); + sphereEntity?.destroy(); + groundEntity.destroy(); + meshMaterial?.destroy(); + sphereMaterial?.destroy(); + } + }); }); describe("Triangle Mesh with DynamicCollider", () => { diff --git a/tests/src/core/physics/PhysicsMaterial.test.ts b/tests/src/core/physics/PhysicsMaterial.test.ts index 676f917fe3..7bfd0e0e88 100644 --- a/tests/src/core/physics/PhysicsMaterial.test.ts +++ b/tests/src/core/physics/PhysicsMaterial.test.ts @@ -8,7 +8,7 @@ import { StaticCollider, PlaneColliderShape } from "@galacean/engine-core"; -import { WebGLEngine } from "@galacean/engine"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; import { PhysXPhysics } from "@galacean/engine-physics-physx"; import { Vector3 } from "@galacean/engine-math"; import { describe, beforeAll, beforeEach, expect, it } from "vitest"; @@ -79,6 +79,56 @@ describe("PhysicsMaterial", () => { expect(formatValue(boxEntity2.transform.position.y)).eq(0); }); + it("cloned collider shape material keeps native values", () => { + const scene = engine.sceneManager.activeScene; + const originalGravity = scene.physics.gravity.clone(); + const originalFixedTimeStep = scene.physics.fixedTimeStep; + scene.physics.gravity = new Vector3(0, 0, 0); + scene.physics.fixedTimeStep = 1 / 60; + + try { + const wallEntity = addBox(new Vector3(1, 8, 8), StaticCollider, new Vector3(0, 0, 0)); + const wallMaterial = wallEntity.getComponent(StaticCollider).shapes[0].material; + wallMaterial.bounciness = 1; + wallMaterial.dynamicFriction = 0; + wallMaterial.staticFriction = 0; + wallMaterial.bounceCombine = PhysicsMaterialCombineMode.Multiply; + wallMaterial.frictionCombine = PhysicsMaterialCombineMode.Multiply; + + const sourceEntity = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const sourceCollider = sourceEntity.getComponent(DynamicCollider); + sourceCollider.linearDamping = 0; + sourceCollider.angularDamping = 0; + sourceCollider.automaticCenterOfMass = false; + sourceCollider.automaticInertiaTensor = false; + + const sourceMaterial = sourceCollider.shapes[0].material; + sourceMaterial.bounciness = 1; + sourceMaterial.dynamicFriction = 0; + sourceMaterial.staticFriction = 0; + sourceMaterial.bounceCombine = PhysicsMaterialCombineMode.Multiply; + sourceMaterial.frictionCombine = PhysicsMaterialCombineMode.Multiply; + + const cloneEntity = sourceEntity.clone(); + sourceEntity.destroy(); + rootEntity.addChild(cloneEntity); + cloneEntity.transform.setPosition(-3, 0, 0); + + const cloneCollider = cloneEntity.getComponent(DynamicCollider); + cloneCollider.linearVelocity = new Vector3(10, 0, 0); + + for (let i = 0; i < 40; i++) { + // @ts-ignore + scene.physics._update(scene.physics.fixedTimeStep); + } + + expect(cloneCollider.linearVelocity.x).lessThan(-1); + } finally { + scene.physics.gravity = originalGravity; + scene.physics.fixedTimeStep = originalFixedTimeStep; + } + }); + it("bounceCombine Average", () => { const boxEntity = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(0, 5, 0)); const ground = addPlane(0, -0.5, 0); From 3969fec293fd864fd6f5451b9c1d1ba994f9b39a Mon Sep 17 00:00:00 2001 From: luzhuang Date: Wed, 17 Jun 2026 20:45:25 +0800 Subject: [PATCH 04/23] fix(physics): keep mesh recooking transactional --- packages/core/src/physics/Collider.ts | 7 --- packages/core/src/physics/DynamicCollider.ts | 1 - .../core/src/physics/shape/ColliderShape.ts | 11 ---- .../src/physics/shape/MeshColliderShape.ts | 63 +++++++------------ .../core/physics/MeshColliderShape.test.ts | 31 +++++++++ 5 files changed, 53 insertions(+), 60 deletions(-) diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index c7ca7dac36..a8a591fe56 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -122,13 +122,6 @@ export class Collider extends Component implements ICustomClone { } this._updateFlag.flag = false; } - - // Drive per-shape physics update. MeshColliderShape uses this to retry - // native shape creation after PhysX cooking returned null despite valid - // extracted mesh data. - for (let i = 0, n = shapes.length; i < n; i++) { - shapes[i]._onPhysicsUpdate(); - } } /** diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 7483b6ffe1..80bef21016 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -13,7 +13,6 @@ import { MeshColliderShape } from "./shape/MeshColliderShape"; */ export class DynamicCollider extends Collider { private static _tempVector3 = new Vector3(); - private static _tempVector3_1 = new Vector3(); private static _tempQuat = new Quaternion(); private _linearDamping = 0; diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index 28eb87ba0e..6287ab6063 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -168,17 +168,6 @@ export abstract class ColliderShape implements ICustomClone { target._syncNative(); } - /** - * @internal - * - * Called once per physics update tick by `Collider._onUpdate`. Base no-op. - * - * Subclasses can override for frame-driven maintenance. Currently used by - * `MeshColliderShape` to retry native shape creation after PhysX cooking - * returned null despite valid extracted mesh data. - */ - _onPhysicsUpdate(): void {} - /** * @internal */ diff --git a/packages/core/src/physics/shape/MeshColliderShape.ts b/packages/core/src/physics/shape/MeshColliderShape.ts index aba038ce1c..d9d9ce9eb0 100644 --- a/packages/core/src/physics/shape/MeshColliderShape.ts +++ b/packages/core/src/physics/shape/MeshColliderShape.ts @@ -16,11 +16,6 @@ export class MeshColliderShape extends ColliderShape { private _indices: Uint8Array | Uint16Array | Uint32Array | null = null; private _cookingFlags = MeshColliderShapeCookingFlag.Cleaning | MeshColliderShapeCookingFlag.VertexWelding; private _isShapeAttached = false; - /** - * `true` if PhysX cooking returned null after valid mesh data was extracted. - * Unsupported collider/mesh combinations are terminal and must not enter retry. - */ - private _pendingNativeShapeCreation = false; /** * Cooking flags for this mesh collider shape. @@ -31,9 +26,12 @@ export class MeshColliderShape extends ColliderShape { set cookingFlags(value: MeshColliderShapeCookingFlag) { if (this._cookingFlags !== value) { + const previousValue = this._cookingFlags; this._cookingFlags = value; if (this._nativeShape) { - this._updateNativeShapeData(); + if (!this._updateNativeShapeData()) { + this._cookingFlags = previousValue; + } } else if (this._mesh && this._extractMeshData(this._mesh)) { this._createNativeShape(); } @@ -61,7 +59,6 @@ export class MeshColliderShape extends ColliderShape { this._createNativeShape(); } else { this._clearMeshData(); - this._pendingNativeShapeCreation = false; } } } @@ -77,13 +74,22 @@ export class MeshColliderShape extends ColliderShape { set mesh(value: ModelMesh) { if (this._mesh !== value) { + const previousMesh = this._mesh; + const previousPositions = this._positions; + const previousIndices = this._indices; this._mesh?._addReferCount(-1); value?._addReferCount(1); this._mesh = value; if (value) { if (this._extractMeshData(value)) { if (this._nativeShape) { - this._updateNativeShapeData(); + if (!this._updateNativeShapeData()) { + value?._addReferCount(-1); + previousMesh?._addReferCount(1); + this._mesh = previousMesh; + this._positions = previousPositions; + this._indices = previousIndices; + } } else { this._createNativeShape(); } @@ -91,12 +97,10 @@ export class MeshColliderShape extends ColliderShape { // Mesh data extraction cannot recover without assigning a new mesh. this._destroyNativeShape(); this._clearMeshData(); - this._pendingNativeShapeCreation = false; } } else { this._destroyNativeShape(); this._clearMeshData(); - this._pendingNativeShapeCreation = false; } } } @@ -166,7 +170,7 @@ export class MeshColliderShape extends ColliderShape { return true; } - private _updateNativeShapeData(): void { + private _updateNativeShapeData(): boolean { if ( (this._nativeShape).setMeshData( this._positions, @@ -175,11 +179,12 @@ export class MeshColliderShape extends ColliderShape { this._cookingFlags ) ) { - // Re-add to collider if previously removed due to cooking failure if (this._collider && !this._isShapeAttached) { this._attachToCollider(); } + return true; } + return false; } private _detachFromCollider(): void { @@ -192,12 +197,11 @@ export class MeshColliderShape extends ColliderShape { this._isShapeAttached = true; } - private _createNativeShape(): void { + private _createNativeShape(): boolean { // Non-convex MeshColliderShape is only supported on StaticCollider or kinematic DynamicCollider if (!this._isConvex && this._collider instanceof DynamicCollider && !this._collider.isKinematic) { - this._pendingNativeShapeCreation = false; console.error("MeshColliderShape: Non-convex mesh is not supported on non-kinematic DynamicCollider."); - return; + return false; } const nativeShape = Engine._nativePhysics.createMeshColliderShape( @@ -210,13 +214,10 @@ export class MeshColliderShape extends ColliderShape { ); if (!nativeShape) { - // Cook failed — `_onPhysicsUpdate` will retry next frame - this._pendingNativeShapeCreation = true; - return; + return false; } this._nativeShape = nativeShape; - this._pendingNativeShapeCreation = false; // Sync base class properties (position, rotation, contactOffset, isTrigger, material) super._syncNative(); @@ -226,34 +227,14 @@ export class MeshColliderShape extends ColliderShape { nativeShape.setWorldScale(this._collider.entity.transform.lossyWorldScale); this._attachToCollider(); } - } - - /** - * @internal - * Retry hook: keep attempting `_createNativeShape` until it succeeds. - * - * Triggered every physics update tick by `Collider._onUpdate`. Handles the case - * where `_cookMesh` fails on first attempt due to PhysX cooking pipeline timing - * (the mesh data was extracted successfully at `set mesh` time, but the native - * cook call returned null). No-op once a valid native shape exists. - * - * We DO NOT re-call `_extractMeshData` here — once `set mesh` finished, either: - * - extraction succeeded → `_positions` is populated and we reuse it - * - extraction failed → `_clearMeshData` cleared `_positions`, and `mesh.accessible` - * won't recover (GPU upload is one-way), so re-extracting would fail again - */ - override _onPhysicsUpdate(): void { - if (!this._pendingNativeShapeCreation || !this._mesh || !this._positions) return; - this._createNativeShape(); + return true; } /** * @internal * After CloneManager deep-copies `_positions` / `_indices` / `_mesh` and remaps `_collider`, * the cloned shape still has no native PhysX shape because `_nativeShape` is `@ignoreClone`. - * Cook a fresh native shape now using the already-cloned vertex/index buffers; on transient - * cook failure `_createNativeShape` sets `_pendingNativeShapeCreation = true` and - * `_onPhysicsUpdate` will retry next tick. + * Cook a fresh native shape now using the already-cloned vertex/index buffers. */ override _cloneTo(target: MeshColliderShape): void { super._cloneTo(target); diff --git a/tests/src/core/physics/MeshColliderShape.test.ts b/tests/src/core/physics/MeshColliderShape.test.ts index 76c9227f0c..9d7ce99381 100644 --- a/tests/src/core/physics/MeshColliderShape.test.ts +++ b/tests/src/core/physics/MeshColliderShape.test.ts @@ -5,6 +5,7 @@ import { SphereColliderShape, BoxColliderShape, DynamicCollider, + Engine, StaticCollider, PhysicsMaterial, Script, @@ -517,6 +518,7 @@ describe("MeshColliderShape PhysX", () => { meshShape.mesh = replacementMesh; expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to create triangle mesh")); + expect(meshShape.mesh).toBe(groundMesh); sphereEntity = root.createChild("transactionalMeshUpdateSphere"); sphereEntity.transform.setPosition(0, 2, 0); @@ -540,6 +542,35 @@ describe("MeshColliderShape PhysX", () => { sphereMaterial?.destroy(); } }); + + it("does not retry terminal native shape creation failures every physics tick", () => { + const entity = root.createChild("terminalCookFailure"); + const staticCollider = entity.addComponent(StaticCollider); + const meshShape = new MeshColliderShape(); + const meshMaterial = meshShape.material; + const mesh = createModelMesh(engine, [-1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, 1], [0, 2, 1, 1, 2, 3]); + const nativePhysics = (Engine as any)._nativePhysics; + const originalCreateMeshColliderShape = nativePhysics.createMeshColliderShape; + const createMeshColliderShapeSpy = vi.fn(() => null); + + try { + nativePhysics.createMeshColliderShape = createMeshColliderShapeSpy; + staticCollider.addShape(meshShape); + meshShape.mesh = mesh; + + expect(createMeshColliderShapeSpy).toHaveBeenCalledTimes(1); + + for (let i = 0; i < 3; i++) { + physicsScene._update(1 / 60); + } + + expect(createMeshColliderShapeSpy).toHaveBeenCalledTimes(1); + } finally { + nativePhysics.createMeshColliderShape = originalCreateMeshColliderShape; + entity.destroy(); + meshMaterial?.destroy(); + } + }); }); describe("Triangle Mesh with DynamicCollider", () => { From 7bcc2e18c1b73424ec0dc60154f1ce84ceaca59d Mon Sep 17 00:00:00 2001 From: luzhuang Date: Sun, 12 Jul 2026 03:19:44 +0800 Subject: [PATCH 05/23] fix(physics): harden cloned mesh ownership --- ...-physics-mesh-clone-and-cache-ownership.md | 29 +++++ packages/core/src/asset/ResourceManager.ts | 2 +- packages/core/src/physics/Collider.ts | 20 +++- packages/core/src/physics/DynamicCollider.ts | 8 +- .../core/src/physics/shape/ColliderShape.ts | 3 + .../src/physics/shape/MeshColliderShape.ts | 38 +++---- .../src/core/physics/DynamicCollider.test.ts | 75 ++++++++++++- .../core/physics/MeshColliderShape.test.ts | 104 ++++++++++++++++++ .../src/core/resource/ResourceManager.test.ts | 22 ++++ 9 files changed, 269 insertions(+), 32 deletions(-) create mode 100644 notes/2026-07-11-physics-mesh-clone-and-cache-ownership.md diff --git a/notes/2026-07-11-physics-mesh-clone-and-cache-ownership.md b/notes/2026-07-11-physics-mesh-clone-and-cache-ownership.md new file mode 100644 index 0000000000..fa54c99ef7 --- /dev/null +++ b/notes/2026-07-11-physics-mesh-clone-and-cache-ownership.md @@ -0,0 +1,29 @@ +# Physics mesh clone and cache ownership + +## Context + +CatchPig's converted 2D polygon colliders exercise dynamic prefab cloning, convex mesh cooking, non-unit scale, and project-specific PhysX tolerance scale. Open PR `galacean/engine#3042` addressed the same mesh-shape and scaled-default boundary and was integrated before local follow-up work. + +## Integrated upstream commits + +- `fix(physics): rebuild mesh shapes and scaled defaults` +- `fix(physics): keep mesh recooking transactional` + +The integration preserved the current `dev/2.0` PhysX CDN configuration and the removal of physics-lite. + +## Follow-up design + +- `Collider` is the sole owner of native shape attachment and detachment. +- `ColliderShape` records whether its native shape is currently attached. +- `MeshColliderShape` clone and recook paths construct detached native shapes; Collider attaches each shape exactly once. +- Mesh recooking remains transactional: a failed cook retains the previous usable native shape. +- Mesh resource references are balanced across clone and destroy. +- `DynamicCollider` copies explicit center of mass and inertia tensor only when the corresponding automatic mode is disabled. +- `ResourceManager.getFromCache` normalizes virtual paths through the same remote-URL mapping as load requests. + +## Verification + +- Module and declaration builds passed. +- Focused suites passed 78 tests total: MeshColliderShape 31, DynamicCollider 31, ResourceManager 16. +- Tests cover delayed mesh assignment, inaccessible meshes, failed recooking, exact native attach/detach counts, clone ref-count balance, and virtual-path cache lookup. +- CatchPig ran with gravity `y = -640`, tolerance scale `{ length: 32, speed: 640 }`, 30 attached dynamic colliders, stable bounds, and successful pointer-driven gameplay after Editor selector lowering was corrected. diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 54b8309589..24a7b11ac1 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -104,7 +104,7 @@ export class ResourceManager { * @returns Resource object */ getFromCache(url: string): T { - return (this._assetUrlPool[url] as T) ?? null; + return (this._assetUrlPool[this._getRemoteUrl(url)] as T) ?? null; } /** diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index a8a591fe56..e132b95e1e 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -183,16 +183,28 @@ export class Collider extends Component implements ICustomClone { protected _addNativeShape(shape: ColliderShape): void { shape._collider = this; - if (shape._nativeShape) { + this._attachNativeShape(shape); + } + + protected _removeNativeShape(shape: ColliderShape): void { + this._detachNativeShape(shape); + shape._collider = null; + } + + /** @internal */ + _attachNativeShape(shape: ColliderShape): void { + if (shape._nativeShape && !shape._isShapeAttached) { shape._nativeShape.setWorldScale(this.entity.transform.lossyWorldScale); this._nativeCollider.addShape(shape._nativeShape); + shape._isShapeAttached = true; } } - protected _removeNativeShape(shape: ColliderShape): void { - shape._collider = null; - if (shape._nativeShape) { + /** @internal */ + _detachNativeShape(shape: ColliderShape): void { + if (shape._nativeShape && shape._isShapeAttached) { this._nativeCollider.removeShape(shape._nativeShape); + shape._isShapeAttached = false; } } diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 80bef21016..16fec72cc7 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -467,8 +467,12 @@ export class DynamicCollider extends Collider { override _cloneTo(target: DynamicCollider): void { target._linearVelocity.copyFrom(this.linearVelocity); target._angularVelocity.copyFrom(this.angularVelocity); - target._centerOfMass.copyFrom(this.centerOfMass); - target._inertiaTensor.copyFrom(this.inertiaTensor); + if (!this._automaticCenterOfMass) { + target._centerOfMass.copyFrom(this.centerOfMass); + } + if (!this._automaticInertiaTensor) { + target._inertiaTensor.copyFrom(this.inertiaTensor); + } super._cloneTo(target); } diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index 6287ab6063..ac12ae9d19 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -17,6 +17,9 @@ export abstract class ColliderShape implements ICustomClone { _collider: Collider; /** @internal */ @ignoreClone + _isShapeAttached = false; + /** @internal */ + @ignoreClone _nativeShape: IColliderShape; @ignoreClone diff --git a/packages/core/src/physics/shape/MeshColliderShape.ts b/packages/core/src/physics/shape/MeshColliderShape.ts index d9d9ce9eb0..193fc34d3a 100644 --- a/packages/core/src/physics/shape/MeshColliderShape.ts +++ b/packages/core/src/physics/shape/MeshColliderShape.ts @@ -15,7 +15,6 @@ export class MeshColliderShape extends ColliderShape { private _positions: Vector3[] = null; private _indices: Uint8Array | Uint16Array | Uint32Array | null = null; private _cookingFlags = MeshColliderShapeCookingFlag.Cleaning | MeshColliderShapeCookingFlag.VertexWelding; - private _isShapeAttached = false; /** * Cooking flags for this mesh collider shape. @@ -135,9 +134,7 @@ export class MeshColliderShape extends ColliderShape { private _destroyNativeShape(): void { if (this._nativeShape) { - if (this._isShapeAttached) { - this._detachFromCollider(); - } + this._collider?._detachNativeShape(this); this._nativeShape.destroy(); this._nativeShape = null; } @@ -179,27 +176,20 @@ export class MeshColliderShape extends ColliderShape { this._cookingFlags ) ) { - if (this._collider && !this._isShapeAttached) { - this._attachToCollider(); - } + this._collider?._attachNativeShape(this); return true; } return false; } - private _detachFromCollider(): void { - this._collider._nativeCollider.removeShape(this._nativeShape); - this._isShapeAttached = false; - } - - private _attachToCollider(): void { - this._collider._nativeCollider.addShape(this._nativeShape); - this._isShapeAttached = true; - } - - private _createNativeShape(): boolean { + private _createNativeShape(attachToCollider = true): boolean { // Non-convex MeshColliderShape is only supported on StaticCollider or kinematic DynamicCollider - if (!this._isConvex && this._collider instanceof DynamicCollider && !this._collider.isKinematic) { + if ( + attachToCollider && + !this._isConvex && + this._collider instanceof DynamicCollider && + !this._collider.isKinematic + ) { console.error("MeshColliderShape: Non-convex mesh is not supported on non-kinematic DynamicCollider."); return false; } @@ -223,9 +213,8 @@ export class MeshColliderShape extends ColliderShape { super._syncNative(); // If already attached to a collider, add the newly created native shape to it - if (this._collider) { - nativeShape.setWorldScale(this._collider.entity.transform.lossyWorldScale); - this._attachToCollider(); + if (attachToCollider && this._collider) { + this._collider._attachNativeShape(this); } return true; } @@ -237,9 +226,10 @@ export class MeshColliderShape extends ColliderShape { * Cook a fresh native shape now using the already-cloned vertex/index buffers. */ override _cloneTo(target: MeshColliderShape): void { + target._mesh?._addReferCount(1); super._cloneTo(target); - if (target._positions) { - target._createNativeShape(); + if (this._nativeShape) { + target._createNativeShape(false); } } } diff --git a/tests/src/core/physics/DynamicCollider.test.ts b/tests/src/core/physics/DynamicCollider.test.ts index 1e4274da55..4ec1aeb55e 100644 --- a/tests/src/core/physics/DynamicCollider.test.ts +++ b/tests/src/core/physics/DynamicCollider.test.ts @@ -12,7 +12,7 @@ import { import { WebGLEngine } from "@galacean/engine"; import { PhysXPhysics, PhysXRuntimeMode } from "@galacean/engine-physics-physx"; import { Vector3 } from "@galacean/engine-math"; -import { vi, describe, beforeAll, beforeEach, expect, it } from "vitest"; +import { vi, describe, beforeAll, beforeEach, afterEach, expect, it } from "vitest"; const physXWasmModeUrl = new URL("../../../../packages/physics-physx/libs/physx.release.js", import.meta.url).href; const physXWasmSIMDModeUrl = new URL("../../../../packages/physics-physx/libs/physx.release.simd.js", import.meta.url) @@ -52,6 +52,16 @@ describe("DynamicCollider", function () { return boxEntity; } + function addOffsetBoxCollider(name: string) { + const entity = rootEntity.createChild(name); + const collider = entity.addComponent(DynamicCollider); + const shape = new BoxColliderShape(); + shape.size = new Vector3(2, 4, 6); + shape.position = new Vector3(1, 2, 3); + collider.addShape(shape); + return { entity, collider }; + } + function formatValue(value: number) { return Math.round(value * 100000) / 100000; } @@ -72,6 +82,10 @@ describe("DynamicCollider", function () { rootEntity.clearChildren(); }); + afterEach(function () { + vi.restoreAllMocks(); + }); + it("tolerancesScale drives PhysX default contact offset and sleep threshold", function () { const physics = new PhysXPhysics({ tolerancesScale: { length: 2, speed: 20 } }); @@ -432,6 +446,65 @@ describe("DynamicCollider", function () { expect(Math.abs(formatValue(boxCollider2.linearVelocity.x))).lessThan(4); }); + it("clone keeps automatic mass properties without warnings", function () { + const { entity } = addOffsetBoxCollider("AutomaticCollider"); + const consoleWarnSpy = vi.spyOn(console, "warn"); + + const clone = entity.clone().getComponent(DynamicCollider); + + expect(clone.automaticCenterOfMass).toBe(true); + expect(clone.automaticInertiaTensor).toBe(true); + expect(clone.centerOfMass).to.deep.include({ x: 1, y: 2, z: 3 }); + expect(formatValue(clone.inertiaTensor.x)).eq(4.33333); + expect(formatValue(clone.inertiaTensor.y)).eq(3.33333); + expect(formatValue(clone.inertiaTensor.z)).eq(1.66667); + expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + }); + + it("clone keeps manual mass properties", function () { + const { entity, collider } = addOffsetBoxCollider("ManualCollider"); + collider.automaticCenterOfMass = false; + collider.automaticInertiaTensor = false; + collider.centerOfMass = new Vector3(1, 2, 3); + collider.inertiaTensor = new Vector3(4, 5, 6); + + const clone = entity.clone().getComponent(DynamicCollider); + + expect(clone.automaticCenterOfMass).toBe(false); + expect(clone.automaticInertiaTensor).toBe(false); + expect(clone.centerOfMass).to.deep.include({ x: 1, y: 2, z: 3 }); + expect(clone.inertiaTensor).to.deep.include({ x: 4, y: 5, z: 6 }); + }); + + it("clone keeps mixed automatic mass properties", function () { + const { entity: manualCenterEntity, collider: manualCenterCollider } = addOffsetBoxCollider("ManualCenterCollider"); + manualCenterCollider.automaticCenterOfMass = false; + manualCenterCollider.centerOfMass = new Vector3(7, 8, 9); + + const manualCenterClone = manualCenterEntity.clone().getComponent(DynamicCollider); + + expect(manualCenterClone.automaticCenterOfMass).toBe(false); + expect(manualCenterClone.automaticInertiaTensor).toBe(true); + expect(manualCenterClone.centerOfMass).to.deep.include({ x: 7, y: 8, z: 9 }); + expect(formatValue(manualCenterClone.inertiaTensor.x)).eq(4.33333); + expect(formatValue(manualCenterClone.inertiaTensor.y)).eq(3.33333); + expect(formatValue(manualCenterClone.inertiaTensor.z)).eq(1.66667); + + const { entity: manualInertiaEntity, collider: manualInertiaCollider } = + addOffsetBoxCollider("ManualInertiaCollider"); + manualInertiaCollider.automaticInertiaTensor = false; + manualInertiaCollider.inertiaTensor = new Vector3(7, 8, 9); + + const manualInertiaClone = manualInertiaEntity.clone().getComponent(DynamicCollider); + + expect(manualInertiaClone.automaticCenterOfMass).toBe(true); + expect(manualInertiaClone.automaticInertiaTensor).toBe(false); + expect(manualInertiaClone.centerOfMass).to.deep.include({ x: 1, y: 2, z: 3 }); + expect(formatValue(manualInertiaClone.inertiaTensor.x)).eq(7); + expect(formatValue(manualInertiaClone.inertiaTensor.y)).eq(8); + expect(formatValue(manualInertiaClone.inertiaTensor.z)).eq(9); + }); + it("useGravity", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 10, 0)); const boxCollider = box.getComponent(DynamicCollider); diff --git a/tests/src/core/physics/MeshColliderShape.test.ts b/tests/src/core/physics/MeshColliderShape.test.ts index 9d7ce99381..e1031dc7b1 100644 --- a/tests/src/core/physics/MeshColliderShape.test.ts +++ b/tests/src/core/physics/MeshColliderShape.test.ts @@ -46,6 +46,10 @@ function createModelMesh(engine: WebGLEngine, positions: number[], indices?: num return mesh; } +function getNativeShapeCount(collider: DynamicCollider): number { + return (collider as any)._nativeCollider._shapes.length; +} + describe("MeshColliderShape PhysX", () => { let engine: WebGLEngine; let root: Entity; @@ -282,6 +286,106 @@ describe("MeshColliderShape PhysX", () => { } }); + it("should clone convex mesh with exactly one native shape", () => { + const entity = root.createChild("cloneConvexMesh"); + const dynamicCollider = entity.addComponent(DynamicCollider); + const meshShape = new MeshColliderShape(); + const defaultMaterial = meshShape.material; + meshShape.isConvex = true; + dynamicCollider.addShape(meshShape); + + const mesh = createModelMesh(engine, [-1, -1, -1, 1, -1, -1, 0, 1, -1, -1, -1, 1, 1, -1, 1, 0, 1, 1]); + meshShape.mesh = mesh; + + expect(dynamicCollider.shapes).toHaveLength(1); + expect(getNativeShapeCount(dynamicCollider)).toBe(1); + expect(mesh._getReferCount()).toBe(1); + + const clonedEntity = entity.clone(); + const clonedCollider = clonedEntity.getComponent(DynamicCollider); + const clonedShape = clonedCollider.shapes[0] as MeshColliderShape; + + expect(clonedCollider.shapes).toHaveLength(1); + expect(clonedShape.mesh).toBe(mesh); + expect((clonedShape as any)._nativeShape).toBeTruthy(); + expect((clonedShape as any)._isShapeAttached).toBe(true); + expect(getNativeShapeCount(clonedCollider)).toBe(1); + expect(mesh._getReferCount()).toBe(2); + + clonedEntity.destroy(); + expect(mesh._getReferCount()).toBe(1); + entity.destroy(); + expect(mesh._getReferCount()).toBe(0); + defaultMaterial?.destroy(); + }); + + it("should create a cloned native shape when mesh data becomes available later", () => { + const entity = root.createChild("cloneDelayedConvexMesh"); + const dynamicCollider = entity.addComponent(DynamicCollider); + const meshShape = new MeshColliderShape(); + const defaultMaterial = meshShape.material; + meshShape.isConvex = true; + dynamicCollider.addShape(meshShape); + + const clonedEntity = entity.clone(); + const clonedCollider = clonedEntity.getComponent(DynamicCollider); + const clonedShape = clonedCollider.shapes[0] as MeshColliderShape; + + expect(clonedCollider.shapes).toHaveLength(1); + expect((clonedShape as any)._nativeShape).toBeFalsy(); + expect((clonedShape as any)._isShapeAttached).toBe(false); + expect(getNativeShapeCount(clonedCollider)).toBe(0); + + const mesh = createModelMesh(engine, [-1, -1, -1, 1, -1, -1, 0, 1, -1, -1, -1, 1, 1, -1, 1, 0, 1, 1]); + clonedShape.mesh = mesh; + + expect((clonedShape as any)._nativeShape).toBeTruthy(); + expect((clonedShape as any)._isShapeAttached).toBe(true); + expect(getNativeShapeCount(clonedCollider)).toBe(1); + expect(mesh._getReferCount()).toBe(1); + + clonedEntity.destroy(); + expect(mesh._getReferCount()).toBe(0); + entity.destroy(); + defaultMaterial?.destroy(); + }); + + it("should keep an inaccessible mesh native-less when cloned", () => { + const warnSpy = vi.spyOn(console, "warn"); + const entity = root.createChild("cloneInaccessibleConvexMesh"); + const dynamicCollider = entity.addComponent(DynamicCollider); + const meshShape = new MeshColliderShape(); + const defaultMaterial = meshShape.material; + meshShape.isConvex = true; + dynamicCollider.addShape(meshShape); + + const mesh = new ModelMesh(engine); + mesh.setPositions([new Vector3(-1, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 1, 0)]); + mesh.uploadData(true); + meshShape.mesh = mesh; + + expect((meshShape as any)._nativeShape).toBeFalsy(); + expect(getNativeShapeCount(dynamicCollider)).toBe(0); + expect(mesh._getReferCount()).toBe(1); + + const clonedEntity = entity.clone(); + const clonedCollider = clonedEntity.getComponent(DynamicCollider); + const clonedShape = clonedCollider.shapes[0] as MeshColliderShape; + + expect(clonedShape.mesh).toBe(mesh); + expect((clonedShape as any)._nativeShape).toBeFalsy(); + expect((clonedShape as any)._isShapeAttached).toBe(false); + expect(getNativeShapeCount(clonedCollider)).toBe(0); + expect(mesh._getReferCount()).toBe(2); + + clonedEntity.destroy(); + expect(mesh._getReferCount()).toBe(1); + entity.destroy(); + expect(mesh._getReferCount()).toBe(0); + defaultMaterial?.destroy(); + warnSpy.mockRestore(); + }); + it("should allow convex mesh on dynamic collider", async () => { // Create ground const groundEntity = root.createChild("ground2"); diff --git a/tests/src/core/resource/ResourceManager.test.ts b/tests/src/core/resource/ResourceManager.test.ts index b0fea63848..266aa3c850 100644 --- a/tests/src/core/resource/ResourceManager.test.ts +++ b/tests/src/core/resource/ResourceManager.test.ts @@ -28,6 +28,28 @@ describe("ResourceManager", () => { getResource = engine.resourceManager.getFromCache(wrongUrl); expect(getResource).equal(null); }); + + it("resolves virtual paths to physical cache keys", () => { + const resourceManager = engine.resourceManager; + const virtualPath = "Prefab/Character.prefab"; + const physicalPath = "/Prefab/Character.prefab"; + const remoteUrl = "https://cdn.ali.com/Prefab/Remote.prefab"; + const physicalResource = {}; + const remoteResource = {}; + + resourceManager.initVirtualResources([ + { virtualPath, path: physicalPath, type: AssetType.Prefab, params: { keep: true } } + ]); + // @ts-ignore + resourceManager._assetUrlPool[physicalPath] = physicalResource; + // @ts-ignore + resourceManager._assetUrlPool[remoteUrl] = remoteResource; + + expect(resourceManager.getFromCache(virtualPath)).equal(physicalResource); + expect(resourceManager.getFromCache(physicalPath)).equal(physicalResource); + expect(resourceManager.getFromCache(remoteUrl)).equal(remoteResource); + expect(resourceManager.getFromCache("Prefab/Missing.prefab")).equal(null); + }); }); describe("findResourcesByType", () => { From 2552ce6f9d461abcd147e5e85de49713055db8cc Mon Sep 17 00:00:00 2001 From: luzhuang Date: Sun, 12 Jul 2026 10:01:34 +0800 Subject: [PATCH 06/23] docs: record engine compatibility decisions --- notes/2026-05-21-physx-tolerances-scale.md | 34 ++++++ .../2026-05-22-collider-reenable-teleport.md | 36 ++++++ notes/2026-05-22-physics-max-substeps-rfc.md | 94 ++++++++++++++++ .../2026-06-01-physics-contact-auto-demand.md | 101 +++++++++++++++++ .../2026-06-11-pr3014-domain-split.md | 32 ++++++ packages/loader/ASSET_EXTENSIONS.md | 103 ++++++++++++++++++ 6 files changed, 400 insertions(+) create mode 100644 notes/2026-05-21-physx-tolerances-scale.md create mode 100644 notes/2026-05-22-collider-reenable-teleport.md create mode 100644 notes/2026-05-22-physics-max-substeps-rfc.md create mode 100644 notes/2026-06-01-physics-contact-auto-demand.md create mode 100644 notes/shaderlab/2026-06-11-pr3014-domain-split.md create mode 100644 packages/loader/ASSET_EXTENSIONS.md diff --git a/notes/2026-05-21-physx-tolerances-scale.md b/notes/2026-05-21-physx-tolerances-scale.md new file mode 100644 index 0000000000..2895e8d2be --- /dev/null +++ b/notes/2026-05-21-physx-tolerances-scale.md @@ -0,0 +1,34 @@ +# PhysX tolerance scale configuration + +> Status: implemented by `be1d4bf32` on the local `dev/2.0` branch and tracked by draft PR #3042. + +## Context + +Cocos 2D migration can run physics in pixel-like units. PhysX supports non-meter +unit systems through `PxTolerancesScale`, but Galacean previously always created +PhysX with default `length=1` and `speed=10`. + +## Code facts + +- `PhysXPhysics._init()` creates one `PxTolerancesScale`, then passes it to both + `PxCreatePhysics` and `PxCookingParams`. +- PhysX requires scene scale to match the scale used to create `PxPhysics`. +- PhysX derives defaults such as contact offset and sleep threshold from this scale. +- Galacean core previously stored fixed defaults: + - `ColliderShape._contactOffset = 0.02` + - `DynamicCollider._sleepThreshold = 0.005` + and synced them unconditionally to native, overriding scaled PhysX defaults. + +## Fix + +- `PhysXPhysics` now accepts `PhysXPhysicsOptions.tolerancesScale`. +- The scale is applied before creating `PxPhysics` and `PxCookingParams`. +- `PhysXPhysics` exposes scaled default contact offset and sleep threshold. +- Core `ColliderShape` and `DynamicCollider` leave native defaults untouched unless + the user explicitly sets `contactOffset` or `sleepThreshold`. + +## Boundary + +This change does not scale damping or solver iterations. Those are not PhysX +tolerance defaults. Callers should still set explicit damping only when the +source project authored it. diff --git a/notes/2026-05-22-collider-reenable-teleport.md b/notes/2026-05-22-collider-reenable-teleport.md new file mode 100644 index 0000000000..848db12e83 --- /dev/null +++ b/notes/2026-05-22-collider-reenable-teleport.md @@ -0,0 +1,36 @@ +# Collider re-enable teleport regression test + +> Status: implemented on `fix/physics-kinematic-sync` and tracked by draft PR #3041; not yet part of `dev/2.0`. + +## Context + +Commit `bab0ae0a2` fixes a PhysX lifecycle bug where a disabled collider's native +actor is removed from the simulation scene but keeps its old native pose. When a +kinematic dynamic collider is re-enabled after its entity transform changed, the +first transform sync must not use `setKinematicTarget`, otherwise PhysX treats it +as a swept move from the stale pose and can emit spurious contacts. + +## Test added + +- `tests/src/core/physics/DynamicCollider.test.ts` now covers a kinematic + `DynamicCollider` in `Target` sync mode. +- The test disables the entity, moves its transform while inactive, re-enables it, + then verifies the first physics update calls native `setWorldTransform` exactly + once and does not call native `move`. +- A temporary local red-check that removed the pending re-enter teleport failed + with `expected 1 to equal +0` for `moveCalls`, proving the test catches the old + sweep path. + +## Naming cleanup + +The transient collider state is named `_pendingReenterTeleport` rather than +`_pendingReenterSync` because the important behavior is not generic transform +sync. It specifically forces the first sync after re-entering the physics scene +to use teleport semantics instead of swept kinematic target movement. + +## Verification + +- `pnpm vitest run tests/src/core/physics/DynamicCollider.test.ts -t "teleports kinematic target collider"` +- `pnpm vitest run tests/src/core/physics/DynamicCollider.test.ts` + +Both passed after restoring the engine fix. diff --git a/notes/2026-05-22-physics-max-substeps-rfc.md b/notes/2026-05-22-physics-max-substeps-rfc.md new file mode 100644 index 0000000000..d433888b3e --- /dev/null +++ b/notes/2026-05-22-physics-max-substeps-rfc.md @@ -0,0 +1,94 @@ +# RFC: Physics fixed-step catch-up cap + +> Status: resolved locally by `a18a3d4a2` with `PhysicsScene.maximumDeltaTime`. The proposed +> `maxSubSteps` API was not adopted. + +## Resolution + +The implemented boundary caps the frame delta admitted into the fixed-step accumulator: + +```ts +const simulateTime = this._restTime + Math.min(deltaTime, this._maximumDeltaTime); +``` + +This follows Unity's maximum-delta model, preserves unlimited catch-up by default with +`Infinity`, and avoids adding a second step-count API. Migration callers can set +`maximumDeltaTime` explicitly when source-runtime compatibility requires a cap. + +## Problem + +`PhysicsScene._update(deltaTime)` currently consumes all accumulated fixed steps in a single engine frame: + +```ts +const simulateTime = this._restTime + deltaTime; +const step = Math.floor(simulateTime / fixedTimeStep); +this._restTime = simulateTime - step * fixedTimeStep; +``` + +This is mathematically valid for simulation catch-up, but it has no public cap. When frame time fluctuates around `fixedTimeStep`, gameplay code that writes forces or velocity in `onPhysicsUpdate()` can observe uneven render-frame motion such as: + +```text +0 physics step in one render frame +2 physics steps in the next render frame +``` + +The issue is not specific to migration. Any Galacean project that drives gameplay through `onPhysicsUpdate()` can hit the same tradeoff between simulation catch-up and render-frame smoothness. + +## External references + +Unity exposes this concept through `Time.fixedDeltaTime` plus `Time.maximumDeltaTime` / Project Settings `Maximum Allowed Timestep`. + +Unity's docs describe that: + +- fixed updates can backlog when frame rate is lower than the fixed timestep rate +- Unity executes multiple fixed updates to catch up +- there is a maximum timestep period beyond which Unity does not keep catching up +- `Maximum Allowed Timestep` caps the worst-case time spent on physics and FixedUpdate in a low-frame-rate frame + +Cocos Creator 3D also exposes the concept directly as `PhysicsSystem.maxSubSteps`; in Cocos Creator 3.8.8 the default is `1`. + +## Proposed engine capability + +Add a public physics catch-up cap to Galacean, for example: + +```ts +class PhysicsScene { + fixedTimeStep: number; + maxSubSteps: number; +} +``` + +Suggested semantics: + +```ts +const rawStep = Math.floor((restTime + deltaTime) / fixedTimeStep); +const step = Math.min(rawStep, maxSubSteps); +restTime = simulateTime - step * fixedTimeStep; +``` + +Open default-value question: + +- `Infinity` preserves current Galacean behavior and makes this a non-breaking API addition. +- `Math.floor(engine.time.maximumDeltaTime / fixedTimeStep)` aligns with Unity's `maximumDeltaTime` style, but changes behavior if applied by default. +- `1` matches Cocos Creator's default, but would be a behavioral change for native Galacean projects. + +Conservative recommendation: add `maxSubSteps` as engine API with a non-breaking default first, then let migration/runtime layers explicitly set it to Cocos defaults when needed. + +## Non-goals + +- Do not solve render interpolation in this RFC. +- Do not change `Engine.targetFrameRate` or vSync behavior in the same change. +- Do not silently change native Galacean projects' physics catch-up behavior without a compatibility decision. + +## Validation plan + +Add focused engine tests for: + +- `maxSubSteps = Infinity` preserves current multi-step catch-up behavior. +- `maxSubSteps = 1` executes at most one `onPhysicsUpdate()` and one native physics update per engine update. +- accumulated `restTime` is preserved when the cap prevents full catch-up. +- `maxSubSteps = 0` disables fixed-step simulation while preserving accumulated time, or is rejected/clamped depending on the chosen API contract. + +## Migration relevance + +For migrated Cocos projects, the migration compat layer can set `scene.physics.maxSubSteps = 1` to match Cocos 3D default behavior. That should be explicit migration policy, not an implicit engine default forced onto all Galacean projects. diff --git a/notes/2026-06-01-physics-contact-auto-demand.md b/notes/2026-06-01-physics-contact-auto-demand.md new file mode 100644 index 0000000000..84fd93f96e --- /dev/null +++ b/notes/2026-06-01-physics-contact-auto-demand.md @@ -0,0 +1,101 @@ +# Physics contact event auto demand + +> Status: implemented on `fix/physics-shaderlab-split` and tracked by draft PR #3025; not yet +> part of `dev/2.0`. The physics-lite no-op described below belongs to the pre-#3053 base and +> must be dropped when the PR rebases onto the current branch. + +## Problem + +CatchPigGame dense 2D physics traces showed a large share of time under PhysX `fetchResults`, specifically the JS contact callback path: + +```text +PhysXPhysicsScene.update + _fetchResults + onContactPersist + _bufferContactEvent + copy contact points from WASM to JS +``` + +The affected migrated scene had no active collision callbacks. The engine still requested and buffered every contact event, so dense resting contacts paid event-marshalling cost even when no script could consume those events. + +## Root Cause + +`physX.js` currently configures non-trigger pairs with contact notification flags in its simulation filter shader: + +```text +eCONTACT_DEFAULT +eNOTIFY_TOUCH_FOUND +eNOTIFY_TOUCH_PERSISTS +eNOTIFY_TOUCH_LOST +eNOTIFY_CONTACT_POINTS +``` + +Galacean then unconditionally copied those reports into `_contactEvents` in `PhysXPhysicsScene._bufferContactEvent()`. + +This was a systemic engine boundary issue: the Core layer already knows which active `Script` instances can receive `onCollisionEnter`, `onCollisionStay`, or `onCollisionExit`, but the native physics scene had no demand signal for contact buffering. + +## Fix + +Add an explicit native physics scene demand API: + +```ts +IPhysicsScene.setContactEventEnabled(enabled: boolean): void +``` + +Core synchronizes contact-event demand immediately before each fixed physics step and only +rescans consumers after script or collider lifecycle changes mark the cache dirty: + +- scan collider entities' active `entity._scripts` +- compare collision lifecycle methods against `Script.prototype` +- enable contact buffering only if at least one active script overrides `onCollisionEnter`, `onCollisionStay`, or `onCollisionExit` + +PhysX honors that demand by skipping `_bufferContactEvent()` when disabled and clearing stale contact count on disable. Physics-lite implements the API as a no-op because it only produces trigger events. + +This keeps trigger events unchanged and preserves collision callbacks for projects that actually declare them. + +## Why This Is Not A Workaround + +The previous migration-layer mitigation monkey-patched the private PhysX `_bufferContactEvent` method based on generated config. That fixed one class of migrated output but depended on private engine internals. + +The engine fix moves the policy to the owning layers: + +- Core owns script lifecycle and can know whether collision callbacks exist. +- Design exposes a backend-agnostic demand API. +- PhysX owns whether to buffer contact reports. + +No project-specific or migration-specific data is needed. + +## Boundary + +This avoids the expensive JS-side buffering and contact-point copying when there is no consumer. It does not yet change the underlying `physX.js` simulation filter shader, which still asks native PhysX to produce contact reports for non-trigger pairs. A deeper future optimization could add native pair-flag demand support in `physX.js`, but that requires changing the wrapper/filter-shader layer rather than the Galacean TypeScript packages. + +## Files + +- `packages/design/src/physics/IPhysicsScene.ts` +- `packages/core/src/physics/PhysicsScene.ts` +- `packages/physics-physx/src/PhysXPhysicsScene.ts` +- `packages/physics-lite/src/LitePhysicsScene.ts` +- `tests/src/core/physics/PhysicsScene.test.ts` + +## Verification + +Passed: + +```bash +pnpm -r --filter @galacean/engine-design --filter @galacean/engine-core --filter @galacean/engine-physics-lite --filter @galacean/engine-physics-physx run b:types +pnpm exec eslint packages/core/src/physics/PhysicsScene.ts packages/physics-physx/src/PhysXPhysicsScene.ts packages/physics-lite/src/LitePhysicsScene.ts packages/design/src/physics/IPhysicsScene.ts tests/src/core/physics/PhysicsScene.test.ts --ext .ts +``` + +Targeted Vitest command was attempted but blocked before tests executed by the local Rollup native optional dependency being rejected by macOS system policy: + +```text +Error: Cannot find module @rollup/rollup-darwin-arm64 +cause: ERR_DLOPEN_FAILED ... library load denied by system policy +``` + +The new tests cover: + +- no active collision callback -> native contact events are disabled +- active collision callback -> native contact events are enabled +- disabling that script -> native contact events are disabled again +- trigger-only callbacks -> native contact events remain disabled diff --git a/notes/shaderlab/2026-06-11-pr3014-domain-split.md b/notes/shaderlab/2026-06-11-pr3014-domain-split.md new file mode 100644 index 0000000000..12875f66b4 --- /dev/null +++ b/notes/shaderlab/2026-06-11-pr3014-domain-split.md @@ -0,0 +1,32 @@ +# PR #3014 Domain Split + +## Context + +PR #3014 originally combined shaderlab fixes across Animation, Physics, Audio, and glTF Loader. The branch was updated first so the original PR head contained the latest relevant `fix/shaderlab` work, then the changes were split into domain-scoped PRs against the latest `origin/dev/2.0` (`de7549687`). + +## Branches And PRs + +- Animation: `fix/animation-shaderlab-split` -> https://github.com/galacean/engine/pull/3024 +- Physics contact events and collision normals: `fix/physics-shaderlab-split` -> https://github.com/galacean/engine/pull/3025 +- Physics kinematic synchronization: `fix/physics-kinematic-sync` -> https://github.com/galacean/engine/pull/3041 +- Physics mesh rebuild and scaled defaults: `fix/physics-mesh-defaults` -> https://github.com/galacean/engine/pull/3042 +- Audio: `fix/audio-shaderlab-split` -> https://github.com/galacean/engine/pull/3026 +- glTF Loader: `fix/gltf-loader-shaderlab-split` -> https://github.com/galacean/engine/pull/3027 +- Original combined PR #3014 was closed in favor of the scoped PRs. + +## Scope Notes + +- Animation includes Animator play-data cleanup, clip-change listener disposal, WeakMap-backed instance tracking, `_reset` cleanup guards, stale `AnimatorStateInstance` removal, and the hang regression test. +- Physics was subsequently split into a stack: #3025 owns contact-event demand and collision-normal orientation; #3041 owns target-versus-teleport kinematic synchronization, re-entry pose recovery, and CCD state; #3042 owns mesh rebuild, scaled PhysX defaults, and material/clone synchronization. +- Audio keeps pending AudioSource playback and AudioLoader fixes together. +- glTF Loader keeps schema/parser/skin parser fixes and related loader regression coverage together. + +## Verification + +- All split branches: `git diff --check origin/dev/2.0...HEAD` passed. +- Animation: `pnpm -F @galacean/engine-design run b:types`; `pnpm -F @galacean/engine-core run b:types`. +- Physics: `pnpm -F @galacean/engine-design run b:types`; `pnpm -F @galacean/engine-core run b:types`; `pnpm -F @galacean/engine-physics-lite run b:types`; `pnpm -F @galacean/engine-physics-physx run b:types`. +- Audio: `pnpm -F @galacean/engine-design run b:types`; `pnpm -F @galacean/engine-core run b:types`; `pnpm -F @galacean/engine-loader run b:types`. +- glTF Loader: `pnpm -F @galacean/engine-design run b:types`; `pnpm -F @galacean/engine-core run b:types`; `pnpm -F @galacean/engine-loader run b:types`. + +Focused Vitest commands were attempted in clean split worktrees, but Vite dependency scanning failed before executing tests because local workspace package JavaScript entries were not built. diff --git a/packages/loader/ASSET_EXTENSIONS.md b/packages/loader/ASSET_EXTENSIONS.md new file mode 100644 index 0000000000..240500c4fb --- /dev/null +++ b/packages/loader/ASSET_EXTENSIONS.md @@ -0,0 +1,103 @@ +# Loader 资产后缀名 + +> 数据来源:`packages/loader/src/*Loader.ts` 的 `@resourceLoader` 装饰器。共 25 个 loader,37 个后缀名(去重后 `mesh` 只算一个)。 + +## 分类说明 + +### 模型/场景 +3D 资源入口:场景、预制体、项目、glTF 模型、网格。 +``` +.gltf +.glb +.mesh +.prefab +.project +.scene +``` + +### 纹理 +2D 贴图、立方体贴图、压缩纹理。 +``` +.tex +.png +.jpg +.jpeg +.webp +.hdr +.ktx +.ktx2 +``` + +### 材质 +材质资源。 +``` +.mat +``` + +### 物理材质 +物理材质资源。 +``` +.physMat +``` + +### 动画 +动画片段、动画控制器。 +``` +.anim +.animCtrl +``` + +### 环境光照 +环境光贴图、渲染目标。 +``` +.ambLight +.renderTarget +``` + +### 着色器 +着色器代码。 +``` +.shader +.shaderc +``` + +### 字体 +位图字体、源字体。 +``` +.font +.ttf +.otf +.woff +``` + +### 精灵/UI +精灵图、图集。 +``` +.sprite +.atlas +``` + +### 音频 +音频文件。 +``` +.mp3 +.ogg +.wav +.m4a +.aac +.flac +``` + +### 数据/通用 +JSON、文本、二进制缓冲。 +``` +.json +.txt +.bin +``` + +## 全部(comma-separated) + +``` +.aac,.ambLight,.anim,.animCtrl,.atlas,.bin,.flac,.gltf,.glb,.hdr,.jpg,.jpeg,.json,.ktx,.ktx2,.m4a,.mat,.mesh,.mp3,.ogg,.otf,.physMat,.png,.prefab,.project,.renderTarget,.scene,.shader,.shaderc,.sprite,.tex,.ttf,.txt,.wav,.webp,.woff,.font +``` From 6eb384c8b97f3d537ea2e0c5e1f25cf4ec511d1a Mon Sep 17 00:00:00 2001 From: luzhuang Date: Sun, 12 Jul 2026 14:43:09 +0800 Subject: [PATCH 07/23] fix: support source-v2 runtime serialization --- packages/core/src/Entity.ts | 12 +++-- .../src/particle/modules/EmissionModule.ts | 14 ++++++ .../src/particle/modules/ParticleCurve.ts | 10 ++++- .../src/particle/modules/ParticleGradient.ts | 24 ++++++---- packages/galacean/src/index.ts | 5 ++- .../shader/src/ShaderLibrary/Skin/Skin.glsl | 45 +++++++++++++------ tests/src/core/Transform.test.ts | 6 +++ tests/src/core/particle/Burst.test.ts | 15 +++++++ .../particle/ParticleSerialization.test.ts | 33 ++++++++++++++ tests/src/loader/SceneFormatV2.test.ts | 10 ++++- 10 files changed, 146 insertions(+), 28 deletions(-) create mode 100644 tests/src/core/particle/ParticleSerialization.test.ts diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index eab8ce5aa2..b072fa4261 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -422,7 +422,7 @@ export class Entity extends EngineObject { */ clone(): Entity { const cloneEntity = this._createCloneEntity(); - this._parseCloneEntity(this, cloneEntity, this, cloneEntity, new Map()); + this._parseCloneEntity(this, cloneEntity, this, cloneEntity, new Map()); return cloneEntity; } @@ -477,7 +477,7 @@ export class Entity extends EngineObject { target: Entity, srcRoot: Entity, targetRoot: Entity, - deepInstanceMap: Map + deepInstanceMap: Map ): void { const srcChildren = src._children; const targetChildren = target._children; @@ -763,7 +763,13 @@ export class Entity extends EngineObject { } private _setTransform(value: Transform): void { - this._transform?.destroy(); + const previous = this._transform; + if (previous) { + value.position.copyFrom(previous.position); + value.rotationQuaternion.copyFrom(previous.rotationQuaternion); + value.scale.copyFrom(previous.scale); + previous.destroy(); + } this._transform = value; const children = this._children; for (let i = 0, n = children.length; i < n; i++) { diff --git a/packages/core/src/particle/modules/EmissionModule.ts b/packages/core/src/particle/modules/EmissionModule.ts index a2d303a7d3..3463a11f8b 100644 --- a/packages/core/src/particle/modules/EmissionModule.ts +++ b/packages/core/src/particle/modules/EmissionModule.ts @@ -97,6 +97,20 @@ export class EmissionModule extends ParticleGeneratorModule { return this._bursts; } + /** + * Replaces the burst array, sorted by emission time. + */ + set bursts(value: ReadonlyArray) { + const bursts = this._bursts; + if (value === bursts) return; + + bursts.length = 0; + for (let i = 0, n = value.length; i < n; i++) { + this.addBurst(value[i]); + } + this._resyncCursors(this._generator._playTime); + } + /** * Add a single burst. * @param burst - The burst diff --git a/packages/core/src/particle/modules/ParticleCurve.ts b/packages/core/src/particle/modules/ParticleCurve.ts index fdba01cb62..8da2cb12b5 100644 --- a/packages/core/src/particle/modules/ParticleCurve.ts +++ b/packages/core/src/particle/modules/ParticleCurve.ts @@ -22,6 +22,14 @@ export class ParticleCurve { return this._keys; } + /** + * Replaces the keys of the curve. + */ + set keys(value: ReadonlyArray) { + if (value === this._keys) return; + this.setKeys(value); + } + /** * Create a new particle curve. * @param keys - The keys of the curve @@ -79,7 +87,7 @@ export class ParticleCurve { * Set the keys of the curve. * @param keys - The keys */ - setKeys(keys: CurveKey[]): void { + setKeys(keys: ReadonlyArray): void { this._keys.length = 0; for (let i = 0, n = keys.length; i < n; i++) { this.addKey(keys[i]); diff --git a/packages/core/src/particle/modules/ParticleGradient.ts b/packages/core/src/particle/modules/ParticleGradient.ts index 732f879f5c..20d735143d 100644 --- a/packages/core/src/particle/modules/ParticleGradient.ts +++ b/packages/core/src/particle/modules/ParticleGradient.ts @@ -24,6 +24,12 @@ export class ParticleGradient { return this._colorKeys; } + /** Replaces the color keys while preserving the alpha keys. */ + set colorKeys(value: ReadonlyArray) { + if (value === this._colorKeys) return; + this.setKeys(value, [...this._alphaKeys]); + } + /** * The alpha keys of the gradient. */ @@ -31,6 +37,12 @@ export class ParticleGradient { return this._alphaKeys; } + /** Replaces the alpha keys while preserving the color keys. */ + set alphaKeys(value: ReadonlyArray) { + if (value === this._alphaKeys) return; + this.setKeys([...this._colorKeys], value); + } + /** * Create a new particle gradient. * @param colorKeys - The color keys of the gradient @@ -128,7 +140,7 @@ export class ParticleGradient { * @param colorKeys - The color keys * @param alphaKeys - The alpha keys */ - setKeys(colorKeys: GradientColorKey[], alphaKeys: GradientAlphaKey[]): void { + setKeys(colorKeys: ReadonlyArray, alphaKeys: ReadonlyArray): void { if (colorKeys.length > 4) { throw new Error("Gradient can only have 4 color keys"); } @@ -147,14 +159,8 @@ export class ParticleGradient { currentColorKeys.length = 0; currentAlphaKeys.length = 0; - for (let i = 0, n = colorKeys.length; i < n; i++) { - this._addKey(currentColorKeys, colorKeys[i]); - } - for (let i = 0, n = alphaKeys.length; i < n; i++) { - this._addKey(currentAlphaKeys, alphaKeys[i]); - } - this._alphaTypeArrayDirty = true; - this._colorTypeArrayDirty = true; + for (let i = 0, n = colorKeys.length; i < n; i++) this.addColorKey(colorKeys[i]); + for (let i = 0, n = alphaKeys.length; i < n; i++) this.addAlphaKey(alphaKeys[i]); } /** diff --git a/packages/galacean/src/index.ts b/packages/galacean/src/index.ts index fe03bb1da9..c897d7390d 100644 --- a/packages/galacean/src/index.ts +++ b/packages/galacean/src/index.ts @@ -12,9 +12,12 @@ export * from "@galacean/engine-loader"; export * from "@galacean/engine-math"; export * from "@galacean/engine-rhi-webgl"; -for (let key in CoreObjects) { +for (const key in CoreObjects) { Loader.registerClass(key, CoreObjects[key]); } +for (const key in MathObjects) { + Loader.registerClass(key, MathObjects[key]); +} for (let key in MathObjects) { Loader.registerClass(key, MathObjects[key]); diff --git a/packages/shader/src/ShaderLibrary/Skin/Skin.glsl b/packages/shader/src/ShaderLibrary/Skin/Skin.glsl index 3f8e1cb1ab..9c8dc88233 100644 --- a/packages/shader/src/ShaderLibrary/Skin/Skin.glsl +++ b/packages/shader/src/ShaderLibrary/Skin/Skin.glsl @@ -7,18 +7,37 @@ sampler2D renderer_JointSampler; float renderer_JointCount; - mat4 getJointMatrix(sampler2D smp, float index){ - float base = index / renderer_JointCount; - float hf = 0.5 / renderer_JointCount; - float v = base + hf; - - vec4 m0 = texture2D(smp, vec2(0.125, v )); - vec4 m1 = texture2D(smp, vec2(0.375, v )); - vec4 m2 = texture2D(smp, vec2(0.625, v )); - vec4 m3 = texture2D(smp, vec2(0.875, v )); - - return mat4(m0, m1, m2, m3); - } + #ifdef RENDERER_USE_BAKED_SKINNING + float renderer_BakedFrame; + float renderer_BoneCount; + + mat4 getJointMatrix(sampler2D smp, float index){ + float frameCount = renderer_JointCount / renderer_BoneCount; + float texel = 1.0 / (frameCount * 4.0); + float u = (renderer_BakedFrame * 4.0 + 0.5) * texel; + float v = (index + 0.5) / renderer_BoneCount; + + vec4 m0 = texture2D(smp, vec2(u, v)); + vec4 m1 = texture2D(smp, vec2(u + texel, v)); + vec4 m2 = texture2D(smp, vec2(u + texel * 2.0, v)); + vec4 m3 = texture2D(smp, vec2(u + texel * 3.0, v)); + + return mat4(m0, m1, m2, m3); + } + #else + mat4 getJointMatrix(sampler2D smp, float index){ + float base = index / renderer_JointCount; + float hf = 0.5 / renderer_JointCount; + float v = base + hf; + + vec4 m0 = texture2D(smp, vec2(0.125, v )); + vec4 m1 = texture2D(smp, vec2(0.375, v )); + vec4 m2 = texture2D(smp, vec2(0.625, v )); + vec4 m3 = texture2D(smp, vec2(0.875, v )); + + return mat4(m0, m1, m2, m3); + } + #endif #else mat4 renderer_JointMatrix[ RENDERER_JOINTS_NUM ]; #endif @@ -44,4 +63,4 @@ #endif -#endif \ No newline at end of file +#endif diff --git a/tests/src/core/Transform.test.ts b/tests/src/core/Transform.test.ts index 980edd3572..927d740ed9 100644 --- a/tests/src/core/Transform.test.ts +++ b/tests/src/core/Transform.test.ts @@ -116,12 +116,18 @@ describe("Transform test", function () { expect(preTransform0.destroyed).to.equal(true); expect(entity0.transform instanceof Transform).to.equal(true); expect(entity0.transform instanceof SubClassOfTransform).to.equal(true); + expect(entity0.transform.position).to.deep.include({ x: 1, y: 2, z: 3 }); + expect(entity0.transform.rotation).to.deep.include({ x: 0, y: 45, z: 0 }); + expect(entity0.transform.scale).to.deep.include({ x: 1, y: 2, z: 3 }); const preTransform1 = entity1.transform; entity1.addComponent(Transform); expect(preTransform1.destroyed).to.equal(true); expect(entity1.transform instanceof Transform).to.equal(true); expect(entity1.transform instanceof SubClassOfTransform).to.equal(false); + expect(entity1.transform.position).to.deep.include({ x: 4, y: 5, z: 6 }); + expect(entity1.transform.rotation).to.deep.include({ x: 0, y: 90, z: 0 }); + expect(entity1.transform.scale).to.deep.include({ x: 4, y: 5, z: 6 }); }); it("clone with worldMatrix listener should not produce stale parent cache after reparent", () => { diff --git a/tests/src/core/particle/Burst.test.ts b/tests/src/core/particle/Burst.test.ts index 7765171e54..5a6ff8761c 100644 --- a/tests/src/core/particle/Burst.test.ts +++ b/tests/src/core/particle/Burst.test.ts @@ -67,6 +67,21 @@ describe("Burst", () => { expect(burst.repeatInterval).to.equal(0.1); }); + it("Replace bursts through the serializable property contract", () => { + const scene = engine.sceneManager.activeScene; + const entity = scene.createRootEntity("ReplaceBursts"); + const emission = entity.addComponent(ParticleRenderer).generator.emission; + const existing = new Burst(1, new ParticleCompositeCurve(1)); + const early = new Burst(0.25, new ParticleCompositeCurve(2)); + const late = new Burst(0.75, new ParticleCompositeCurve(3)); + emission.addBurst(existing); + + emission.bursts = [late, early]; + + expect(emission.bursts).to.deep.equal([early, late]); + entity.destroy(); + }); + it("Single cycle backward compatible", () => { const scene = engine.sceneManager.activeScene; const entity = scene.createRootEntity("SingleCycle"); diff --git a/tests/src/core/particle/ParticleSerialization.test.ts b/tests/src/core/particle/ParticleSerialization.test.ts new file mode 100644 index 0000000000..f47f0ac793 --- /dev/null +++ b/tests/src/core/particle/ParticleSerialization.test.ts @@ -0,0 +1,33 @@ +import { + CurveKey, + GradientAlphaKey, + GradientColorKey, + ParticleCurve, + ParticleGradient +} from "@galacean/engine-core"; +import { Color } from "@galacean/engine-math"; +import { describe, expect, it } from "vitest"; + +describe("particle serializable collection properties", () => { + it("replaces and sorts ParticleCurve keys", () => { + const curve = new ParticleCurve(new CurveKey(0.5, 1)); + const early = new CurveKey(0.25, 2); + const late = new CurveKey(0.75, 3); + + curve.keys = [late, early]; + + expect(curve.keys).to.deep.equal([early, late]); + }); + + it("replaces ParticleGradient color and alpha keys independently", () => { + const gradient = new ParticleGradient(); + const colorKey = new GradientColorKey(0.5, new Color(1, 0.5, 0.25, 1)); + const alphaKey = new GradientAlphaKey(0.5, 0.75); + + gradient.colorKeys = [colorKey]; + gradient.alphaKeys = [alphaKey]; + + expect(gradient.colorKeys).to.deep.equal([colorKey]); + expect(gradient.alphaKeys).to.deep.equal([alphaKey]); + }); +}); diff --git a/tests/src/loader/SceneFormatV2.test.ts b/tests/src/loader/SceneFormatV2.test.ts index ceb4ce57ec..b6b7296199 100644 --- a/tests/src/loader/SceneFormatV2.test.ts +++ b/tests/src/loader/SceneFormatV2.test.ts @@ -25,7 +25,7 @@ import { ReflectionParser } from "../../../packages/loader/src/resource-deserial import { SceneParser } from "../../../packages/loader/src/resource-deserialize/resources/scene/SceneParser"; import { GLTFResource } from "../../../packages/loader/src/gltf/GLTFResource"; import { WebGLEngine } from "@galacean/engine"; -import { Vector2 } from "@galacean/engine-math"; +import { Color, Quaternion, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; Loader.registerClass("Transform", Transform); @@ -81,6 +81,14 @@ describe("SceneFile v2 schema enums", () => { }); }); +describe("Galacean flavor class registration", () => { + it("registers public math value types for source-v2 $type values", () => { + for (const [name, Class] of Object.entries({ Color, Quaternion, Vector2, Vector3, Vector4 })) { + expect(Loader.getClass(name)).to.equal(Class); + } + }); +}); + beforeAll(async function () { const canvasDOM = document.createElement("canvas"); canvasDOM.width = 100; From 80c4bce520a5d67c00b44fc7d4228a5e9a72645c Mon Sep 17 00:00:00 2001 From: luzhuang Date: Wed, 17 Jun 2026 20:23:10 +0800 Subject: [PATCH 08/23] fix(physics): gate contact event buffering --- packages/core/src/Script.ts | 15 +++ packages/core/src/physics/Collision.ts | 3 +- packages/core/src/physics/PhysicsScene.ts | 53 ++++++++ packages/design/src/physics/IPhysicsScene.ts | 6 + .../physics-physx/src/PhysXPhysicsScene.ts | 17 +++ tests/src/core/physics/Collision.test.ts | 28 ++++- tests/src/core/physics/PhysicsScene.test.ts | 116 +++++++++++++++++- 7 files changed, 234 insertions(+), 4 deletions(-) diff --git a/packages/core/src/Script.ts b/packages/core/src/Script.ts index 997901573d..5d96703d53 100644 --- a/packages/core/src/Script.ts +++ b/packages/core/src/Script.ts @@ -229,6 +229,9 @@ export class Script extends Component { } this._entity._addScript(this); + if (this._hasCollisionEventCallbacks()) { + this.scene.physics._markCollisionEventConsumersDirty(); + } } /** @@ -252,6 +255,18 @@ export class Script extends Component { } this._entity._removeScript(this); + if (this._hasCollisionEventCallbacks()) { + this.scene.physics._markCollisionEventConsumersDirty(); + } + } + + private _hasCollisionEventCallbacks(): boolean { + const { prototype } = Script; + return ( + this.onCollisionEnter !== prototype.onCollisionEnter || + this.onCollisionExit !== prototype.onCollisionExit || + this.onCollisionStay !== prototype.onCollisionStay + ); } /** diff --git a/packages/core/src/physics/Collision.ts b/packages/core/src/physics/Collision.ts index 16edfd5bb3..440864e4fe 100644 --- a/packages/core/src/physics/Collision.ts +++ b/packages/core/src/physics/Collision.ts @@ -29,8 +29,7 @@ export class Collision { */ getContacts(outContacts: ContactPoint[]): number { const nativeCollision = this._nativeCollision; - const smallerShapeId = Math.min(nativeCollision.shape0Id, nativeCollision.shape1Id); - const factor = this.shape.id === smallerShapeId ? 1 : -1; + const factor = this.shape.id === nativeCollision.shape1Id ? 1 : -1; const nativeContactPoints = nativeCollision.getContacts(); const length = nativeContactPoints.size(); for (let i = 0; i < length; i++) { diff --git a/packages/core/src/physics/PhysicsScene.ts b/packages/core/src/physics/PhysicsScene.ts index 1357b8ba90..806146d55b 100644 --- a/packages/core/src/physics/PhysicsScene.ts +++ b/packages/core/src/physics/PhysicsScene.ts @@ -30,6 +30,9 @@ export class PhysicsScene { private _gravity: Vector3 = new Vector3(0, -9.81, 0); private _nativePhysicsScene: IPhysicsScene; + private _collisionEventConsumersDirty = true; + private _hasCollisionEventConsumersCache = false; + private _contactEventEnabled: boolean | undefined; /** * The gravity of physics scene. @@ -658,6 +661,7 @@ export class PhysicsScene { for (let i = 0; i < step; i++) { componentsManager.callScriptOnPhysicsUpdate(); this._callColliderOnUpdate(); + this._syncContactEventDemand(); nativePhysicsManager.update(fixedTimeStep); this._callColliderOnLateUpdate(); this._dispatchEvents(nativePhysicsManager.updateEvents()); @@ -673,6 +677,7 @@ export class PhysicsScene { if (collider._index === -1) { collider._index = this._colliders.length; this._colliders.add(collider); + this._markCollisionEventConsumersDirty(); } this._nativePhysicsScene.addCollider(collider._nativeCollider); } @@ -686,6 +691,7 @@ export class PhysicsScene { if (controller._index === -1) { controller._index = this._colliders.length; this._colliders.add(controller); + this._markCollisionEventConsumersDirty(); } this._nativePhysicsScene.addCharacterController(controller._nativeCollider); } @@ -699,6 +705,7 @@ export class PhysicsScene { const replaced = this._colliders.deleteByIndex(collider._index); replaced && (replaced._index = collider._index); collider._index = -1; + this._markCollisionEventConsumersDirty(); this._nativePhysicsScene.removeCollider(collider._nativeCollider); } @@ -711,9 +718,17 @@ export class PhysicsScene { const replaced = this._colliders.deleteByIndex(controller._index); replaced && (replaced._index = controller._index); controller._index = -1; + this._markCollisionEventConsumersDirty(); this._nativePhysicsScene.removeCharacterController(controller._nativeCollider); } + /** + * @internal + */ + _markCollisionEventConsumersDirty(): void { + this._collisionEventConsumersDirty = true; + } + /** * @internal */ @@ -837,6 +852,44 @@ export class PhysicsScene { } } + private _hasCollisionEventConsumers(): boolean { + if (!this._collisionEventConsumersDirty) { + return this._hasCollisionEventConsumersCache; + } + + const { _elements: colliders } = this._colliders; + const { onCollisionEnter, onCollisionExit, onCollisionStay } = Script.prototype; + + for (let i = this._colliders.length - 1; i >= 0; --i) { + const scripts = colliders[i].entity._scripts; + const scriptElements = scripts._elements; + for (let j = scripts.length - 1; j >= 0; --j) { + const script = scriptElements[j]; + if ( + script.onCollisionEnter !== onCollisionEnter || + script.onCollisionExit !== onCollisionExit || + script.onCollisionStay !== onCollisionStay + ) { + this._collisionEventConsumersDirty = false; + this._hasCollisionEventConsumersCache = true; + return true; + } + } + } + + this._collisionEventConsumersDirty = false; + this._hasCollisionEventConsumersCache = false; + return this._hasCollisionEventConsumersCache; + } + + private _syncContactEventDemand(): void { + const enabled = this._hasCollisionEventConsumers(); + if (this._contactEventEnabled !== enabled) { + this._nativePhysicsScene.setContactEventEnabled?.(enabled); + this._contactEventEnabled = enabled; + } + } + private _setGravity(): void { this._nativePhysicsScene.setGravity(this._gravity); } diff --git a/packages/design/src/physics/IPhysicsScene.ts b/packages/design/src/physics/IPhysicsScene.ts index 5520114104..51bba67a57 100644 --- a/packages/design/src/physics/IPhysicsScene.ts +++ b/packages/design/src/physics/IPhysicsScene.ts @@ -43,6 +43,12 @@ export interface IPhysicsScene { */ update(elapsedTime: number): void; + /** + * Enable contact event buffering. + * @param enabled - Whether collision contact events should be buffered for dispatch. + */ + setContactEventEnabled?(enabled: boolean): void; + /** * Collect buffered collision and trigger events. * Must be called after update() and after syncing transforms back from physics. diff --git a/packages/physics-physx/src/PhysXPhysicsScene.ts b/packages/physics-physx/src/PhysXPhysicsScene.ts index d026304e4d..bee2ab2dc2 100644 --- a/packages/physics-physx/src/PhysXPhysicsScene.ts +++ b/packages/physics-physx/src/PhysXPhysicsScene.ts @@ -50,6 +50,7 @@ export class PhysXPhysicsScene implements IPhysicsScene { private _activeTriggers: DisorderedArray = new DisorderedArray(); private _contactEvents: ContactEvent[] = []; private _contactEventCount = 0; + private _contactEventEnabled = true; private _triggerEvents: TriggerEvent[] = []; private _physicsEvents: IPhysicsEvents = { contactEvents: [], contactEventCount: 0, triggerEvents: [] }; @@ -191,6 +192,20 @@ export class PhysXPhysicsScene implements IPhysicsScene { this._fetchResults(); } + /** + * {@inheritDoc IPhysicsScene.setContactEventEnabled } + */ + setContactEventEnabled(enabled: boolean): void { + if (this._contactEventEnabled === enabled) { + return; + } + + this._contactEventEnabled = enabled; + if (!enabled) { + this._contactEventCount = 0; + } + } + /** * {@inheritDoc IPhysicsScene.updateEvents } */ @@ -547,6 +562,8 @@ export class PhysXPhysicsScene implements IPhysicsScene { } private _bufferContactEvent(collision: ICollision, state: number): void { + if (!this._contactEventEnabled) return; + const index = this._contactEventCount++; const event = (this._contactEvents[index] ||= new ContactEvent()); event.shape0Id = collision.shape0Id; diff --git a/tests/src/core/physics/Collision.test.ts b/tests/src/core/physics/Collision.test.ts index 54b8f3809c..8fc27ef8b9 100644 --- a/tests/src/core/physics/Collision.test.ts +++ b/tests/src/core/physics/Collision.test.ts @@ -1,7 +1,7 @@ import { BoxColliderShape, DynamicCollider, Entity, Engine, Script, StaticCollider } from "@galacean/engine-core"; import { Vector3 } from "@galacean/engine-math"; import { PhysXPhysics } from "@galacean/engine-physics-physx"; -import { WebGLEngine } from "@galacean/engine"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; import { Collision } from "packages/core/types/physics/Collision"; import { describe, beforeAll, beforeEach, expect, it } from "vitest"; @@ -164,4 +164,30 @@ describe("Collision", function () { engine.sceneManager.activeScene.physics._update(1); }); }); + + it("reports contact normal from static other shape to dynamic self shape", function () { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const dynamicBox = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const staticBox = addBox(new Vector3(1, 1, 1), StaticCollider, new Vector3(0, 0, 0)); + + return new Promise((done) => { + dynamicBox.addComponent( + class extends Script { + onCollisionEnter(other: Collision): void { + expect(other.shape).toBe(staticBox.getComponent(StaticCollider).shapes[0]); + const contacts = []; + other.getContacts(contacts); + expect(contacts.length).toBeGreaterThan(0); + expect(formatValue(contacts[0].normal.x)).toBe(-1); + + done(); + } + } + ); + + dynamicBox.getComponent(DynamicCollider).applyForce(new Vector3(1000, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1); + }); + }); }); diff --git a/tests/src/core/physics/PhysicsScene.test.ts b/tests/src/core/physics/PhysicsScene.test.ts index a93028a1e6..b1758c7cdf 100644 --- a/tests/src/core/physics/PhysicsScene.test.ts +++ b/tests/src/core/physics/PhysicsScene.test.ts @@ -17,7 +17,7 @@ import { } from "@galacean/engine-core"; import { Ray, Vector3, Quaternion } from "@galacean/engine-math"; import { PhysXPhysics } from "@galacean/engine-physics-physx"; -import { WebGLEngine } from "@galacean/engine"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; import { vi, describe, beforeAll, expect, it, afterEach } from "vitest"; class CollisionTestScript extends Script { @@ -62,12 +62,44 @@ class CollisionTestScript extends Script { } } +class CollisionDemandScript extends Script { + onCollisionEnter(): void {} +} + +class TriggerDemandScript extends Script { + onTriggerEnter(): void {} +} + function updatePhysics(physics) { for (let i = 0; i < 5; ++i) { physics._update(8); } } +function watchNativeContactEventDemand(physicsScene: PhysicsScene) { + const nativeScene = (physicsScene as any)._nativePhysicsScene; + const original = nativeScene.setContactEventEnabled; + const calls: boolean[] = []; + nativeScene.setContactEventEnabled = (enabled: boolean) => { + calls.push(enabled); + original?.call(nativeScene, enabled); + }; + return { + calls, + restore() { + if (original) { + nativeScene.setContactEventEnabled = original; + } else { + delete nativeScene.setContactEventEnabled; + } + } + }; +} + +function getLastContactEventDemandCall(calls: boolean[]): boolean { + return calls[calls.length - 1] ?? false; +} + function resetSpy() { // reset spy on collision test script. CollisionTestScript.prototype.onCollisionEnter = vi.fn(CollisionTestScript.prototype.onCollisionEnter); @@ -166,6 +198,88 @@ describe("Physics Test", () => { } }); + it("auto-disables native contact events when no active collision callback exists", () => { + const scene = enginePhysX.sceneManager.activeScene; + const physicsScene = scene.physics; + const root = scene.createRootEntity("contact-demand-disabled"); + const entity = root.createChild("body"); + const collider = entity.addComponent(StaticCollider); + collider.addShape(new BoxColliderShape()); + const contactEventDemand = watchNativeContactEventDemand(physicsScene); + + try { + physicsScene._update(physicsScene.fixedTimeStep); + expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false); + } finally { + contactEventDemand.restore(); + root.destroy(); + } + }); + + it("auto-enables native contact events only while an active collision callback exists", () => { + const scene = enginePhysX.sceneManager.activeScene; + const physicsScene = scene.physics; + const root = scene.createRootEntity("contact-demand-enabled"); + const entity = root.createChild("body"); + const collider = entity.addComponent(StaticCollider); + collider.addShape(new BoxColliderShape()); + const script = entity.addComponent(CollisionDemandScript); + const contactEventDemand = watchNativeContactEventDemand(physicsScene); + + try { + physicsScene._update(physicsScene.fixedTimeStep); + expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(true); + + script.enabled = false; + physicsScene._update(physicsScene.fixedTimeStep); + expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false); + } finally { + contactEventDemand.restore(); + root.destroy(); + } + }); + + it("does not rescan contact event demand on every fixed substep", () => { + const scene = enginePhysX.sceneManager.activeScene; + const physicsScene = scene.physics; + const fixedTimeStep = physicsScene.fixedTimeStep; + const root = scene.createRootEntity("contact-demand-substeps"); + const entity = root.createChild("body"); + const collider = entity.addComponent(StaticCollider); + collider.addShape(new BoxColliderShape()); + entity.addComponent(CollisionDemandScript); + const contactEventDemand = watchNativeContactEventDemand(physicsScene); + + try { + physicsScene.fixedTimeStep = 1 / 480; + physicsScene._update(1 / 60); + expect(contactEventDemand.calls).to.deep.eq([true]); + } finally { + physicsScene.fixedTimeStep = fixedTimeStep; + contactEventDemand.restore(); + root.destroy(); + } + }); + + it("keeps native contact events disabled when only trigger callbacks exist", () => { + const scene = enginePhysX.sceneManager.activeScene; + const physicsScene = scene.physics; + const root = scene.createRootEntity("contact-demand-trigger-only"); + const entity = root.createChild("body"); + const collider = entity.addComponent(StaticCollider); + collider.addShape(new BoxColliderShape()); + entity.addComponent(TriggerDemandScript); + const contactEventDemand = watchNativeContactEventDemand(physicsScene); + + try { + physicsScene._update(physicsScene.fixedTimeStep); + expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false); + } finally { + contactEventDemand.restore(); + root.destroy(); + } + }); + it("raycast", () => { const scene = enginePhysX.sceneManager.activeScene; const physicsScene = scene.physics; From 5391644940c69b8712e08f9b58943e5b83c7876d Mon Sep 17 00:00:00 2001 From: luzhuang Date: Wed, 17 Jun 2026 20:25:36 +0800 Subject: [PATCH 09/23] fix(physics): preserve kinematic sync semantics --- .../core/src/physics/CharacterController.ts | 6 +- packages/core/src/physics/Collider.ts | 57 ++++- packages/core/src/physics/DynamicCollider.ts | 53 +++++ packages/core/src/physics/index.ts | 7 +- .../physics-physx/src/PhysXDynamicCollider.ts | 104 +++++++-- tests/src/core/physics/Collision.test.ts | 212 +++++++++++++++++- .../src/core/physics/DynamicCollider.test.ts | 157 +++++++++++++ tests/src/core/physics/PhysicsScene.test.ts | 18 +- 8 files changed, 573 insertions(+), 41 deletions(-) diff --git a/packages/core/src/physics/CharacterController.ts b/packages/core/src/physics/CharacterController.ts index f3c9e6f048..ea6961b70f 100644 --- a/packages/core/src/physics/CharacterController.ts +++ b/packages/core/src/physics/CharacterController.ts @@ -1,5 +1,5 @@ import { ICharacterController } from "@galacean/engine-design"; -import { Vector3 } from "@galacean/engine-math"; +import { Quaternion, Vector3 } from "@galacean/engine-math"; import { Engine } from "../Engine"; import { Entity } from "../Entity"; import { Collider } from "./Collider"; @@ -162,6 +162,10 @@ export class CharacterController extends Collider { (this._nativeCollider).setSlopeLimit(this._slopeLimit); } + protected override _teleportToEntityTransform(worldPosition: Vector3, _worldRotation: Quaternion): void { + (this._nativeCollider).setWorldPosition(worldPosition); + } + private _syncWorldPositionFromPhysicalSpace(): void { (this._nativeCollider).getWorldPosition(this.entity.transform.worldPosition); } diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index e132b95e1e..ce7f56f2f7 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -1,4 +1,5 @@ import { ICollider, IStaticCollider } from "@galacean/engine-design"; +import { Quaternion, Vector3 } from "@galacean/engine-math"; import { BoolUpdateFlag } from "../BoolUpdateFlag"; import { deepClone, ignoreClone } from "../clone/CloneManager"; import { ICustomClone } from "../clone/ComponentCloner"; @@ -28,6 +29,17 @@ export class Collider extends Component implements ICustomClone { protected _shapes: ColliderShape[] = []; protected _collisionLayerIndex: number = 0; + /** + * A collider must teleport on the next transform sync when its native actor + * already exists at a stale pose, such as after re-entering the scene or after + * clone-time native reconstruction. Ordinary first entry can use subclass sync + * semantics so kinematic actors still use setKinematicTarget. + */ + @ignoreClone + private _pendingReenterTeleport: boolean = false; + @ignoreClone + private _enteredScene: boolean = false; + /** * The shapes of this collider. */ @@ -108,15 +120,17 @@ export class Collider extends Component implements ICustomClone { * @internal */ _onUpdate(): void { - if (this._updateFlag.flag) { + const shapes = this._shapes; + if (this._pendingReenterTeleport || this._updateFlag.flag) { const { transform } = this.entity; - (this._nativeCollider).setWorldTransform( - transform.worldPosition, - transform.worldRotationQuaternion - ); + if (this._pendingReenterTeleport) { + this._teleportToEntityTransform(transform.worldPosition, transform.worldRotationQuaternion); + this._pendingReenterTeleport = false; + } else { + this._syncEntityTransformToNative(transform.worldPosition, transform.worldRotationQuaternion); + } const worldScale = transform.lossyWorldScale; - const shapes = this._shapes; for (let i = 0, n = shapes.length; i < n; i++) { shapes[i]._nativeShape?.setWorldScale(worldScale); } @@ -134,6 +148,10 @@ export class Collider extends Component implements ICustomClone { */ override _onEnableInScene(): void { this.scene.physics._addCollider(this); + if (this._enteredScene) { + this._pendingReenterTeleport = true; + } + this._enteredScene = true; } /** @@ -148,6 +166,7 @@ export class Collider extends Component implements ICustomClone { */ _cloneTo(target: Collider): void { target._syncNative(); + target._pendingReenterTeleport = true; } /** @@ -164,6 +183,32 @@ export class Collider extends Component implements ICustomClone { this._addNativeShape(this.shapes[i]); } this._setCollisionLayer(); + // Teleport native actor to entity's current world pose. + // The native actor was created in constructor() with the entity's then-current + // worldPosition/Rotation. On clone, the entity's transform fields are deep-cloned + // AFTER the Component (and its native actor) are constructed, so the native actor's + // pose lags behind the cloned entity transform until this sync. + const { transform } = this.entity; + this._teleportToEntityTransform(transform.worldPosition, transform.worldRotationQuaternion); + } + + /** + * Teleport native actor to a world pose (instant, no implied velocity). + * Used during initialization paths (clone) where the native actor must be re-aligned + * with the entity transform after construction-time pose was based on stale defaults. + */ + protected _teleportToEntityTransform(worldPosition: Vector3, worldRotation: Quaternion): void { + (this._nativeCollider).setWorldTransform(worldPosition, worldRotation); + } + + /** + * Sync entity world transform to native actor for per-frame updates. + * Default semantics: teleport (setGlobalPose). Subclasses override to express + * physics-aware movement (e.g. DynamicCollider routes kinematic actors through + * setKinematicTarget to generate contact events on swept motion). + */ + protected _syncEntityTransformToNative(worldPosition: Vector3, worldRotation: Quaternion): void { + (this._nativeCollider).setWorldTransform(worldPosition, worldRotation); } /** diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 16fec72cc7..15c12dbd2d 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -33,6 +33,8 @@ export class DynamicCollider extends Collider { private _isKinematic = false; private _constraints: DynamicColliderConstraints = 0; private _collisionDetectionMode: CollisionDetectionMode = CollisionDetectionMode.Discrete; + private _kinematicTransformSyncMode: DynamicColliderKinematicTransformSyncMode = + DynamicColliderKinematicTransformSyncMode.Target; private _sleepThreshold: number | undefined; private _automaticCenterOfMass = true; private _automaticInertiaTensor = true; @@ -325,6 +327,22 @@ export class DynamicCollider extends Collider { } } + /** + * Controls how entity transform changes are synchronized to a kinematic native actor. + * + * @remarks + * `Target` routes transform changes through {@link move}, so PhysX treats the + * actor as moving between frames and can generate swept contacts. `Teleport` + * writes the native pose directly and does not imply velocity. + */ + get kinematicTransformSyncMode(): DynamicColliderKinematicTransformSyncMode { + return this._kinematicTransformSyncMode; + } + + set kinematicTransformSyncMode(value: DynamicColliderKinematicTransformSyncMode) { + this._kinematicTransformSyncMode = value; + } + /** * @internal */ @@ -442,6 +460,30 @@ export class DynamicCollider extends Collider { super.addShape(shape); } + /** + * Route per-frame entity → native transform sync to the correct physics API based + * on kinematic state. + * + * PhysX 4.x docs (PxRigidDynamic): + * "If you intend to move a kinematic actor with [setGlobalPose] and want + * collision detection, use setKinematicTarget() instead." + * + * setGlobalPose is a teleport: PhysX skips contact detection between the old + * and new pose. setKinematicTarget tells PhysX the actor is animating to the + * target during the next simulate(), enabling swept contacts. Some compatibility + * layers need transform writes to stay teleport-like, so the sync mode is + * explicit while {@link move} always keeps target semantics. + * + * @internal + */ + protected override _syncEntityTransformToNative(worldPosition: Vector3, worldRotation: Quaternion): void { + if (this._isKinematic && this._kinematicTransformSyncMode === DynamicColliderKinematicTransformSyncMode.Target) { + (this._nativeCollider).move(worldPosition, worldRotation); + } else { + super._syncEntityTransformToNative(worldPosition, worldRotation); + } + } + /** * @internal */ @@ -473,6 +515,7 @@ export class DynamicCollider extends Collider { if (!this._automaticInertiaTensor) { target._inertiaTensor.copyFrom(this.inertiaTensor); } + target._kinematicTransformSyncMode = this._kinematicTransformSyncMode; super._cloneTo(target); } @@ -570,6 +613,16 @@ export enum CollisionDetectionMode { ContinuousSpeculative } +/** + * Kinematic transform synchronization mode. + */ +export enum DynamicColliderKinematicTransformSyncMode { + /** Synchronize transform changes through PhysX setKinematicTarget. */ + Target, + /** Synchronize transform changes by directly teleporting the native actor. */ + Teleport +} + /** * Use these flags to constrain motion of dynamic collider. */ diff --git a/packages/core/src/physics/index.ts b/packages/core/src/physics/index.ts index 537bd367d6..f2d4cdf576 100644 --- a/packages/core/src/physics/index.ts +++ b/packages/core/src/physics/index.ts @@ -1,6 +1,11 @@ export { CharacterController } from "./CharacterController"; export { Collider } from "./Collider"; -export { CollisionDetectionMode, DynamicCollider, DynamicColliderConstraints } from "./DynamicCollider"; +export { + CollisionDetectionMode, + DynamicCollider, + DynamicColliderConstraints, + DynamicColliderKinematicTransformSyncMode +} from "./DynamicCollider"; export { HitResult } from "./HitResult"; export { PhysicsMaterial } from "./PhysicsMaterial"; export { PhysicsScene } from "./PhysicsScene"; diff --git a/packages/physics-physx/src/PhysXDynamicCollider.ts b/packages/physics-physx/src/PhysXDynamicCollider.ts index 207795ec32..0d9364723b 100644 --- a/packages/physics-physx/src/PhysXDynamicCollider.ts +++ b/packages/physics-physx/src/PhysXDynamicCollider.ts @@ -24,6 +24,20 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli private static _tempTranslation = new Vector3(); private static _tempRotation = new Quaternion(); + /** + * Whether actor is currently kinematic. + * PhysX 拒绝在 kinematic actor 上启用 CCD(会打印警告并忽略), + * 所以 setCollisionDetectionMode 在 kinematic 状态下只缓存目标值, + * 等切回 dynamic 时再真正写到 PhysX。 + */ + private _isKinematic: boolean = false; + + /** + * Cached collision detection mode. Always reflects user's intent. + * 实际 PhysX CCD flag 可能跟这个不一致(kinematic 时强制 Discrete)。 + */ + private _collisionDetectionMode: number = CollisionDetectionMode.Discrete; + constructor(physXPhysics: PhysXPhysics, position: Vector3, rotation: Quaternion) { super(physXPhysics); const transform = this._transform(position, rotation); @@ -143,7 +157,7 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli /** * {@inheritDoc IDynamicCollider.setSleepThreshold } - * @default 1e-5f * PxTolerancesScale::speed * PxTolerancesScale::speed + * @default 5e-5f * PxTolerancesScale::speed * PxTolerancesScale::speed */ setSleepThreshold(value: number): void { this._pxActor.setSleepThreshold(value); @@ -158,10 +172,52 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli /** * {@inheritDoc IDynamicCollider.setCollisionDetectionMode } + * + * PhysX 在 kinematic actor 上调用 setRigidBodyFlag(eENABLE_CCD, true) 会触发警告: + * "kinematic bodies with CCD enabled are not supported! CCD will be ignored" + * 虽然 PhysX 会忽略这次调用而非真的拒绝(切回 dynamic 时 flag 不会自动恢复), + * 但每次 setIsKinematic 切换都会让这个 warning 重复打印,污染日志, + * 同时让 actor 在 dynamic 状态下 CCD flag 状态不确定。 + * + * 解决: 只在 dynamic 状态时立即 apply CCD flags。kinematic 时仅缓存到 + * `_collisionDetectionMode`,等切回 dynamic 时由 setIsKinematic 重新 apply。 */ setCollisionDetectionMode(value: number): void { + this._collisionDetectionMode = value; + if (!this._isKinematic) { + this._applyCollisionDetectionFlags(value); + } + } + + /** + * {@inheritDoc IDynamicCollider.setUseGravity } + */ + setUseGravity(value: boolean): void { + this._pxActor.setActorFlag(this._physXPhysics._physX.PxActorFlag.eDISABLE_GRAVITY, !value); + } + + /** + * {@inheritDoc IDynamicCollider.setIsKinematic } + * + * 切换 kinematic 状态时同步处理 CCD flag: + * - 切到 kinematic 前先关 CCD(避免 PhysX 警告 + 让状态显式) + * - 切回 dynamic 后恢复用户期望的 CCD mode(来自 `_collisionDetectionMode` 缓存) + */ + setIsKinematic(value: boolean): void { + if (this._isKinematic === value) return; const physX = this._physXPhysics._physX; + if (value) { + this._applyCollisionDetectionFlags(CollisionDetectionMode.Discrete); + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eKINEMATIC, true); + } else { + this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eKINEMATIC, false); + this._applyCollisionDetectionFlags(this._collisionDetectionMode); + } + this._isKinematic = value; + } + private _applyCollisionDetectionFlags(value: number): void { + const physX = this._physXPhysics._physX; switch (value) { case CollisionDetectionMode.Continuous: this._pxActor.setRigidBodyFlag(physX.PxRigidBodyFlag.eENABLE_CCD, true); @@ -186,24 +242,6 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli } } - /** - * {@inheritDoc IDynamicCollider.setUseGravity } - */ - setUseGravity(value: boolean): void { - this._pxActor.setActorFlag(this._physXPhysics._physX.PxActorFlag.eDISABLE_GRAVITY, !value); - } - - /** - * {@inheritDoc IDynamicCollider.setIsKinematic } - */ - setIsKinematic(value: boolean): void { - if (value) { - this._pxActor.setRigidBodyFlag(this._physXPhysics._physX.PxRigidBodyFlag.eKINEMATIC, true); - } else { - this._pxActor.setRigidBodyFlag(this._physXPhysics._physX.PxRigidBodyFlag.eKINEMATIC, false); - } - } - /** * {@inheritDoc IDynamicCollider.setConstraints } */ @@ -213,8 +251,15 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli /** * {@inheritDoc IDynamicCollider.addForce } + * + * PhysX 在 kinematic actor 上调 addForce 是 no-op(doc: "kinematic bodies don't + * respond to forces")。提前 return 避免无意义的 wasm boundary cross。 + * + * Sleeping actor 不需要显式 wakeUp — wasm binding 调用 `addForce(force, eFORCE, + * autowake=true)`,PhysX 自动唤醒(已通过 `applyForce on sleeping actor` 测试验证)。 */ addForce(force: Vector3) { + if (this._isKinematic) return; this._pxActor.addForce(force); } @@ -227,27 +272,38 @@ export class PhysXDynamicCollider extends PhysXCollider implements IDynamicColli /** * {@inheritDoc IDynamicCollider.addTorque } + * + * 同 addForce — kinematic 提前 return,sleeping 由 PhysX autowake 自动处理。 */ addTorque(torque: Vector3) { + if (this._isKinematic) return; this._pxActor.addTorque(torque); } /** * {@inheritDoc IDynamicCollider.move } + * + * PhysX 要求 setKinematicTarget 的 rotation 是 normalized quaternion,否则会触发 + * 内部 assertion / 警告,并把 actor 转到错误的姿态。所以在写入 wasm 边界前统一 normalize。 */ move(positionOrRotation: Vector3 | Quaternion, rotation?: Quaternion): void { + const tempTranslation = PhysXDynamicCollider._tempTranslation; + const tempRotation = PhysXDynamicCollider._tempRotation; + if (rotation) { - this._pxActor.setKinematicTarget(positionOrRotation, rotation); + tempRotation.copyFrom(rotation).normalize(); + this._pxActor.setKinematicTarget(positionOrRotation, tempRotation); return; } - const tempTranslation = PhysXDynamicCollider._tempTranslation; - const tempRotation = PhysXDynamicCollider._tempRotation; - this.getWorldTransform(tempTranslation, tempRotation); if (positionOrRotation instanceof Vector3) { + this.getWorldTransform(tempTranslation, tempRotation); + // current rotation read from PhysX is already normalized; no extra work needed this._pxActor.setKinematicTarget(positionOrRotation, tempRotation); } else { - this._pxActor.setKinematicTarget(tempTranslation, positionOrRotation); + this.getWorldTransform(tempTranslation, tempRotation); + tempRotation.copyFrom(positionOrRotation).normalize(); + this._pxActor.setKinematicTarget(tempTranslation, tempRotation); } } diff --git a/tests/src/core/physics/Collision.test.ts b/tests/src/core/physics/Collision.test.ts index 8fc27ef8b9..812821639c 100644 --- a/tests/src/core/physics/Collision.test.ts +++ b/tests/src/core/physics/Collision.test.ts @@ -1,4 +1,13 @@ -import { BoxColliderShape, DynamicCollider, Entity, Engine, Script, StaticCollider } from "@galacean/engine-core"; +import { + BoxColliderShape, + DynamicCollider, + DynamicColliderConstraints, + Entity, + Engine, + Script, + SphereColliderShape, + StaticCollider +} from "@galacean/engine-core"; import { Vector3 } from "@galacean/engine-math"; import { PhysXPhysics } from "@galacean/engine-physics-physx"; import { WebGLEngine } from "@galacean/engine-rhi-webgl"; @@ -22,6 +31,20 @@ describe("Collision", function () { return boxEntity; } + function addSphere(radius: number, pos: Vector3) { + const sphereEntity = rootEntity.createChild("SphereEntity"); + sphereEntity.transform.setPosition(pos.x, pos.y, pos.z); + + const sphereShape = new SphereColliderShape(); + sphereShape.material.dynamicFriction = 0; + sphereShape.material.staticFriction = 0; + sphereShape.radius = radius; + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.addShape(sphereShape); + sphereCollider.useGravity = false; + return sphereEntity; + } + function formatValue(value: number) { return Math.round(value * 100000) / 100000; } @@ -190,4 +213,191 @@ describe("Collision", function () { engine.sceneManager.activeScene.physics._update(1); }); }); + + it("reports billiard hitBall sphere normal from kinematic other to dynamic target self", function () { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const targetBall = addSphere(0.5, new Vector3(0, 0, 0)); + const hitBall = addSphere(0.5, new Vector3(-3, 0, 0)); + const hitCollider = hitBall.getComponent(DynamicCollider); + hitCollider.isKinematic = true; + + return new Promise((done) => { + targetBall.addComponent( + class extends Script { + onCollisionEnter(other: Collision): void { + expect(other.shape).toBe(hitCollider.shapes[0]); + const contacts = []; + other.getContacts(contacts); + expect(contacts.length).toBeGreaterThan(0); + + const contactNormal = contacts[0].normal; + expect(formatValue(contactNormal.x)).toBe(1); + expect(formatValue(contactNormal.y)).toBe(0); + expect(formatValue(contactNormal.z)).toBe(0); + + const hitToTarget = new Vector3(); + Vector3.subtract(targetBall.transform.worldPosition, hitBall.transform.worldPosition, hitToTarget); + hitToTarget.normalize(); + expect(formatValue(Vector3.dot(contactNormal, hitToTarget))).toBe(1); + + const scaledNormal = new Vector3(); + Vector3.scale(contactNormal, 2 * Vector3.dot(hitToTarget, contactNormal), scaledNormal); + const reflected = new Vector3(); + Vector3.subtract(hitToTarget, scaledNormal, reflected); + reflected.normalize(); + expect(formatValue(reflected.x)).toBe(-1); + + done(); + } + } + ); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + hitCollider.move(new Vector3(-0.9, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + }); + }); + + function probeKinematicCallback(opts: { + aKine: boolean; + bKine: boolean; + timeoutMs?: number; + }): Promise<{ fired: boolean }> { + return new Promise((resolve) => { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const boxA = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const boxB = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(3, 0, 0)); + const colA = boxA.getComponent(DynamicCollider); + const colB = boxB.getComponent(DynamicCollider); + colA.useGravity = false; + colB.useGravity = false; + colA.isKinematic = opts.aKine; + colB.isKinematic = opts.bKine; + + let fired = false; + boxA.addComponent( + class extends Script { + onCollisionEnter(_other: Collision): void { + fired = true; + resolve({ fired: true }); + } + } + ); + + // Step a few frames to let PhysX settle initial state. + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // Teleport B onto A → expect onCollisionEnter. + boxB.transform.setPosition(-3, 0, 0); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + if (!fired) resolve({ fired: false }); + }); + } + + it("kinematic-kinematic overlap via transform.setPosition fires onCollisionEnter", async function () { + const r = await probeKinematicCallback({ aKine: true, bKine: true }); + expect(r.fired).toBe(true); + }); + + it("kinematic-dynamic overlap via transform.setPosition fires onCollisionEnter", async function () { + const r = await probeKinematicCallback({ aKine: true, bKine: false }); + expect(r.fired).toBe(true); + }); + + it("dynamic-dynamic overlap via transform.setPosition fires onCollisionEnter", async function () { + const r = await probeKinematicCallback({ aKine: false, bKine: false }); + expect(r.fired).toBe(true); + }); + + it("kinematic-kinematic overlap via move fires onCollisionEnter", function () { + return new Promise((resolve, reject) => { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const boxA = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const boxB = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(3, 0, 0)); + const colA = boxA.getComponent(DynamicCollider); + const colB = boxB.getComponent(DynamicCollider); + colA.useGravity = false; + colB.useGravity = false; + colA.isKinematic = true; + colB.isKinematic = true; + + let fired = false; + boxA.addComponent( + class extends Script { + onCollisionEnter(_other: Collision): void { + fired = true; + resolve(); + } + } + ); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // Move B onto A via DynamicCollider.move() — this internally calls setKinematicTarget. + colB.move(new Vector3(-3, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + if (!fired) reject(new Error("kine-kine setKinematicTarget did NOT fire onCollisionEnter")); + }); + }); + + it("fully constrained dynamic overlap via transform.setPosition fires onCollisionEnter", function () { + return new Promise((resolve) => { + engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0); + const boxA = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const boxB = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(3, 0, 0)); + const colA = boxA.getComponent(DynamicCollider); + const colB = boxB.getComponent(DynamicCollider); + const FREEZE_ALL = + DynamicColliderConstraints.FreezePositionX | + DynamicColliderConstraints.FreezePositionY | + DynamicColliderConstraints.FreezePositionZ | + DynamicColliderConstraints.FreezeRotationX | + DynamicColliderConstraints.FreezeRotationY | + DynamicColliderConstraints.FreezeRotationZ; + colA.constraints = FREEZE_ALL; + colB.constraints = FREEZE_ALL; + colA.useGravity = false; + colB.useGravity = false; + colA.isKinematic = false; + colB.isKinematic = false; + + let fired = false; + boxA.addComponent( + class extends Script { + onCollisionEnter(_other: Collision): void { + fired = true; + resolve(); + } + } + ); + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + boxB.transform.setPosition(-3, 0, 0); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + if (!fired) { + expect.fail("expected onCollisionEnter to fire for dynamic-frozen pair after teleport"); + } + }); + }); }); diff --git a/tests/src/core/physics/DynamicCollider.test.ts b/tests/src/core/physics/DynamicCollider.test.ts index 4ec1aeb55e..33c5d76a96 100644 --- a/tests/src/core/physics/DynamicCollider.test.ts +++ b/tests/src/core/physics/DynamicCollider.test.ts @@ -6,6 +6,7 @@ import { DynamicCollider, DynamicColliderConstraints, CollisionDetectionMode, + DynamicColliderKinematicTransformSyncMode, StaticCollider, PlaneColliderShape } from "@galacean/engine-core"; @@ -309,6 +310,80 @@ describe("DynamicCollider", function () { expect(formatValue(boxCollider.inertiaTensor.y)).eq(1); }); + it("applyForce on sleeping actor must wake up and apply force", function () { + // Validates whether PhysX wasm `addForce(force, eFORCE, autowake=true)` actually wakes a + // sleeping actor on its own — or whether the engine's explicit wakeUp() call is required. + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.mass = 1; + boxCollider.useGravity = false; + boxCollider.linearDamping = 0; + + boxCollider.sleep(); + expect(boxCollider.isSleeping()).toBe(true); + + boxCollider.applyForce(new Vector3(1, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + expect(formatValue(boxCollider.linearVelocity.x)).eq(0.01667); + expect(boxCollider.isSleeping()).toBe(false); + }); + + it("applyForce after kinematic→dynamic switch (mimic billiards game break flow)", function () { + // Game pattern: all balls set kinematic at init, switched back to dynamic on break, + // then applyForce. Verifies the original 'force lost' bug was actually from this path. + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.mass = 1; + boxCollider.useGravity = false; + boxCollider.linearDamping = 0; + + boxCollider.isKinematic = true; + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + boxCollider.isKinematic = false; + + boxCollider.applyForce(new Vector3(1, 0, 0)); + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + expect(formatValue(boxCollider.linearVelocity.x)).eq(0.01667); + }); + + it("applyForce is consumed by the first fixed substep in a frame", function () { + const scene = engine.sceneManager.activeScene; + const originalFTS = scene.physics.fixedTimeStep; + let dv_1_60 = 0; + let dv_1_480 = 0; + + try { + const probe = (fts: number) => { + rootEntity.clearChildren(); + scene.physics.fixedTimeStep = fts; + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const c = box.getComponent(DynamicCollider); + c.mass = 1; + c.useGravity = false; + c.linearDamping = 0; + c.angularDamping = 0; + c.applyForce(new Vector3(100, 0, 0)); + // @ts-ignore + scene.physics._update(1 / 60); + return c.linearVelocity.x; + }; + + dv_1_60 = probe(1 / 60); + dv_1_480 = probe(1 / 480); + } finally { + scene.physics.fixedTimeStep = originalFTS; + } + + expect(dv_1_60).toBeCloseTo(100 / 60, 2); + expect(dv_1_480).toBeCloseTo(100 / 480, 2); + expect(dv_1_60 / dv_1_480).toBeCloseTo(8, 1); + }); + it("maxAngularVelocity", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider); @@ -539,6 +614,48 @@ describe("DynamicCollider", function () { expect(box.transform.position.y).below(1); }); + it("teleports kinematic target collider on re-enable instead of sweeping from stale native pose", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(-10, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + boxCollider.useGravity = false; + boxCollider.isKinematic = true; + boxCollider.kinematicTransformSyncMode = DynamicColliderKinematicTransformSyncMode.Target; + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + // @ts-ignore - intentionally observe the native boundary used by Collider sync. + const nativeCollider = boxCollider._nativeCollider; + const originalMove = nativeCollider.move.bind(nativeCollider); + const originalSetWorldTransform = nativeCollider.setWorldTransform.bind(nativeCollider); + let moveCalls = 0; + let setWorldTransformCalls = 0; + nativeCollider.move = (...args: Parameters) => { + moveCalls++; + return originalMove(...args); + }; + nativeCollider.setWorldTransform = (...args: Parameters) => { + setWorldTransformCalls++; + return originalSetWorldTransform(...args); + }; + + try { + box.isActive = false; + box.transform.setPosition(10, 0, 0); + box.isActive = true; + + // @ts-ignore + engine.sceneManager.activeScene.physics._update(1 / 60); + + expect(moveCalls).eq(0); + expect(setWorldTransformCalls).eq(1); + expect(formatValue(box.transform.position.x)).eq(10); + } finally { + nativeCollider.move = originalMove; + nativeCollider.setWorldTransform = originalSetWorldTransform; + } + }); + it("constraints", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider); @@ -583,6 +700,46 @@ describe("DynamicCollider", function () { ).toBeTruthy(); }); + it("CCD mode survives kinematic toggle", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + // @ts-ignore + const physX = boxCollider._nativeCollider._physXPhysics._physX; + const ccdFlag = () => + // @ts-ignore + boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_CCD); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + expect(ccdFlag()).toBeTruthy(); + + boxCollider.isKinematic = true; + expect(ccdFlag()).toBeFalsy(); + + boxCollider.isKinematic = false; + expect(ccdFlag()).toBeTruthy(); + expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.Continuous); + }); + + it("setCollisionDetectionMode in kinematic state defers native CCD flag application", function () { + const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); + const boxCollider = box.getComponent(DynamicCollider); + // @ts-ignore + const physX = boxCollider._nativeCollider._physXPhysics._physX; + const ccdFlag = () => + // @ts-ignore + boxCollider._nativeCollider._pxActor.getRigidBodyFlags(physX.PxRigidBodyFlag.eENABLE_CCD); + + boxCollider.isKinematic = true; + expect(ccdFlag()).toBeFalsy(); + + boxCollider.collisionDetectionMode = CollisionDetectionMode.Continuous; + expect(ccdFlag()).toBeFalsy(); + expect(boxCollider.collisionDetectionMode).toEqual(CollisionDetectionMode.Continuous); + + boxCollider.isKinematic = false; + expect(ccdFlag()).toBeTruthy(); + }); + it("sleep", function () { const box = addBox(new Vector3(2, 2, 2), DynamicCollider, new Vector3(0, 0, 0)); const boxCollider = box.getComponent(DynamicCollider); diff --git a/tests/src/core/physics/PhysicsScene.test.ts b/tests/src/core/physics/PhysicsScene.test.ts index b1758c7cdf..eaf4bdbe29 100644 --- a/tests/src/core/physics/PhysicsScene.test.ts +++ b/tests/src/core/physics/PhysicsScene.test.ts @@ -1626,15 +1626,16 @@ describe("Physics Test", () => { const entity2 = raycastTestRoot.createChild("entity2"); const collisionTestScript = entity1.addComponent(CollisionTestScript); - // Test that collision works correctly, A is dynamic and kinematic, B is static. + // SceneDesc.staticKineFilteringMode = eKEEP + Collider.move() routing kinematic + // to setKinematicTarget make static-kinematic pairs generate contact events. resetSpy(); setColliderProps(entity1, true, false, true); setColliderProps(entity2, false, false, false); updatePhysics(physicsMgr); - expect(collisionTestScript.onCollisionEnter).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionStay).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionExit).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionEnter).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionStay).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionExit).toHaveBeenCalled(); expect(collisionTestScript.onTriggerEnter).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerStay).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerExit).not.toHaveBeenCalled(); @@ -1741,15 +1742,16 @@ describe("Physics Test", () => { const entity2 = raycastTestRoot.createChild("entity2"); const collisionTestScript = entity1.addComponent(CollisionTestScript); - // Test that collision works correctly, both A,B are dynamic, kinematic. + // SceneDesc.kineKineFilteringMode = eKEEP + Collider.move() routing kinematic + // to setKinematicTarget make kine-kine pairs generate contact events. resetSpy(); setColliderProps(entity1, true, false, true); setColliderProps(entity2, true, false, true); updatePhysics(physicsMgr); - expect(collisionTestScript.onCollisionEnter).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionStay).not.toHaveBeenCalled(); - expect(collisionTestScript.onCollisionExit).not.toHaveBeenCalled(); + expect(collisionTestScript.onCollisionEnter).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionStay).toHaveBeenCalled(); + expect(collisionTestScript.onCollisionExit).toHaveBeenCalled(); expect(collisionTestScript.onTriggerEnter).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerStay).not.toHaveBeenCalled(); expect(collisionTestScript.onTriggerExit).not.toHaveBeenCalled(); From 9eeb20af1b78ca341a0d55645bc2df6ff6575126 Mon Sep 17 00:00:00 2001 From: luzhuang Date: Wed, 17 Jun 2026 20:27:24 +0800 Subject: [PATCH 10/23] fix(physics): rebuild mesh shapes and scaled defaults --- packages/core/src/physics/Collider.ts | 7 +++++++ packages/core/src/physics/DynamicCollider.ts | 1 + packages/core/src/physics/shape/ColliderShape.ts | 11 +++++++++++ 3 files changed, 19 insertions(+) diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index ce7f56f2f7..011a834495 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -136,6 +136,13 @@ export class Collider extends Component implements ICustomClone { } this._updateFlag.flag = false; } + + // Drive per-shape physics update. MeshColliderShape uses this to retry + // native shape creation after PhysX cooking returned null despite valid + // extracted mesh data. + for (let i = 0, n = shapes.length; i < n; i++) { + shapes[i]._onPhysicsUpdate(); + } } /** diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 15c12dbd2d..ccc1ace67b 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -13,6 +13,7 @@ import { MeshColliderShape } from "./shape/MeshColliderShape"; */ export class DynamicCollider extends Collider { private static _tempVector3 = new Vector3(); + private static _tempVector3_1 = new Vector3(); private static _tempQuat = new Quaternion(); private _linearDamping = 0; diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index ac12ae9d19..8cf6492c8e 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -171,6 +171,17 @@ export abstract class ColliderShape implements ICustomClone { target._syncNative(); } + /** + * @internal + * + * Called once per physics update tick by `Collider._onUpdate`. Base no-op. + * + * Subclasses can override for frame-driven maintenance. Currently used by + * `MeshColliderShape` to retry native shape creation after PhysX cooking + * returned null despite valid extracted mesh data. + */ + _onPhysicsUpdate(): void {} + /** * @internal */ From 5b7bda95fd68300d93e2ae61e86fb9209c8fcedd Mon Sep 17 00:00:00 2001 From: luzhuang Date: Mon, 13 Jul 2026 11:00:19 +0800 Subject: [PATCH 11/23] chore: keep engineering notes local --- .gitignore | 3 +- notes/2026-05-21-physx-tolerances-scale.md | 34 ------ .../2026-05-22-collider-reenable-teleport.md | 36 ------- notes/2026-05-22-physics-max-substeps-rfc.md | 94 ---------------- .../2026-06-01-physics-contact-auto-demand.md | 101 ------------------ notes/2026-07-11-gltf-scene-subasset-key.md | 39 ------- ...-physics-mesh-clone-and-cache-ownership.md | 29 ----- .../2026-06-11-pr3014-domain-split.md | 32 ------ 8 files changed, 2 insertions(+), 366 deletions(-) delete mode 100644 notes/2026-05-21-physx-tolerances-scale.md delete mode 100644 notes/2026-05-22-collider-reenable-teleport.md delete mode 100644 notes/2026-05-22-physics-max-substeps-rfc.md delete mode 100644 notes/2026-06-01-physics-contact-auto-demand.md delete mode 100644 notes/2026-07-11-gltf-scene-subasset-key.md delete mode 100644 notes/2026-07-11-physics-mesh-clone-and-cache-ownership.md delete mode 100644 notes/shaderlab/2026-06-11-pr3014-domain-split.md diff --git a/.gitignore b/.gitignore index f6049b3640..c692c405e6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ tsconfig.tsbuildinfo api docs/api docs/plans +/notes/ yarn.lock e2e/videos/* e2e/screenshots/* @@ -47,4 +48,4 @@ CLAUDE.md # For bison generated files used by ShaderLab *.tab.c -*.output \ No newline at end of file +*.output diff --git a/notes/2026-05-21-physx-tolerances-scale.md b/notes/2026-05-21-physx-tolerances-scale.md deleted file mode 100644 index 2895e8d2be..0000000000 --- a/notes/2026-05-21-physx-tolerances-scale.md +++ /dev/null @@ -1,34 +0,0 @@ -# PhysX tolerance scale configuration - -> Status: implemented by `be1d4bf32` on the local `dev/2.0` branch and tracked by draft PR #3042. - -## Context - -Cocos 2D migration can run physics in pixel-like units. PhysX supports non-meter -unit systems through `PxTolerancesScale`, but Galacean previously always created -PhysX with default `length=1` and `speed=10`. - -## Code facts - -- `PhysXPhysics._init()` creates one `PxTolerancesScale`, then passes it to both - `PxCreatePhysics` and `PxCookingParams`. -- PhysX requires scene scale to match the scale used to create `PxPhysics`. -- PhysX derives defaults such as contact offset and sleep threshold from this scale. -- Galacean core previously stored fixed defaults: - - `ColliderShape._contactOffset = 0.02` - - `DynamicCollider._sleepThreshold = 0.005` - and synced them unconditionally to native, overriding scaled PhysX defaults. - -## Fix - -- `PhysXPhysics` now accepts `PhysXPhysicsOptions.tolerancesScale`. -- The scale is applied before creating `PxPhysics` and `PxCookingParams`. -- `PhysXPhysics` exposes scaled default contact offset and sleep threshold. -- Core `ColliderShape` and `DynamicCollider` leave native defaults untouched unless - the user explicitly sets `contactOffset` or `sleepThreshold`. - -## Boundary - -This change does not scale damping or solver iterations. Those are not PhysX -tolerance defaults. Callers should still set explicit damping only when the -source project authored it. diff --git a/notes/2026-05-22-collider-reenable-teleport.md b/notes/2026-05-22-collider-reenable-teleport.md deleted file mode 100644 index 848db12e83..0000000000 --- a/notes/2026-05-22-collider-reenable-teleport.md +++ /dev/null @@ -1,36 +0,0 @@ -# Collider re-enable teleport regression test - -> Status: implemented on `fix/physics-kinematic-sync` and tracked by draft PR #3041; not yet part of `dev/2.0`. - -## Context - -Commit `bab0ae0a2` fixes a PhysX lifecycle bug where a disabled collider's native -actor is removed from the simulation scene but keeps its old native pose. When a -kinematic dynamic collider is re-enabled after its entity transform changed, the -first transform sync must not use `setKinematicTarget`, otherwise PhysX treats it -as a swept move from the stale pose and can emit spurious contacts. - -## Test added - -- `tests/src/core/physics/DynamicCollider.test.ts` now covers a kinematic - `DynamicCollider` in `Target` sync mode. -- The test disables the entity, moves its transform while inactive, re-enables it, - then verifies the first physics update calls native `setWorldTransform` exactly - once and does not call native `move`. -- A temporary local red-check that removed the pending re-enter teleport failed - with `expected 1 to equal +0` for `moveCalls`, proving the test catches the old - sweep path. - -## Naming cleanup - -The transient collider state is named `_pendingReenterTeleport` rather than -`_pendingReenterSync` because the important behavior is not generic transform -sync. It specifically forces the first sync after re-entering the physics scene -to use teleport semantics instead of swept kinematic target movement. - -## Verification - -- `pnpm vitest run tests/src/core/physics/DynamicCollider.test.ts -t "teleports kinematic target collider"` -- `pnpm vitest run tests/src/core/physics/DynamicCollider.test.ts` - -Both passed after restoring the engine fix. diff --git a/notes/2026-05-22-physics-max-substeps-rfc.md b/notes/2026-05-22-physics-max-substeps-rfc.md deleted file mode 100644 index d433888b3e..0000000000 --- a/notes/2026-05-22-physics-max-substeps-rfc.md +++ /dev/null @@ -1,94 +0,0 @@ -# RFC: Physics fixed-step catch-up cap - -> Status: resolved locally by `a18a3d4a2` with `PhysicsScene.maximumDeltaTime`. The proposed -> `maxSubSteps` API was not adopted. - -## Resolution - -The implemented boundary caps the frame delta admitted into the fixed-step accumulator: - -```ts -const simulateTime = this._restTime + Math.min(deltaTime, this._maximumDeltaTime); -``` - -This follows Unity's maximum-delta model, preserves unlimited catch-up by default with -`Infinity`, and avoids adding a second step-count API. Migration callers can set -`maximumDeltaTime` explicitly when source-runtime compatibility requires a cap. - -## Problem - -`PhysicsScene._update(deltaTime)` currently consumes all accumulated fixed steps in a single engine frame: - -```ts -const simulateTime = this._restTime + deltaTime; -const step = Math.floor(simulateTime / fixedTimeStep); -this._restTime = simulateTime - step * fixedTimeStep; -``` - -This is mathematically valid for simulation catch-up, but it has no public cap. When frame time fluctuates around `fixedTimeStep`, gameplay code that writes forces or velocity in `onPhysicsUpdate()` can observe uneven render-frame motion such as: - -```text -0 physics step in one render frame -2 physics steps in the next render frame -``` - -The issue is not specific to migration. Any Galacean project that drives gameplay through `onPhysicsUpdate()` can hit the same tradeoff between simulation catch-up and render-frame smoothness. - -## External references - -Unity exposes this concept through `Time.fixedDeltaTime` plus `Time.maximumDeltaTime` / Project Settings `Maximum Allowed Timestep`. - -Unity's docs describe that: - -- fixed updates can backlog when frame rate is lower than the fixed timestep rate -- Unity executes multiple fixed updates to catch up -- there is a maximum timestep period beyond which Unity does not keep catching up -- `Maximum Allowed Timestep` caps the worst-case time spent on physics and FixedUpdate in a low-frame-rate frame - -Cocos Creator 3D also exposes the concept directly as `PhysicsSystem.maxSubSteps`; in Cocos Creator 3.8.8 the default is `1`. - -## Proposed engine capability - -Add a public physics catch-up cap to Galacean, for example: - -```ts -class PhysicsScene { - fixedTimeStep: number; - maxSubSteps: number; -} -``` - -Suggested semantics: - -```ts -const rawStep = Math.floor((restTime + deltaTime) / fixedTimeStep); -const step = Math.min(rawStep, maxSubSteps); -restTime = simulateTime - step * fixedTimeStep; -``` - -Open default-value question: - -- `Infinity` preserves current Galacean behavior and makes this a non-breaking API addition. -- `Math.floor(engine.time.maximumDeltaTime / fixedTimeStep)` aligns with Unity's `maximumDeltaTime` style, but changes behavior if applied by default. -- `1` matches Cocos Creator's default, but would be a behavioral change for native Galacean projects. - -Conservative recommendation: add `maxSubSteps` as engine API with a non-breaking default first, then let migration/runtime layers explicitly set it to Cocos defaults when needed. - -## Non-goals - -- Do not solve render interpolation in this RFC. -- Do not change `Engine.targetFrameRate` or vSync behavior in the same change. -- Do not silently change native Galacean projects' physics catch-up behavior without a compatibility decision. - -## Validation plan - -Add focused engine tests for: - -- `maxSubSteps = Infinity` preserves current multi-step catch-up behavior. -- `maxSubSteps = 1` executes at most one `onPhysicsUpdate()` and one native physics update per engine update. -- accumulated `restTime` is preserved when the cap prevents full catch-up. -- `maxSubSteps = 0` disables fixed-step simulation while preserving accumulated time, or is rejected/clamped depending on the chosen API contract. - -## Migration relevance - -For migrated Cocos projects, the migration compat layer can set `scene.physics.maxSubSteps = 1` to match Cocos 3D default behavior. That should be explicit migration policy, not an implicit engine default forced onto all Galacean projects. diff --git a/notes/2026-06-01-physics-contact-auto-demand.md b/notes/2026-06-01-physics-contact-auto-demand.md deleted file mode 100644 index 84fd93f96e..0000000000 --- a/notes/2026-06-01-physics-contact-auto-demand.md +++ /dev/null @@ -1,101 +0,0 @@ -# Physics contact event auto demand - -> Status: implemented on `fix/physics-shaderlab-split` and tracked by draft PR #3025; not yet -> part of `dev/2.0`. The physics-lite no-op described below belongs to the pre-#3053 base and -> must be dropped when the PR rebases onto the current branch. - -## Problem - -CatchPigGame dense 2D physics traces showed a large share of time under PhysX `fetchResults`, specifically the JS contact callback path: - -```text -PhysXPhysicsScene.update - _fetchResults - onContactPersist - _bufferContactEvent - copy contact points from WASM to JS -``` - -The affected migrated scene had no active collision callbacks. The engine still requested and buffered every contact event, so dense resting contacts paid event-marshalling cost even when no script could consume those events. - -## Root Cause - -`physX.js` currently configures non-trigger pairs with contact notification flags in its simulation filter shader: - -```text -eCONTACT_DEFAULT -eNOTIFY_TOUCH_FOUND -eNOTIFY_TOUCH_PERSISTS -eNOTIFY_TOUCH_LOST -eNOTIFY_CONTACT_POINTS -``` - -Galacean then unconditionally copied those reports into `_contactEvents` in `PhysXPhysicsScene._bufferContactEvent()`. - -This was a systemic engine boundary issue: the Core layer already knows which active `Script` instances can receive `onCollisionEnter`, `onCollisionStay`, or `onCollisionExit`, but the native physics scene had no demand signal for contact buffering. - -## Fix - -Add an explicit native physics scene demand API: - -```ts -IPhysicsScene.setContactEventEnabled(enabled: boolean): void -``` - -Core synchronizes contact-event demand immediately before each fixed physics step and only -rescans consumers after script or collider lifecycle changes mark the cache dirty: - -- scan collider entities' active `entity._scripts` -- compare collision lifecycle methods against `Script.prototype` -- enable contact buffering only if at least one active script overrides `onCollisionEnter`, `onCollisionStay`, or `onCollisionExit` - -PhysX honors that demand by skipping `_bufferContactEvent()` when disabled and clearing stale contact count on disable. Physics-lite implements the API as a no-op because it only produces trigger events. - -This keeps trigger events unchanged and preserves collision callbacks for projects that actually declare them. - -## Why This Is Not A Workaround - -The previous migration-layer mitigation monkey-patched the private PhysX `_bufferContactEvent` method based on generated config. That fixed one class of migrated output but depended on private engine internals. - -The engine fix moves the policy to the owning layers: - -- Core owns script lifecycle and can know whether collision callbacks exist. -- Design exposes a backend-agnostic demand API. -- PhysX owns whether to buffer contact reports. - -No project-specific or migration-specific data is needed. - -## Boundary - -This avoids the expensive JS-side buffering and contact-point copying when there is no consumer. It does not yet change the underlying `physX.js` simulation filter shader, which still asks native PhysX to produce contact reports for non-trigger pairs. A deeper future optimization could add native pair-flag demand support in `physX.js`, but that requires changing the wrapper/filter-shader layer rather than the Galacean TypeScript packages. - -## Files - -- `packages/design/src/physics/IPhysicsScene.ts` -- `packages/core/src/physics/PhysicsScene.ts` -- `packages/physics-physx/src/PhysXPhysicsScene.ts` -- `packages/physics-lite/src/LitePhysicsScene.ts` -- `tests/src/core/physics/PhysicsScene.test.ts` - -## Verification - -Passed: - -```bash -pnpm -r --filter @galacean/engine-design --filter @galacean/engine-core --filter @galacean/engine-physics-lite --filter @galacean/engine-physics-physx run b:types -pnpm exec eslint packages/core/src/physics/PhysicsScene.ts packages/physics-physx/src/PhysXPhysicsScene.ts packages/physics-lite/src/LitePhysicsScene.ts packages/design/src/physics/IPhysicsScene.ts tests/src/core/physics/PhysicsScene.test.ts --ext .ts -``` - -Targeted Vitest command was attempted but blocked before tests executed by the local Rollup native optional dependency being rejected by macOS system policy: - -```text -Error: Cannot find module @rollup/rollup-darwin-arm64 -cause: ERR_DLOPEN_FAILED ... library load denied by system policy -``` - -The new tests cover: - -- no active collision callback -> native contact events are disabled -- active collision callback -> native contact events are enabled -- disabling that script -> native contact events are disabled again -- trigger-only callbacks -> native contact events remain disabled diff --git a/notes/2026-07-11-gltf-scene-subasset-key.md b/notes/2026-07-11-gltf-scene-subasset-key.md deleted file mode 100644 index cbcf93c660..0000000000 --- a/notes/2026-07-11-gltf-scene-subasset-key.md +++ /dev/null @@ -1,39 +0,0 @@ -# glTF scene sub-asset key alignment - -## Symptom - -Editor source-v2 uses the glTF schema path `scenes[0]` for an instance asset key. The Engine loaded and cached the main GLTFResource, but a concurrent `?q=scenes[0]` request never resolved, so Scene and Prefab loading remained pending without a network or parser error. - -## Root cause - -`GLTFParserContext` stored scene roots in the internal `_sceneRoots` field and only published `_sceneRoots[index]` plus `defaultSceneRoot` to ResourceManager. Editor metadata and source-v2 use `scenes[index]`, matching the glTF schema rather than the Engine's private storage field. Cached query traversal also failed because GLTFResource exposed no `scenes` property. - -## Decision - -- `scenes[index]` is the canonical public sub-asset query key. -- GLTFResource exposes a read-only `scenes` view for cached query traversal. -- The parser publishes `scenes[index]` while retaining `_sceneRoots[index]` and `defaultSceneRoot` as compatibility aliases. -- The fix belongs in Engine, not in Editor CLI lowering or migration-specific key rewriting, because Editor already uses the glTF schema key consistently. - -## Verification - -- Loader test covers both first-load callback resolution and cached lookup for `?q=scenes[0]`. -- The migration runtime reproduction uses two source-v2 glTF instances and previously left both query promises pending while their main GLTFResource objects were cached. - -## Source-v2 instance consumption - -Editor source-v2 instance refs can select a glTF scene with key scenes[n]. Generic resource-ref resolution must not consume that key first, because it returns the scene Entity while HierarchyParser needs the owning GLTFResource to clone the selected scene. - -HierarchyParser now: - -- parses scenes[n] without regex and keeps defaultSceneRoot compatibility; -- loads the main glTF resource by URL; -- passes the selected index to instantiateSceneRoot(index); -- keeps ordinary Prefab refs on the existing path; -- rejects resources that are neither PrefabResource nor GLTFResource with a boundary-specific error. - -Verification: - -- pnpm run b:module: passed. -- HEADLESS=true pnpm exec vitest run tests/src/loader/GLTFLoader.test.ts tests/src/loader/SceneFormatV2.test.ts: 60 passed. -- A locally linked 3DCube source-v2 build loaded, entered gameplay, instantiated repeated glTF-backed dice, and produced no console errors. diff --git a/notes/2026-07-11-physics-mesh-clone-and-cache-ownership.md b/notes/2026-07-11-physics-mesh-clone-and-cache-ownership.md deleted file mode 100644 index fa54c99ef7..0000000000 --- a/notes/2026-07-11-physics-mesh-clone-and-cache-ownership.md +++ /dev/null @@ -1,29 +0,0 @@ -# Physics mesh clone and cache ownership - -## Context - -CatchPig's converted 2D polygon colliders exercise dynamic prefab cloning, convex mesh cooking, non-unit scale, and project-specific PhysX tolerance scale. Open PR `galacean/engine#3042` addressed the same mesh-shape and scaled-default boundary and was integrated before local follow-up work. - -## Integrated upstream commits - -- `fix(physics): rebuild mesh shapes and scaled defaults` -- `fix(physics): keep mesh recooking transactional` - -The integration preserved the current `dev/2.0` PhysX CDN configuration and the removal of physics-lite. - -## Follow-up design - -- `Collider` is the sole owner of native shape attachment and detachment. -- `ColliderShape` records whether its native shape is currently attached. -- `MeshColliderShape` clone and recook paths construct detached native shapes; Collider attaches each shape exactly once. -- Mesh recooking remains transactional: a failed cook retains the previous usable native shape. -- Mesh resource references are balanced across clone and destroy. -- `DynamicCollider` copies explicit center of mass and inertia tensor only when the corresponding automatic mode is disabled. -- `ResourceManager.getFromCache` normalizes virtual paths through the same remote-URL mapping as load requests. - -## Verification - -- Module and declaration builds passed. -- Focused suites passed 78 tests total: MeshColliderShape 31, DynamicCollider 31, ResourceManager 16. -- Tests cover delayed mesh assignment, inaccessible meshes, failed recooking, exact native attach/detach counts, clone ref-count balance, and virtual-path cache lookup. -- CatchPig ran with gravity `y = -640`, tolerance scale `{ length: 32, speed: 640 }`, 30 attached dynamic colliders, stable bounds, and successful pointer-driven gameplay after Editor selector lowering was corrected. diff --git a/notes/shaderlab/2026-06-11-pr3014-domain-split.md b/notes/shaderlab/2026-06-11-pr3014-domain-split.md deleted file mode 100644 index 12875f66b4..0000000000 --- a/notes/shaderlab/2026-06-11-pr3014-domain-split.md +++ /dev/null @@ -1,32 +0,0 @@ -# PR #3014 Domain Split - -## Context - -PR #3014 originally combined shaderlab fixes across Animation, Physics, Audio, and glTF Loader. The branch was updated first so the original PR head contained the latest relevant `fix/shaderlab` work, then the changes were split into domain-scoped PRs against the latest `origin/dev/2.0` (`de7549687`). - -## Branches And PRs - -- Animation: `fix/animation-shaderlab-split` -> https://github.com/galacean/engine/pull/3024 -- Physics contact events and collision normals: `fix/physics-shaderlab-split` -> https://github.com/galacean/engine/pull/3025 -- Physics kinematic synchronization: `fix/physics-kinematic-sync` -> https://github.com/galacean/engine/pull/3041 -- Physics mesh rebuild and scaled defaults: `fix/physics-mesh-defaults` -> https://github.com/galacean/engine/pull/3042 -- Audio: `fix/audio-shaderlab-split` -> https://github.com/galacean/engine/pull/3026 -- glTF Loader: `fix/gltf-loader-shaderlab-split` -> https://github.com/galacean/engine/pull/3027 -- Original combined PR #3014 was closed in favor of the scoped PRs. - -## Scope Notes - -- Animation includes Animator play-data cleanup, clip-change listener disposal, WeakMap-backed instance tracking, `_reset` cleanup guards, stale `AnimatorStateInstance` removal, and the hang regression test. -- Physics was subsequently split into a stack: #3025 owns contact-event demand and collision-normal orientation; #3041 owns target-versus-teleport kinematic synchronization, re-entry pose recovery, and CCD state; #3042 owns mesh rebuild, scaled PhysX defaults, and material/clone synchronization. -- Audio keeps pending AudioSource playback and AudioLoader fixes together. -- glTF Loader keeps schema/parser/skin parser fixes and related loader regression coverage together. - -## Verification - -- All split branches: `git diff --check origin/dev/2.0...HEAD` passed. -- Animation: `pnpm -F @galacean/engine-design run b:types`; `pnpm -F @galacean/engine-core run b:types`. -- Physics: `pnpm -F @galacean/engine-design run b:types`; `pnpm -F @galacean/engine-core run b:types`; `pnpm -F @galacean/engine-physics-lite run b:types`; `pnpm -F @galacean/engine-physics-physx run b:types`. -- Audio: `pnpm -F @galacean/engine-design run b:types`; `pnpm -F @galacean/engine-core run b:types`; `pnpm -F @galacean/engine-loader run b:types`. -- glTF Loader: `pnpm -F @galacean/engine-design run b:types`; `pnpm -F @galacean/engine-core run b:types`; `pnpm -F @galacean/engine-loader run b:types`. - -Focused Vitest commands were attempted in clean split worktrees, but Vite dependency scanning failed before executing tests because local workspace package JavaScript entries were not built. From 6cd7b21e9c02cdca5a248968dfa454b7eee4e47d Mon Sep 17 00:00:00 2001 From: luzhuang Date: Mon, 13 Jul 2026 11:01:51 +0800 Subject: [PATCH 12/23] chore(loader): remove asset extensions note --- packages/loader/ASSET_EXTENSIONS.md | 103 ---------------------------- 1 file changed, 103 deletions(-) delete mode 100644 packages/loader/ASSET_EXTENSIONS.md diff --git a/packages/loader/ASSET_EXTENSIONS.md b/packages/loader/ASSET_EXTENSIONS.md deleted file mode 100644 index 240500c4fb..0000000000 --- a/packages/loader/ASSET_EXTENSIONS.md +++ /dev/null @@ -1,103 +0,0 @@ -# Loader 资产后缀名 - -> 数据来源:`packages/loader/src/*Loader.ts` 的 `@resourceLoader` 装饰器。共 25 个 loader,37 个后缀名(去重后 `mesh` 只算一个)。 - -## 分类说明 - -### 模型/场景 -3D 资源入口:场景、预制体、项目、glTF 模型、网格。 -``` -.gltf -.glb -.mesh -.prefab -.project -.scene -``` - -### 纹理 -2D 贴图、立方体贴图、压缩纹理。 -``` -.tex -.png -.jpg -.jpeg -.webp -.hdr -.ktx -.ktx2 -``` - -### 材质 -材质资源。 -``` -.mat -``` - -### 物理材质 -物理材质资源。 -``` -.physMat -``` - -### 动画 -动画片段、动画控制器。 -``` -.anim -.animCtrl -``` - -### 环境光照 -环境光贴图、渲染目标。 -``` -.ambLight -.renderTarget -``` - -### 着色器 -着色器代码。 -``` -.shader -.shaderc -``` - -### 字体 -位图字体、源字体。 -``` -.font -.ttf -.otf -.woff -``` - -### 精灵/UI -精灵图、图集。 -``` -.sprite -.atlas -``` - -### 音频 -音频文件。 -``` -.mp3 -.ogg -.wav -.m4a -.aac -.flac -``` - -### 数据/通用 -JSON、文本、二进制缓冲。 -``` -.json -.txt -.bin -``` - -## 全部(comma-separated) - -``` -.aac,.ambLight,.anim,.animCtrl,.atlas,.bin,.flac,.gltf,.glb,.hdr,.jpg,.jpeg,.json,.ktx,.ktx2,.m4a,.mat,.mesh,.mp3,.ogg,.otf,.physMat,.png,.prefab,.project,.renderTarget,.scene,.shader,.shaderc,.sprite,.tex,.ttf,.txt,.wav,.webp,.woff,.font -``` From fe0af8a7caa2d65a6d68aa53f6b4b439f21c75d6 Mon Sep 17 00:00:00 2001 From: luzhuang Date: Mon, 13 Jul 2026 17:42:08 +0800 Subject: [PATCH 13/23] feat(loader): support constructed values and nested calls --- .../resources/parser/ReflectionParser.ts | 33 +++++++--- packages/loader/src/schema/CommonSchema.ts | 9 +++ tests/src/loader/SceneFormatV2.test.ts | 60 +++++++++++++++++++ 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts index 2e038238ce..634bb15498 100644 --- a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts +++ b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts @@ -40,13 +40,21 @@ export class ReflectionParser { for (let i = 0, n = calls.length; i < n; i++) { const call = calls[i]; chain = chain.then(() => { - const method = instance?.[call.method]; + let target = instance; + if (call.target !== undefined) { + if (!Array.isArray(call.target) || call.target.some((key) => typeof key !== "string" || key.length === 0)) { + return Promise.reject(new Error(`Call "${call.method}" target must be an array of non-empty strings`)); + } + for (const key of call.target) target = target?.[key]; + } + const method = target?.[call.method]; if (typeof method !== "function") { - return Promise.reject(new Error(`Call target does not have method "${call.method}"`)); + const path = call.target?.length ? `${call.target.join(".")}.` : ""; + return Promise.reject(new Error(`Call target does not have method "${path}${call.method}"`)); } return Promise.all((call.args ?? []).map((arg) => this._resolveValue(arg))) - .then((resolvedArgs) => Promise.resolve(method.apply(instance, resolvedArgs))) + .then((resolvedArgs) => Promise.resolve(method.apply(target, resolvedArgs))) .then((result) => { if (!call.result) return result; if (result == null || (typeof result !== "object" && typeof result !== "function")) { @@ -77,7 +85,7 @@ export class ReflectionParser { * 1. null/undefined/primitive → passthrough * 2. Array → recurse each element * 3. { $ref } → asset reference - * 4. { $type } → polymorphic type construct + * 4. { $type, $args? } → polymorphic type construct * 5. { $class } → registered class constructor * 6. { $entity } → entity reference by path (flat index + optional children descent) * 7. { $component } → component reference @@ -109,12 +117,17 @@ export class ReflectionParser { }); } - // $type — polymorphic type: construct instance and apply remaining props + // $type — polymorphic type: resolve constructor args, construct instance, then apply remaining props if ("$type" in obj) { - const { $type, ...rest } = obj; + const { $type, $args, ...rest } = obj; + if ($args !== undefined && !Array.isArray($args)) { + return Promise.reject(new Error("$args must be an array when used with $type")); + } return this._resolveRegisteredClass($type, "$type").then((Class) => { - const instance = new Class(); - return Object.keys(rest).length > 0 ? this.parseProps(instance, rest) : instance; + return Promise.all(($args ?? []).map((arg) => this._resolveValue(arg))).then((args) => { + const instance = new Class(...args); + return Object.keys(rest).length > 0 ? this.parseProps(instance, rest) : instance; + }); }); } @@ -138,6 +151,10 @@ export class ReflectionParser { return this._resolveSignal(originValue, obj.$signal as SignalListener[]); } + if ("$args" in obj) { + return Promise.reject(new Error("$args requires $type")); + } + // Plain object — recurse each value, modifying originValue in place or building a new object const target = originValue && typeof originValue === "object" && !Array.isArray(originValue) diff --git a/packages/loader/src/schema/CommonSchema.ts b/packages/loader/src/schema/CommonSchema.ts index ae61dc8ce4..b162231768 100644 --- a/packages/loader/src/schema/CommonSchema.ts +++ b/packages/loader/src/schema/CommonSchema.ts @@ -20,6 +20,13 @@ export interface ClassRef { $class: string; } +/** Registered runtime value with optional recursively-resolved constructor arguments. */ +export interface TypeValue { + $type: string; + $args?: unknown[]; + [key: string]: unknown; +} + export interface SignalListener { target: { $component: ComponentRef }; methodName: string; @@ -27,6 +34,8 @@ export interface SignalListener { } export interface CallSpec { + /** Optional property path from the mutation root to the method owner. */ + target?: string[]; method: string; args?: unknown[]; result?: MutationBlock; diff --git a/tests/src/loader/SceneFormatV2.test.ts b/tests/src/loader/SceneFormatV2.test.ts index b6b7296199..5c7b51b85f 100644 --- a/tests/src/loader/SceneFormatV2.test.ts +++ b/tests/src/loader/SceneFormatV2.test.ts @@ -39,10 +39,26 @@ class TestValueType { } Loader.registerClass("TestValueType", TestValueType); +class ConstructorValueType { + label = ""; + + constructor( + readonly seed: string, + readonly child: TestValueType + ) {} +} +Loader.registerClass("ConstructorValueType", ConstructorValueType); + class CallOrderComponent extends Script { value = ""; receivedArgs: any[] = []; lastResult: any = null; + nested = { + value: "", + setValue(value: string) { + this.value = value; + } + }; appendSuffix(suffix: string): void { this.value += suffix; @@ -341,6 +357,17 @@ describe("ReflectionParser calls resolution", () => { expect(target.lastResult.y).to.equal(2); }); + it("should invoke methods on a nested target path", async () => { + const scene = new Scene(engine); + const context = new ParserContext(engine, ParserType.Scene, scene); + const parser = new ReflectionParser(context, []); + const target = new CallOrderComponent(new Entity(engine, "host")); + + await parser.parseCalls(target, [{ target: ["nested"], method: "setValue", args: ["ready"] }]); + + expect(target.nested.value).to.equal("ready"); + }); + it("should await each call before executing the next one", async () => { const scene = new Scene(engine); const context = new ParserContext(engine, ParserType.Scene, scene); @@ -849,6 +876,39 @@ describe("ReflectionParser $type resolution", () => { expect(target.value.y).to.equal(0); }); + it("should recursively resolve $args before construction and then apply props", async () => { + const scene = new Scene(engine); + const context = new ParserContext(engine, ParserType.Scene, scene); + const parser = new ReflectionParser(context, []); + const target: any = {}; + await parser.parseProps(target, { + value: { + $type: "ConstructorValueType", + $args: ["seed", { $type: "TestValueType", x: 3, y: 4 }], + label: "ready" + } + }); + expect(target.value).to.be.instanceOf(ConstructorValueType); + expect(target.value.seed).to.equal("seed"); + expect(target.value.child).to.be.instanceOf(TestValueType); + expect(target.value.child).to.deep.include({ x: 3, y: 4 }); + expect(target.value.label).to.equal("ready"); + }); + + it("should reject $args without an array-valued $type constructor contract", async () => { + const scene = new Scene(engine); + const context = new ParserContext(engine, ParserType.Scene, scene); + const parser = new ReflectionParser(context, []); + const target: any = {}; + + await expect( + parser.parseProps(target, { + value: { $type: "TestValueType", $args: "invalid" } as any + }) + ).rejects.toThrow("$args must be an array when used with $type"); + await expect(parser.parseProps(target, { value: { $args: [] } })).rejects.toThrow("$args requires $type"); + }); + it("should throw a clear error when $type references an unregistered class", async () => { const scene = new Scene(engine); const context = new ParserContext(engine, ParserType.Scene, scene); From 4252f17abbf427dc1a04c389d8f0ba6bc99885c9 Mon Sep 17 00:00:00 2001 From: luzhuang Date: Tue, 14 Jul 2026 17:08:06 +0800 Subject: [PATCH 14/23] fix: resolve migration runtime review findings --- .../src/particle/modules/ParticleCurve.ts | 18 ++++++-- .../src/particle/modules/ParticleGradient.ts | 6 ++- packages/core/src/physics/Collider.ts | 7 --- packages/core/src/physics/DynamicCollider.ts | 1 - .../core/src/physics/shape/ColliderShape.ts | 11 ----- .../src/physics/shape/MeshColliderShape.ts | 26 +++++++---- packages/galacean/src/index.ts | 4 -- packages/loader/src/gltf/GLTFResource.ts | 11 +++++ .../resources/parser/ReflectionParser.ts | 5 ++- packages/physics-physx/src/PhysXPhysics.ts | 13 ++++-- .../shader/src/ShaderLibrary/Skin/Skin.glsl | 45 ++++++------------- tests/src/core/Transform.test.ts | 8 +++- .../particle/ParticleSerialization.test.ts | 27 +++++++---- .../src/core/physics/DynamicCollider.test.ts | 9 ++++ .../core/physics/MeshColliderShape.test.ts | 32 +++++++++++++ tests/src/core/physics/PhysicsScene.test.ts | 9 ++-- tests/src/loader/GLTFLoader.test.ts | 17 +++++-- 17 files changed, 159 insertions(+), 90 deletions(-) diff --git a/packages/core/src/particle/modules/ParticleCurve.ts b/packages/core/src/particle/modules/ParticleCurve.ts index 8da2cb12b5..77afd49f84 100644 --- a/packages/core/src/particle/modules/ParticleCurve.ts +++ b/packages/core/src/particle/modules/ParticleCurve.ts @@ -88,11 +88,23 @@ export class ParticleCurve { * @param keys - The keys */ setKeys(keys: ReadonlyArray): void { - this._keys.length = 0; - for (let i = 0, n = keys.length; i < n; i++) { - this.addKey(keys[i]); + if (keys.length > 4) { + throw new Error("Curve can only have 4 keys"); + } + + const nextKeys = [...keys]; + const currentKeys = this._keys; + for (let i = 0, n = currentKeys.length; i < n; i++) { + currentKeys[i]._unRegisterOnValueChanged(this._updateDispatch); + } + currentKeys.length = 0; + for (let i = 0, n = nextKeys.length; i < n; i++) { + const key = nextKeys[i]; + this._addKey(currentKeys, key); + key._registerOnValueChanged(this._updateDispatch); } this._typeArrayDirty = true; + this._updateDispatch(); } /** diff --git a/packages/core/src/particle/modules/ParticleGradient.ts b/packages/core/src/particle/modules/ParticleGradient.ts index 20d735143d..34b896f12e 100644 --- a/packages/core/src/particle/modules/ParticleGradient.ts +++ b/packages/core/src/particle/modules/ParticleGradient.ts @@ -148,6 +148,8 @@ export class ParticleGradient { throw new Error("Gradient can only have 4 alpha keys"); } + const nextColorKeys = [...colorKeys]; + const nextAlphaKeys = [...alphaKeys]; const currentColorKeys = this._colorKeys; const currentAlphaKeys = this._alphaKeys; for (let i = 0, n = currentColorKeys.length; i < n; i++) { @@ -159,8 +161,8 @@ export class ParticleGradient { currentColorKeys.length = 0; currentAlphaKeys.length = 0; - for (let i = 0, n = colorKeys.length; i < n; i++) this.addColorKey(colorKeys[i]); - for (let i = 0, n = alphaKeys.length; i < n; i++) this.addAlphaKey(alphaKeys[i]); + for (let i = 0, n = nextColorKeys.length; i < n; i++) this.addColorKey(nextColorKeys[i]); + for (let i = 0, n = nextAlphaKeys.length; i < n; i++) this.addAlphaKey(nextAlphaKeys[i]); } /** diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index 011a834495..ce7f56f2f7 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -136,13 +136,6 @@ export class Collider extends Component implements ICustomClone { } this._updateFlag.flag = false; } - - // Drive per-shape physics update. MeshColliderShape uses this to retry - // native shape creation after PhysX cooking returned null despite valid - // extracted mesh data. - for (let i = 0, n = shapes.length; i < n; i++) { - shapes[i]._onPhysicsUpdate(); - } } /** diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index ccc1ace67b..15c12dbd2d 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -13,7 +13,6 @@ import { MeshColliderShape } from "./shape/MeshColliderShape"; */ export class DynamicCollider extends Collider { private static _tempVector3 = new Vector3(); - private static _tempVector3_1 = new Vector3(); private static _tempQuat = new Quaternion(); private _linearDamping = 0; diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index 8cf6492c8e..ac12ae9d19 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -171,17 +171,6 @@ export abstract class ColliderShape implements ICustomClone { target._syncNative(); } - /** - * @internal - * - * Called once per physics update tick by `Collider._onUpdate`. Base no-op. - * - * Subclasses can override for frame-driven maintenance. Currently used by - * `MeshColliderShape` to retry native shape creation after PhysX cooking - * returned null despite valid extracted mesh data. - */ - _onPhysicsUpdate(): void {} - /** * @internal */ diff --git a/packages/core/src/physics/shape/MeshColliderShape.ts b/packages/core/src/physics/shape/MeshColliderShape.ts index 193fc34d3a..253e188736 100644 --- a/packages/core/src/physics/shape/MeshColliderShape.ts +++ b/packages/core/src/physics/shape/MeshColliderShape.ts @@ -28,8 +28,13 @@ export class MeshColliderShape extends ColliderShape { const previousValue = this._cookingFlags; this._cookingFlags = value; if (this._nativeShape) { - if (!this._updateNativeShapeData()) { - this._cookingFlags = previousValue; + let updated = false; + try { + updated = this._updateNativeShapeData(); + } finally { + if (!updated) { + this._cookingFlags = previousValue; + } } } else if (this._mesh && this._extractMeshData(this._mesh)) { this._createNativeShape(); @@ -82,12 +87,17 @@ export class MeshColliderShape extends ColliderShape { if (value) { if (this._extractMeshData(value)) { if (this._nativeShape) { - if (!this._updateNativeShapeData()) { - value?._addReferCount(-1); - previousMesh?._addReferCount(1); - this._mesh = previousMesh; - this._positions = previousPositions; - this._indices = previousIndices; + let updated = false; + try { + updated = this._updateNativeShapeData(); + } finally { + if (!updated) { + value?._addReferCount(-1); + previousMesh?._addReferCount(1); + this._mesh = previousMesh; + this._positions = previousPositions; + this._indices = previousIndices; + } } } else { this._createNativeShape(); diff --git a/packages/galacean/src/index.ts b/packages/galacean/src/index.ts index c897d7390d..b5036df5d5 100644 --- a/packages/galacean/src/index.ts +++ b/packages/galacean/src/index.ts @@ -19,10 +19,6 @@ for (const key in MathObjects) { Loader.registerClass(key, MathObjects[key]); } -for (let key in MathObjects) { - Loader.registerClass(key, MathObjects[key]); -} - // Bootstrap the Galacean engine flavor: browser polyfills first (must // patch globals before anything else runs), then platform detection // (other modules may read `SystemInfo.platform`), then built-in shader diff --git a/packages/loader/src/gltf/GLTFResource.ts b/packages/loader/src/gltf/GLTFResource.ts index 7c58958195..7edffea503 100644 --- a/packages/loader/src/gltf/GLTFResource.ts +++ b/packages/loader/src/gltf/GLTFResource.ts @@ -59,7 +59,18 @@ export class GLTFResource extends ReferResource { * @returns Root entity */ instantiateSceneRoot(sceneIndex?: number): Entity { + if ( + sceneIndex !== undefined && + (!Number.isSafeInteger(sceneIndex) || sceneIndex < 0 || sceneIndex >= this._sceneRoots.length) + ) { + throw new RangeError( + `GLTFResource: scene index ${sceneIndex} is out of range for "${this.url}" (${this._sceneRoots.length} scenes).` + ); + } const sceneRoot = sceneIndex === undefined ? this._defaultSceneRoot : this._sceneRoots[sceneIndex]; + if (!sceneRoot) { + throw new Error(`GLTFResource: no default scene is available for "${this.url}".`); + } return sceneRoot.clone(); } diff --git a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts index 634bb15498..a2548c7293 100644 --- a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts +++ b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts @@ -120,11 +120,12 @@ export class ReflectionParser { // $type — polymorphic type: resolve constructor args, construct instance, then apply remaining props if ("$type" in obj) { const { $type, $args, ...rest } = obj; - if ($args !== undefined && !Array.isArray($args)) { + const constructorArgs = $args === undefined ? [] : $args; + if (!Array.isArray(constructorArgs)) { return Promise.reject(new Error("$args must be an array when used with $type")); } return this._resolveRegisteredClass($type, "$type").then((Class) => { - return Promise.all(($args ?? []).map((arg) => this._resolveValue(arg))).then((args) => { + return Promise.all(constructorArgs.map((arg) => this._resolveValue(arg))).then((args) => { const instance = new Class(...args); return Object.keys(rest).length > 0 ? this.parseProps(instance, rest) : instance; }); diff --git a/packages/physics-physx/src/PhysXPhysics.ts b/packages/physics-physx/src/PhysXPhysics.ts index 56a2704113..577f5644e3 100644 --- a/packages/physics-physx/src/PhysXPhysics.ts +++ b/packages/physics-physx/src/PhysXPhysics.ts @@ -85,7 +85,10 @@ export class PhysXPhysics implements IPhysics { runtimeUrls?.wasmModeUrl ?? "https://mdn.alipayobjects.com/rms/uri/file/as/apwallet/1781696156399/suyi/physx.release.js"; this._tolerancesScaleOptions = resolvedOptions?.tolerancesScale; - this._updateScaledDefaults(this._tolerancesScaleOptions?.length ?? 1, this._tolerancesScaleOptions?.speed ?? 10); + const length = this._tolerancesScaleOptions?.length ?? 1; + const speed = this._tolerancesScaleOptions?.speed ?? 10; + this._validateTolerancesScale(length, speed); + this._updateScaledDefaults(length, speed); } /** @@ -337,8 +340,7 @@ export class PhysXPhysics implements IPhysics { const length = this._tolerancesScaleOptions?.length ?? tolerancesScale.length; const speed = this._tolerancesScaleOptions?.speed ?? tolerancesScale.speed; - this._assertPositiveFinite(length, "tolerancesScale.length"); - this._assertPositiveFinite(speed, "tolerancesScale.speed"); + this._validateTolerancesScale(length, speed); tolerancesScale.length = length; tolerancesScale.speed = speed; @@ -351,6 +353,11 @@ export class PhysXPhysics implements IPhysics { } } + private _validateTolerancesScale(length: number, speed: number): void { + this._assertPositiveFinite(length, "tolerancesScale.length"); + this._assertPositiveFinite(speed, "tolerancesScale.speed"); + } + private _updateScaledDefaults(length: number, speed: number): void { this._defaultContactOffset = 0.02 * length; this._defaultSleepThreshold = 5e-5 * speed * speed; diff --git a/packages/shader/src/ShaderLibrary/Skin/Skin.glsl b/packages/shader/src/ShaderLibrary/Skin/Skin.glsl index 9c8dc88233..3f8e1cb1ab 100644 --- a/packages/shader/src/ShaderLibrary/Skin/Skin.glsl +++ b/packages/shader/src/ShaderLibrary/Skin/Skin.glsl @@ -7,37 +7,18 @@ sampler2D renderer_JointSampler; float renderer_JointCount; - #ifdef RENDERER_USE_BAKED_SKINNING - float renderer_BakedFrame; - float renderer_BoneCount; - - mat4 getJointMatrix(sampler2D smp, float index){ - float frameCount = renderer_JointCount / renderer_BoneCount; - float texel = 1.0 / (frameCount * 4.0); - float u = (renderer_BakedFrame * 4.0 + 0.5) * texel; - float v = (index + 0.5) / renderer_BoneCount; - - vec4 m0 = texture2D(smp, vec2(u, v)); - vec4 m1 = texture2D(smp, vec2(u + texel, v)); - vec4 m2 = texture2D(smp, vec2(u + texel * 2.0, v)); - vec4 m3 = texture2D(smp, vec2(u + texel * 3.0, v)); - - return mat4(m0, m1, m2, m3); - } - #else - mat4 getJointMatrix(sampler2D smp, float index){ - float base = index / renderer_JointCount; - float hf = 0.5 / renderer_JointCount; - float v = base + hf; - - vec4 m0 = texture2D(smp, vec2(0.125, v )); - vec4 m1 = texture2D(smp, vec2(0.375, v )); - vec4 m2 = texture2D(smp, vec2(0.625, v )); - vec4 m3 = texture2D(smp, vec2(0.875, v )); - - return mat4(m0, m1, m2, m3); - } - #endif + mat4 getJointMatrix(sampler2D smp, float index){ + float base = index / renderer_JointCount; + float hf = 0.5 / renderer_JointCount; + float v = base + hf; + + vec4 m0 = texture2D(smp, vec2(0.125, v )); + vec4 m1 = texture2D(smp, vec2(0.375, v )); + vec4 m2 = texture2D(smp, vec2(0.625, v )); + vec4 m3 = texture2D(smp, vec2(0.875, v )); + + return mat4(m0, m1, m2, m3); + } #else mat4 renderer_JointMatrix[ RENDERER_JOINTS_NUM ]; #endif @@ -63,4 +44,4 @@ #endif -#endif +#endif \ No newline at end of file diff --git a/tests/src/core/Transform.test.ts b/tests/src/core/Transform.test.ts index 927d740ed9..038cb93eef 100644 --- a/tests/src/core/Transform.test.ts +++ b/tests/src/core/Transform.test.ts @@ -117,7 +117,9 @@ describe("Transform test", function () { expect(entity0.transform instanceof Transform).to.equal(true); expect(entity0.transform instanceof SubClassOfTransform).to.equal(true); expect(entity0.transform.position).to.deep.include({ x: 1, y: 2, z: 3 }); - expect(entity0.transform.rotation).to.deep.include({ x: 0, y: 45, z: 0 }); + expect(entity0.transform.rotation.x).to.be.approximately(0, 1e-6); + expect(entity0.transform.rotation.y).to.be.approximately(45, 1e-6); + expect(entity0.transform.rotation.z).to.be.approximately(0, 1e-6); expect(entity0.transform.scale).to.deep.include({ x: 1, y: 2, z: 3 }); const preTransform1 = entity1.transform; @@ -126,7 +128,9 @@ describe("Transform test", function () { expect(entity1.transform instanceof Transform).to.equal(true); expect(entity1.transform instanceof SubClassOfTransform).to.equal(false); expect(entity1.transform.position).to.deep.include({ x: 4, y: 5, z: 6 }); - expect(entity1.transform.rotation).to.deep.include({ x: 0, y: 90, z: 0 }); + expect(entity1.transform.rotation.x).to.be.approximately(0, 1e-6); + expect(entity1.transform.rotation.y).to.be.approximately(90, 1e-6); + expect(entity1.transform.rotation.z).to.be.approximately(0, 1e-6); expect(entity1.transform.scale).to.deep.include({ x: 4, y: 5, z: 6 }); }); diff --git a/tests/src/core/particle/ParticleSerialization.test.ts b/tests/src/core/particle/ParticleSerialization.test.ts index f47f0ac793..88c8647b0b 100644 --- a/tests/src/core/particle/ParticleSerialization.test.ts +++ b/tests/src/core/particle/ParticleSerialization.test.ts @@ -1,22 +1,32 @@ -import { - CurveKey, - GradientAlphaKey, - GradientColorKey, - ParticleCurve, - ParticleGradient -} from "@galacean/engine-core"; +import { CurveKey, GradientAlphaKey, GradientColorKey, ParticleCurve, ParticleGradient } from "@galacean/engine-core"; import { Color } from "@galacean/engine-math"; import { describe, expect, it } from "vitest"; describe("particle serializable collection properties", () => { it("replaces and sorts ParticleCurve keys", () => { - const curve = new ParticleCurve(new CurveKey(0.5, 1)); + const original = new CurveKey(0.5, 1); + const curve = new ParticleCurve(original); const early = new CurveKey(0.25, 2); const late = new CurveKey(0.75, 3); + let changes = 0; + curve._registerOnValueChanged(() => changes++); curve.keys = [late, early]; expect(curve.keys).to.deep.equal([early, late]); + changes = 0; + original.value = 4; + expect(changes).to.equal(0); + early.value = 5; + expect(changes).to.equal(1); + + const currentKeys = curve.keys; + curve.setKeys(currentKeys); + expect(curve.keys).to.deep.equal([early, late]); + + const tooManyKeys = Array.from({ length: 5 }, (_, index) => new CurveKey(index, index)); + expect(() => curve.setKeys(tooManyKeys)).to.throw("Curve can only have 4 keys"); + expect(curve.keys).to.deep.equal([early, late]); }); it("replaces ParticleGradient color and alpha keys independently", () => { @@ -26,6 +36,7 @@ describe("particle serializable collection properties", () => { gradient.colorKeys = [colorKey]; gradient.alphaKeys = [alphaKey]; + gradient.setKeys(gradient.colorKeys, gradient.alphaKeys); expect(gradient.colorKeys).to.deep.equal([colorKey]); expect(gradient.alphaKeys).to.deep.equal([alphaKey]); diff --git a/tests/src/core/physics/DynamicCollider.test.ts b/tests/src/core/physics/DynamicCollider.test.ts index 33c5d76a96..dde821480e 100644 --- a/tests/src/core/physics/DynamicCollider.test.ts +++ b/tests/src/core/physics/DynamicCollider.test.ts @@ -94,6 +94,15 @@ describe("DynamicCollider", function () { expect(physics.getDefaultSleepThreshold()).to.closeTo(0.02, 1e-6); }); + it("rejects invalid tolerancesScale before initialization", function () { + expect(() => new PhysXPhysics({ tolerancesScale: { length: 0 } })).to.throw( + "tolerancesScale.length must be a positive finite number" + ); + expect(() => new PhysXPhysics({ tolerancesScale: { speed: Number.NaN } })).to.throw( + "tolerancesScale.speed must be a positive finite number" + ); + }); + it("addShape and removeShape", function () { const collider = rootEntity.createChild("entity").addComponent(DynamicCollider); const boxCollider = new BoxColliderShape(); diff --git a/tests/src/core/physics/MeshColliderShape.test.ts b/tests/src/core/physics/MeshColliderShape.test.ts index e1031dc7b1..77fbb2dd1a 100644 --- a/tests/src/core/physics/MeshColliderShape.test.ts +++ b/tests/src/core/physics/MeshColliderShape.test.ts @@ -977,6 +977,38 @@ describe("MeshColliderShape PhysX", () => { defaultMaterial?.destroy(); }); + it("rolls back cached state when native recooking throws", () => { + const entity = root.createChild("recookingRollback"); + const staticCollider = entity.addComponent(StaticCollider); + const meshShape = new MeshColliderShape(); + const defaultMaterial = meshShape.material; + const originalMesh = createModelMesh(engine, [0, 0, 0, 1, 0, 0, 0, 1, 0], [0, 1, 2]); + const replacementMesh = createModelMesh(engine, [0, 0, 0, 2, 0, 0, 0, 2, 0], [0, 1, 2]); + meshShape.mesh = originalMesh; + staticCollider.addShape(meshShape); + + const originalFlags = meshShape.cookingFlags; + const nativeShape = (meshShape as any)._nativeShape; + vi.spyOn(nativeShape, "setMeshData").mockImplementation(() => { + throw new Error("recook failed"); + }); + + expect(() => { + meshShape.cookingFlags = MeshColliderShapeCookingFlag.Cleaning; + }).toThrow("recook failed"); + expect(meshShape.cookingFlags).toBe(originalFlags); + + expect(() => { + meshShape.mesh = replacementMesh; + }).toThrow("recook failed"); + expect(meshShape.mesh).toBe(originalMesh); + expect(originalMesh._getReferCount()).toBe(1); + expect(replacementMesh._getReferCount()).toBe(0); + + entity.destroy(); + defaultMaterial?.destroy(); + }); + it("should not update when no mesh is set", () => { const meshShape = new MeshColliderShape(); const defaultMaterial = meshShape.material; diff --git a/tests/src/core/physics/PhysicsScene.test.ts b/tests/src/core/physics/PhysicsScene.test.ts index eaf4bdbe29..72382e3ecd 100644 --- a/tests/src/core/physics/PhysicsScene.test.ts +++ b/tests/src/core/physics/PhysicsScene.test.ts @@ -96,8 +96,8 @@ function watchNativeContactEventDemand(physicsScene: PhysicsScene) { }; } -function getLastContactEventDemandCall(calls: boolean[]): boolean { - return calls[calls.length - 1] ?? false; +function getLastContactEventDemandCall(calls: boolean[]): boolean | undefined { + return calls[calls.length - 1]; } function resetSpy() { @@ -199,7 +199,8 @@ describe("Physics Test", () => { }); it("auto-disables native contact events when no active collision callback exists", () => { - const scene = enginePhysX.sceneManager.activeScene; + const scene = new Scene(enginePhysX); + enginePhysX.sceneManager.addScene(scene); const physicsScene = scene.physics; const root = scene.createRootEntity("contact-demand-disabled"); const entity = root.createChild("body"); @@ -212,7 +213,7 @@ describe("Physics Test", () => { expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false); } finally { contactEventDemand.restore(); - root.destroy(); + scene.destroy(); } }); diff --git a/tests/src/loader/GLTFLoader.test.ts b/tests/src/loader/GLTFLoader.test.ts index acdc6a69af..4b9d50b646 100644 --- a/tests/src/loader/GLTFLoader.test.ts +++ b/tests/src/loader/GLTFLoader.test.ts @@ -603,16 +603,27 @@ afterAll(() => { describe("glTF Loader test", function () { it("resolves scene sub-assets by the canonical glTF schema key", async () => { + const sceneRoot = await engine.resourceManager.load({ + type: AssetType.GLTF, + url: "mock/path/testRoot.gltf?q=scenes[0]" + }); const glTFResource = await engine.resourceManager.load({ type: AssetType.GLTF, url: "mock/path/testRoot.gltf" }); - const sceneRoot = await engine.resourceManager.load({ + + expect(sceneRoot).to.equal(glTFResource.scenes[0]); + }); + + it("rejects an out-of-range glTF scene index with a descriptive error", async () => { + const glTFResource = await engine.resourceManager.load({ type: AssetType.GLTF, - url: "mock/path/testRoot.gltf?q=scenes[0]" + url: "mock/path/testRoot.gltf" }); - expect(sceneRoot).to.equal(glTFResource.scenes[0]); + expect(() => glTFResource.instantiateSceneRoot(glTFResource.scenes.length)).to.throw( + `scene index ${glTFResource.scenes.length} is out of range` + ); }); it("Pipeline Parser", async () => { From 2f0e8ef4b16b15d43a23ee30256ec245aba9aa73 Mon Sep 17 00:00:00 2001 From: luzhuang Date: Tue, 14 Jul 2026 23:08:00 +0800 Subject: [PATCH 15/23] refactor: isolate loader runtime contract --- .../src/particle/modules/EmissionModule.ts | 14 ---- .../src/particle/modules/ParticleCurve.ts | 28 +------ .../src/particle/modules/ParticleGradient.ts | 26 ++---- packages/galacean/src/index.ts | 6 +- .../resources/parser/ReflectionParser.ts | 34 ++------ packages/loader/src/schema/CommonSchema.ts | 9 -- tests/src/core/particle/Burst.test.ts | 15 ---- .../particle/ParticleSerialization.test.ts | 44 ---------- tests/src/loader/SceneFormatV2.test.ts | 84 ------------------- 9 files changed, 22 insertions(+), 238 deletions(-) delete mode 100644 tests/src/core/particle/ParticleSerialization.test.ts diff --git a/packages/core/src/particle/modules/EmissionModule.ts b/packages/core/src/particle/modules/EmissionModule.ts index 3463a11f8b..a2d303a7d3 100644 --- a/packages/core/src/particle/modules/EmissionModule.ts +++ b/packages/core/src/particle/modules/EmissionModule.ts @@ -97,20 +97,6 @@ export class EmissionModule extends ParticleGeneratorModule { return this._bursts; } - /** - * Replaces the burst array, sorted by emission time. - */ - set bursts(value: ReadonlyArray) { - const bursts = this._bursts; - if (value === bursts) return; - - bursts.length = 0; - for (let i = 0, n = value.length; i < n; i++) { - this.addBurst(value[i]); - } - this._resyncCursors(this._generator._playTime); - } - /** * Add a single burst. * @param burst - The burst diff --git a/packages/core/src/particle/modules/ParticleCurve.ts b/packages/core/src/particle/modules/ParticleCurve.ts index 77afd49f84..fdba01cb62 100644 --- a/packages/core/src/particle/modules/ParticleCurve.ts +++ b/packages/core/src/particle/modules/ParticleCurve.ts @@ -22,14 +22,6 @@ export class ParticleCurve { return this._keys; } - /** - * Replaces the keys of the curve. - */ - set keys(value: ReadonlyArray) { - if (value === this._keys) return; - this.setKeys(value); - } - /** * Create a new particle curve. * @param keys - The keys of the curve @@ -87,24 +79,12 @@ export class ParticleCurve { * Set the keys of the curve. * @param keys - The keys */ - setKeys(keys: ReadonlyArray): void { - if (keys.length > 4) { - throw new Error("Curve can only have 4 keys"); - } - - const nextKeys = [...keys]; - const currentKeys = this._keys; - for (let i = 0, n = currentKeys.length; i < n; i++) { - currentKeys[i]._unRegisterOnValueChanged(this._updateDispatch); - } - currentKeys.length = 0; - for (let i = 0, n = nextKeys.length; i < n; i++) { - const key = nextKeys[i]; - this._addKey(currentKeys, key); - key._registerOnValueChanged(this._updateDispatch); + setKeys(keys: CurveKey[]): void { + this._keys.length = 0; + for (let i = 0, n = keys.length; i < n; i++) { + this.addKey(keys[i]); } this._typeArrayDirty = true; - this._updateDispatch(); } /** diff --git a/packages/core/src/particle/modules/ParticleGradient.ts b/packages/core/src/particle/modules/ParticleGradient.ts index 34b896f12e..732f879f5c 100644 --- a/packages/core/src/particle/modules/ParticleGradient.ts +++ b/packages/core/src/particle/modules/ParticleGradient.ts @@ -24,12 +24,6 @@ export class ParticleGradient { return this._colorKeys; } - /** Replaces the color keys while preserving the alpha keys. */ - set colorKeys(value: ReadonlyArray) { - if (value === this._colorKeys) return; - this.setKeys(value, [...this._alphaKeys]); - } - /** * The alpha keys of the gradient. */ @@ -37,12 +31,6 @@ export class ParticleGradient { return this._alphaKeys; } - /** Replaces the alpha keys while preserving the color keys. */ - set alphaKeys(value: ReadonlyArray) { - if (value === this._alphaKeys) return; - this.setKeys([...this._colorKeys], value); - } - /** * Create a new particle gradient. * @param colorKeys - The color keys of the gradient @@ -140,7 +128,7 @@ export class ParticleGradient { * @param colorKeys - The color keys * @param alphaKeys - The alpha keys */ - setKeys(colorKeys: ReadonlyArray, alphaKeys: ReadonlyArray): void { + setKeys(colorKeys: GradientColorKey[], alphaKeys: GradientAlphaKey[]): void { if (colorKeys.length > 4) { throw new Error("Gradient can only have 4 color keys"); } @@ -148,8 +136,6 @@ export class ParticleGradient { throw new Error("Gradient can only have 4 alpha keys"); } - const nextColorKeys = [...colorKeys]; - const nextAlphaKeys = [...alphaKeys]; const currentColorKeys = this._colorKeys; const currentAlphaKeys = this._alphaKeys; for (let i = 0, n = currentColorKeys.length; i < n; i++) { @@ -161,8 +147,14 @@ export class ParticleGradient { currentColorKeys.length = 0; currentAlphaKeys.length = 0; - for (let i = 0, n = nextColorKeys.length; i < n; i++) this.addColorKey(nextColorKeys[i]); - for (let i = 0, n = nextAlphaKeys.length; i < n; i++) this.addAlphaKey(nextAlphaKeys[i]); + for (let i = 0, n = colorKeys.length; i < n; i++) { + this._addKey(currentColorKeys, colorKeys[i]); + } + for (let i = 0, n = alphaKeys.length; i < n; i++) { + this._addKey(currentAlphaKeys, alphaKeys[i]); + } + this._alphaTypeArrayDirty = true; + this._colorTypeArrayDirty = true; } /** diff --git a/packages/galacean/src/index.ts b/packages/galacean/src/index.ts index b5036df5d5..de3768419f 100644 --- a/packages/galacean/src/index.ts +++ b/packages/galacean/src/index.ts @@ -1,5 +1,4 @@ import * as CoreObjects from "@galacean/engine-core"; -import * as MathObjects from "@galacean/engine-math"; import { Loader, Polyfill, SystemInfo } from "@galacean/engine-core"; import { ShaderPool } from "./ShaderPool"; //@ts-ignore @@ -12,12 +11,9 @@ export * from "@galacean/engine-loader"; export * from "@galacean/engine-math"; export * from "@galacean/engine-rhi-webgl"; -for (const key in CoreObjects) { +for (let key in CoreObjects) { Loader.registerClass(key, CoreObjects[key]); } -for (const key in MathObjects) { - Loader.registerClass(key, MathObjects[key]); -} // Bootstrap the Galacean engine flavor: browser polyfills first (must // patch globals before anything else runs), then platform detection diff --git a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts index a2548c7293..2e038238ce 100644 --- a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts +++ b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts @@ -40,21 +40,13 @@ export class ReflectionParser { for (let i = 0, n = calls.length; i < n; i++) { const call = calls[i]; chain = chain.then(() => { - let target = instance; - if (call.target !== undefined) { - if (!Array.isArray(call.target) || call.target.some((key) => typeof key !== "string" || key.length === 0)) { - return Promise.reject(new Error(`Call "${call.method}" target must be an array of non-empty strings`)); - } - for (const key of call.target) target = target?.[key]; - } - const method = target?.[call.method]; + const method = instance?.[call.method]; if (typeof method !== "function") { - const path = call.target?.length ? `${call.target.join(".")}.` : ""; - return Promise.reject(new Error(`Call target does not have method "${path}${call.method}"`)); + return Promise.reject(new Error(`Call target does not have method "${call.method}"`)); } return Promise.all((call.args ?? []).map((arg) => this._resolveValue(arg))) - .then((resolvedArgs) => Promise.resolve(method.apply(target, resolvedArgs))) + .then((resolvedArgs) => Promise.resolve(method.apply(instance, resolvedArgs))) .then((result) => { if (!call.result) return result; if (result == null || (typeof result !== "object" && typeof result !== "function")) { @@ -85,7 +77,7 @@ export class ReflectionParser { * 1. null/undefined/primitive → passthrough * 2. Array → recurse each element * 3. { $ref } → asset reference - * 4. { $type, $args? } → polymorphic type construct + * 4. { $type } → polymorphic type construct * 5. { $class } → registered class constructor * 6. { $entity } → entity reference by path (flat index + optional children descent) * 7. { $component } → component reference @@ -117,18 +109,12 @@ export class ReflectionParser { }); } - // $type — polymorphic type: resolve constructor args, construct instance, then apply remaining props + // $type — polymorphic type: construct instance and apply remaining props if ("$type" in obj) { - const { $type, $args, ...rest } = obj; - const constructorArgs = $args === undefined ? [] : $args; - if (!Array.isArray(constructorArgs)) { - return Promise.reject(new Error("$args must be an array when used with $type")); - } + const { $type, ...rest } = obj; return this._resolveRegisteredClass($type, "$type").then((Class) => { - return Promise.all(constructorArgs.map((arg) => this._resolveValue(arg))).then((args) => { - const instance = new Class(...args); - return Object.keys(rest).length > 0 ? this.parseProps(instance, rest) : instance; - }); + const instance = new Class(); + return Object.keys(rest).length > 0 ? this.parseProps(instance, rest) : instance; }); } @@ -152,10 +138,6 @@ export class ReflectionParser { return this._resolveSignal(originValue, obj.$signal as SignalListener[]); } - if ("$args" in obj) { - return Promise.reject(new Error("$args requires $type")); - } - // Plain object — recurse each value, modifying originValue in place or building a new object const target = originValue && typeof originValue === "object" && !Array.isArray(originValue) diff --git a/packages/loader/src/schema/CommonSchema.ts b/packages/loader/src/schema/CommonSchema.ts index b162231768..ae61dc8ce4 100644 --- a/packages/loader/src/schema/CommonSchema.ts +++ b/packages/loader/src/schema/CommonSchema.ts @@ -20,13 +20,6 @@ export interface ClassRef { $class: string; } -/** Registered runtime value with optional recursively-resolved constructor arguments. */ -export interface TypeValue { - $type: string; - $args?: unknown[]; - [key: string]: unknown; -} - export interface SignalListener { target: { $component: ComponentRef }; methodName: string; @@ -34,8 +27,6 @@ export interface SignalListener { } export interface CallSpec { - /** Optional property path from the mutation root to the method owner. */ - target?: string[]; method: string; args?: unknown[]; result?: MutationBlock; diff --git a/tests/src/core/particle/Burst.test.ts b/tests/src/core/particle/Burst.test.ts index 5a6ff8761c..7765171e54 100644 --- a/tests/src/core/particle/Burst.test.ts +++ b/tests/src/core/particle/Burst.test.ts @@ -67,21 +67,6 @@ describe("Burst", () => { expect(burst.repeatInterval).to.equal(0.1); }); - it("Replace bursts through the serializable property contract", () => { - const scene = engine.sceneManager.activeScene; - const entity = scene.createRootEntity("ReplaceBursts"); - const emission = entity.addComponent(ParticleRenderer).generator.emission; - const existing = new Burst(1, new ParticleCompositeCurve(1)); - const early = new Burst(0.25, new ParticleCompositeCurve(2)); - const late = new Burst(0.75, new ParticleCompositeCurve(3)); - emission.addBurst(existing); - - emission.bursts = [late, early]; - - expect(emission.bursts).to.deep.equal([early, late]); - entity.destroy(); - }); - it("Single cycle backward compatible", () => { const scene = engine.sceneManager.activeScene; const entity = scene.createRootEntity("SingleCycle"); diff --git a/tests/src/core/particle/ParticleSerialization.test.ts b/tests/src/core/particle/ParticleSerialization.test.ts deleted file mode 100644 index 88c8647b0b..0000000000 --- a/tests/src/core/particle/ParticleSerialization.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CurveKey, GradientAlphaKey, GradientColorKey, ParticleCurve, ParticleGradient } from "@galacean/engine-core"; -import { Color } from "@galacean/engine-math"; -import { describe, expect, it } from "vitest"; - -describe("particle serializable collection properties", () => { - it("replaces and sorts ParticleCurve keys", () => { - const original = new CurveKey(0.5, 1); - const curve = new ParticleCurve(original); - const early = new CurveKey(0.25, 2); - const late = new CurveKey(0.75, 3); - let changes = 0; - curve._registerOnValueChanged(() => changes++); - - curve.keys = [late, early]; - - expect(curve.keys).to.deep.equal([early, late]); - changes = 0; - original.value = 4; - expect(changes).to.equal(0); - early.value = 5; - expect(changes).to.equal(1); - - const currentKeys = curve.keys; - curve.setKeys(currentKeys); - expect(curve.keys).to.deep.equal([early, late]); - - const tooManyKeys = Array.from({ length: 5 }, (_, index) => new CurveKey(index, index)); - expect(() => curve.setKeys(tooManyKeys)).to.throw("Curve can only have 4 keys"); - expect(curve.keys).to.deep.equal([early, late]); - }); - - it("replaces ParticleGradient color and alpha keys independently", () => { - const gradient = new ParticleGradient(); - const colorKey = new GradientColorKey(0.5, new Color(1, 0.5, 0.25, 1)); - const alphaKey = new GradientAlphaKey(0.5, 0.75); - - gradient.colorKeys = [colorKey]; - gradient.alphaKeys = [alphaKey]; - gradient.setKeys(gradient.colorKeys, gradient.alphaKeys); - - expect(gradient.colorKeys).to.deep.equal([colorKey]); - expect(gradient.alphaKeys).to.deep.equal([alphaKey]); - }); -}); diff --git a/tests/src/loader/SceneFormatV2.test.ts b/tests/src/loader/SceneFormatV2.test.ts index 5c7b51b85f..d03f397f64 100644 --- a/tests/src/loader/SceneFormatV2.test.ts +++ b/tests/src/loader/SceneFormatV2.test.ts @@ -25,7 +25,6 @@ import { ReflectionParser } from "../../../packages/loader/src/resource-deserial import { SceneParser } from "../../../packages/loader/src/resource-deserialize/resources/scene/SceneParser"; import { GLTFResource } from "../../../packages/loader/src/gltf/GLTFResource"; import { WebGLEngine } from "@galacean/engine"; -import { Color, Quaternion, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; Loader.registerClass("Transform", Transform); @@ -39,26 +38,10 @@ class TestValueType { } Loader.registerClass("TestValueType", TestValueType); -class ConstructorValueType { - label = ""; - - constructor( - readonly seed: string, - readonly child: TestValueType - ) {} -} -Loader.registerClass("ConstructorValueType", ConstructorValueType); - class CallOrderComponent extends Script { value = ""; receivedArgs: any[] = []; lastResult: any = null; - nested = { - value: "", - setValue(value: string) { - this.value = value; - } - }; appendSuffix(suffix: string): void { this.value += suffix; @@ -97,14 +80,6 @@ describe("SceneFile v2 schema enums", () => { }); }); -describe("Galacean flavor class registration", () => { - it("registers public math value types for source-v2 $type values", () => { - for (const [name, Class] of Object.entries({ Color, Quaternion, Vector2, Vector3, Vector4 })) { - expect(Loader.getClass(name)).to.equal(Class); - } - }); -}); - beforeAll(async function () { const canvasDOM = document.createElement("canvas"); canvasDOM.width = 100; @@ -357,17 +332,6 @@ describe("ReflectionParser calls resolution", () => { expect(target.lastResult.y).to.equal(2); }); - it("should invoke methods on a nested target path", async () => { - const scene = new Scene(engine); - const context = new ParserContext(engine, ParserType.Scene, scene); - const parser = new ReflectionParser(context, []); - const target = new CallOrderComponent(new Entity(engine, "host")); - - await parser.parseCalls(target, [{ target: ["nested"], method: "setValue", args: ["ready"] }]); - - expect(target.nested.value).to.equal("ready"); - }); - it("should await each call before executing the next one", async () => { const scene = new Scene(engine); const context = new ParserContext(engine, ParserType.Scene, scene); @@ -835,21 +799,6 @@ describe("ReflectionParser $signal resolution", () => { // --------------------------------------------------------------------------- describe("ReflectionParser $type resolution", () => { - it("should construct built-in math types registered by the engine package", async () => { - const scene = new Scene(engine); - const context = new ParserContext(engine, ParserType.Scene, scene); - const parser = new ReflectionParser(context, []); - const target: any = {}; - - await parser.parseProps(target, { - value: { $type: "Vector2", x: 10, y: 20 } - }); - - expect(target.value).to.be.instanceOf(Vector2); - expect(target.value.x).to.equal(10); - expect(target.value.y).to.equal(20); - }); - it("should construct $type instance and apply remaining props", async () => { const scene = new Scene(engine); const context = new ParserContext(engine, ParserType.Scene, scene); @@ -876,39 +825,6 @@ describe("ReflectionParser $type resolution", () => { expect(target.value.y).to.equal(0); }); - it("should recursively resolve $args before construction and then apply props", async () => { - const scene = new Scene(engine); - const context = new ParserContext(engine, ParserType.Scene, scene); - const parser = new ReflectionParser(context, []); - const target: any = {}; - await parser.parseProps(target, { - value: { - $type: "ConstructorValueType", - $args: ["seed", { $type: "TestValueType", x: 3, y: 4 }], - label: "ready" - } - }); - expect(target.value).to.be.instanceOf(ConstructorValueType); - expect(target.value.seed).to.equal("seed"); - expect(target.value.child).to.be.instanceOf(TestValueType); - expect(target.value.child).to.deep.include({ x: 3, y: 4 }); - expect(target.value.label).to.equal("ready"); - }); - - it("should reject $args without an array-valued $type constructor contract", async () => { - const scene = new Scene(engine); - const context = new ParserContext(engine, ParserType.Scene, scene); - const parser = new ReflectionParser(context, []); - const target: any = {}; - - await expect( - parser.parseProps(target, { - value: { $type: "TestValueType", $args: "invalid" } as any - }) - ).rejects.toThrow("$args must be an array when used with $type"); - await expect(parser.parseProps(target, { value: { $args: [] } })).rejects.toThrow("$args requires $type"); - }); - it("should throw a clear error when $type references an unregistered class", async () => { const scene = new Scene(engine); const context = new ParserContext(engine, ParserType.Scene, scene); From f2203ee8f48b42497baa5b735f161bf4bb331332 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Sun, 19 Jul 2026 23:21:24 +0800 Subject: [PATCH 16/23] feat: migrate shaderlab fixes to 2.0 --- .gitignore | 3 + e2e/.dev/public/spineboy.atlas | 94 + e2e/.dev/public/spineboy.json | 8723 +++++++++++++++++ e2e/.dev/public/spineboy.png | Bin 0 -> 245321 bytes e2e/.dev/public/tank-pro.atlas | 65 + e2e/.dev/public/tank-pro.json | 1 + e2e/.dev/public/tank-pro.png | Bin 0 -> 533353 bytes e2e/case/spine-spineboy.ts | 36 + e2e/case/spine-tint-black.ts | 41 + e2e/case/sprite-filled.ts | 63 + e2e/config.ts | 22 + .../originImage/Spine_spine-spineboy.jpg | 3 + .../originImage/Spine_spine-tint-black.jpg | 3 + .../originImage/Sprite_sprite-filled.jpg | 3 + e2e/package.json | 2 + examples/package.json | 3 + examples/src/spine-keli-4.2.ts | 60 + examples/src/spine-otakugirl-3.8.ts | 55 + examples/src/sprite-mask.ts | 87 + examples/src/ui-mask-alpha.ts | 97 + examples/src/ui-mask-overlay.ts | 120 + examples/src/ui-mask.ts | 85 + examples/src/ui-rect-mask-nested.ts | 76 + examples/src/ui-rect-mask.ts | 135 + examples/src/ui-text-outline.ts | 121 + .../src/2d/assembler/FilledSpriteAssembler.ts | 619 ++ .../core/src/2d/assembler/ISpriteRenderer.ts | 6 + packages/core/src/2d/atlas/FontAtlas.ts | 8 +- packages/core/src/2d/enums/SpriteDrawMode.ts | 4 +- .../core/src/2d/enums/SpriteFilledMode.ts | 15 + .../core/src/2d/enums/SpriteFilledOrigin.ts | 21 + packages/core/src/2d/enums/TextOverflow.ts | 6 +- packages/core/src/2d/index.ts | 3 + packages/core/src/2d/sprite/MaskRenderable.ts | 398 + packages/core/src/2d/sprite/SpriteMask.ts | 270 +- .../core/src/2d/sprite/SpriteMaskUtils.ts | 136 + packages/core/src/2d/sprite/SpriteRenderer.ts | 126 +- packages/core/src/2d/sprite/index.ts | 3 + packages/core/src/2d/text/TextRenderer.ts | 149 +- packages/core/src/2d/text/TextUtils.ts | 99 +- packages/core/src/Engine.ts | 1 + packages/core/src/Entity.ts | 16 +- .../core/src/RenderPipeline/MaskManager.ts | 74 +- .../src/RenderPipeline/VertexMergeBatcher.ts | 54 + packages/core/src/SceneManager.ts | 7 +- packages/core/src/Transform.ts | 48 +- packages/core/src/asset/AssetPromise.ts | 8 +- packages/core/src/asset/ResourceManager.ts | 6 +- packages/core/src/clone/CloneManager.ts | 157 +- packages/core/src/input/InputManager.ts | 7 + packages/core/src/input/pointer/Pointer.ts | 14 +- .../src/input/pointer/PointerEventData.ts | 7 + .../core/src/input/pointer/PointerManager.ts | 26 +- .../emitter/PhysicsPointerEventEmitter.ts | 18 +- .../pointer/emitter/PointerEventEmitter.ts | 4 +- packages/core/src/mesh/ModelMesh.ts | 2 +- packages/core/src/shader/ShaderData.ts | 12 +- packages/core/src/shader/ShaderFactory.ts | 68 +- packages/core/src/shader/ShaderPass.ts | 6 +- packages/core/src/ui/UIUtils.ts | 70 +- packages/galacean/src/ShaderPool.ts | 4 + packages/loader/src/SceneLoader.ts | 11 +- .../resources/parser/ReflectionParser.ts | 7 +- packages/loader/src/schema/SceneSchema.ts | 4 + .../shader-compiler/src/ShaderCompiler.ts | 9 + .../src/ShaderCompilerUtils.ts | 7 + packages/shader-compiler/src/lalr/LALR1.ts | 7 +- packages/shader-compiler/src/lalr/State.ts | 6 + packages/shader/src/Shaders/2D/Spine.shader | 80 + packages/shader/src/Shaders/2D/Text.shader | 64 +- .../shader/src/Shaders/2D/UIDefault.shader | 27 + .../src/Shaders/2D/UIOverlayBlit.shader | 64 + packages/shader/src/Shaders/index.ts | 4 + packages/spine-core-3.8/package.json | 40 + packages/spine-core-3.8/src/Spine38Runtime.ts | 94 + packages/spine-core-3.8/src/SpineGenerator.ts | 358 + packages/spine-core-3.8/src/SpineTexture.ts | 50 + packages/spine-core-3.8/src/index.ts | 17 + .../src/spine-core/Animation.ts | 1828 ++++ .../src/spine-core/AnimationState.ts | 1193 +++ .../src/spine-core/AnimationStateData.ts | 48 + .../src/spine-core/AtlasAttachmentLoader.ts | 55 + .../src/spine-core/BlendMode.ts | 7 + .../spine-core-3.8/src/spine-core/Bone.ts | 391 + .../spine-core-3.8/src/spine-core/BoneData.ts | 66 + .../src/spine-core/ConstraintData.ts | 8 + .../spine-core-3.8/src/spine-core/Event.ts | 22 + .../src/spine-core/EventData.ts | 16 + .../src/spine-core/IkConstraint.ts | 346 + .../src/spine-core/IkConstraintData.ts | 37 + .../src/spine-core/PathConstraint.ts | 500 + .../src/spine-core/PathConstraintData.ts | 68 + .../spine-core-3.8/src/spine-core/Skeleton.ts | 592 ++ .../src/spine-core/SkeletonBinary.ts | 942 ++ .../src/spine-core/SkeletonBounds.ts | 215 + .../src/spine-core/SkeletonClipping.ts | 363 + .../src/spine-core/SkeletonData.ts | 198 + .../src/spine-core/SkeletonJson.ts | 906 ++ .../spine-core-3.8/src/spine-core/Skin.ts | 182 + .../spine-core-3.8/src/spine-core/Slot.ts | 86 + .../spine-core-3.8/src/spine-core/SlotData.ts | 38 + .../spine-core-3.8/src/spine-core/Texture.ts | 96 + .../src/spine-core/TextureAtlas.ts | 187 + .../src/spine-core/TransformConstraint.ts | 296 + .../src/spine-core/TransformConstraintData.ts | 50 + .../src/spine-core/Triangulator.ts | 269 + .../src/spine-core/Updatable.ts | 10 + .../spine-core-3.8/src/spine-core/Utils.ts | 417 + .../src/spine-core/VertexEffect.ts | 8 + .../src/spine-core/attachments/Attachment.ts | 147 + .../attachments/AttachmentLoader.ts | 31 + .../spine-core/attachments/AttachmentType.ts | 9 + .../attachments/BoundingBoxAttachment.ts | 22 + .../attachments/ClippingAttachment.ts | 27 + .../spine-core/attachments/MeshAttachment.ts | 175 + .../spine-core/attachments/PathAttachment.ts | 36 + .../spine-core/attachments/PointAttachment.ts | 46 + .../attachments/RegionAttachment.ts | 211 + .../spine-core-3.8/src/spine-core/index.ts | 50 + .../src/spine-core/polyfills.ts | 13 + .../spine-core/vertexeffects/JitterEffect.ts | 22 + .../spine-core/vertexeffects/SwirlEffect.ts | 38 + .../spine-core-3.8/src/util/ClearablePool.ts | 26 + .../spine-core-3.8/src/util/ReturnablePool.ts | 25 + packages/spine-core-3.8/tsconfig.json | 18 + packages/spine-core-4.2/package.json | 41 + packages/spine-core-4.2/src/Spine42Runtime.ts | 81 + packages/spine-core-4.2/src/SpineGenerator.ts | 342 + packages/spine-core-4.2/src/SpineTexture.ts | 49 + packages/spine-core-4.2/src/index.ts | 15 + .../spine-core-4.2/src/util/ClearablePool.ts | 26 + .../spine-core-4.2/src/util/ReturnablePool.ts | 25 + packages/spine-core-4.2/tsconfig.json | 18 + packages/spine/package.json | 37 + packages/spine/src/SpineConstant.ts | 13 + packages/spine/src/enums/SpineBlendMode.ts | 14 + packages/spine/src/index.ts | 21 + packages/spine/src/loader/LoaderUtils.ts | 96 + packages/spine/src/loader/SpineAtlasLoader.ts | 107 + packages/spine/src/loader/SpineLoader.ts | 144 + packages/spine/src/loader/SpineResource.ts | 112 + packages/spine/src/loader/index.ts | 5 + .../src/renderer/SpineAnimationRenderer.ts | 422 + packages/spine/src/renderer/SpineMaterial.ts | 106 + packages/spine/src/renderer/index.ts | 2 + .../spine/src/runtime/ISpineRenderTarget.ts | 30 + packages/spine/src/runtime/ISpineRuntime.ts | 52 + .../spine/src/runtime/SpineRuntimeRegistry.ts | 25 + packages/spine/tsconfig.json | 18 + packages/ui/src/Utils.ts | 59 +- packages/ui/src/component/UICanvas.ts | 23 +- packages/ui/src/component/UIRenderer.ts | 219 +- packages/ui/src/component/advanced/Image.ts | 119 +- packages/ui/src/component/advanced/Mask.ts | 80 + .../ui/src/component/advanced/RectMask2D.ts | 157 + packages/ui/src/component/advanced/Text.ts | 157 +- packages/ui/src/component/index.ts | 10 +- .../ui/src/input/UIPointerEventEmitter.ts | 21 +- pnpm-lock.yaml | 61 + rollup.config.js | 5 +- tests/package.json | 5 +- tests/src/core/2d/text/TextUtils.test.ts | 84 + tests/src/core/CloneUtils.test.ts | 162 +- tests/src/core/Entity.test.ts | 28 +- tests/src/core/Scene.test.ts | 35 +- tests/src/core/SpriteRenderer.test.ts | 432 + tests/src/core/input/InputManager.test.ts | 60 + .../core/particle/ParticleStopResume.test.ts | 142 + .../core/resource/SceneLoaderCache.test.ts | 98 + tests/src/loader/ScenePhysics.test.ts | 53 + tests/src/spine/LoaderUtils.test.ts | 16 + tests/src/spine/Pool.test.ts | 41 + .../src/spine/SpineAnimationRenderer.test.ts | 72 + tests/src/spine/SpineConstant.test.ts | 20 + tests/src/spine/SpineLoader.test.ts | 18 + tests/src/spine/SpineMaterial.test.ts | 36 + tests/src/spine/SpineResource.test.ts | 33 + tests/src/spine/SpineRuntimeRegistry.test.ts | 24 + tests/src/spine/SpineTexture.test.ts | 44 + tests/src/ui/Mask.test.ts | 75 + tests/src/ui/RectMask2D.test.ts | 78 + tests/src/ui/UIEvent.test.ts | 49 +- tests/src/ui/UIInteractive.test.ts | 1 + 183 files changed, 27398 insertions(+), 475 deletions(-) create mode 100644 e2e/.dev/public/spineboy.atlas create mode 100644 e2e/.dev/public/spineboy.json create mode 100644 e2e/.dev/public/spineboy.png create mode 100644 e2e/.dev/public/tank-pro.atlas create mode 100644 e2e/.dev/public/tank-pro.json create mode 100644 e2e/.dev/public/tank-pro.png create mode 100644 e2e/case/spine-spineboy.ts create mode 100644 e2e/case/spine-tint-black.ts create mode 100644 e2e/case/sprite-filled.ts create mode 100644 e2e/fixtures/originImage/Spine_spine-spineboy.jpg create mode 100644 e2e/fixtures/originImage/Spine_spine-tint-black.jpg create mode 100644 e2e/fixtures/originImage/Sprite_sprite-filled.jpg create mode 100644 examples/src/spine-keli-4.2.ts create mode 100644 examples/src/spine-otakugirl-3.8.ts create mode 100644 examples/src/sprite-mask.ts create mode 100644 examples/src/ui-mask-alpha.ts create mode 100644 examples/src/ui-mask-overlay.ts create mode 100644 examples/src/ui-mask.ts create mode 100644 examples/src/ui-rect-mask-nested.ts create mode 100644 examples/src/ui-rect-mask.ts create mode 100644 examples/src/ui-text-outline.ts create mode 100644 packages/core/src/2d/assembler/FilledSpriteAssembler.ts create mode 100644 packages/core/src/2d/enums/SpriteFilledMode.ts create mode 100644 packages/core/src/2d/enums/SpriteFilledOrigin.ts create mode 100644 packages/core/src/2d/sprite/MaskRenderable.ts create mode 100644 packages/core/src/2d/sprite/SpriteMaskUtils.ts create mode 100644 packages/shader/src/Shaders/2D/Spine.shader create mode 100644 packages/shader/src/Shaders/2D/UIOverlayBlit.shader create mode 100644 packages/spine-core-3.8/package.json create mode 100644 packages/spine-core-3.8/src/Spine38Runtime.ts create mode 100644 packages/spine-core-3.8/src/SpineGenerator.ts create mode 100644 packages/spine-core-3.8/src/SpineTexture.ts create mode 100644 packages/spine-core-3.8/src/index.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Animation.ts create mode 100644 packages/spine-core-3.8/src/spine-core/AnimationState.ts create mode 100644 packages/spine-core-3.8/src/spine-core/AnimationStateData.ts create mode 100644 packages/spine-core-3.8/src/spine-core/AtlasAttachmentLoader.ts create mode 100644 packages/spine-core-3.8/src/spine-core/BlendMode.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Bone.ts create mode 100644 packages/spine-core-3.8/src/spine-core/BoneData.ts create mode 100644 packages/spine-core-3.8/src/spine-core/ConstraintData.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Event.ts create mode 100644 packages/spine-core-3.8/src/spine-core/EventData.ts create mode 100644 packages/spine-core-3.8/src/spine-core/IkConstraint.ts create mode 100644 packages/spine-core-3.8/src/spine-core/IkConstraintData.ts create mode 100644 packages/spine-core-3.8/src/spine-core/PathConstraint.ts create mode 100644 packages/spine-core-3.8/src/spine-core/PathConstraintData.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Skeleton.ts create mode 100644 packages/spine-core-3.8/src/spine-core/SkeletonBinary.ts create mode 100644 packages/spine-core-3.8/src/spine-core/SkeletonBounds.ts create mode 100644 packages/spine-core-3.8/src/spine-core/SkeletonClipping.ts create mode 100644 packages/spine-core-3.8/src/spine-core/SkeletonData.ts create mode 100644 packages/spine-core-3.8/src/spine-core/SkeletonJson.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Skin.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Slot.ts create mode 100644 packages/spine-core-3.8/src/spine-core/SlotData.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Texture.ts create mode 100644 packages/spine-core-3.8/src/spine-core/TextureAtlas.ts create mode 100644 packages/spine-core-3.8/src/spine-core/TransformConstraint.ts create mode 100644 packages/spine-core-3.8/src/spine-core/TransformConstraintData.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Triangulator.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Updatable.ts create mode 100644 packages/spine-core-3.8/src/spine-core/Utils.ts create mode 100644 packages/spine-core-3.8/src/spine-core/VertexEffect.ts create mode 100644 packages/spine-core-3.8/src/spine-core/attachments/Attachment.ts create mode 100644 packages/spine-core-3.8/src/spine-core/attachments/AttachmentLoader.ts create mode 100644 packages/spine-core-3.8/src/spine-core/attachments/AttachmentType.ts create mode 100644 packages/spine-core-3.8/src/spine-core/attachments/BoundingBoxAttachment.ts create mode 100644 packages/spine-core-3.8/src/spine-core/attachments/ClippingAttachment.ts create mode 100644 packages/spine-core-3.8/src/spine-core/attachments/MeshAttachment.ts create mode 100644 packages/spine-core-3.8/src/spine-core/attachments/PathAttachment.ts create mode 100644 packages/spine-core-3.8/src/spine-core/attachments/PointAttachment.ts create mode 100644 packages/spine-core-3.8/src/spine-core/attachments/RegionAttachment.ts create mode 100644 packages/spine-core-3.8/src/spine-core/index.ts create mode 100644 packages/spine-core-3.8/src/spine-core/polyfills.ts create mode 100644 packages/spine-core-3.8/src/spine-core/vertexeffects/JitterEffect.ts create mode 100644 packages/spine-core-3.8/src/spine-core/vertexeffects/SwirlEffect.ts create mode 100644 packages/spine-core-3.8/src/util/ClearablePool.ts create mode 100644 packages/spine-core-3.8/src/util/ReturnablePool.ts create mode 100644 packages/spine-core-3.8/tsconfig.json create mode 100644 packages/spine-core-4.2/package.json create mode 100644 packages/spine-core-4.2/src/Spine42Runtime.ts create mode 100644 packages/spine-core-4.2/src/SpineGenerator.ts create mode 100644 packages/spine-core-4.2/src/SpineTexture.ts create mode 100644 packages/spine-core-4.2/src/index.ts create mode 100644 packages/spine-core-4.2/src/util/ClearablePool.ts create mode 100644 packages/spine-core-4.2/src/util/ReturnablePool.ts create mode 100644 packages/spine-core-4.2/tsconfig.json create mode 100644 packages/spine/package.json create mode 100644 packages/spine/src/SpineConstant.ts create mode 100644 packages/spine/src/enums/SpineBlendMode.ts create mode 100644 packages/spine/src/index.ts create mode 100644 packages/spine/src/loader/LoaderUtils.ts create mode 100644 packages/spine/src/loader/SpineAtlasLoader.ts create mode 100644 packages/spine/src/loader/SpineLoader.ts create mode 100644 packages/spine/src/loader/SpineResource.ts create mode 100644 packages/spine/src/loader/index.ts create mode 100644 packages/spine/src/renderer/SpineAnimationRenderer.ts create mode 100644 packages/spine/src/renderer/SpineMaterial.ts create mode 100644 packages/spine/src/renderer/index.ts create mode 100644 packages/spine/src/runtime/ISpineRenderTarget.ts create mode 100644 packages/spine/src/runtime/ISpineRuntime.ts create mode 100644 packages/spine/src/runtime/SpineRuntimeRegistry.ts create mode 100644 packages/spine/tsconfig.json create mode 100644 packages/ui/src/component/advanced/Mask.ts create mode 100644 packages/ui/src/component/advanced/RectMask2D.ts create mode 100644 tests/src/core/particle/ParticleStopResume.test.ts create mode 100644 tests/src/core/resource/SceneLoaderCache.test.ts create mode 100644 tests/src/loader/ScenePhysics.test.ts create mode 100644 tests/src/spine/LoaderUtils.test.ts create mode 100644 tests/src/spine/Pool.test.ts create mode 100644 tests/src/spine/SpineAnimationRenderer.test.ts create mode 100644 tests/src/spine/SpineConstant.test.ts create mode 100644 tests/src/spine/SpineLoader.test.ts create mode 100644 tests/src/spine/SpineMaterial.test.ts create mode 100644 tests/src/spine/SpineResource.test.ts create mode 100644 tests/src/spine/SpineRuntimeRegistry.test.ts create mode 100644 tests/src/spine/SpineTexture.test.ts create mode 100644 tests/src/ui/Mask.test.ts create mode 100644 tests/src/ui/RectMask2D.test.ts diff --git a/.gitignore b/.gitignore index c692c405e6..44803f44fc 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ CLAUDE.md # For bison generated files used by ShaderLab *.tab.c *.output + +# Local-only test assets (third-party game art, not for redistribution) +examples/public/spine/ diff --git a/e2e/.dev/public/spineboy.atlas b/e2e/.dev/public/spineboy.atlas new file mode 100644 index 0000000000..eca542b711 --- /dev/null +++ b/e2e/.dev/public/spineboy.atlas @@ -0,0 +1,94 @@ +spineboy.png + size: 1024, 256 + filter: Linear, Linear + scale: 0.5 +crosshair + bounds: 352, 7, 45, 45 +eye-indifferent + bounds: 862, 105, 47, 45 +eye-surprised + bounds: 505, 79, 47, 45 +front-bracer + bounds: 826, 66, 29, 40 +front-fist-closed + bounds: 786, 65, 38, 41 +front-fist-open + bounds: 710, 51, 43, 44 + rotate: 90 +front-foot + bounds: 210, 6, 63, 35 +front-shin + bounds: 665, 128, 41, 92 + rotate: 90 +front-thigh + bounds: 2, 2, 23, 56 + rotate: 90 +front-upper-arm + bounds: 250, 205, 23, 49 +goggles + bounds: 665, 171, 131, 83 +gun + bounds: 798, 152, 105, 102 +head + bounds: 2, 27, 136, 149 +hoverboard-board + bounds: 2, 178, 246, 76 +hoverboard-thruster + bounds: 722, 96, 30, 32 + rotate: 90 +hoverglow-small + bounds: 275, 81, 137, 38 +mouth-grind + bounds: 614, 97, 47, 30 +mouth-oooo + bounds: 612, 65, 47, 30 +mouth-smile + bounds: 661, 64, 47, 30 +muzzle-glow + bounds: 382, 54, 25, 25 +muzzle-ring + bounds: 275, 54, 25, 105 + rotate: 90 +muzzle01 + bounds: 911, 95, 67, 40 + rotate: 90 +muzzle02 + bounds: 792, 108, 68, 42 +muzzle03 + bounds: 956, 171, 83, 53 + rotate: 90 +muzzle04 + bounds: 275, 7, 75, 45 +muzzle05 + bounds: 140, 3, 68, 38 +neck + bounds: 250, 182, 18, 21 +portal-bg + bounds: 140, 43, 133, 133 +portal-flare1 + bounds: 554, 65, 56, 30 +portal-flare2 + bounds: 759, 112, 57, 31 + rotate: 90 +portal-flare3 + bounds: 554, 97, 58, 30 +portal-shade + bounds: 275, 121, 133, 133 +portal-streaks1 + bounds: 410, 126, 126, 128 +portal-streaks2 + bounds: 538, 129, 125, 125 +rear-bracer + bounds: 857, 67, 28, 36 +rear-foot + bounds: 663, 96, 57, 30 +rear-shin + bounds: 414, 86, 38, 89 + rotate: 90 +rear-thigh + bounds: 756, 63, 28, 47 +rear-upper-arm + bounds: 60, 5, 20, 44 + rotate: 90 +torso + bounds: 905, 164, 49, 90 diff --git a/e2e/.dev/public/spineboy.json b/e2e/.dev/public/spineboy.json new file mode 100644 index 0000000000..c0eb0ae927 --- /dev/null +++ b/e2e/.dev/public/spineboy.json @@ -0,0 +1,8723 @@ +{ + "skeleton": { + "hash": "dr3Kr/vMgPA", + "spine": "4.2.22", + "x": -188.63, + "y": -7.94, + "width": 418.45, + "height": 686.2, + "images": "./images/", + "audio": "" + }, + "bones": [ + { "name": "root", "rotation": 0.05 }, + { "name": "hip", "parent": "root", "y": 247.27 }, + { "name": "crosshair", "parent": "root", "x": 302.83, "y": 569.45, "color": "ff3f00ff", "icon": "circle" }, + { + "name": "aim-constraint-target", + "parent": "hip", + "length": 26.24, + "rotation": 19.61, + "x": 1.02, + "y": 5.62, + "color": "abe323ff" + }, + { "name": "rear-foot-target", "parent": "root", "x": 61.91, "y": 0.42, "color": "ff3f00ff", "icon": "ik" }, + { "name": "rear-leg-target", "parent": "rear-foot-target", "x": -33.91, "y": 37.34, "color": "ff3f00ff", "icon": "ik" }, + { + "name": "rear-thigh", + "parent": "hip", + "length": 85.72, + "rotation": -72.54, + "x": 8.91, + "y": -5.63, + "color": "ff000dff" + }, + { + "name": "rear-shin", + "parent": "rear-thigh", + "length": 121.88, + "rotation": -19.83, + "x": 86.1, + "y": -1.33, + "color": "ff000dff" + }, + { + "name": "rear-foot", + "parent": "rear-shin", + "length": 51.58, + "rotation": 45.78, + "x": 121.46, + "y": -0.76, + "color": "ff000dff" + }, + { + "name": "back-foot-tip", + "parent": "rear-foot", + "length": 50.3, + "rotation": -0.85, + "x": 51.17, + "y": 0.24, + "inherit": "noRotationOrReflection", + "color": "ff000dff" + }, + { "name": "board-ik", "parent": "root", "x": -131.78, "y": 69.09, "color": "4c56ffff", "icon": "arrows" }, + { "name": "clipping", "parent": "root" }, + { + "name": "hoverboard-controller", + "parent": "root", + "rotation": -0.28, + "x": -329.69, + "y": 69.82, + "color": "ff0004ff", + "icon": "arrowsB" + }, + { "name": "exhaust1", "parent": "hoverboard-controller", "rotation": 3.02, "x": -249.68, "y": 53.39 }, + { "name": "exhaust2", "parent": "hoverboard-controller", "rotation": 26.34, "x": -191.6, "y": -22.92 }, + { + "name": "exhaust3", + "parent": "hoverboard-controller", + "rotation": -12.34, + "x": -236.03, + "y": 80.54, + "scaleX": 0.7847, + "scaleY": 0.7847 + }, + { "name": "portal-root", "parent": "root", "x": 12.9, "y": 328.54, "scaleX": 2.0334, "scaleY": 2.0334 }, + { "name": "flare1", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare10", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare2", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare3", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare4", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare5", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare6", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare7", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare8", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { "name": "flare9", "parent": "portal-root", "x": -6.34, "y": -161.57, "icon": "particles" }, + { + "name": "torso", + "parent": "hip", + "length": 42.52, + "rotation": 103.82, + "x": -1.62, + "y": 4.9, + "color": "e0da19ff" + }, + { "name": "torso2", "parent": "torso", "length": 42.52, "x": 42.52, "color": "e0da19ff" }, + { "name": "torso3", "parent": "torso2", "length": 42.52, "x": 42.52, "color": "e0da19ff" }, + { "name": "front-shoulder", "parent": "torso3", "rotation": 255.89, "x": 18.72, "y": 19.33, "color": "00ff04ff" }, + { "name": "front-upper-arm", "parent": "front-shoulder", "length": 69.45, "rotation": -87.51, "color": "00ff04ff" }, + { + "name": "front-bracer", + "parent": "front-upper-arm", + "length": 40.57, + "rotation": 18.3, + "x": 68.8, + "y": -0.68, + "color": "00ff04ff" + }, + { + "name": "front-fist", + "parent": "front-bracer", + "length": 65.39, + "rotation": 12.43, + "x": 40.57, + "y": 0.2, + "color": "00ff04ff" + }, + { "name": "front-foot-target", "parent": "root", "x": -13.53, "y": 0.04, "color": "ff3f00ff", "icon": "ik" }, + { "name": "front-leg-target", "parent": "front-foot-target", "x": -28.4, "y": 29.06, "color": "ff3f00ff", "icon": "ik" }, + { + "name": "front-thigh", + "parent": "hip", + "length": 74.81, + "rotation": -95.51, + "x": -17.46, + "y": -11.64, + "color": "00ff04ff" + }, + { + "name": "front-shin", + "parent": "front-thigh", + "length": 128.77, + "rotation": -2.21, + "x": 78.69, + "y": 1.6, + "color": "00ff04ff" + }, + { + "name": "front-foot", + "parent": "front-shin", + "length": 41.01, + "rotation": 51.27, + "x": 128.76, + "y": -0.34, + "color": "00ff04ff" + }, + { + "name": "front-foot-tip", + "parent": "front-foot", + "length": 56.03, + "rotation": -1.68, + "x": 41.42, + "y": -0.09, + "inherit": "noRotationOrReflection", + "color": "00ff04ff" + }, + { "name": "back-shoulder", "parent": "torso3", "rotation": -104.11, "x": 7.32, "y": -19.22, "color": "ff000dff" }, + { "name": "rear-upper-arm", "parent": "back-shoulder", "length": 51.94, "rotation": -65.45, "color": "ff000dff" }, + { "name": "rear-bracer", "parent": "rear-upper-arm", "length": 34.56, "rotation": 23.15, "x": 51.36, "color": "ff000dff" }, + { + "name": "gun", + "parent": "rear-bracer", + "length": 43.11, + "rotation": -5.43, + "x": 34.42, + "y": -0.45, + "color": "ff000dff" + }, + { "name": "gun-tip", "parent": "gun", "rotation": 7.1, "x": 200.78, "y": 52.5, "color": "ff0000ff" }, + { + "name": "neck", + "parent": "torso3", + "length": 25.45, + "rotation": -31.54, + "x": 42.46, + "y": -0.31, + "color": "e0da19ff" + }, + { + "name": "head", + "parent": "neck", + "length": 131.79, + "rotation": 26.1, + "x": 27.66, + "y": -0.26, + "color": "e0da19ff" + }, + { + "name": "hair1", + "parent": "head", + "length": 47.23, + "rotation": -49.1, + "x": 149.83, + "y": -59.77, + "color": "e0da19ff" + }, + { + "name": "hair2", + "parent": "hair1", + "length": 55.57, + "rotation": 50.42, + "x": 47.23, + "y": 0.19, + "color": "e0da19ff" + }, + { + "name": "hair3", + "parent": "head", + "length": 62.22, + "rotation": -32.17, + "x": 164.14, + "y": 3.68, + "color": "e0da19ff" + }, + { + "name": "hair4", + "parent": "hair3", + "length": 80.28, + "rotation": 83.71, + "x": 62.22, + "y": -0.04, + "color": "e0da19ff" + }, + { "name": "hoverboard-thruster-front", "parent": "hoverboard-controller", "rotation": -29.2, "x": 95.77, "y": -2.99, "inherit": "noRotationOrReflection" }, + { "name": "hoverboard-thruster-rear", "parent": "hoverboard-controller", "rotation": -29.2, "x": -76.47, "y": -4.88, "inherit": "noRotationOrReflection" }, + { "name": "hoverglow-front", "parent": "hoverboard-thruster-front", "rotation": 0.17, "x": -1.78, "y": -37.79 }, + { "name": "hoverglow-rear", "parent": "hoverboard-thruster-rear", "rotation": 0.17, "x": 1.06, "y": -35.66 }, + { + "name": "muzzle", + "parent": "rear-bracer", + "rotation": 3.06, + "x": 242.34, + "y": 34.26, + "color": "ffb900ff", + "icon": "muzzleFlash" + }, + { "name": "muzzle-ring", "parent": "muzzle", "color": "ffb900ff" }, + { "name": "muzzle-ring2", "parent": "muzzle", "color": "ffb900ff" }, + { "name": "muzzle-ring3", "parent": "muzzle", "color": "ffb900ff" }, + { "name": "muzzle-ring4", "parent": "muzzle", "color": "ffb900ff" }, + { "name": "portal", "parent": "portal-root" }, + { "name": "portal-shade", "parent": "portal-root" }, + { "name": "portal-streaks1", "parent": "portal-root" }, + { "name": "portal-streaks2", "parent": "portal-root" }, + { "name": "side-glow1", "parent": "hoverboard-controller", "x": -110.56, "y": 2.62, "color": "000effff" }, + { + "name": "side-glow2", + "parent": "hoverboard-controller", + "x": -110.56, + "y": 2.62, + "scaleX": 0.738, + "scaleY": 0.738, + "color": "000effff" + }, + { "name": "head-control", "parent": "head", "x": 110.21, "color": "00a220ff", "icon": "arrows" } + ], + "slots": [ + { "name": "portal-bg", "bone": "portal" }, + { "name": "portal-shade", "bone": "portal-shade" }, + { "name": "portal-streaks2", "bone": "portal-streaks2", "blend": "additive" }, + { "name": "portal-streaks1", "bone": "portal-streaks1", "blend": "additive" }, + { "name": "portal-flare8", "bone": "flare8", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare9", "bone": "flare9", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare10", "bone": "flare10", "color": "c3cbffff", "blend": "additive" }, + { "name": "clipping", "bone": "clipping" }, + { "name": "exhaust3", "bone": "exhaust3", "color": "5eb4ffff", "blend": "additive" }, + { "name": "hoverboard-thruster-rear", "bone": "hoverboard-thruster-rear" }, + { "name": "hoverboard-thruster-front", "bone": "hoverboard-thruster-front" }, + { "name": "hoverboard-board", "bone": "hoverboard-controller" }, + { "name": "side-glow1", "bone": "side-glow1", "color": "ff8686ff", "blend": "additive" }, + { "name": "side-glow3", "bone": "side-glow1", "color": "ff8686ff", "blend": "additive" }, + { "name": "side-glow2", "bone": "side-glow2", "color": "ff8686ff", "blend": "additive" }, + { "name": "hoverglow-front", "bone": "hoverglow-front", "color": "5eb4ffff", "blend": "additive" }, + { "name": "hoverglow-rear", "bone": "hoverglow-rear", "color": "5eb4ffff", "blend": "additive" }, + { "name": "exhaust1", "bone": "exhaust2", "color": "5eb4ffff", "blend": "additive" }, + { "name": "exhaust2", "bone": "exhaust1", "color": "5eb4ffff", "blend": "additive" }, + { "name": "rear-upper-arm", "bone": "rear-upper-arm", "attachment": "rear-upper-arm" }, + { "name": "rear-bracer", "bone": "rear-bracer", "attachment": "rear-bracer" }, + { "name": "gun", "bone": "gun", "attachment": "gun" }, + { "name": "rear-foot", "bone": "rear-foot", "attachment": "rear-foot" }, + { "name": "rear-thigh", "bone": "rear-thigh", "attachment": "rear-thigh" }, + { "name": "rear-shin", "bone": "rear-shin", "attachment": "rear-shin" }, + { "name": "neck", "bone": "neck", "attachment": "neck" }, + { "name": "torso", "bone": "torso", "attachment": "torso" }, + { "name": "front-upper-arm", "bone": "front-upper-arm", "attachment": "front-upper-arm" }, + { "name": "head", "bone": "head", "attachment": "head" }, + { "name": "eye", "bone": "head", "attachment": "eye-indifferent" }, + { "name": "front-thigh", "bone": "front-thigh", "attachment": "front-thigh" }, + { "name": "front-foot", "bone": "front-foot", "attachment": "front-foot" }, + { "name": "front-shin", "bone": "front-shin", "attachment": "front-shin" }, + { "name": "mouth", "bone": "head", "attachment": "mouth-smile" }, + { "name": "goggles", "bone": "head", "attachment": "goggles" }, + { "name": "front-bracer", "bone": "front-bracer", "attachment": "front-bracer" }, + { "name": "front-fist", "bone": "front-fist", "attachment": "front-fist-closed" }, + { "name": "muzzle", "bone": "muzzle" }, + { "name": "head-bb", "bone": "head" }, + { "name": "portal-flare1", "bone": "flare1", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare2", "bone": "flare2", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare3", "bone": "flare3", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare4", "bone": "flare4", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare5", "bone": "flare5", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare6", "bone": "flare6", "color": "c3cbffff", "blend": "additive" }, + { "name": "portal-flare7", "bone": "flare7", "color": "c3cbffff", "blend": "additive" }, + { "name": "crosshair", "bone": "crosshair" }, + { "name": "muzzle-glow", "bone": "gun-tip", "color": "ffffff00", "blend": "additive" }, + { "name": "muzzle-ring", "bone": "muzzle-ring", "color": "d8baffff", "blend": "additive" }, + { "name": "muzzle-ring2", "bone": "muzzle-ring2", "color": "d8baffff", "blend": "additive" }, + { "name": "muzzle-ring3", "bone": "muzzle-ring3", "color": "d8baffff", "blend": "additive" }, + { "name": "muzzle-ring4", "bone": "muzzle-ring4", "color": "d8baffff", "blend": "additive" } + ], + "ik": [ + { + "name": "aim-ik", + "order": 13, + "bones": [ "rear-upper-arm" ], + "target": "crosshair", + "mix": 0 + }, + { + "name": "aim-torso-ik", + "order": 8, + "bones": [ "aim-constraint-target" ], + "target": "crosshair" + }, + { + "name": "board-ik", + "order": 1, + "bones": [ "hoverboard-controller" ], + "target": "board-ik" + }, + { + "name": "front-foot-ik", + "order": 6, + "bones": [ "front-foot" ], + "target": "front-foot-target" + }, + { + "name": "front-leg-ik", + "order": 4, + "bones": [ "front-thigh", "front-shin" ], + "target": "front-leg-target", + "bendPositive": false + }, + { + "name": "rear-foot-ik", + "order": 7, + "bones": [ "rear-foot" ], + "target": "rear-foot-target" + }, + { + "name": "rear-leg-ik", + "order": 5, + "bones": [ "rear-thigh", "rear-shin" ], + "target": "rear-leg-target", + "bendPositive": false + } + ], + "transform": [ + { + "name": "aim-front-arm-transform", + "order": 11, + "bones": [ "front-upper-arm" ], + "target": "aim-constraint-target", + "rotation": -180, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "aim-head-transform", + "order": 10, + "bones": [ "head" ], + "target": "aim-constraint-target", + "rotation": 84.3, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "aim-rear-arm-transform", + "order": 12, + "bones": [ "rear-upper-arm" ], + "target": "aim-constraint-target", + "x": 57.7, + "y": 56.4, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "aim-torso-transform", + "order": 9, + "bones": [ "torso" ], + "target": "aim-constraint-target", + "rotation": 69.5, + "shearY": -36, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "front-foot-board-transform", + "order": 2, + "bones": [ "front-foot-target" ], + "target": "hoverboard-controller", + "x": -69.8, + "y": 20.7, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "rear-foot-board-transform", + "order": 3, + "bones": [ "rear-foot-target" ], + "target": "hoverboard-controller", + "x": 86.6, + "y": 21.3, + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "shoulder", + "bones": [ "back-shoulder" ], + "target": "front-shoulder", + "x": 40.17, + "y": -1.66, + "mixRotate": 0, + "mixX": -1, + "mixScaleX": 0, + "mixShearY": 0 + }, + { + "name": "toes-board", + "order": 14, + "bones": [ "front-foot-tip", "back-foot-tip" ], + "target": "hoverboard-controller", + "mixRotate": 0, + "mixX": 0, + "mixScaleX": 0, + "mixShearY": 0 + } + ], + "skins": [ + { + "name": "default", + "attachments": { + "clipping": { + "clipping": { + "type": "clipping", + "end": "head-bb", + "vertexCount": 9, + "vertices": [ 66.76, 509.48, 19.98, 434.54, 5.34, 336.28, 22.19, 247.93, 77.98, 159.54, 182.21, -97.56, 1452.26, -99.8, 1454.33, 843.61, 166.57, 841.02 ], + "color": "ce3a3aff" + } + }, + "crosshair": { + "crosshair": { "width": 89, "height": 89 } + }, + "exhaust1": { + "hoverglow-small": { "scaleX": 0.4629, "scaleY": 0.8129, "rotation": -83.07, "width": 274, "height": 75 } + }, + "exhaust2": { + "hoverglow-small": { + "x": 0.01, + "y": -0.76, + "scaleX": 0.4208, + "scaleY": 0.8403, + "rotation": -89.25, + "width": 274, + "height": 75 + } + }, + "exhaust3": { + "hoverglow-small": { "scaleX": 0.4629, "scaleY": 0.8129, "rotation": -83.07, "width": 274, "height": 75 } + }, + "eye": { + "eye-indifferent": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 3, 0, 1, 2, 3 ], + "vertices": [ 2, 66, -36.8, -91.35, 0.3, 46, 73.41, -91.35, 0.7, 2, 66, -87.05, -13.11, 0.70968, 46, 23.16, -13.11, 0.29032, 2, 66, -12.18, 34.99, 0.82818, 46, 98.03, 34.99, 0.17182, 2, 66, 38.07, -43.25, 0.59781, 46, 148.28, -43.25, 0.40219 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 89 + }, + "eye-surprised": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 2, 3, 1, 3, 0 ], + "vertices": [ 2, 66, -46.74, -89.7, 0.3, 46, 63.47, -89.7, 0.7, 2, 66, -77.58, -1.97, 0.71, 46, 32.63, -1.97, 0.29, 2, 66, 6.38, 27.55, 0.83, 46, 116.59, 27.55, 0.17, 2, 66, 37.22, -60.19, 0.6, 46, 147.44, -60.19, 0.4 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 89 + } + }, + "front-bracer": { + "front-bracer": { "x": 12.03, "y": -1.68, "rotation": 79.6, "width": 58, "height": 80 } + }, + "front-fist": { + "front-fist-closed": { "x": 35.5, "y": 6, "rotation": 67.16, "width": 75, "height": 82 }, + "front-fist-open": { "x": 39.57, "y": 7.76, "rotation": 67.16, "width": 86, "height": 87 } + }, + "front-foot": { + "front-foot": { + "type": "mesh", + "uvs": [ 0.59417, 0.23422, 0.62257, 0.30336, 0.6501, 0.37036, 0.67637, 0.38404, 0.72068, 0.4071, 0.76264, 0.42894, 1, 0.70375, 1, 1, 0.65517, 1, 0.46923, 0.99999, 0, 1, 0, 0.39197, 0.17846, 0, 0.49796, 0 ], + "triangles": [ 8, 9, 3, 4, 8, 3, 5, 8, 4, 6, 8, 5, 8, 6, 7, 11, 1, 10, 0, 12, 13, 0, 11, 12, 0, 1, 11, 9, 2, 3, 1, 2, 10, 9, 10, 2 ], + "vertices": [ 2, 38, 18.17, 41.57, 0.7896, 39, 12.46, 46.05, 0.2104, 2, 38, 24.08, 40.76, 0.71228, 39, 16.12, 41.34, 0.28772, 2, 38, 29.81, 39.98, 0.55344, 39, 19.67, 36.78, 0.44656, 2, 38, 32.81, 41.67, 0.38554, 39, 23, 35.89, 0.61446, 2, 38, 37.86, 44.52, 0.25567, 39, 28.61, 34.4, 0.74433, 2, 38, 42.65, 47.22, 0.17384, 39, 33.92, 32.99, 0.82616, 1, 39, 64.15, 14.56, 1, 1, 39, 64.51, -5.87, 1, 1, 39, 21.08, -6.64, 1, 2, 38, 44.67, -6.77, 0.5684, 39, -2.34, -6.97, 0.4316, 1, 38, 3.1, -48.81, 1, 1, 38, -26.73, -19.31, 1, 1, 38, -30.15, 15.69, 1, 1, 38, -1.84, 44.32, 1 ], + "hull": 14, + "edges": [ 14, 16, 16, 18, 18, 20, 4, 18, 20, 22, 24, 26, 22, 24, 12, 14, 10, 12, 2, 4, 2, 20, 4, 6, 6, 16, 2, 0, 0, 26, 6, 8, 8, 10 ], + "width": 126, + "height": 69 + } + }, + "front-shin": { + "front-shin": { + "type": "mesh", + "uvs": [ 0.90031, 0.05785, 1, 0.12828, 1, 0.21619, 0.9025, 0.31002, 0.78736, 0.35684, 0.78081, 0.39874, 0.77215, 0.45415, 0.77098, 0.51572, 0.84094, 0.63751, 0.93095, 0.7491, 0.95531, 0.7793, 0.78126, 0.87679, 0.5613, 1, 0.2687, 1, 0, 1, 0.00279, 0.96112, 0.01358, 0.81038, 0.02822, 0.60605, 0.08324, 0.45142, 0.18908, 0.31882, 0.29577, 0.2398, 0.30236, 0.14941, 0.37875, 0.05902, 0.53284, 0, 0.70538, 0, 0.41094, 0.71968, 0.40743, 0.54751, 0.41094, 0.4536, 0.4724, 0.35186, 0.33367, 0.27829, 0.50226, 0.31664, 0.65328, 0.67507, 0.60762, 0.52716, 0.6006, 0.45125, 0.62747, 0.37543, 0.6573, 0.3385, 0.27843, 0.32924, 0.18967, 0.45203, 0.16509, 0.58586, 0.18265, 0.7682, 0.50532, 0.24634, 0.59473, 0.17967, 0.60161, 0.10611, 0.51392, 0.04327, 0.72198, 0.28849, 0.82343, 0.20266, 0.86814, 0.11377, 0.79592, 0.04634, 0.44858, 0.15515, 0.25466, 0.96219, 0.53169, 0.9448, 0.7531, 0.8324 ], + "triangles": [ 24, 0, 47, 43, 23, 24, 47, 43, 24, 43, 22, 23, 42, 43, 47, 46, 47, 0, 42, 47, 46, 46, 0, 1, 48, 22, 43, 48, 43, 42, 21, 22, 48, 41, 48, 42, 45, 42, 46, 41, 42, 45, 46, 1, 2, 45, 46, 2, 40, 48, 41, 48, 20, 21, 29, 48, 40, 29, 20, 48, 44, 41, 45, 40, 41, 44, 3, 45, 2, 44, 45, 3, 30, 29, 40, 35, 30, 40, 36, 19, 20, 36, 20, 29, 44, 35, 40, 28, 29, 30, 4, 44, 3, 35, 44, 4, 34, 30, 35, 5, 35, 4, 34, 28, 30, 33, 28, 34, 37, 19, 36, 18, 19, 37, 27, 29, 28, 27, 28, 33, 36, 29, 27, 37, 36, 27, 5, 34, 35, 6, 34, 5, 33, 34, 6, 6, 32, 33, 7, 32, 6, 26, 37, 27, 38, 18, 37, 38, 37, 26, 17, 18, 38, 31, 32, 7, 31, 7, 8, 32, 25, 26, 38, 26, 25, 27, 33, 32, 32, 26, 27, 39, 38, 25, 17, 38, 39, 16, 17, 39, 51, 31, 8, 51, 8, 9, 11, 51, 9, 11, 9, 10, 31, 50, 25, 31, 25, 32, 50, 31, 51, 49, 39, 25, 49, 25, 50, 15, 16, 39, 49, 15, 39, 13, 49, 50, 14, 15, 49, 13, 14, 49, 12, 50, 51, 12, 51, 11, 13, 50, 12 ], + "vertices": [ -23.66, 19.37, -11.73, 28.98, 4.34, 30.83, 22.41, 24.87, 32.05, 16.48, 39.77, 16.83, 49.98, 17.3, 61.25, 18.5, 82.85, 26.78, 102.4, 36.46, 107.69, 39.09, 127.15, 26.97, 151.74, 11.65, 154.49, -12.18, 157.02, -34.07, 149.89, -34.66, 122.23, -36.97, 84.75, -40.09, 55.97, -38.88, 30.73, -33.05, 15.29, -26.03, -1.3, -27.41, -18.54, -23.09, -30.78, -11.79, -32.4, 2.27, 101.92, -6.52, 70.48, -10.44, 53.28, -12.14, 34.11, -9.28, 21.96, -22.13, 27.39, -7.59, 91.48, 12.28, 64.88, 5.44, 51.07, 3.26, 36.95, 3.85, 29.92, 5.5, 31.8, -25.56, 55.08, -30.19, 79.77, -29.37, 112.93, -24.09, 14.51, -8.83, 1.48, -2.95, -12.03, -3.94, -22.69, -12.41, 20.17, 9.71, 3.53, 16.16, -13.14, 17.93, -24.78, 10.62, -1.62, -15.37, 147.71, -14.13, 141.93, 8.07, 119.3, 23.74 ], + "hull": 25, + "edges": [ 8, 6, 6, 4, 4, 2, 2, 0, 0, 48, 46, 48, 46, 44, 44, 42, 42, 40, 40, 38, 38, 36, 36, 34, 32, 34, 50, 52, 52, 54, 54, 56, 40, 58, 58, 60, 8, 10, 20, 22, 22, 24, 62, 64, 64, 66, 66, 68, 8, 70, 70, 60, 68, 70, 58, 72, 72, 74, 74, 76, 76, 78, 24, 26, 26, 28, 58, 80, 80, 82, 82, 84, 84, 86, 86, 44, 70, 88, 88, 90, 90, 92, 92, 94, 94, 48, 80, 88, 88, 6, 82, 90, 90, 4, 84, 92, 92, 2, 86, 94, 94, 0, 56, 60, 10, 12, 12, 14, 14, 16, 28, 30, 30, 32, 26, 98, 98, 78, 30, 98, 24, 100, 100, 50, 98, 100, 22, 102, 102, 62, 100, 102, 16, 18, 18, 20, 102, 18 ], + "width": 82, + "height": 184 + } + }, + "front-thigh": { + "front-thigh": { "x": 42.48, "y": 4.45, "rotation": 84.87, "width": 45, "height": 112 } + }, + "front-upper-arm": { + "front-upper-arm": { "x": 28.31, "y": 7.37, "rotation": 97.9, "width": 46, "height": 97 } + }, + "goggles": { + "goggles": { + "type": "mesh", + "uvs": [ 0.53653, 0.04114, 0.72922, 0.16036, 0.91667, 0.33223, 0.97046, 0.31329, 1, 0.48053, 0.95756, 0.5733, 0.88825, 0.6328, 0.86878, 0.78962, 0.77404, 0.8675, 0.72628, 1, 0.60714, 0.93863, 0.49601, 0.88138, 0.41558, 0.75027, 0.32547, 0.70084, 0.2782, 0.58257, 0.1721, 0.63281, 0.17229, 0.75071, 0.10781, 0.79898, 0, 0.32304, 0, 0.12476, 0.07373, 0.07344, 0.15423, 0.10734, 0.23165, 0.13994, 0.30313, 0.02256, 0.34802, 0, 0.42979, 0.69183, 0.39476, 0.51042, 0.39488, 0.31512, 0.45878, 0.23198, 0.56501, 0.28109, 0.69961, 0.39216, 0.82039, 0.54204, 0.85738, 0.62343, 0.91107, 0.51407, 0.72639, 0.32147, 0.58764, 0.19609, 0.48075, 0.11269, 0.37823, 0.05501, 0.3287, 0.17866, 0.319, 0.305, 0.36036, 0.53799, 0.40327, 0.70072, 0.30059, 0.55838, 0.21957, 0.2815, 0.09963, 0.28943, 0.56863, 0.4368, 0.4911, 0.37156, 0.51185, 0.52093, 0.67018, 0.59304, 0.7619, 0.68575, 0.73296, 0.43355 ], + "triangles": [ 18, 44, 15, 21, 19, 20, 17, 18, 15, 44, 19, 21, 2, 3, 4, 18, 19, 44, 2, 33, 34, 33, 2, 4, 5, 33, 4, 5, 6, 33, 7, 32, 6, 31, 50, 33, 32, 31, 33, 6, 32, 33, 31, 49, 50, 49, 31, 32, 49, 32, 7, 8, 49, 7, 33, 50, 34, 17, 15, 16, 9, 48, 8, 49, 48, 50, 50, 48, 45, 47, 45, 48, 50, 45, 30, 45, 47, 46, 45, 46, 29, 30, 45, 29, 30, 29, 34, 30, 34, 50, 47, 26, 46, 25, 10, 11, 12, 25, 11, 41, 12, 42, 42, 44, 43, 43, 21, 22, 41, 40, 25, 41, 42, 40, 29, 35, 34, 40, 26, 25, 25, 26, 47, 37, 24, 0, 36, 37, 0, 42, 43, 39, 42, 39, 40, 28, 38, 36, 40, 39, 26, 28, 27, 38, 26, 39, 27, 37, 38, 23, 39, 43, 38, 38, 37, 36, 27, 39, 38, 43, 22, 38, 37, 23, 24, 22, 23, 38, 36, 0, 35, 28, 36, 35, 29, 28, 35, 27, 28, 46, 26, 27, 46, 35, 0, 1, 34, 35, 1, 12, 41, 25, 47, 10, 25, 44, 21, 43, 42, 14, 44, 14, 15, 44, 13, 14, 42, 12, 13, 42, 46, 28, 29, 47, 48, 10, 48, 9, 10, 49, 8, 48, 2, 34, 1 ], + "vertices": [ 2, 66, 61.88, 22.81, 0.832, 46, 172.09, 22.81, 0.168, 2, 66, 59.89, -31.19, 0.6855, 46, 170.1, -31.19, 0.3145, 2, 66, 49.2, -86.8, 0.32635, 46, 159.41, -86.8, 0.67365, 2, 66, 56.82, -99.01, 0.01217, 46, 167.03, -99.01, 0.98783, 1, 46, 143.4, -115.48, 1, 2, 66, 15, -110.14, 0.0041, 46, 125.21, -110.14, 0.9959, 2, 66, -0.32, -96.36, 0.07948, 46, 109.89, -96.36, 0.92052, 2, 66, -26.56, -100.19, 0.01905, 46, 83.65, -100.19, 0.98095, 2, 66, -46.96, -81.16, 0.4921, 46, 63.26, -81.16, 0.50791, 2, 66, -71.84, -76.69, 0.56923, 46, 38.37, -76.69, 0.43077, 2, 66, -72.54, -43.98, 0.74145, 46, 37.67, -43.98, 0.25855, 2, 66, -73.2, -13.47, 0.87929, 46, 37.01, -13.47, 0.12071, 2, 66, -59.63, 13.55, 0.864, 46, 50.58, 13.55, 0.136, 2, 66, -59.69, 38.45, 0.85289, 46, 50.52, 38.45, 0.14711, 2, 66, -45.26, 56.6, 0.74392, 46, 64.95, 56.6, 0.25608, 2, 66, -62.31, 79.96, 0.624, 46, 47.9, 79.96, 0.376, 2, 66, -80.76, 73.42, 0.616, 46, 29.45, 73.42, 0.384, 2, 66, -93.9, 86.64, 0.288, 46, 16.31, 86.64, 0.712, 1, 46, 81.51, 139.38, 1, 1, 46, 112.56, 150.3, 1, 2, 66, 16.76, 134.97, 0.02942, 46, 126.97, 134.97, 0.97058, 2, 66, 18.42, 113.28, 0.36147, 46, 128.63, 113.28, 0.63853, 2, 66, 20.02, 92.43, 0.7135, 46, 130.23, 92.43, 0.2865, 2, 66, 44.58, 81.29, 0.69603, 46, 154.79, 81.29, 0.30397, 2, 66, 52, 71.48, 0.848, 46, 162.21, 71.48, 0.152, 2, 66, -49.25, 13.27, 0.8, 46, 60.96, 13.27, 0.2, 2, 66, -23.88, 31.88, 0.896, 46, 86.33, 31.88, 0.104, 2, 66, 6.72, 42.6, 0.928, 46, 116.93, 42.6, 0.072, 2, 66, 25.26, 31.44, 0.8, 46, 135.47, 31.44, 0.2, 2, 66, 26.77, 2.59, 0.75, 46, 136.98, 2.59, 0.25, 2, 66, 21.02, -36.66, 0.54887, 46, 131.23, -36.66, 0.45113, 2, 66, 8.01, -74.65, 0.36029, 46, 118.22, -74.65, 0.63971, 2, 66, -1.52, -88.24, 0.1253, 46, 108.69, -88.24, 0.8747, 2, 66, 20.25, -95.44, 0.08687, 46, 130.46, -95.44, 0.91313, 2, 66, 34.42, -39.36, 0.72613, 46, 144.63, -39.36, 0.27387, 2, 66, 42.03, 1.7, 0.824, 46, 152.25, 1.7, 0.176, 2, 66, 45.85, 32.6, 0.856, 46, 156.06, 32.6, 0.144, 1, 66, 46.01, 61.02, 1, 1, 66, 22.35, 66.41, 1, 1, 66, 1.73, 61.84, 1, 2, 66, -31.17, 38.83, 0.928, 46, 79.04, 38.83, 0.072, 2, 66, -52.94, 19.31, 0.79073, 46, 57.27, 19.31, 0.20927, 2, 66, -39.54, 52.42, 0.912, 46, 70.67, 52.42, 0.088, 2, 66, -3.2, 87.61, 0.744, 46, 107.02, 87.61, 0.256, 2, 66, -14.82, 116.7, 0.6368, 46, 95.4, 116.7, 0.3632, 2, 66, 2.7, -6.87, 0.856, 46, 112.91, -6.87, 0.144, 2, 66, 6.21, 15.8, 0.744, 46, 116.42, 15.8, 0.256, 2, 66, -15.39, 2.47, 0.856, 46, 94.82, 2.47, 0.144, 2, 66, -12.98, -40.48, 0.72102, 46, 97.24, -40.48, 0.27898, 2, 66, -19.55, -68.16, 0.59162, 46, 90.66, -68.16, 0.40838, 2, 66, 17.44, -47.15, 0.53452, 46, 127.65, -47.15, 0.46548 ], + "hull": 25, + "edges": [ 36, 34, 34, 32, 32, 30, 30, 28, 28, 26, 26, 24, 24, 22, 18, 16, 16, 14, 14, 12, 12, 10, 10, 8, 8, 6, 6, 4, 4, 2, 2, 0, 0, 48, 48, 46, 46, 44, 36, 38, 40, 38, 24, 50, 50, 52, 52, 54, 54, 56, 56, 58, 58, 60, 62, 64, 64, 12, 8, 66, 66, 68, 68, 70, 70, 72, 72, 74, 74, 76, 76, 78, 78, 80, 80, 82, 82, 24, 24, 84, 84, 86, 86, 44, 40, 42, 42, 44, 42, 88, 88, 30, 58, 90, 90, 92, 92, 94, 18, 20, 20, 22, 94, 20, 18, 96, 96, 98, 60, 100, 100, 62, 98, 100 ], + "width": 261, + "height": 166 + } + }, + "gun": { + "gun": { "x": 77.3, "y": 16.4, "rotation": 60.83, "width": 210, "height": 203 } + }, + "head": { + "head": { + "type": "mesh", + "uvs": [ 0.75919, 0.06107, 0.88392, 0.17893, 0.90174, 0.30856, 0.94224, 0.1966, 1, 0.26584, 1, 0.422, 0.95864, 0.46993, 0.92118, 0.51333, 0.85957, 0.5347, 0.78388, 0.65605, 0.74384, 0.74838, 0.85116, 0.75151, 0.84828, 0.82564, 0.81781, 0.85367, 0.75599, 0.85906, 0.76237, 0.90468, 0.65875, 1, 0.38337, 1, 0.1858, 0.85404, 0.12742, 0.81091, 0.06025, 0.69209, 0, 0.58552, 0, 0.41021, 0.0853, 0.20692, 0.24243, 0.14504, 0.5, 0.1421, 0.50324, 0.07433, 0.41738, 0, 0.57614, 0, 0.85059, 0.36087, 0.73431, 0.43206, 0.68481, 0.31271, 0.72165, 0.16718, 0.55931, 0.04154, 0.44764, 0.22895, 0.23926, 0.26559, 0.71272, 0.44036, 0.56993, 0.383, 0.41678, 0.33511, 0.293, 0.31497, 0.70802, 0.44502, 0.56676, 0.38976, 0.41521, 0.34416, 0.28754, 0.33017, 0.88988, 0.50177, 0.30389, 0.73463, 0.2646, 0.65675, 0.21414, 0.61584, 0.14613, 0.62194, 0.10316, 0.66636, 0.10358, 0.72557, 0.14505, 0.79164, 0.20263, 0.81355, 0.27873, 0.80159, 0.34947, 0.7376, 0.23073, 0.57073, 0.08878, 0.60707, 0.29461, 0.8129, 0.73006, 0.87883, 0.69805, 0.87348, 0.66166, 0.79681, 0.22468, 0.69824, 0.14552, 0.67405 ], + "triangles": [ 50, 49, 62, 34, 25, 31, 39, 35, 34, 38, 39, 34, 37, 38, 34, 42, 39, 38, 43, 39, 42, 32, 2, 31, 31, 37, 34, 42, 38, 37, 41, 42, 37, 43, 22, 39, 30, 31, 29, 36, 37, 31, 30, 36, 31, 40, 41, 37, 36, 40, 37, 36, 30, 44, 55, 22, 43, 55, 48, 56, 47, 48, 55, 46, 55, 54, 42, 55, 43, 47, 55, 46, 62, 49, 48, 61, 47, 46, 62, 48, 47, 61, 62, 47, 46, 54, 45, 42, 41, 55, 61, 46, 45, 55, 41, 54, 61, 51, 50, 61, 50, 62, 60, 41, 40, 54, 41, 60, 53, 61, 45, 52, 51, 61, 57, 53, 45, 57, 45, 54, 53, 52, 61, 52, 19, 51, 57, 18, 52, 57, 52, 53, 17, 54, 60, 57, 54, 17, 18, 57, 17, 19, 50, 51, 33, 27, 28, 26, 27, 33, 0, 33, 28, 32, 33, 0, 32, 0, 1, 33, 25, 26, 33, 32, 25, 31, 25, 32, 2, 32, 1, 2, 3, 4, 29, 31, 2, 2, 4, 5, 29, 2, 5, 6, 29, 5, 30, 29, 6, 44, 30, 6, 18, 19, 52, 49, 56, 48, 34, 24, 25, 35, 23, 24, 35, 24, 34, 39, 22, 35, 22, 23, 35, 7, 44, 6, 8, 36, 44, 40, 36, 8, 8, 44, 7, 56, 21, 22, 55, 56, 22, 9, 40, 8, 20, 21, 56, 20, 56, 49, 9, 60, 40, 10, 60, 9, 20, 50, 19, 12, 10, 11, 13, 10, 12, 14, 60, 10, 13, 14, 10, 59, 60, 14, 58, 59, 14, 58, 14, 15, 16, 17, 60, 59, 16, 60, 15, 16, 59, 15, 59, 58, 20, 49, 50 ], + "vertices": [ 2, 50, 41.97, -41.8, 0.94074, 66, 165.41, -22.6, 0.05926, 4, 48, 73.47, 27.54, 0.26795, 50, -5.75, -51.71, 0.4738, 49, 112.99, -11.41, 0.12255, 66, 143.5, -66.13, 0.1357, 4, 48, 38.23, 10.99, 0.6831, 50, -41.01, -35.22, 0.07866, 49, 92.73, -44.66, 0.04872, 66, 108.65, -83.49, 0.18952, 2, 48, 73.35, 10.89, 0.8455, 66, 143.77, -82.78, 0.1545, 2, 48, 58.59, -10.38, 0.91607, 66, 129.5, -104.39, 0.08393, 3, 46, 195.82, -119.82, 0.104, 47, 75.49, -4.55, 0.09191, 48, 14.36, -24.8, 0.80409, 4, 46, 178.62, -113.98, 0.19022, 47, 59.82, -13.72, 0.33409, 48, -2.7, -18.57, 0.46643, 66, 68.41, -113.98, 0.00926, 4, 46, 163.06, -108.69, 0.18724, 47, 45.64, -22.03, 0.3133, 48, -18.14, -12.93, 0.47469, 66, 52.84, -108.69, 0.02477, 2, 46, 151.52, -95.05, 0.91122, 66, 41.31, -95.05, 0.08878, 2, 46, 110.61, -87.69, 0.70564, 66, 0.4, -87.69, 0.29436, 2, 46, 81.05, -86.58, 0.63951, 66, -29.16, -86.58, 0.36049, 2, 46, 89.82, -114.32, 0.57, 66, -20.39, -114.32, 0.43, 2, 46, 68.72, -120.91, 0.57, 66, -41.49, -120.91, 0.43, 2, 46, 58.1, -115.9, 0.57, 66, -52.11, -115.9, 0.43, 2, 46, 51.03, -100.63, 0.64242, 66, -59.18, -100.63, 0.35758, 2, 46, 38.79, -106.76, 0.81659, 66, -71.43, -106.76, 0.18341, 2, 46, 2.68, -89.7, 0.77801, 66, -107.53, -89.7, 0.22199, 2, 46, -22.07, -19.3, 0.823, 66, -132.28, -19.3, 0.177, 2, 46, 1.2, 45.63, 0.51204, 66, -109.01, 45.63, 0.48796, 2, 46, 8.07, 64.81, 0.60869, 66, -102.14, 64.81, 0.39131, 2, 46, 35.44, 93.73, 0.80009, 66, -74.77, 93.73, 0.19991, 2, 46, 59.98, 119.66, 0.93554, 66, -50.23, 119.66, 0.06446, 2, 46, 109.26, 136.99, 0.99895, 66, -0.95, 136.99, 0.00105, 1, 46, 174.07, 135.27, 1, 3, 46, 205.59, 101.22, 0.80778, 49, -16.84, 104.63, 0.15658, 66, 95.38, 101.22, 0.03564, 3, 50, 58.94, 30.5, 0.43491, 49, 38.36, 61.89, 0.28116, 66, 119.35, 35.65, 0.28393, 2, 50, 75.56, 19.01, 0.92164, 66, 138.68, 41.52, 0.07836, 1, 50, 106.7, 26.9, 1, 1, 50, 83.79, -9.51, 1, 5, 47, 44.51, 27.24, 0.15139, 48, 19.12, 19.33, 0.44847, 50, -46.82, -15.19, 0.05757, 49, 72.19, -48.24, 0.1149, 66, 89.35, -75.58, 0.22767, 3, 47, 7.42, 19.08, 0.37772, 49, 34.32, -45.24, 0.09918, 66, 58.9, -52.89, 0.52311, 2, 49, 45.94, -9.07, 0.4826, 66, 87.99, -28.45, 0.5174, 2, 50, 20.62, -16.35, 0.7435, 66, 132.21, -23.49, 0.2565, 2, 50, 75.74, 0.94, 0.97172, 66, 152.95, 30.42, 0.02828, 4, 46, 200.45, 40.46, 0.18809, 50, 44.6, 56.29, 0.05831, 49, 11.15, 50.46, 0.14366, 66, 90.24, 40.46, 0.60994, 2, 46, 171.41, 90.12, 0.48644, 66, 61.2, 90.12, 0.51356, 2, 46, 164.84, -48.18, 0.43217, 66, 54.62, -48.18, 0.56783, 4, 46, 168.13, -6.02, 0.01949, 47, -28.65, 49.02, 0.02229, 49, 8.54, -6.09, 0.12791, 66, 57.92, -6.02, 0.83031, 2, 46, 167.84, 37.87, 0.15, 66, 57.63, 37.87, 0.85, 2, 46, 162.36, 71.5, 0.24107, 66, 52.15, 71.5, 0.75893, 2, 46, 163.11, -47.44, 0.41951, 66, 52.9, -47.44, 0.58049, 2, 46, 165.94, -5.87, 0.16355, 66, 55.73, -5.87, 0.83645, 2, 46, 165.14, 37.38, 0.15, 66, 54.93, 37.38, 0.85, 2, 46, 157.6, 71.4, 0.21735, 66, 47.39, 71.4, 0.78265, 3, 46, 163.5, -99.54, 0.61812, 47, 39.01, -15.71, 0.30445, 66, 53.29, -99.54, 0.07744, 2, 46, 45.38, 27.24, 0.16741, 66, -64.83, 27.24, 0.83259, 2, 46, 63.74, 44.98, 0.15, 66, -46.47, 44.98, 0.85, 2, 46, 70.7, 61.92, 0.22175, 66, -39.51, 61.92, 0.77825, 2, 46, 62.88, 78.71, 0.38, 66, -47.34, 78.71, 0.62, 2, 46, 46.53, 85.3, 0.51, 66, -63.68, 85.3, 0.49, 2, 46, 29.92, 79.34, 0.388, 66, -80.29, 79.34, 0.612, 2, 46, 15.08, 62.21, 0.38, 66, -95.13, 62.21, 0.62, 2, 46, 14.09, 45.32, 0.41, 66, -96.12, 45.32, 0.59, 2, 46, 24.3, 27.06, 0.192, 66, -85.91, 27.06, 0.808, 1, 66, -61.57, 15.3, 1, 2, 46, 84.87, 62.14, 0.16757, 66, -25.34, 62.14, 0.83243, 2, 46, 61.9, 94.84, 0.68145, 66, -48.31, 94.84, 0.31855, 2, 46, 22.54, 21.88, 0.16, 66, -87.67, 21.88, 0.84, 2, 46, 43.15, -95.95, 0.73445, 66, -67.06, -95.95, 0.26555, 2, 46, 41.77, -87.24, 0.67858, 66, -68.44, -87.24, 0.32142, 2, 46, 60.05, -70.36, 0.50195, 66, -50.16, -70.36, 0.49805, 2, 46, 48.49, 51.09, 0.25, 66, -61.72, 51.09, 0.75, 2, 46, 48.17, 73.71, 0.15634, 66, -62.04, 73.71, 0.84366 ], + "hull": 29, + "edges": [ 10, 8, 8, 6, 6, 4, 4, 2, 2, 0, 0, 56, 54, 56, 54, 52, 52, 50, 50, 48, 48, 46, 46, 44, 42, 44, 32, 34, 4, 58, 58, 60, 62, 64, 64, 66, 66, 54, 50, 68, 68, 70, 70, 44, 60, 72, 62, 74, 72, 74, 74, 76, 76, 78, 78, 44, 16, 80, 80, 82, 82, 84, 84, 86, 86, 44, 14, 88, 88, 72, 14, 16, 10, 12, 12, 14, 12, 60, 90, 92, 92, 94, 94, 96, 96, 98, 98, 100, 100, 102, 102, 104, 104, 106, 106, 90, 108, 110, 110, 112, 38, 40, 40, 42, 112, 40, 34, 36, 36, 38, 36, 114, 114, 108, 30, 32, 30, 28, 24, 26, 28, 26, 22, 24, 22, 20, 20, 18, 18, 16, 28, 116, 116, 118, 118, 120, 120, 20 ], + "width": 271, + "height": 298 + } + }, + "head-bb": { + "head": { + "type": "boundingbox", + "vertexCount": 6, + "vertices": [ -19.14, -70.3, 40.8, -118.08, 257.78, -115.62, 285.17, 57.18, 120.77, 164.95, -5.07, 76.95 ] + } + }, + "hoverboard-board": { + "hoverboard-board": { + "type": "mesh", + "uvs": [ 0.13865, 0.56624, 0.11428, 0.51461, 0.07619, 0.52107, 0.02364, 0.52998, 0.01281, 0.53182, 0, 0.37979, 0, 0.2206, 0.00519, 0.10825, 0.01038, 0.10726, 0.03834, 0.10194, 0.05091, 0, 0.08326, 0, 0.10933, 0.04206, 0.1382, 0.08865, 0.18916, 0.24067, 0.22234, 0.4063, 0.23886, 0.44063, 0.83412, 0.44034, 0.88444, 0.38296, 0.92591, 0.32639, 0.95996, 0.28841, 0.98612, 0.28542, 1, 0.38675, 0.99494, 0.47104, 0.97883, 0.53251, 0.94409, 0.62135, 0.90206, 0.69492, 0.86569, 0.71094, 0.82822, 0.70791, 0.81286, 0.77127, 0.62931, 0.77266, 0.61364, 0.70645, 0.47166, 0.70664, 0.45901, 0.77827, 0.27747, 0.76986, 0.2658, 0.70372, 0.24976, 0.71381, 0.24601, 0.77827, 0.23042, 0.84931, 0.20926, 0.90956, 0.17299, 1, 0.15077, 0.99967, 0.12906, 0.90192, 0.10369, 0.73693, 0.10198, 0.62482, 0.09131, 0.47272, 0.09133, 0.41325, 0.15082, 0.41868, 0.21991, 0.51856, 0.06331, 0.10816, 0.08383, 0.21696, 0.08905, 0.37532, 0.15903, 0.58726, 0.17538, 0.65706, 0.20118, 0.8029, 0.17918, 0.55644, 0.22166, 0.5802, 0.86259, 0.57962, 0.92346, 0.48534, 0.96691, 0.36881, 0.0945, 0.13259, 0.12688, 0.17831, 0.15986, 0.24682, 0.18036, 0.31268, 0.20607, 0.4235, 0.16074, 0.85403, 0.13624, 0.70122, 0.12096, 0.64049, 0.02396, 0.21811, 0.02732, 0.37839, 0.02557, 0.4972, 0.14476, 0.45736, 0.18019, 0.51689, 0.19692, 0.56636 ], + "triangles": [ 10, 11, 12, 9, 10, 12, 49, 9, 12, 60, 49, 12, 13, 60, 12, 61, 60, 13, 50, 49, 60, 50, 60, 61, 68, 8, 9, 68, 9, 49, 68, 49, 50, 7, 8, 68, 6, 7, 68, 61, 13, 14, 62, 61, 14, 50, 61, 62, 63, 62, 14, 59, 20, 21, 19, 20, 59, 51, 50, 62, 51, 62, 63, 51, 69, 68, 51, 68, 50, 6, 68, 69, 5, 6, 69, 18, 19, 59, 15, 63, 14, 59, 21, 22, 47, 51, 63, 47, 46, 51, 47, 63, 64, 15, 64, 63, 64, 15, 16, 71, 46, 47, 23, 59, 22, 69, 51, 70, 45, 46, 71, 70, 51, 2, 58, 18, 59, 58, 59, 23, 17, 18, 58, 70, 5, 69, 2, 51, 46, 1, 45, 71, 47, 48, 71, 47, 64, 48, 48, 72, 71, 1, 71, 72, 16, 48, 64, 45, 2, 46, 2, 45, 1, 70, 4, 5, 3, 70, 2, 3, 4, 70, 24, 58, 23, 72, 0, 1, 73, 55, 72, 55, 0, 72, 48, 73, 72, 57, 17, 58, 25, 57, 58, 56, 48, 16, 73, 48, 56, 56, 16, 17, 56, 17, 57, 52, 0, 55, 24, 25, 58, 44, 0, 52, 67, 44, 52, 52, 56, 53, 73, 52, 55, 56, 52, 73, 67, 52, 53, 26, 57, 25, 66, 67, 53, 56, 32, 35, 53, 56, 35, 56, 57, 32, 28, 31, 57, 57, 31, 32, 57, 27, 28, 26, 27, 57, 36, 53, 35, 43, 44, 67, 43, 67, 66, 34, 35, 32, 29, 31, 28, 30, 31, 29, 53, 54, 66, 53, 36, 54, 33, 34, 32, 37, 54, 36, 65, 43, 66, 38, 54, 37, 54, 65, 66, 39, 65, 54, 42, 43, 65, 38, 39, 54, 40, 42, 65, 40, 41, 42, 65, 39, 40 ], + "vertices": [ -189.36, 15.62, -201.35, 23.47, -220.09, 22.49, -245.95, 21.13, -251.28, 20.86, -257.58, 43.96, -257.57, 68.16, -255.02, 85.24, -252.47, 85.39, -238.71, 86.2, -232.52, 101.69, -216.61, 101.69, -203.78, 95.3, -189.58, 88.21, -164.51, 65.1, -148.19, 39.93, -140.06, 34.71, 152.82, 34.73, 177.57, 43.45, 197.97, 52.05, 214.72, 57.82, 227.6, 58.27, 234.42, 42.87, 231.94, 30.06, 224.01, 20.72, 206.91, 7.21, 186.23, -3.97, 168.34, -6.4, 149.9, -5.94, 142.35, -15.57, 52.04, -15.77, 44.33, -5.71, -25.52, -5.73, -31.75, -16.62, -121.07, -15.34, -126.81, -5.28, -134.7, -6.81, -136.54, -16.61, -144.22, -27.41, -154.63, -36.57, -172.47, -50.31, -183.41, -50.26, -194.09, -35.4, -206.56, -10.32, -207.4, 6.72, -212.65, 29.84, -212.64, 38.88, -183.37, 38.05, -149.38, 22.86, -226.43, 85.25, -216.33, 68.71, -213.76, 44.64, -179.34, 12.42, -171.29, 1.81, -158.6, -20.36, -169.42, 17.11, -148.52, 13.49, 166.82, 13.56, 196.76, 27.89, 218.14, 45.6, -211.08, 81.54, -195.15, 74.59, -178.93, 64.17, -168.84, 54.16, -156.19, 37.31, -178.5, -28.13, -190.55, -4.9, -198.07, 4.33, -245.79, 68.54, -244.14, 44.18, -245, 26.12, -186.36, 32.17, -168.92, 23.12, -160.69, 15.6 ], + "hull": 45, + "edges": [ 0, 2, 8, 10, 10, 12, 12, 14, 18, 20, 20, 22, 26, 28, 28, 30, 30, 32, 32, 34, 34, 36, 36, 38, 38, 40, 40, 42, 42, 44, 44, 46, 46, 48, 48, 50, 50, 52, 52, 54, 54, 56, 56, 58, 58, 60, 60, 62, 62, 64, 64, 66, 66, 68, 68, 70, 70, 72, 72, 74, 80, 82, 82, 84, 84, 86, 86, 88, 0, 88, 2, 90, 90, 92, 92, 94, 94, 96, 96, 32, 18, 98, 98, 100, 100, 102, 2, 4, 102, 4, 92, 102, 0, 104, 104, 106, 106, 108, 78, 80, 108, 78, 74, 76, 76, 78, 62, 56, 64, 70, 0, 110, 112, 114, 114, 116, 116, 118, 118, 42, 50, 116, 114, 34, 98, 120, 120, 122, 22, 24, 24, 26, 120, 24, 122, 124, 124, 126, 126, 128, 128, 96, 80, 130, 130, 132, 132, 134, 134, 88, 14, 16, 16, 18, 136, 16, 136, 138, 138, 140, 4, 6, 6, 8, 140, 6, 96, 112, 92, 142, 142, 144, 110, 146, 146, 112, 144, 146 ], + "width": 492, + "height": 152 + } + }, + "hoverboard-thruster-front": { + "hoverboard-thruster": { "x": 0.02, "y": -7.08, "rotation": 0.17, "width": 60, "height": 64 } + }, + "hoverboard-thruster-rear": { + "hoverboard-thruster": { "x": 1.1, "y": -6.29, "rotation": 0.17, "width": 60, "height": 64 } + }, + "hoverglow-front": { + "hoverglow-small": { + "x": 2.13, + "y": -2, + "scaleX": 0.303, + "scaleY": 0.495, + "rotation": 0.15, + "width": 274, + "height": 75 + } + }, + "hoverglow-rear": { + "hoverglow-small": { + "x": 1.39, + "y": -2.09, + "scaleX": 0.303, + "scaleY": 0.495, + "rotation": 0.61, + "width": 274, + "height": 75 + } + }, + "mouth": { + "mouth-grind": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 3, 0, 1, 2, 3 ], + "vertices": [ 2, 66, -98.93, -85.88, 0.22, 46, 11.28, -85.88, 0.78, 2, 66, -129.77, 1.84, 0.6, 46, -19.56, 1.84, 0.4, 2, 66, -74.12, 21.41, 0.6, 46, 36.09, 21.41, 0.4, 2, 66, -43.28, -66.32, 0.4, 46, 66.93, -66.32, 0.6 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 59 + }, + "mouth-oooo": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 3, 0, 1, 2, 3 ], + "vertices": [ 2, 46, 11.28, -85.89, 0.22, 66, -98.93, -85.89, 0.78, 2, 46, -19.56, 1.85, 0.6, 66, -129.78, 1.85, 0.4, 2, 46, 36.1, 21.42, 0.6, 66, -74.12, 21.42, 0.4, 2, 46, 66.94, -66.32, 0.4, 66, -43.27, -66.32, 0.6 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 59 + }, + "mouth-smile": { + "type": "mesh", + "uvs": [ 1, 1, 0, 1, 0, 0, 1, 0 ], + "triangles": [ 1, 3, 0, 1, 2, 3 ], + "vertices": [ 2, 66, -98.93, -85.89, 0.21075, 46, 11.28, -85.89, 0.78925, 2, 66, -129.77, 1.85, 0.6, 46, -19.56, 1.85, 0.4, 2, 66, -74.11, 21.42, 0.6, 46, 36.1, 21.42, 0.4, 2, 66, -43.27, -66.32, 0.40772, 46, 66.94, -66.32, 0.59228 ], + "hull": 4, + "edges": [ 0, 2, 2, 4, 4, 6, 0, 6 ], + "width": 93, + "height": 59 + } + }, + "muzzle": { + "muzzle01": { + "x": 151.97, + "y": 5.81, + "scaleX": 3.7361, + "scaleY": 3.7361, + "rotation": 0.15, + "width": 133, + "height": 79 + }, + "muzzle02": { + "x": 187.25, + "y": 5.9, + "scaleX": 4.0623, + "scaleY": 4.0623, + "rotation": 0.15, + "width": 135, + "height": 84 + }, + "muzzle03": { + "x": 231.96, + "y": 6.02, + "scaleX": 4.1325, + "scaleY": 4.1325, + "rotation": 0.15, + "width": 166, + "height": 106 + }, + "muzzle04": { + "x": 231.96, + "y": 6.02, + "scaleX": 4.0046, + "scaleY": 4.0046, + "rotation": 0.15, + "width": 149, + "height": 90 + }, + "muzzle05": { + "x": 293.8, + "y": 6.19, + "scaleX": 4.4673, + "scaleY": 4.4673, + "rotation": 0.15, + "width": 135, + "height": 75 + } + }, + "muzzle-glow": { + "muzzle-glow": { "width": 50, "height": 50 } + }, + "muzzle-ring": { + "muzzle-ring": { "x": -1.3, "y": 0.32, "scaleX": 0.3147, "scaleY": 0.3147, "width": 49, "height": 209 } + }, + "muzzle-ring2": { + "muzzle-ring": { "x": -1.3, "y": 0.32, "scaleX": 0.3147, "scaleY": 0.3147, "width": 49, "height": 209 } + }, + "muzzle-ring3": { + "muzzle-ring": { "x": -1.3, "y": 0.32, "scaleX": 0.3147, "scaleY": 0.3147, "width": 49, "height": 209 } + }, + "muzzle-ring4": { + "muzzle-ring": { "x": -1.3, "y": 0.32, "scaleX": 0.3147, "scaleY": 0.3147, "width": 49, "height": 209 } + }, + "neck": { + "neck": { "x": 9.77, "y": -3.01, "rotation": -55.22, "width": 36, "height": 41 } + }, + "portal-bg": { + "portal-bg": { "x": -3.1, "y": 7.25, "scaleX": 1.0492, "scaleY": 1.0492, "width": 266, "height": 266 } + }, + "portal-flare1": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare2": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare3": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare4": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare5": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare6": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare7": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare8": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare9": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-flare10": { + "portal-flare1": { "width": 111, "height": 60 }, + "portal-flare2": { "width": 114, "height": 61 }, + "portal-flare3": { "width": 115, "height": 59 } + }, + "portal-shade": { + "portal-shade": { "width": 266, "height": 266 } + }, + "portal-streaks1": { + "portal-streaks1": { "scaleX": 0.9774, "scaleY": 0.9774, "width": 252, "height": 256 } + }, + "portal-streaks2": { + "portal-streaks2": { "x": -1.64, "y": 2.79, "width": 250, "height": 249 } + }, + "rear-bracer": { + "rear-bracer": { "x": 11.15, "y": -2.2, "rotation": 66.17, "width": 56, "height": 72 } + }, + "rear-foot": { + "rear-foot": { + "type": "mesh", + "uvs": [ 0.48368, 0.1387, 0.51991, 0.21424, 0.551, 0.27907, 0.58838, 0.29816, 0.63489, 0.32191, 0.77342, 0.39267, 1, 0.73347, 1, 1, 0.54831, 0.99883, 0.31161, 1, 0, 1, 0, 0.41397, 0.13631, 0, 0.41717, 0 ], + "triangles": [ 8, 3, 4, 8, 4, 5, 8, 5, 6, 8, 6, 7, 11, 1, 10, 3, 9, 2, 2, 10, 1, 12, 13, 0, 0, 11, 12, 1, 11, 0, 2, 9, 10, 3, 8, 9 ], + "vertices": [ 2, 8, 10.45, 29.41, 0.90802, 9, -6.74, 49.62, 0.09198, 2, 8, 16.56, 29.27, 0.84259, 9, -2.65, 45.09, 0.15741, 2, 8, 21.8, 29.15, 0.69807, 9, 0.85, 41.2, 0.30193, 2, 8, 25.53, 31.43, 0.52955, 9, 5.08, 40.05, 0.47045, 2, 8, 30.18, 34.27, 0.39303, 9, 10.33, 38.62, 0.60697, 2, 8, 44.02, 42.73, 0.27525, 9, 25.98, 34.36, 0.72475, 2, 8, 76.47, 47.28, 0.21597, 9, 51.56, 13.9, 0.78403, 2, 8, 88.09, 36.29, 0.28719, 9, 51.55, -2.09, 0.71281, 2, 8, 52.94, -0.73, 0.47576, 9, 0.52, -1.98, 0.52424, 2, 8, 34.63, -20.23, 0.68757, 9, -26.23, -2.03, 0.31243, 2, 8, 10.44, -45.81, 0.84141, 9, -61.43, -2, 0.15859, 2, 8, -15.11, -21.64, 0.93283, 9, -61.4, 33.15, 0.06717, 1, 8, -22.57, 6.61, 1, 1, 8, -0.76, 29.67, 1 ], + "hull": 14, + "edges": [ 14, 12, 10, 12, 14, 16, 16, 18, 18, 20, 4, 18, 20, 22, 24, 26, 22, 24, 4, 2, 2, 20, 4, 6, 6, 16, 6, 8, 8, 10, 2, 0, 0, 26 ], + "width": 113, + "height": 60 + } + }, + "rear-shin": { + "rear-shin": { "x": 58.29, "y": -2.75, "rotation": 92.37, "width": 75, "height": 178 } + }, + "rear-thigh": { + "rear-thigh": { "x": 33.11, "y": -4.11, "rotation": 72.54, "width": 55, "height": 94 } + }, + "rear-upper-arm": { + "rear-upper-arm": { "x": 21.13, "y": 4.09, "rotation": 89.33, "width": 40, "height": 87 } + }, + "side-glow1": { + "hoverglow-small": { "x": 2.09, "scaleX": 0.2353, "scaleY": 0.4132, "width": 274, "height": 75 } + }, + "side-glow2": { + "hoverglow-small": { "x": 2.09, "scaleX": 0.2353, "scaleY": 0.4132, "width": 274, "height": 75 } + }, + "side-glow3": { + "hoverglow-small": { "x": 2.09, "scaleX": 0.3586, "scaleY": 0.6297, "width": 274, "height": 75 } + }, + "torso": { + "torso": { + "type": "mesh", + "uvs": [ 0.6251, 0.12672, 1, 0.26361, 1, 0.28871, 1, 0.66021, 1, 0.68245, 0.92324, 0.69259, 0.95116, 0.84965, 0.77124, 1, 0.49655, 1, 0.27181, 1, 0.13842, 0.77196, 0.09886, 0.6817, 0.05635, 0.58471, 0, 0.45614, 0, 0.33778, 0, 0.19436, 0.14463, 0, 0.27802, 0, 0.72525, 0.27835, 0.76091, 0.46216, 0.84888, 0.67963, 0.68257, 0.63249, 0.53986, 0.3847, 0.25443, 0.3217, 0.30063, 0.55174, 0.39553, 0.79507, 0.26389, 0.17007, 0.5241, 0.18674, 0.71492, 0.76655, 0.82151, 0.72956, 0.27626, 0.4304, 0.62327, 0.52952, 0.3455, 0.66679, 0.53243, 0.2914 ], + "triangles": [ 18, 1, 2, 19, 2, 3, 18, 0, 1, 23, 15, 26, 27, 26, 16, 14, 15, 23, 15, 16, 26, 17, 27, 16, 13, 14, 23, 0, 27, 17, 13, 23, 30, 11, 12, 24, 21, 31, 19, 12, 13, 30, 24, 22, 31, 31, 22, 19, 12, 30, 24, 32, 24, 31, 24, 30, 22, 3, 20, 19, 32, 31, 21, 11, 24, 32, 4, 5, 3, 8, 28, 7, 7, 29, 6, 7, 28, 29, 9, 25, 8, 8, 25, 28, 9, 10, 25, 29, 5, 6, 10, 32, 25, 25, 21, 28, 25, 32, 21, 10, 11, 32, 28, 21, 29, 29, 20, 5, 29, 21, 20, 5, 20, 3, 20, 21, 19, 33, 26, 27, 22, 18, 19, 19, 18, 2, 33, 27, 18, 30, 23, 22, 22, 33, 18, 23, 33, 22, 33, 23, 26, 27, 0, 18 ], + "vertices": [ 2, 29, 44.59, -10.39, 0.88, 40, -17.65, 33.99, 0.12, 3, 28, 59.65, -45.08, 0.12189, 29, 17.13, -45.08, 0.26811, 40, 22.68, 15.82, 0.61, 3, 28, 55.15, -44.72, 0.1345, 29, 12.63, -44.72, 0.2555, 40, 23.43, 11.37, 0.61, 3, 27, 31.01, -39.45, 0.51133, 28, -11.51, -39.45, 0.30867, 40, 34.58, -54.57, 0.18, 3, 27, 27.01, -39.14, 0.53492, 28, -15.5, -39.14, 0.28508, 40, 35.25, -58.52, 0.18, 2, 27, 25.79, -31.5, 0.75532, 28, -16.73, -31.5, 0.24468, 1, 27, -2.61, -32, 1, 1, 27, -28.2, -12.29, 1, 1, 27, -26.08, 14.55, 1, 1, 27, -24.35, 36.5, 1, 2, 27, 17.6, 46.3, 0.8332, 28, -24.92, 46.3, 0.1668, 2, 27, 34.1, 48.89, 0.59943, 28, -8.42, 48.89, 0.40058, 3, 27, 51.83, 51.67, 0.29262, 28, 9.32, 51.67, 0.63181, 29, -33.2, 51.67, 0.07557, 3, 27, 75.34, 55.35, 0.06656, 28, 32.82, 55.35, 0.62298, 29, -9.7, 55.35, 0.31046, 2, 28, 54.06, 53.67, 0.37296, 29, 11.54, 53.67, 0.62704, 2, 28, 79.79, 51.64, 0.10373, 29, 37.27, 51.64, 0.89627, 1, 29, 71.04, 34.76, 1, 1, 29, 70.01, 21.72, 1, 1, 30, 36.74, 7.06, 1, 3, 30, 45.7, -24.98, 0.67, 28, 25.87, -18.9, 0.3012, 29, -16.65, -18.9, 0.0288, 2, 27, 28.69, -24.42, 0.77602, 28, -13.83, -24.42, 0.22398, 3, 30, 43.24, -56.49, 0.064, 27, 38.43, -8.84, 0.67897, 28, -4.09, -8.84, 0.25703, 3, 30, 22.02, -14.85, 0.29, 28, 41.48, 1.59, 0.53368, 29, -1.04, 1.59, 0.17632, 3, 30, -7.45, -8.33, 0.76, 28, 54.98, 28.59, 0.06693, 29, 12.46, 28.59, 0.17307, 3, 30, 3.91, -48.4, 0.25, 27, 55.87, 27.33, 0.15843, 28, 13.35, 27.33, 0.59157, 1, 27, 11.47, 21.51, 1, 2, 30, -11.09, 18.74, 0.416, 29, 39.6, 25.51, 0.584, 2, 30, 14.56, 20.03, 0.53, 29, 34.6, 0.33, 0.47, 1, 27, 14.12, -10.1, 1, 2, 27, 19.94, -21.03, 0.92029, 28, -22.58, -21.03, 0.07971, 3, 30, -2.08, -27.26, 0.29, 28, 35.31, 27.99, 0.49582, 29, -7.21, 27.99, 0.21418, 2, 30, 34.42, -39.19, 0.25, 28, 14.84, -4.5, 0.75, 2, 27, 34.87, 24.58, 0.67349, 28, -7.64, 24.58, 0.32651, 2, 30, 18.5, 1.59, 0.76, 29, 15.76, 1, 0.24 ], + "hull": 18, + "edges": [ 14, 12, 12, 10, 10, 8, 18, 20, 32, 34, 30, 32, 2, 4, 36, 4, 36, 38, 38, 40, 4, 6, 6, 8, 40, 6, 40, 42, 14, 16, 16, 18, 50, 16, 46, 52, 54, 36, 2, 0, 0, 34, 54, 0, 54, 32, 20, 50, 14, 56, 56, 42, 50, 56, 56, 58, 58, 40, 58, 10, 46, 60, 60, 48, 26, 60, 60, 44, 24, 26, 24, 48, 42, 62, 62, 44, 48, 62, 48, 64, 64, 50, 42, 64, 20, 22, 22, 24, 64, 22, 26, 28, 28, 30, 28, 46, 44, 66, 66, 54, 46, 66, 66, 36, 62, 38 ], + "width": 98, + "height": 180 + } + } + } + } + ], + "events": { + "footstep": {} + }, + "animations": { + "aim": { + "slots": { + "crosshair": { + "attachment": [ + { "name": "crosshair" } + ] + } + }, + "bones": { + "front-fist": { + "rotate": [ + { "value": 36.08 } + ] + }, + "rear-bracer": { + "rotate": [ + { "value": -26.55 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { "value": 62.31 } + ] + }, + "front-bracer": { + "rotate": [ + { "value": 9.11 } + ] + }, + "gun": { + "rotate": [ + { "value": -0.31 } + ] + } + }, + "ik": { + "aim-ik": [ + { "mix": 0.995 } + ] + }, + "transform": { + "aim-front-arm-transform": [ + { "mixRotate": 0.784, "mixX": 0, "mixScaleX": 0, "mixShearY": 0 } + ], + "aim-head-transform": [ + { "mixRotate": 0.659, "mixX": 0, "mixScaleX": 0, "mixShearY": 0 } + ], + "aim-torso-transform": [ + { "mixRotate": 0.423, "mixX": 0, "mixScaleX": 0, "mixShearY": 0 } + ] + } + }, + "death": { + "slots": { + "eye": { + "attachment": [ + { "name": "eye-surprised" }, + { "time": 0.5333, "name": "eye-indifferent" }, + { "time": 2.2, "name": "eye-surprised" }, + { "time": 4.6, "name": "eye-indifferent" } + ] + }, + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + }, + "mouth": { + "attachment": [ + { "name": "mouth-oooo" }, + { "time": 0.5333, "name": "mouth-grind" }, + { "time": 1.4, "name": "mouth-oooo" }, + { "time": 2.1667, "name": "mouth-grind" }, + { "time": 4.5333, "name": "mouth-oooo" } + ] + } + }, + "bones": { + "head": { + "rotate": [ + { + "value": -2.83, + "curve": [ 0.015, -2.83, 0.036, 12.72 ] + }, + { + "time": 0.0667, + "value": 12.19, + "curve": [ 0.096, 11.68, 0.119, -1.14 ] + }, + { + "time": 0.1333, + "value": -6.86, + "curve": [ 0.149, -13.27, 0.21, -37.28 ] + }, + { + "time": 0.3, + "value": -36.86, + "curve": [ 0.354, -36.61, 0.412, -32.35 ] + }, + { + "time": 0.4667, + "value": -23.49, + "curve": [ 0.49, -19.87, 0.512, -3.29 ] + }, + { + "time": 0.5333, + "value": -3.24, + "curve": [ 0.56, -3.39, 0.614, -67.25 ] + }, + { + "time": 0.6333, + "value": -74.4, + "curve": [ 0.652, -81.58, 0.702, -88.94 ] + }, + { + "time": 0.7333, + "value": -88.93, + "curve": [ 0.805, -88.91, 0.838, -80.87 ] + }, + { + "time": 0.8667, + "value": -81.03, + "curve": [ 0.922, -81.32, 0.976, -85.29 ] + }, + { "time": 1, "value": -85.29, "curve": "stepped" }, + { + "time": 2.2333, + "value": -85.29, + "curve": [ 2.314, -85.29, 2.382, -68.06 ] + }, + { + "time": 2.4667, + "value": -63.48, + "curve": [ 2.57, -57.87, 2.916, -55.24 ] + }, + { + "time": 3.2, + "value": -55.1, + "curve": [ 3.447, -54.98, 4.135, -56.61 ] + }, + { + "time": 4.2667, + "value": -58.23, + "curve": [ 4.672, -63.24, 4.646, -82.69 ] + }, + { "time": 4.9333, "value": -85.29 } + ], + "scale": [ + { + "time": 0.4667, + "curve": [ 0.469, 1.005, 0.492, 1.065, 0.475, 1.018, 0.492, 0.94 ] + }, + { + "time": 0.5, + "x": 1.065, + "y": 0.94, + "curve": [ 0.517, 1.065, 0.541, 0.991, 0.517, 0.94, 0.542, 1.026 ] + }, + { + "time": 0.5667, + "x": 0.99, + "y": 1.025, + "curve": [ 0.593, 0.988, 0.609, 1.002, 0.595, 1.024, 0.607, 1.001 ] + }, + { "time": 0.6333 } + ] + }, + "neck": { + "rotate": [ + { + "value": -2.83, + "curve": [ 0.114, 1.33, 0.195, 4.13 ] + }, + { + "time": 0.2667, + "value": 4.13, + "curve": [ 0.351, 4.14, 0.444, -24.5 ] + }, + { + "time": 0.5, + "value": -24.69, + "curve": [ 0.571, -23.89, 0.55, 34.22 ] + }, + { + "time": 0.6667, + "value": 35.13, + "curve": [ 0.713, 34.81, 0.756, 22.76 ] + }, + { + "time": 0.8333, + "value": 22.82, + "curve": [ 0.868, 22.84, 0.916, 47.95 ] + }, + { "time": 0.9667, "value": 47.95, "curve": "stepped" }, + { + "time": 2.2333, + "value": 47.95, + "curve": [ 2.3, 47.95, 2.617, 18.72 ] + }, + { + "time": 2.6667, + "value": 18.51, + "curve": [ 3.172, 16.58, 4.06, 16.79 ] + }, + { + "time": 4.5333, + "value": 18.51, + "curve": [ 4.707, 19.13, 4.776, 41.11 ] + }, + { "time": 4.8, "value": 47.95 } + ] + }, + "torso": { + "rotate": [ + { + "value": -8.62, + "curve": [ 0.01, -16.71, 0.032, -33.6 ] + }, + { + "time": 0.0667, + "value": -33.37, + "curve": [ 0.182, -32.61, 0.298, 123.07 ] + }, + { + "time": 0.4667, + "value": 122.77, + "curve": [ 0.511, 122.69, 0.52, 100.2 ] + }, + { + "time": 0.5667, + "value": 88.96, + "curve": [ 0.588, 83.89, 0.667, 75.34 ] + }, + { + "time": 0.7, + "value": 75.34, + "curve": [ 0.767, 75.34, 0.9, 76.03 ] + }, + { "time": 0.9667, "value": 76.03 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -38.86, + "curve": [ 0.022, -40.38, 0.096, -41.92 ] + }, + { + "time": 0.1333, + "value": -41.92, + "curve": [ 0.176, -41.92, 0.216, -16.92 ] + }, + { + "time": 0.2333, + "value": -4.35, + "curve": [ 0.258, 13.69, 0.308, 60.35 ] + }, + { + "time": 0.4, + "value": 60.17, + "curve": [ 0.496, 59.98, 0.539, 33.63 ] + }, + { + "time": 0.5667, + "value": 23.06, + "curve": [ 0.595, 32.71, 0.675, 53.71 ] + }, + { + "time": 0.7333, + "value": 53.61, + "curve": [ 0.797, 53.51, 0.926, 30.98 ] + }, + { "time": 0.9333, "value": 19.57, "curve": "stepped" }, + { + "time": 1.9667, + "value": 19.57, + "curve": [ 2.245, 19.57, 2.702, 77.03 ] + }, + { + "time": 3.0667, + "value": 77.06, + "curve": [ 3.209, 77.33, 3.291, 67.99 ] + }, + { + "time": 3.4333, + "value": 67.96, + "curve": [ 3.608, 68.34, 3.729, 73.88 ] + }, + { + "time": 3.8333, + "value": 73.42, + "curve": [ 4.152, 73.91, 4.46, 71.98 ] + }, + { + "time": 4.6333, + "value": 64.77, + "curve": [ 4.688, 62.5, 4.847, 26.42 ] + }, + { "time": 4.8667, "value": 10.94 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": -44.7, + "curve": [ 0.033, -44.7, 0.12, 54.89 ] + }, + { + "time": 0.1333, + "value": 64.62, + "curve": [ 0.154, 79.18, 0.214, 79.42 ] + }, + { + "time": 0.2667, + "value": 63.4, + "curve": [ 0.293, 55.19, 0.332, 30.13 ] + }, + { + "time": 0.3667, + "value": 30.13, + "curve": [ 0.4, 30.13, 0.441, 39.87 ] + }, + { + "time": 0.4667, + "value": 55.13, + "curve": [ 0.488, 68.18, 0.52, 100.72 ] + }, + { + "time": 0.5333, + "value": 111.96, + "curve": [ 0.551, 126.88, 0.627, 185.97 ] + }, + { + "time": 0.6667, + "value": 185.97, + "curve": [ 0.692, 185.97, 0.736, 162.43 ] + }, + { + "time": 0.8, + "value": 158.01, + "curve": [ 0.9, 151.12, 1.017, 144.01 ] + }, + { "time": 1.1, "value": 144.01, "curve": "stepped" }, + { + "time": 2.3667, + "value": 144.01, + "curve": [ 2.492, 144.01, 2.742, 138.63 ] + }, + { + "time": 2.8667, + "value": 138.63, + "curve": [ 3.067, 138.63, 3.467, 138.63 ] + }, + { + "time": 3.6667, + "value": 138.63, + "curve": [ 3.883, 138.63, 4.317, 135.18 ] + }, + { + "time": 4.5333, + "value": 135.18, + "curve": [ 4.575, 135.18, 4.692, 131.59 ] + }, + { + "time": 4.7333, + "value": 131.59, + "curve": [ 4.758, 131.59, 4.517, 144.01 ] + }, + { "time": 4.8333, "value": 144.01 } + ], + "translate": [ + { + "time": 0.4667, + "curve": [ 0.517, 0, 0.617, -34.96, 0.517, 0, 0.617, -16.59 ] + }, + { "time": 0.6667, "x": -35.02, "y": -16.62 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 21.88, + "curve": [ 0.033, 21.88, 0.099, 20.44 ] + }, + { + "time": 0.1333, + "value": 9.43, + "curve": [ 0.164, -0.29, 0.162, -38.26 ] + }, + { + "time": 0.2, + "value": -38.05, + "curve": [ 0.24, -37.96, 0.228, -17.82 ] + }, + { + "time": 0.3333, + "value": -9.73, + "curve": [ 0.372, -6.76, 0.431, -0.74 ] + }, + { + "time": 0.4667, + "value": 6.47, + "curve": [ 0.489, 11.05, 0.503, 19.09 ] + }, + { + "time": 0.5333, + "value": 19.09, + "curve": [ 0.571, 19.09, 0.554, -42.67 ] + }, + { + "time": 0.6, + "value": -42.67, + "curve": [ 0.653, -42.67, 0.691, -13.8 ] + }, + { + "time": 0.7, + "value": -3.54, + "curve": [ 0.707, 3.8, 0.719, 24.94 ] + }, + { + "time": 0.8, + "value": 25.31, + "curve": [ 0.902, 24.75, 0.992, -0.34 ] + }, + { "time": 1, "value": -32.16, "curve": "stepped" }, + { + "time": 2.2333, + "value": -32.16, + "curve": [ 2.6, -32.16, 2.638, -5.3 ] + }, + { + "time": 2.7, + "value": -1.96, + "curve": [ 2.707, -1.56, 2.775, 1.67 ] + }, + { + "time": 2.8, + "value": 1.67, + "curve": [ 2.825, 1.67, 2.875, -0.39 ] + }, + { + "time": 2.9, + "value": -0.39, + "curve": [ 2.925, -0.39, 2.975, 0.26 ] + }, + { + "time": 3, + "value": 0.26, + "curve": [ 3.025, 0.26, 3.075, -1.81 ] + }, + { + "time": 3.1, + "value": -1.81, + "curve": [ 3.125, -1.81, 3.175, -0.52 ] + }, + { + "time": 3.2, + "value": -0.52, + "curve": [ 3.225, -0.52, 3.275, -2.41 ] + }, + { + "time": 3.3, + "value": -2.41, + "curve": [ 3.333, -2.41, 3.4, -0.38 ] + }, + { + "time": 3.4333, + "value": -0.38, + "curve": [ 3.467, -0.38, 3.533, -2.25 ] + }, + { + "time": 3.5667, + "value": -2.25, + "curve": [ 3.592, -2.25, 3.642, -0.33 ] + }, + { + "time": 3.6667, + "value": -0.33, + "curve": [ 3.7, -0.33, 3.767, -1.34 ] + }, + { + "time": 3.8, + "value": -1.34, + "curve": [ 3.825, -1.34, 3.862, -0.77 ] + }, + { + "time": 3.9, + "value": -0.77, + "curve": [ 3.942, -0.77, 3.991, -1.48 ] + }, + { + "time": 4, + "value": -1.87, + "curve": [ 4.167, -1.87, 4.5, -1.96 ] + }, + { + "time": 4.6667, + "value": -1.96, + "curve": [ 4.709, 18.05, 4.767, 34.55 ] + }, + { + "time": 4.8, + "value": 34.55, + "curve": [ 4.84, 34.24, 4.902, 12.03 ] + }, + { "time": 4.9333, "value": -18.75 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -2.33, + "curve": [ 0.019, 4.43, 0.069, 10.82 ] + }, + { + "time": 0.1, + "value": 10.6, + "curve": [ 0.148, 10.6, 0.123, -15.24 ] + }, + { + "time": 0.2, + "value": -15.35, + "curve": [ 0.266, -15.44, 0.316, -6.48 ] + }, + { + "time": 0.3333, + "value": -3.9, + "curve": [ 0.362, 0.43, 0.479, 22.36 ] + }, + { + "time": 0.5667, + "value": 22.01, + "curve": [ 0.61, 21.84, 0.627, 12.85 ] + }, + { + "time": 0.6333, + "value": 9.05, + "curve": [ 0.643, 2.77, 0.622, -39.43 ] + }, + { + "time": 0.7, + "value": -39.5, + "curve": [ 0.773, -39.57, 0.814, 14.77 ] + }, + { + "time": 0.8667, + "value": 14.81, + "curve": [ 0.965, 14.88, 1.1, 5.64 ] + }, + { "time": 1.1, "value": -6.08, "curve": "stepped" }, + { + "time": 2.2333, + "value": -6.08, + "curve": [ 2.307, -6.08, 2.427, -25.89 ] + }, + { + "time": 2.5333, + "value": -22.42, + "curve": [ 2.598, -20.38, 2.657, 5.73 ] + }, + { + "time": 2.7, + "value": 5.73, + "curve": [ 2.77, 5.73, 2.851, -5.38 ] + }, + { + "time": 2.9333, + "value": -5.38, + "curve": [ 3.008, -5.38, 3.087, -4.54 ] + }, + { + "time": 3.1667, + "value": -4.17, + "curve": [ 3.223, -3.91, 4.486, 5.73 ] + }, + { + "time": 4.6667, + "value": 5.73, + "curve": [ 4.733, 5.73, 4.886, -2.47 ] + }, + { "time": 4.9333, "value": -6.52 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 10.36, + "curve": [ 0.033, 10.36, 0.1, -32.89 ] + }, + { + "time": 0.1333, + "value": -32.89, + "curve": [ 0.183, -32.89, 0.283, -4.45 ] + }, + { + "time": 0.3333, + "value": -4.45, + "curve": [ 0.367, -4.45, 0.438, -6.86 ] + }, + { + "time": 0.4667, + "value": -8.99, + "curve": [ 0.529, -13.62, 0.605, -20.58 ] + }, + { + "time": 0.6333, + "value": -23.2, + "curve": [ 0.708, -30.18, 0.758, -35.56 ] + }, + { + "time": 0.8, + "value": -35.56, + "curve": [ 0.875, -35.56, 1.025, -23.2 ] + }, + { "time": 1.1, "value": -23.2 } + ] + }, + "gun": { + "rotate": [ + { + "value": -2.79, + "curve": [ 0.033, -2.79, 0.12, -7.22 ] + }, + { + "time": 0.1333, + "value": -8.52, + "curve": [ 0.168, -11.87, 0.29, -23.71 ] + }, + { + "time": 0.3333, + "value": -26.24, + "curve": [ 0.369, -28.31, 0.436, -29.75 ] + }, + { + "time": 0.5, + "value": -29.66, + "curve": [ 0.552, -29.58, 0.611, -25.47 ] + }, + { + "time": 0.6333, + "value": -22.68, + "curve": [ 0.656, -19.76, 0.68, -10.02 ] + }, + { + "time": 0.7, + "value": -6.49, + "curve": [ 0.722, -2.6, 0.75, -1.22 ] + }, + { + "time": 0.7667, + "value": -1.35, + "curve": [ 0.792, -1.55, 0.842, -19.74 ] + }, + { "time": 0.8667, "value": -19.8 } + ] + }, + "hip": { + "translate": [ + { + "curve": [ 0.098, -42.62, 0.166, -79.85, 0.029, 84.97, 0.109, 155.93 ] + }, + { + "time": 0.2667, + "x": -133.79, + "y": 152.44, + "curve": [ 0.361, -184.63, 0.392, -203.69, 0.42, 149.12, 0.467, -15.7 ] + }, + { + "time": 0.4667, + "x": -230.02, + "y": -113.87, + "curve": [ 0.523, -249.86, 0.565, -261.7, 0.473, -133.1, 0.583, -203.43 ] + }, + { + "time": 0.6, + "x": -268.57, + "y": -203.43, + "curve": [ 0.663, -280.98, 0.816, -290.05, 0.708, -203.43, 0.892, -203.5 ] + }, + { "time": 1, "x": -290.42, "y": -203.5 } + ] + }, + "front-thigh": { + "rotate": [ + { + "curve": [ 0.06, 1.02, 0.151, 45.23 ] + }, + { + "time": 0.1667, + "value": 54.01, + "curve": [ 0.19, 66.85, 0.358, 169.85 ] + }, + { + "time": 0.5, + "value": 169.51, + "curve": [ 0.628, 169.85, 0.692, 108.85 ] + }, + { + "time": 0.7, + "value": 97.74, + "curve": [ 0.723, 102.6, 0.805, 111.6 ] + }, + { + "time": 0.8667, + "value": 111.69, + "curve": [ 0.899, 111.83, 1.015, 109.15 ] + }, + { "time": 1.0667, "value": 95.8 } + ] + }, + "front-shin": { + "rotate": [ + { + "curve": [ 0.086, -0.02, 0.191, -24.25 ] + }, + { + "time": 0.2, + "value": -26.5, + "curve": [ 0.214, -29.92, 0.249, -40.51 ] + }, + { + "time": 0.3333, + "value": -40.57, + "curve": [ 0.431, -40.7, 0.459, -11.34 ] + }, + { + "time": 0.4667, + "value": -8.71, + "curve": [ 0.477, -5.16, 0.524, 17.13 ] + }, + { + "time": 0.6, + "value": 16.98, + "curve": [ 0.632, 17.09, 0.625, 2.76 ] + }, + { + "time": 0.6333, + "value": 2.76, + "curve": [ 0.648, 2.76, 0.653, 2.75 ] + }, + { + "time": 0.6667, + "value": 2.59, + "curve": [ 0.678, 2.39, 0.733, 2.53 ] + }, + { + "time": 0.7333, + "value": -9.43, + "curve": [ 0.745, -2.48, 0.782, 3.12 ] + }, + { + "time": 0.8, + "value": 4.28, + "curve": [ 0.832, 6.32, 0.895, 8.46 ] + }, + { + "time": 0.9333, + "value": 8.49, + "curve": [ 0.986, 8.53, 1.051, 6.38 ] + }, + { + "time": 1.0667, + "value": 2.28, + "curve": [ 1.078, 4.17, 1.103, 5.86 ] + }, + { + "time": 1.1333, + "value": 5.88, + "curve": [ 1.191, 5.93, 1.209, 4.56 ] + }, + { "time": 1.2333, "value": 2.52 } + ] + }, + "rear-thigh": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.12, 50.26 ] + }, + { + "time": 0.1333, + "value": 57.3, + "curve": [ 0.164, 73.34, 0.274, 147.18 ] + }, + { + "time": 0.3333, + "value": 147.1, + "curve": [ 0.475, 146.45, 0.583, 95.72 ] + }, + { + "time": 0.6, + "value": 79.66, + "curve": [ 0.62, 94.74, 0.732, 103.15 ] + }, + { + "time": 0.7667, + "value": 103.02, + "curve": [ 0.812, 102.85, 0.897, 95.75 ] + }, + { "time": 0.9333, "value": 83.01 } + ] + }, + "rear-shin": { + "rotate": [ + { + "curve": [ 0.021, -16.65, 0.091, -54.82 ] + }, + { + "time": 0.1667, + "value": -55.29, + "curve": [ 0.187, -55.42, 0.213, -52.52 ] + }, + { + "time": 0.2333, + "value": -45.98, + "curve": [ 0.242, -43.1, 0.311, -12.73 ] + }, + { + "time": 0.3333, + "value": -6.32, + "curve": [ 0.356, 0.13, 0.467, 24.5 ] + }, + { + "time": 0.5, + "value": 24.5, + "curve": [ 0.543, 24.5, 0.56, 3.78 ] + }, + { + "time": 0.5667, + "value": -3.53, + "curve": [ 0.585, 3.86, 0.659, 16.63 ] + }, + { + "time": 0.7, + "value": 16.56, + "curve": [ 0.782, 16.43, 0.896, 8.44 ] + }, + { + "time": 0.9333, + "value": 4.04, + "curve": [ 0.956, 6.84, 1.008, 8.41 ] + }, + { + "time": 1.0333, + "value": 8.41, + "curve": [ 1.067, 8.41, 1.122, 8.14 ] + }, + { "time": 1.1667, "value": 5.8 } + ] + }, + "rear-foot": { + "rotate": [ + { + "value": -0.28, + "curve": [ 0.033, -0.28, 0.256, -66.71 ] + }, + { + "time": 0.3667, + "value": -66.84, + "curve": [ 0.418, -66.91, 0.499, -21.79 ] + }, + { + "time": 0.6, + "value": -21.52, + "curve": [ 0.652, -21.38, 0.665, -53.96 ] + }, + { + "time": 0.7, + "value": -54.26, + "curve": [ 0.757, -53.96, 0.843, -2.07 ] + }, + { + "time": 0.9333, + "value": -1.47, + "curve": [ 0.968, -2.07, 0.975, -19.96 ] + }, + { + "time": 1, + "value": -19.96, + "curve": [ 1.025, -19.96, 1.075, -12.42 ] + }, + { + "time": 1.1, + "value": -12.42, + "curve": [ 1.133, -12.42, 1.2, -18.34 ] + }, + { "time": 1.2333, "value": -18.34 } + ] + }, + "front-foot": { + "rotate": [ + { + "curve": [ 0.008, -11.33, 0.108, -57.71 ] + }, + { + "time": 0.1333, + "value": -57.71, + "curve": [ 0.175, -57.71, 0.229, 19.73 ] + }, + { + "time": 0.3, + "value": 19.34, + "curve": [ 0.354, 19.34, 0.4, -57.76 ] + }, + { + "time": 0.4333, + "value": -57.76, + "curve": [ 0.458, -57.76, 0.511, -3.56 ] + }, + { + "time": 0.5333, + "value": 3.7, + "curve": [ 0.563, 13.29, 0.633, 15.79 ] + }, + { + "time": 0.6667, + "value": 15.79, + "curve": [ 0.7, 15.79, 0.767, -48.75 ] + }, + { + "time": 0.8, + "value": -48.75, + "curve": [ 0.842, -48.75, 0.925, 4.7 ] + }, + { + "time": 0.9667, + "value": 4.7, + "curve": [ 1, 4.7, 1.067, -22.9 ] + }, + { + "time": 1.1, + "value": -22.9, + "curve": [ 1.142, -22.9, 1.225, -13.28 ] + }, + { "time": 1.2667, "value": -13.28 } + ] + }, + "rear-foot-target": { + "rotate": [ + { "value": -0.28 } + ] + }, + "front-foot-tip": { + "rotate": [ + { + "value": -0.28, + "curve": [ 0.008, -0.28, 0.003, -66.62 ] + }, + { + "time": 0.0667, + "value": -65.75, + "curve": [ 0.166, -64.42, 0.234, 14.35 ] + }, + { + "time": 0.2667, + "value": 38.25, + "curve": [ 0.294, 57.91, 0.392, 89.79 ] + }, + { + "time": 0.4667, + "value": 90.73, + "curve": [ 0.483, 90.73, 0.55, 177.66 ] + }, + { + "time": 0.5667, + "value": 177.66, + "curve": [ 0.733, 176.24, 0.75, 11.35 ] + }, + { + "time": 0.8, + "value": 11.35, + "curve": [ 0.886, 12.29, 0.911, 47.88 ] + }, + { + "time": 0.9333, + "value": 56.77, + "curve": [ 0.967, 70.59, 1.05, 86.46 ] + }, + { + "time": 1.1, + "value": 86.46, + "curve": [ 1.187, 86.46, 1.214, 66.44 ] + }, + { "time": 1.3333, "value": 64.55 } + ] + }, + "back-foot-tip": { + "rotate": [ + { + "value": -0.28, + "curve": [ 0, -7.97, 0.027, -18.69 ] + }, + { + "time": 0.0667, + "value": -19, + "curve": [ 0.166, -19.3, 0.208, 15.58 ] + }, + { + "time": 0.2667, + "value": 45.95, + "curve": [ 0.306, 66.24, 0.378, 99.08 ] + }, + { + "time": 0.4333, + "value": 99.08, + "curve": [ 0.497, 98.62, 0.488, -1.2 ] + }, + { + "time": 0.5667, + "value": -1.32, + "curve": [ 0.637, -0.84, 0.687, 94.41 ] + }, + { + "time": 0.7333, + "value": 94.33, + "curve": [ 0.832, 94.16, 0.895, 29.6 ] + }, + { + "time": 0.9667, + "value": 28.67, + "curve": [ 1.026, 28.67, 1.045, 53.14 ] + }, + { "time": 1.1, "value": 53.38 } + ] + }, + "hair4": { + "rotate": [ + { + "curve": [ 0.011, 4.5, 0.05, 11.42 ] + }, + { + "time": 0.0667, + "value": 11.42, + "curve": [ 0.1, 11.42, 0.136, -5.92 ] + }, + { + "time": 0.1667, + "value": -10.54, + "curve": [ 0.206, -16.51, 0.327, -22 ] + }, + { + "time": 0.3667, + "value": -24.47, + "curve": [ 0.413, -27.37, 0.467, -43.99 ] + }, + { + "time": 0.5, + "value": -43.99, + "curve": [ 0.533, -43.99, 0.552, 12.12 ] + }, + { + "time": 0.6333, + "value": 11.85, + "curve": [ 0.714, 11.59, 0.758, -34.13 ] + }, + { + "time": 0.8, + "value": -34.13, + "curve": [ 0.858, -34.13, 1.015, -12.47 ] + }, + { + "time": 1.0667, + "value": -8.85, + "curve": [ 1.121, -5.07, 1.219, -0.02 ] + }, + { + "time": 1.3333, + "value": 1.29, + "curve": [ 1.509, 3.3, 1.763, 2.75 ] + }, + { + "time": 1.8667, + "value": 2.78, + "curve": [ 1.974, 2.81, 2.108, 2.81 ] + }, + { + "time": 2.2, + "value": 2.78, + "curve": [ 2.315, 2.74, 2.374, 1.22 ] + }, + { + "time": 2.4667, + "value": 1.18, + "curve": [ 2.525, 1.18, 2.608, 10.79 ] + }, + { + "time": 2.6667, + "value": 10.79, + "curve": [ 2.725, 10.79, 2.893, 4.72 ] + }, + { + "time": 3.0333, + "value": 4.72, + "curve": [ 3.117, 4.72, 3.283, 7.93 ] + }, + { + "time": 3.3667, + "value": 7.93, + "curve": [ 3.492, 7.93, 3.775, 6.93 ] + }, + { + "time": 3.9, + "value": 6.93, + "curve": [ 3.981, 6.93, 4.094, 6.9 ] + }, + { + "time": 4.2, + "value": 8.44, + "curve": [ 4.267, 9.42, 4.401, 16.61 ] + }, + { + "time": 4.5, + "value": 16.33, + "curve": [ 4.582, 16.12, 4.709, 9.94 ] + }, + { + "time": 4.7333, + "value": 6.51, + "curve": [ 4.747, 4.57, 4.779, -1.76 ] + }, + { + "time": 4.8, + "value": -1.75, + "curve": [ 4.823, -1.73, 4.82, 4.47 ] + }, + { + "time": 4.8667, + "value": 6.04, + "curve": [ 4.899, 7.14, 4.913, 6.93 ] + }, + { "time": 4.9333, "value": 6.93 } + ] + }, + "hair2": { + "rotate": [ + { + "value": 10.61, + "curve": [ 0.075, 10.61, 0.05, 12.67 ] + }, + { + "time": 0.0667, + "value": 12.67, + "curve": [ 0.123, 12.67, 0.194, -16.51 ] + }, + { + "time": 0.2, + "value": -19.87, + "curve": [ 0.207, -23.48, 0.236, -31.68 ] + }, + { + "time": 0.3, + "value": -31.8, + "curve": [ 0.356, -31.9, 0.437, -25.61 ] + }, + { + "time": 0.4667, + "value": -19.29, + "curve": [ 0.485, -15.33, 0.529, 6.48 ] + }, + { + "time": 0.5667, + "value": 6.67, + "curve": [ 0.628, 6.97, 0.65, -46.39 ] + }, + { + "time": 0.7333, + "value": -46.3, + "curve": [ 0.843, -46.17, 0.941, -33.37 ] + }, + { + "time": 0.9667, + "value": -23.17, + "curve": [ 0.972, -20.98, 1.047, 15.21 ] + }, + { + "time": 1.1, + "value": 15.21, + "curve": [ 1.142, 15.21, 1.183, 10.73 ] + }, + { + "time": 1.2667, + "value": 10.61, + "curve": [ 1.45, 10.34, 1.817, 10.61 ] + }, + { + "time": 2, + "value": 10.61, + "curve": [ 2.075, 10.61, 2.225, 16.9 ] + }, + { + "time": 2.3, + "value": 16.9, + "curve": [ 2.327, 16.9, 2.347, 6.81 ] + }, + { + "time": 2.4, + "value": 6.83, + "curve": [ 2.492, 6.87, 2.602, 17.39 ] + }, + { + "time": 2.6667, + "value": 17.39, + "curve": [ 2.742, 17.39, 2.892, 10.67 ] + }, + { + "time": 2.9667, + "value": 10.64, + "curve": [ 3.187, 10.57, 3.344, 10.73 ] + }, + { + "time": 3.6, + "value": 11.4, + "curve": [ 3.766, 11.83, 3.874, 14.87 ] + }, + { + "time": 3.9333, + "value": 14.83, + "curve": [ 4.022, 14.76, 4.208, 9.49 ] + }, + { + "time": 4.3, + "value": 9.54, + "curve": [ 4.391, 9.58, 4.441, 14.82 ] + }, + { + "time": 4.5333, + "value": 14.84, + "curve": [ 4.642, 14.88, 4.692, 1.17 ] + }, + { + "time": 4.7667, + "value": 1.24, + "curve": [ 4.823, 1.3, 4.818, 18.35 ] + }, + { + "time": 4.8667, + "value": 18.38, + "curve": [ 4.905, 18.41, 4.901, 10.61 ] + }, + { "time": 4.9333, "value": 10.61 } + ] + }, + "torso2": { + "rotate": [ + { + "curve": [ 0.048, 0, 0.129, -12.73 ] + }, + { + "time": 0.1667, + "value": -15.95, + "curve": [ 0.221, -20.66, 0.254, -21.62 ] + }, + { + "time": 0.3, + "value": -21.59, + "curve": [ 0.458, -21.46, 0.46, -1.67 ] + }, + { + "time": 0.6333, + "value": -1.71, + "curve": [ 0.71, -1.73, 0.715, -4 ] + }, + { + "time": 0.7667, + "value": -3.97, + "curve": [ 0.866, -3.92, 0.84, 0.02 ] + }, + { "time": 1, "curve": "stepped" }, + { + "time": 2, + "curve": [ 2.275, 0, 2.867, -5.8 ] + }, + { + "time": 3.1, + "value": -6.44, + "curve": [ 3.327, -7.06, 3.71, -6.23 ] + }, + { + "time": 3.9333, + "value": -5.41, + "curve": [ 4.168, -4.53, 4.488, -2.83 ] + }, + { "time": 4.8 } + ] + }, + "torso3": { + "rotate": [ + { + "curve": [ 0.025, 0, 0.09, -3.66 ] + }, + { + "time": 0.1, + "value": -4.55, + "curve": [ 0.143, -8.4, 0.223, -17.07 ] + }, + { + "time": 0.2333, + "value": -18.31, + "curve": [ 0.282, -24.44, 0.35, -29 ] + }, + { + "time": 0.3667, + "value": -30.07, + "curve": [ 0.405, -32.58, 0.442, -33.03 ] + }, + { + "time": 0.4667, + "value": -32.99, + "curve": [ 0.491, -33.04, 0.505, -23.56 ] + }, + { + "time": 0.5333, + "value": -23.55, + "curve": [ 0.571, -23.67, 0.599, -27.21 ] + }, + { + "time": 0.6333, + "value": -27.21, + "curve": [ 0.669, -27.2, 0.742, -10.43 ] + }, + { + "time": 0.7667, + "value": -7.79, + "curve": [ 0.788, -5.53, 0.796, -4.42 ] + }, + { + "time": 0.8333, + "value": -2.9, + "curve": [ 0.875, -1.21, 0.933, 0 ] + }, + { "time": 0.9667, "curve": "stepped" }, + { + "time": 2.4333, + "curve": [ 2.517, 0, 2.683, 4.63 ] + }, + { + "time": 2.7667, + "value": 4.66, + "curve": [ 3.084, 4.76, 3.248, 4.37 ] + }, + { + "time": 3.4, + "value": 3.74, + "curve": [ 3.596, 2.92, 3.755, 2.18 ] + }, + { + "time": 3.8667, + "value": 1.72, + "curve": [ 4.136, 0.59, 4.471, 0 ] + }, + { "time": 4.8 } + ] + }, + "hair3": { + "rotate": [ + { + "curve": [ 0, 0, 0.041, 10.74 ] + }, + { + "time": 0.0667, + "value": 14.16, + "curve": [ 0.075, 15.22, 0.148, 18.04 ] + }, + { + "time": 0.2, + "value": 18.13, + "curve": [ 0.251, 18.23, 0.307, -4.75 ] + }, + { + "time": 0.3667, + "value": -5.06, + "curve": [ 0.412, -5.3, 0.47, -0.96 ] + }, + { + "time": 0.5, + "value": 2.21, + "curve": [ 0.512, 3.48, 0.595, 20.31 ] + }, + { + "time": 0.6333, + "value": 24.87, + "curve": [ 0.647, 26.53, 0.719, 29.33 ] + }, + { + "time": 0.8, + "value": 29.22, + "curve": [ 0.859, 29.14, 0.9, 28.48 ] + }, + { + "time": 0.9333, + "value": 26.11, + "curve": [ 0.981, 22.72, 0.998, 2.06 ] + }, + { "time": 1.1, "value": 2.21 } + ] + }, + "hair1": { + "rotate": [ + { + "curve": [ 0.047, -0.21, 0.048, 7.86 ] + }, + { + "time": 0.0667, + "value": 13.27, + "curve": [ 0.083, 18.05, 0.135, 24.44 ] + }, + { + "time": 0.2, + "value": 24.02, + "curve": [ 0.225, 24.02, 0.28, 6.32 ] + }, + { + "time": 0.3, + "value": 3.1, + "curve": [ 0.323, -0.58, 0.382, -7.12 ] + }, + { + "time": 0.4667, + "value": -7.45, + "curve": [ 0.512, -7.66, 0.538, 12.13 ] + }, + { + "time": 0.5667, + "value": 16.46, + "curve": [ 0.609, 22.72, 0.672, 27.4 ] + }, + { + "time": 0.7333, + "value": 27.55, + "curve": [ 0.827, 27.4, 0.933, 23.23 ] + }, + { + "time": 0.9667, + "value": 19.11, + "curve": [ 0.998, 15.27, 1.092, -2.53 ] + }, + { + "time": 1.1333, + "value": -2.53, + "curve": [ 1.158, -2.53, 1.208, 0 ] + }, + { "time": 1.2333, "curve": "stepped" }, + { + "time": 2, + "curve": [ 2.075, 0, 2.248, 0.35 ] + }, + { + "time": 2.3333, + "value": 0.78, + "curve": [ 2.585, 2.06, 2.805, 3.46 ] + }, + { + "time": 3.2, + "value": 3.5, + "curve": [ 3.593, 3.54, 3.979, 2.36 ] + }, + { + "time": 4.1667, + "value": 1.55, + "curve": [ 4.391, 0.59, 4.447, 0.04 ] + }, + { + "time": 4.6, + "value": 0.04, + "curve": [ 4.642, 0.04, 4.742, 0 ] + }, + { "time": 4.9333 } + ] + }, + "head-control": { + "translate": [ + { + "curve": [ 0.025, 0, 0.09, 1.43, 0.025, 0, 0.075, -34.76 ] + }, + { + "time": 0.1, + "x": 1.59, + "y": -34.76, + "curve": [ 0.214, 3.33, 0.375, 5.34, 0.192, -34.76, 0.441, -21.17 ] + }, + { + "time": 0.4667, + "x": 5.34, + "y": -12.57, + "curve": [ 0.492, 5.34, 0.55, 5.24, 0.482, -7.36, 0.504, 4.03 ] + }, + { + "time": 0.5667, + "x": 5.11, + "y": 4.01, + "curve": [ 0.658, 4.45, 0.679, 3.19, 0.649, 3.98, 0.642, -16.84 ] + }, + { + "time": 0.7, + "x": 2.8, + "y": -16.74, + "curve": [ 0.787, 1.15, 0.881, -1.29, 0.772, -16.62, 0.82, 8.95 ] + }, + { + "time": 0.9, + "x": -1.72, + "y": 8.91, + "curve": [ 0.961, -3.06, 1.025, -3.58, 0.975, 8.87, 0.951, -1.37 ] + }, + { + "time": 1.1, + "x": -3.58, + "y": -1.45, + "curve": [ 1.292, -3.58, 2.002, -2.4, 1.292, -1.56, 1.975, -1.45 ] + }, + { + "time": 2.1667, + "x": -1.39, + "y": -1.45, + "curve": [ 2.25, -0.88, 2.503, 1.38, 2.283, -1.45, 2.603, -12.44 ] + }, + { + "time": 2.6667, + "x": 2.13, + "y": -14.45, + "curve": [ 2.766, 2.59, 2.999, 2.81, 2.835, -19.73, 3.003, -25.2 ] + }, + { + "time": 3.1333, + "x": 2.91, + "y": -26.08, + "curve": [ 3.392, 3.1, 4.199, 4.05, 3.483, -28.44, 4.129, -27.23 ] + }, + { + "time": 4.3667, + "x": 4.81, + "y": -19.59, + "curve": [ 4.429, 5.1, 4.594, 8.54, 4.538, -14.08, 4.583, -7.88 ] + }, + { + "time": 4.6667, + "x": 8.65, + "y": -4.56, + "curve": [ 4.794, 8.86, 4.806, 5.93, 4.691, -3.59, 4.8, -1.61 ] + }, + { "time": 4.9333, "x": 5.8, "y": -1.99 } + ] + } + }, + "ik": { + "front-foot-ik": [ + { "mix": 0 } + ], + "front-leg-ik": [ + { "mix": 0, "bendPositive": false } + ], + "rear-foot-ik": [ + { "mix": 0.005 } + ], + "rear-leg-ik": [ + { "mix": 0.005, "bendPositive": false } + ] + } + }, + "hoverboard": { + "slots": { + "exhaust1": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "exhaust2": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "exhaust3": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + }, + "hoverboard-board": { + "attachment": [ + { "name": "hoverboard-board" } + ] + }, + "hoverboard-thruster-front": { + "attachment": [ + { "name": "hoverboard-thruster" } + ] + }, + "hoverboard-thruster-rear": { + "attachment": [ + { "name": "hoverboard-thruster" } + ] + }, + "hoverglow-front": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "hoverglow-rear": { + "attachment": [ + { "name": "hoverglow-small" } + ] + }, + "side-glow1": { + "attachment": [ + { "name": "hoverglow-small" }, + { "time": 0.9667 } + ] + }, + "side-glow2": { + "attachment": [ + { "time": 0.0667, "name": "hoverglow-small" }, + { "time": 1 } + ] + }, + "side-glow3": { + "attachment": [ + { "name": "hoverglow-small" }, + { "time": 0.9667 } + ] + } + }, + "bones": { + "hoverboard-controller": { + "translate": [ + { + "x": 319.55, + "y": -1.59, + "curve": [ 0.064, 319.55, 0.2, 347.85, 0.058, -1.2, 0.2, 23.11 ] + }, + { + "time": 0.2667, + "x": 347.66, + "y": 39.62, + "curve": [ 0.35, 347.41, 0.476, 341.47, 0.323, 53.58, 0.44, 85.82 ] + }, + { + "time": 0.5333, + "x": 338.47, + "y": 85.72, + "curve": [ 0.603, 334.83, 0.913, 319.65, 0.621, 85.62, 0.88, -1.53 ] + }, + { "time": 1, "x": 319.55, "y": -1.59 } + ] + }, + "hip": { + "translate": [ + { + "x": -53.49, + "y": 32.14, + "curve": [ 0.061, -53.77, 0.093, -51.81, 0.044, 16.34, 0.063, 9.67 ] + }, + { + "time": 0.1333, + "x": -49.31, + "y": 7.01, + "curve": [ 0.3, -35.27, 0.461, -20.06, 0.314, 9.52, 0.408, 121.09 ] + }, + { + "time": 0.5667, + "x": -20.06, + "y": 122.72, + "curve": [ 0.716, -20.09, 0.912, -53.29, 0.753, 121.8, 0.946, 51.85 ] + }, + { "time": 1, "x": -53.49, "y": 32.14 } + ] + }, + "exhaust1": { + "scale": [ + { + "x": 1.593, + "y": 0.964, + "curve": [ 0.033, 1.593, 0.1, 1, 0.033, 0.964, 0.1, 0.713 ] + }, + { + "time": 0.1333, + "y": 0.713, + "curve": [ 0.15, 1, 0.183, 1.774, 0.15, 0.713, 0.183, 0.883 ] + }, + { + "time": 0.2, + "x": 1.774, + "y": 0.883, + "curve": [ 0.242, 1.774, 0.325, 1.181, 0.242, 0.883, 0.325, 0.649 ] + }, + { + "time": 0.3667, + "x": 1.181, + "y": 0.649, + "curve": [ 0.408, 1.181, 0.492, 1.893, 0.408, 0.649, 0.492, 0.819 ] + }, + { + "time": 0.5333, + "x": 1.893, + "y": 0.819, + "curve": [ 0.558, 1.893, 0.608, 1.18, 0.558, 0.819, 0.608, 0.686 ] + }, + { + "time": 0.6333, + "x": 1.18, + "y": 0.686, + "curve": [ 0.658, 1.18, 0.708, 1.903, 0.658, 0.686, 0.708, 0.855 ] + }, + { + "time": 0.7333, + "x": 1.903, + "y": 0.855, + "curve": [ 0.767, 1.903, 0.833, 1.311, 0.767, 0.855, 0.833, 0.622 ] + }, + { + "time": 0.8667, + "x": 1.311, + "y": 0.622, + "curve": [ 0.9, 1.311, 0.967, 1.593, 0.9, 0.622, 0.967, 0.964 ] + }, + { "time": 1, "x": 1.593, "y": 0.964 } + ] + }, + "exhaust2": { + "scale": [ + { + "x": 1.88, + "y": 0.832, + "curve": [ 0.025, 1.88, 0.075, 1.311, 0.025, 0.832, 0.075, 0.686 ] + }, + { + "time": 0.1, + "x": 1.311, + "y": 0.686, + "curve": [ 0.133, 1.311, 0.2, 2.01, 0.133, 0.686, 0.208, 0.736 ] + }, + { + "time": 0.2333, + "x": 2.01, + "y": 0.769, + "curve": [ 0.267, 2.01, 0.333, 1, 0.282, 0.831, 0.333, 0.91 ] + }, + { + "time": 0.3667, + "y": 0.91, + "curve": [ 0.4, 1, 0.467, 1.699, 0.4, 0.91, 0.474, 0.891 ] + }, + { + "time": 0.5, + "x": 1.699, + "y": 0.86, + "curve": [ 0.517, 1.699, 0.55, 1.181, 0.54, 0.813, 0.55, 0.713 ] + }, + { + "time": 0.5667, + "x": 1.181, + "y": 0.713, + "curve": [ 0.617, 1.181, 0.717, 1.881, 0.617, 0.713, 0.717, 0.796 ] + }, + { + "time": 0.7667, + "x": 1.881, + "y": 0.796, + "curve": [ 0.8, 1.881, 0.867, 1.3, 0.8, 0.796, 0.867, 0.649 ] + }, + { + "time": 0.9, + "x": 1.3, + "y": 0.649, + "curve": [ 0.925, 1.3, 0.975, 1.88, 0.925, 0.649, 0.975, 0.832 ] + }, + { "time": 1, "x": 1.88, "y": 0.832 } + ] + }, + "hoverboard-thruster-front": { + "rotate": [ + { + "curve": [ 0.125, 0, 0.375, 24.06 ] + }, + { + "time": 0.5, + "value": 24.06, + "curve": [ 0.625, 24.06, 0.875, 0 ] + }, + { "time": 1 } + ] + }, + "hoverglow-front": { + "scale": [ + { + "x": 0.849, + "y": 1.764, + "curve": [ 0.017, 0.849, 0.05, 0.835, 0.017, 1.764, 0.05, 2.033 ] + }, + { + "time": 0.0667, + "x": 0.835, + "y": 2.033, + "curve": [ 0.092, 0.835, 0.142, 0.752, 0.092, 2.033, 0.142, 1.584 ] + }, + { + "time": 0.1667, + "x": 0.752, + "y": 1.584, + "curve": [ 0.183, 0.752, 0.217, 0.809, 0.183, 1.584, 0.217, 1.71 ] + }, + { + "time": 0.2333, + "x": 0.809, + "y": 1.71, + "curve": [ 0.25, 0.809, 0.283, 0.717, 0.25, 1.71, 0.283, 1.45 ] + }, + { + "time": 0.3, + "x": 0.717, + "y": 1.45, + "curve": [ 0.317, 0.717, 0.35, 0.777, 0.317, 1.45, 0.35, 1.698 ] + }, + { + "time": 0.3667, + "x": 0.777, + "y": 1.698, + "curve": [ 0.4, 0.781, 0.45, 0.685, 0.375, 1.698, 0.45, 1.173 ] + }, + { + "time": 0.4667, + "x": 0.685, + "y": 1.173, + "curve": [ 0.492, 0.685, 0.542, 0.825, 0.492, 1.173, 0.542, 1.572 ] + }, + { + "time": 0.5667, + "x": 0.825, + "y": 1.572, + "curve": [ 0.611, 0.816, 0.63, 0.727, 0.611, 1.577, 0.606, 1.255 ] + }, + { + "time": 0.6667, + "x": 0.725, + "y": 1.241, + "curve": [ 0.692, 0.725, 0.742, 0.895, 0.692, 1.241, 0.749, 1.799 ] + }, + { + "time": 0.7667, + "x": 0.895, + "y": 1.857, + "curve": [ 0.783, 0.895, 0.796, 0.892, 0.796, 1.955, 0.817, 1.962 ] + }, + { + "time": 0.8333, + "x": 0.845, + "y": 1.962, + "curve": [ 0.845, 0.831, 0.883, 0.802, 0.85, 1.962, 0.872, 1.704 ] + }, + { + "time": 0.9, + "x": 0.802, + "y": 1.491, + "curve": [ 0.917, 0.802, 0.95, 0.845, 0.907, 1.441, 0.936, 1.508 ] + }, + { + "time": 0.9667, + "x": 0.845, + "y": 1.627, + "curve": [ 0.975, 0.845, 0.992, 0.849, 0.973, 1.652, 0.992, 1.764 ] + }, + { "time": 1, "x": 0.849, "y": 1.764 } + ] + }, + "hoverboard-thruster-rear": { + "rotate": [ + { + "curve": [ 0.125, 0, 0.375, 24.06 ] + }, + { + "time": 0.5, + "value": 24.06, + "curve": [ 0.625, 24.06, 0.875, 0 ] + }, + { "time": 1 } + ] + }, + "hoverglow-rear": { + "scale": [ + { + "x": 0.845, + "y": 1.31, + "curve": [ 0.017, 0.845, 0.117, 0.899, 0.017, 1.31, 0.117, 2.033 ] + }, + { + "time": 0.1333, + "x": 0.899, + "y": 2.033, + "curve": [ 0.15, 0.899, 0.183, 0.752, 0.15, 2.033, 0.183, 1.574 ] + }, + { + "time": 0.2, + "x": 0.752, + "y": 1.574, + "curve": [ 0.225, 0.752, 0.275, 0.809, 0.225, 1.574, 0.275, 1.71 ] + }, + { + "time": 0.3, + "x": 0.809, + "y": 1.71, + "curve": [ 0.317, 0.809, 0.35, 0.717, 0.317, 1.71, 0.35, 1.397 ] + }, + { + "time": 0.3667, + "x": 0.717, + "y": 1.397, + "curve": [ 0.383, 0.717, 0.417, 0.777, 0.383, 1.397, 0.417, 1.45 ] + }, + { + "time": 0.4333, + "x": 0.777, + "y": 1.45, + "curve": [ 0.45, 0.777, 0.496, 0.689, 0.45, 1.45, 0.481, 1.168 ] + }, + { + "time": 0.5333, + "x": 0.685, + "y": 1.173, + "curve": [ 0.565, 0.682, 0.617, 0.758, 0.575, 1.177, 0.617, 1.297 ] + }, + { + "time": 0.6333, + "x": 0.758, + "y": 1.297, + "curve": [ 0.658, 0.758, 0.708, 0.725, 0.658, 1.297, 0.708, 1.241 ] + }, + { + "time": 0.7333, + "x": 0.725, + "y": 1.241, + "curve": [ 0.772, 0.732, 0.796, 0.893, 0.782, 1.238, 0.778, 1.854 ] + }, + { + "time": 0.8333, + "x": 0.895, + "y": 1.857, + "curve": [ 0.878, 0.9, 0.992, 0.845, 0.88, 1.86, 0.992, 1.31 ] + }, + { "time": 1, "x": 0.845, "y": 1.31 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -85.92, + "curve": [ 0.08, -85.59, 0.284, -62.7 ] + }, + { + "time": 0.3667, + "value": -55.14, + "curve": [ 0.438, -48.65, 0.551, -43.21 ] + }, + { + "time": 0.6333, + "value": -43.21, + "curve": [ 0.716, -43.22, 0.908, -85.92 ] + }, + { "time": 1, "value": -85.92 } + ], + "translate": [ + { + "x": -0.59, + "y": -2.94, + "curve": [ 0.1, -1.21, 0.275, -1.74, 0.092, -2.94, 0.275, -6.39 ] + }, + { + "time": 0.3667, + "x": -1.74, + "y": -6.39, + "curve": [ 0.433, -1.74, 0.567, 0.72, 0.433, -6.39, 0.587, -4.48 ] + }, + { + "time": 0.6333, + "x": 0.72, + "y": -4.21, + "curve": [ 0.725, 0.72, 0.908, -0.08, 0.743, -3.57, 0.908, -2.94 ] + }, + { "time": 1, "x": -0.59, "y": -2.94 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": 7.61, + "curve": [ 0.143, 7.62, 0.247, -23.17 ] + }, + { + "time": 0.2667, + "value": -26.56, + "curve": [ 0.281, -29.08, 0.351, -37.36 ] + }, + { + "time": 0.4333, + "value": -37.2, + "curve": [ 0.513, -37.05, 0.562, -29.88 ] + }, + { + "time": 0.6, + "value": -25.18, + "curve": [ 0.621, -22.58, 0.694, -3.98 ] + }, + { + "time": 0.8, + "value": 3.63, + "curve": [ 0.861, 8.03, 0.946, 7.57 ] + }, + { "time": 1, "value": 7.61 } + ], + "translate": [ + { + "curve": [ 0.117, 0, 0.35, 0.52, 0.117, 0, 0.35, -3.27 ] + }, + { + "time": 0.4667, + "x": 0.52, + "y": -3.27, + "curve": [ 0.6, 0.52, 0.867, 0, 0.6, -3.27, 0.867, 0 ] + }, + { "time": 1 } + ], + "shear": [ + { + "y": 19.83, + "curve": [ 0.117, 0, 0.35, 15.28, 0.117, 19.83, 0.35, 28.31 ] + }, + { + "time": 0.4667, + "x": 15.28, + "y": 28.31, + "curve": [ 0.6, 15.28, 0.867, 0, 0.6, 28.31, 0.867, 19.83 ] + }, + { "time": 1, "y": 19.83 } + ] + }, + "board-ik": { + "translate": [ + { + "x": 393.62, + "curve": [ 0.083, 393.62, 0.25, 393.48, 0.083, 0, 0.25, 117.69 ] + }, + { + "time": 0.3333, + "x": 393.48, + "y": 117.69, + "curve": [ 0.375, 393.48, 0.458, 393.62, 0.375, 117.69, 0.458, 83.82 ] + }, + { "time": 0.5, "x": 393.62, "y": 83.82 }, + { "time": 0.6667, "x": 393.62, "y": 30.15 }, + { "time": 1, "x": 393.62 } + ] + }, + "front-thigh": { + "translate": [ + { "x": -7.49, "y": 8.51 } + ] + }, + "front-leg-target": { + "translate": [ + { + "time": 0.3667, + "curve": [ 0.428, 10.83, 0.567, 12.78, 0.414, 7.29, 0.567, 8.79 ] + }, + { + "time": 0.6, + "x": 12.78, + "y": 8.79, + "curve": [ 0.692, 12.78, 0.772, 11.27, 0.692, 8.79, 0.766, 8.62 ] + }, + { "time": 0.8667 } + ] + }, + "rear-leg-target": { + "translate": [ + { + "time": 0.4667, + "curve": [ 0.492, 0, 0.534, 4.47, 0.492, 0, 0.542, 1.63 ] + }, + { + "time": 0.5667, + "x": 4.53, + "y": 1.77, + "curve": [ 0.622, 4.64, 0.717, 3.31, 0.615, 2.06, 0.71, 2.1 ] + }, + { "time": 0.8 } + ] + }, + "exhaust3": { + "scale": [ + { + "x": 1.882, + "y": 0.81, + "curve": [ 0.017, 1.882, 0.167, 1.3, 0.017, 0.81, 0.167, 0.649 ] + }, + { + "time": 0.2, + "x": 1.3, + "y": 0.649, + "curve": [ 0.225, 1.3, 0.275, 2.051, 0.225, 0.649, 0.275, 0.984 ] + }, + { + "time": 0.3, + "x": 2.051, + "y": 0.984, + "curve": [ 0.325, 2.051, 0.375, 1.311, 0.325, 0.984, 0.384, 0.715 ] + }, + { + "time": 0.4, + "x": 1.311, + "y": 0.686, + "curve": [ 0.433, 1.311, 0.5, 1.86, 0.426, 0.638, 0.5, 0.537 ] + }, + { + "time": 0.5333, + "x": 1.86, + "y": 0.537, + "curve": [ 0.567, 1.86, 0.633, 1.187, 0.567, 0.537, 0.604, 0.854 ] + }, + { + "time": 0.6667, + "x": 1.187, + "y": 0.854, + "curve": [ 0.7, 1.187, 0.767, 1.549, 0.707, 0.854, 0.774, 0.775 ] + }, + { + "time": 0.8, + "x": 1.549, + "y": 0.746, + "curve": [ 0.817, 1.549, 0.85, 1.181, 0.815, 0.729, 0.85, 0.713 ] + }, + { + "time": 0.8667, + "x": 1.181, + "y": 0.713, + "curve": [ 0.9, 1.181, 0.967, 1.882, 0.9, 0.713, 0.967, 0.81 ] + }, + { "time": 1, "x": 1.882, "y": 0.81 } + ] + }, + "side-glow1": { + "rotate": [ + { "value": 51.12, "curve": "stepped" }, + { "time": 0.0667, "value": 43.82, "curve": "stepped" }, + { "time": 0.1, "value": 40.95, "curve": "stepped" }, + { "time": 0.1667, "value": 27.78, "curve": "stepped" }, + { "time": 0.2, "value": 10.24, "curve": "stepped" }, + { "time": 0.2667, "curve": "stepped" }, + { "time": 0.8, "value": -25.81 } + ], + "translate": [ + { "x": 338.28, "y": 40.22, "curve": "stepped" }, + { "time": 0.0667, "x": 331.2, "y": 30.39, "curve": "stepped" }, + { "time": 0.1, "x": 318.63, "y": 20.59, "curve": "stepped" }, + { "time": 0.1667, "x": 302.45, "y": 9.64, "curve": "stepped" }, + { "time": 0.2, "x": 276.87, "y": 1.13, "curve": "stepped" }, + { "time": 0.2667, "x": 248.16, "curve": "stepped" }, + { "time": 0.3, "x": 221.36, "curve": "stepped" }, + { "time": 0.3667, "x": 195.69, "curve": "stepped" }, + { "time": 0.4, "x": 171.08, "curve": "stepped" }, + { "time": 0.4667, "x": 144.84, "curve": "stepped" }, + { "time": 0.5, "x": 121.22, "curve": "stepped" }, + { "time": 0.5667, "x": 91.98, "curve": "stepped" }, + { "time": 0.6, "x": 62.63, "curve": "stepped" }, + { "time": 0.6667, "x": 30.78, "curve": "stepped" }, + { "time": 0.7, "curve": "stepped" }, + { "time": 0.7667, "x": -28.45, "curve": "stepped" }, + { "time": 0.8, "x": -67.49, "y": 16.82, "curve": "stepped" }, + { "time": 0.8667, "x": -83.07, "y": 24.36, "curve": "stepped" }, + { "time": 0.9, "x": -93.81, "y": 29.55 } + ], + "scale": [ + { "x": 0.535, "curve": "stepped" }, + { "time": 0.0667, "x": 0.594, "curve": "stepped" }, + { "time": 0.1, "x": 0.844, "curve": "stepped" }, + { "time": 0.1667, "curve": "stepped" }, + { "time": 0.8, "x": 0.534, "curve": "stepped" }, + { "time": 0.8667, "x": 0.428, "y": 0.801, "curve": "stepped" }, + { "time": 0.9, "x": 0.349, "y": 0.654 } + ] + }, + "side-glow2": { + "rotate": [ + { "time": 0.0667, "value": 51.12, "curve": "stepped" }, + { "time": 0.1, "value": 43.82, "curve": "stepped" }, + { "time": 0.1667, "value": 40.95, "curve": "stepped" }, + { "time": 0.2, "value": 27.78, "curve": "stepped" }, + { "time": 0.2667, "value": 10.24, "curve": "stepped" }, + { "time": 0.3, "curve": "stepped" }, + { "time": 0.8667, "value": -25.81 } + ], + "translate": [ + { "time": 0.0667, "x": 338.28, "y": 40.22, "curve": "stepped" }, + { "time": 0.1, "x": 331.2, "y": 30.39, "curve": "stepped" }, + { "time": 0.1667, "x": 318.63, "y": 20.59, "curve": "stepped" }, + { "time": 0.2, "x": 302.45, "y": 9.64, "curve": "stepped" }, + { "time": 0.2667, "x": 276.87, "y": 1.13, "curve": "stepped" }, + { "time": 0.3, "x": 248.16, "curve": "stepped" }, + { "time": 0.3667, "x": 221.36, "curve": "stepped" }, + { "time": 0.4, "x": 195.69, "curve": "stepped" }, + { "time": 0.4667, "x": 171.08, "curve": "stepped" }, + { "time": 0.5, "x": 144.84, "curve": "stepped" }, + { "time": 0.5667, "x": 121.22, "curve": "stepped" }, + { "time": 0.6, "x": 91.98, "curve": "stepped" }, + { "time": 0.6667, "x": 62.63, "curve": "stepped" }, + { "time": 0.7, "x": 30.78, "curve": "stepped" }, + { "time": 0.7667, "curve": "stepped" }, + { "time": 0.8, "x": -28.45, "curve": "stepped" }, + { "time": 0.8667, "x": -67.49, "y": 16.82, "curve": "stepped" }, + { "time": 0.9, "x": -83.07, "y": 24.36, "curve": "stepped" }, + { "time": 0.9667, "x": -93.81, "y": 29.55 } + ], + "scale": [ + { "time": 0.0667, "x": 0.535, "curve": "stepped" }, + { "time": 0.1, "x": 0.594, "curve": "stepped" }, + { "time": 0.1667, "x": 0.844, "curve": "stepped" }, + { "time": 0.2, "curve": "stepped" }, + { "time": 0.8667, "x": 0.534, "curve": "stepped" }, + { "time": 0.9, "x": 0.428, "y": 0.801, "curve": "stepped" }, + { "time": 0.9667, "x": 0.349, "y": 0.654 } + ] + }, + "torso": { + "rotate": [ + { + "value": -34.73, + "curve": [ 0.034, -36.31, 0.162, -39.33 ] + }, + { + "time": 0.2667, + "value": -39.37, + "curve": [ 0.384, -39.37, 0.491, -29.52 ] + }, + { + "time": 0.5, + "value": -28.86, + "curve": [ 0.525, -26.95, 0.571, -21.01 ] + }, + { + "time": 0.6333, + "value": -21.01, + "curve": [ 0.725, -21.01, 0.969, -33.35 ] + }, + { "time": 1, "value": -34.73 } + ] + }, + "neck": { + "rotate": [ + { + "value": 10.2, + "curve": [ 0.07, 12.09, 0.189, 16.03 ] + }, + { + "time": 0.2667, + "value": 16.14, + "curve": [ 0.333, 16.14, 0.449, 8.03 ] + }, + { + "time": 0.5, + "value": 5.83, + "curve": [ 0.542, 4.02, 0.6, 2.68 ] + }, + { + "time": 0.6333, + "value": 2.68, + "curve": [ 0.725, 2.68, 0.943, 8.57 ] + }, + { "time": 1, "value": 10.2 } + ] + }, + "head": { + "rotate": [ + { + "value": 10.2, + "curve": [ 0.044, 11.52, 0.2, 16.12 ] + }, + { + "time": 0.2667, + "value": 16.14, + "curve": [ 0.375, 16.17, 0.492, 2.65 ] + }, + { + "time": 0.6333, + "value": 2.68, + "curve": [ 0.725, 2.7, 0.963, 9.26 ] + }, + { "time": 1, "value": 10.2 } + ], + "translate": [ + { + "curve": [ 0.03, -0.24, 0.2, -4.22, 0.051, -1.06, 0.2, -3.62 ] + }, + { + "time": 0.2667, + "x": -4.22, + "y": -3.62, + "curve": [ 0.358, -4.22, 0.542, 0.84, 0.358, -3.62, 0.542, 6.01 ] + }, + { + "time": 0.6333, + "x": 0.84, + "y": 6.01, + "curve": [ 0.725, 0.84, 0.939, 0.32, 0.725, 6.01, 0.945, 1.14 ] + }, + { "time": 1 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": -11.18, + "curve": [ 0.064, -14.82, 0.25, -20.01 ] + }, + { + "time": 0.3333, + "value": -20.01, + "curve": [ 0.429, -20.12, 0.58, 5.12 ] + }, + { + "time": 0.6, + "value": 8.67, + "curve": [ 0.617, 11.72, 0.687, 20.52 ] + }, + { + "time": 0.7667, + "value": 20.55, + "curve": [ 0.848, 20.7, 0.963, -9.43 ] + }, + { "time": 1, "value": -11.18 } + ] + }, + "hair3": { + "rotate": [ + { + "value": 9.61, + "curve": [ 0.014, 8.51, 0.075, 2.63 ] + }, + { + "time": 0.1, + "value": 2.63, + "curve": [ 0.15, 2.63, 0.25, 13.52 ] + }, + { + "time": 0.3, + "value": 13.52, + "curve": [ 0.35, 13.52, 0.45, 11.28 ] + }, + { + "time": 0.5, + "value": 11.28, + "curve": [ 0.575, 11.28, 0.725, 18.13 ] + }, + { + "time": 0.8, + "value": 18.13, + "curve": [ 0.85, 18.13, 0.978, 11.07 ] + }, + { "time": 1, "value": 9.61 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -17.7, + "curve": [ 0.008, -17.7, 0.025, -23.73 ] + }, + { + "time": 0.0333, + "value": -23.73, + "curve": [ 0.067, -23.73, 0.154, -4.4 ] + }, + { + "time": 0.1667, + "value": -1.92, + "curve": [ 0.197, 4.09, 0.236, 12.91 ] + }, + { + "time": 0.2667, + "value": 17.56, + "curve": [ 0.301, 22.68, 0.342, 27.97 ] + }, + { + "time": 0.3667, + "value": 27.97, + "curve": [ 0.4, 27.97, 0.467, -1.45 ] + }, + { + "time": 0.5, + "value": -1.45, + "curve": [ 0.517, -1.45, 0.55, 3.16 ] + }, + { + "time": 0.5667, + "value": 3.16, + "curve": [ 0.583, 3.16, 0.617, -8.9 ] + }, + { + "time": 0.6333, + "value": -8.9, + "curve": [ 0.642, -8.9, 0.658, -5.4 ] + }, + { + "time": 0.6667, + "value": -5.4, + "curve": [ 0.683, -5.4, 0.717, -15.32 ] + }, + { + "time": 0.7333, + "value": -15.32, + "curve": [ 0.75, -15.32, 0.783, -9.19 ] + }, + { + "time": 0.8, + "value": -9.19, + "curve": [ 0.817, -9.19, 0.85, -23.6 ] + }, + { + "time": 0.8667, + "value": -23.6, + "curve": [ 0.883, -23.6, 0.917, -17.38 ] + }, + { + "time": 0.9333, + "value": -17.38, + "curve": [ 0.942, -17.38, 0.958, -20.46 ] + }, + { + "time": 0.9667, + "value": -20.46, + "curve": [ 0.975, -20.46, 0.992, -17.7 ] + }, + { "time": 1, "value": -17.7 } + ] + }, + "hair1": { + "rotate": [ + { + "value": 9.61, + "curve": [ 0.06, 9.04, 0.25, 8.9 ] + }, + { + "time": 0.3333, + "value": 8.9, + "curve": [ 0.392, 8.9, 0.508, 14.58 ] + }, + { + "time": 0.5667, + "value": 14.58, + "curve": [ 0.675, 14.58, 0.956, 10.28 ] + }, + { "time": 1, "value": 9.61 } + ] + }, + "hair2": { + "rotate": [ + { + "value": -3.82, + "curve": [ 0.017, -3.82, 0.064, -9.16 ] + }, + { + "time": 0.1333, + "value": -9.09, + "curve": [ 0.178, -9.04, 0.234, 1.29 ] + }, + { + "time": 0.2667, + "value": 5.98, + "curve": [ 0.276, 7.27, 0.336, 17.1 ] + }, + { + "time": 0.3667, + "value": 17.1, + "curve": [ 0.413, 17.1, 0.467, 1.59 ] + }, + { + "time": 0.5, + "value": 1.59, + "curve": [ 0.533, 1.59, 0.567, 13.63 ] + }, + { + "time": 0.6, + "value": 13.63, + "curve": [ 0.617, 13.63, 0.683, 0.78 ] + }, + { + "time": 0.7, + "value": 0.78, + "curve": [ 0.717, 0.78, 0.75, 12.01 ] + }, + { + "time": 0.7667, + "value": 11.9, + "curve": [ 0.792, 11.73, 0.817, -0.85 ] + }, + { + "time": 0.8333, + "value": -0.85, + "curve": [ 0.85, -0.85, 0.88, 1.99 ] + }, + { + "time": 0.9, + "value": 1.82, + "curve": [ 0.916, 1.68, 0.95, -6.9 ] + }, + { + "time": 0.9667, + "value": -6.9, + "curve": [ 0.975, -6.9, 0.992, -3.82 ] + }, + { "time": 1, "value": -3.82 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 31.65, + "curve": [ 0.108, 31.65, 0.325, 13.01 ] + }, + { + "time": 0.4333, + "value": 13.01, + "curve": [ 0.71, 13.01, 0.917, 31.65 ] + }, + { "time": 1, "value": 31.65 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 31, + "curve": [ 0.108, 31, 0.325, 12.76 ] + }, + { + "time": 0.4333, + "value": 12.79, + "curve": [ 0.587, 12.82, 0.917, 31 ] + }, + { "time": 1, "value": 31 } + ] + }, + "gun": { + "rotate": [ + { + "value": 1.95, + "curve": [ 0.083, 1.95, 0.245, 36.73 ] + }, + { + "time": 0.3333, + "value": 36.71, + "curve": [ 0.439, 36.69, 0.589, 10.68 ] + }, + { + "time": 0.6333, + "value": 8.75, + "curve": [ 0.701, 5.81, 0.917, 1.95 ] + }, + { "time": 1, "value": 1.95 } + ] + }, + "torso2": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.1, 2.35 ] + }, + { + "time": 0.1333, + "value": 2.35, + "curve": [ 0.225, 2.35, 0.408, -2.4 ] + }, + { + "time": 0.5, + "value": -2.4, + "curve": [ 0.567, -2.4, 0.7, 1.44 ] + }, + { + "time": 0.7667, + "value": 1.44, + "curve": [ 0.825, 1.44, 0.942, 0 ] + }, + { "time": 1 } + ] + }, + "torso3": { + "rotate": [ + { + "curve": [ 0.063, 0.77, 0.106, 1.42 ] + }, + { + "time": 0.1667, + "value": 1.42, + "curve": [ 0.259, 1.42, 0.344, -1.25 ] + }, + { + "time": 0.4667, + "value": -1.26, + "curve": [ 0.656, -1.26, 0.917, -0.78 ] + }, + { "time": 1 } + ] + }, + "head-control": { + "translate": [ + { + "x": 0.37, + "y": -11.17, + "curve": [ 0.133, 0.37, 0.335, -10.23, 0.133, -11.17, 0.335, 3.15 ] + }, + { + "time": 0.5333, + "x": -10.23, + "y": 3.15, + "curve": [ 0.71, -10.23, 0.883, 0.37, 0.71, 3.15, 0.883, -11.17 ] + }, + { "time": 1, "x": 0.37, "y": -11.17 } + ] + }, + "front-shoulder": { + "translate": [ + { + "x": 1.46, + "y": 10.15, + "curve": [ 0.103, 1.46, 0.249, 1.36, 0.103, 10.15, 0.249, -4.39 ] + }, + { + "time": 0.4, + "x": 1.36, + "y": -4.39, + "curve": [ 0.621, 1.36, 0.85, 1.46, 0.621, -4.39, 0.85, 10.15 ] + }, + { "time": 1, "x": 1.46, "y": 10.15 } + ] + }, + "back-shoulder": { + "translate": [ + { + "x": 1.4, + "y": 0.44, + "curve": [ 0.088, 1.4, 0.208, -2.47, 0.088, 0.44, 0.208, 8.61 ] + }, + { + "time": 0.3333, + "x": -2.47, + "y": 8.61, + "curve": [ 0.572, -2.47, 0.833, 1.4, 0.572, 8.61, 0.833, 0.44 ] + }, + { "time": 1, "x": 1.4, "y": 0.44 } + ] + } + }, + "transform": { + "front-foot-board-transform": [ + { "mixRotate": 0.997 } + ], + "rear-foot-board-transform": [ + {} + ], + "toes-board": [ + { "mixX": 0, "mixScaleX": 0, "mixShearY": 0 } + ] + }, + "attachments": { + "default": { + "front-foot": { + "front-foot": { + "deform": [ + { + "offset": 26, + "vertices": [ -0.02832, -5.37024, -0.02832, -5.37024, 3.8188, -3.7757, -0.02832, -5.37024, -3.82159, 3.77847 ] + } + ] + } + }, + "front-shin": { + "front-shin": { + "deform": [ + { + "offset": 14, + "vertices": [ 0.5298, -1.12677, -0.85507, -4.20587, -11.35158, -10.19225, -10.79865, -8.43765, -6.06447, -6.89757, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.54892, -3.06021, 1.48463, -2.29663, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4.80437, -7.01817 ] + }, + { + "time": 0.3667, + "offset": 14, + "vertices": [ 0.5298, -1.12677, -11.66571, -9.07211, -25.65866, -17.53735, -25.53217, -16.50978, -11.78232, -11.26097, 0, 0, 0.60487, -1.63589, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.60487, -1.63589, 0, 0, -2.64522, -7.35739, 1.48463, -2.29663, 0, 0, 0, 0, 0, 0, 0.60487, -1.63589, 0.60487, -1.63589, 0.60487, -1.63589, 0.60487, -1.63589, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.60487, -1.63589, 0, 0, -10.06873, -12.0999 ] + }, + { + "time": 0.5333, + "offset": 14, + "vertices": [ 0.5298, -1.12677, -0.85507, -4.20587, -7.00775, -8.24771, -6.45482, -6.49312, -6.06447, -6.89757, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.54892, -3.06021, 1.48463, -2.29663, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4.80437, -7.01817 ] + }, + { + "time": 1, + "offset": 14, + "vertices": [ 0.5298, -1.12677, -0.85507, -4.20587, -11.35158, -10.19225, -10.79865, -8.43765, -6.06447, -6.89757, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.54892, -3.06021, 1.48463, -2.29663, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4.80437, -7.01817 ] + } + ] + } + }, + "hoverboard-board": { + "hoverboard-board": { + "deform": [ + { + "curve": [ 0.067, 0, 0.2, 1 ] + }, + { + "time": 0.2667, + "offset": 1, + "vertices": [ 2.45856, 0, 0, 0, 0, 0, 0, 0, 0, 3.55673, -3.0E-4, 3.55673, -3.0E-4, 0, 0, 0, 0, 0, 0, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, -7.6E-4, -9.84158, 0, 0, 0, 0, 0, 0, 0, 0, -4.90558, 0.11214, -9.40706, 6.2E-4, -6.34871, 4.3E-4, -6.34925, -6.57018, -6.34925, -6.57018, -6.34871, 4.3E-4, -2.3308, 1.7E-4, -2.33133, -6.57045, -2.33133, -6.57045, -2.3308, 1.7E-4, 0, 0, 1.2E-4, 2.45856, 1.2E-4, 2.45856, 1.2E-4, 2.45856, 1.2E-4, 2.45856, 3.3297, 4.44005, 3.3297, 4.44005, 3.3297, 4.44005, 1.2E-4, 2.45856, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2.46227, 1.7E-4, -2.46227, 1.7E-4, -2.52316, 1.1313, -2.52316, 1.1313, -2.52316, 1.1313, 1.2E-4, 2.45856, 1.2E-4, 2.45856, -9.40694, 2.45918, 1.88063, 0.44197, -2.9E-4, -3.54808, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2.52316, 1.1313, -2.52316, 1.1313, -2.52316, 1.1313, -2.46227, 1.7E-4, -2.46227, 1.7E-4, -2.46227, 1.7E-4, 0, 0, 0, 0, 1.2E-4, 2.45856 ], + "curve": [ 0.45, 0, 0.817, 1 ] + }, + { "time": 1 } + ] + } + }, + "rear-foot": { + "rear-foot": { + "deform": [ + { + "offset": 28, + "vertices": [ -1.93078, 1.34782, -0.31417, 2.33363, 3.05122, 0.33946, 2.31472, -2.01678, 2.17583, -2.05795, -0.04277, -2.99459, 1.15429, 0.26328, 0.97501, -0.67169 ] + } + ] + } + } + } + } + }, + "idle": { + "slots": { + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + } + }, + "bones": { + "front-foot-target": { + "translate": [ + { "x": -69.06 } + ] + }, + "hip": { + "rotate": [ + { + "curve": [ 0.073, 0.35, 0.303, 1.27 ] + }, + { + "time": 0.4, + "value": 1.28, + "curve": [ 0.615, 1.3, 0.847, -1.41 ] + }, + { + "time": 1.2, + "value": -1.38, + "curve": [ 1.344, -1.37, 1.602, -0.28 ] + }, + { "time": 1.6667 } + ], + "translate": [ + { + "x": -11.97, + "y": -23.15, + "curve": [ 0.059, -12.96, 0.258, -15.19, 0.142, -23.15, 0.341, -24.89 ] + }, + { + "time": 0.4667, + "x": -15.14, + "y": -26.74, + "curve": [ 0.62, -15.1, 0.788, -13.28, 0.597, -28.66, 0.75, -30.01 ] + }, + { + "time": 0.9, + "x": -12.02, + "y": -30.01, + "curve": [ 0.978, -11.13, 1.175, -9.05, 1.036, -29.94, 1.234, -28.08 ] + }, + { + "time": 1.3333, + "x": -9.06, + "y": -26.64, + "curve": [ 1.501, -9.06, 1.614, -10.95, 1.454, -24.89, 1.609, -23.15 ] + }, + { "time": 1.6667, "x": -11.97, "y": -23.15 } + ] + }, + "rear-foot-target": { + "translate": [ + { "x": 48.87 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -60.87, + "curve": [ 0.154, -60.85, 0.452, -68.65 ] + }, + { + "time": 0.8333, + "value": -68.65, + "curve": [ 1.221, -68.65, 1.542, -60.87 ] + }, + { "time": 1.6667, "value": -60.87 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 42.46, + "curve": [ 0.029, 42.97, 0.134, 45.28 ] + }, + { + "time": 0.3333, + "value": 45.27, + "curve": [ 0.578, 45.26, 0.798, 40.07 ] + }, + { + "time": 0.8333, + "value": 39.74, + "curve": [ 0.878, 39.32, 1.019, 38.23 ] + }, + { + "time": 1.2, + "value": 38.22, + "curve": [ 1.377, 38.22, 1.619, 41.68 ] + }, + { "time": 1.6667, "value": 42.46 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 39.2, + "curve": [ 0.185, 39.22, 0.5, 29.37 ] + }, + { + "time": 0.6667, + "value": 29.37, + "curve": [ 0.917, 29.37, 1.417, 39.2 ] + }, + { "time": 1.6667, "value": 39.2 } + ] + }, + "head": { + "rotate": [ + { + "value": -6.75, + "curve": [ 0.176, -7.88, 0.349, -8.95 ] + }, + { + "time": 0.4667, + "value": -8.95, + "curve": [ 0.55, -8.95, 0.697, -6.77 ] + }, + { + "time": 0.8333, + "value": -5.44, + "curve": [ 0.88, -4.98, 1.05, -4.12 ] + }, + { + "time": 1.1333, + "value": -4.12, + "curve": [ 1.266, -4.12, 1.469, -5.48 ] + }, + { "time": 1.6667, "value": -6.75 } + ] + }, + "front-fist": { + "rotate": [ + { + "curve": [ 0.086, 0, 0.233, 2.48 ] + }, + { + "time": 0.3333, + "value": 4.13, + "curve": [ 0.429, 5.7, 0.711, 10.06 ] + }, + { + "time": 0.8333, + "value": 10.06, + "curve": [ 0.926, 10.06, 1.092, 4.21 ] + }, + { + "time": 1.2, + "value": 2.78, + "curve": [ 1.349, 0.8, 1.551, 0 ] + }, + { "time": 1.6667 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "curve": [ 0.063, 0.54, 0.367, 3.39 ] + }, + { + "time": 0.5333, + "value": 3.39, + "curve": [ 0.696, 3.39, 0.939, -1.63 ] + }, + { + "time": 1.2, + "value": -1.61, + "curve": [ 1.42, -1.59, 1.574, -0.67 ] + }, + { "time": 1.6667 } + ] + }, + "gun": { + "rotate": [ + { + "curve": [ 0.099, 0.27, 0.367, 1.23 ] + }, + { + "time": 0.5333, + "value": 1.23, + "curve": [ 0.665, 1.23, 0.937, -0.56 ] + }, + { + "time": 1.1333, + "value": -0.55, + "curve": [ 1.316, -0.55, 1.582, -0.21 ] + }, + { "time": 1.6667 } + ] + }, + "torso": { + "rotate": [ + { + "value": -22.88, + "curve": [ 0.099, -23.45, 0.363, -24.74 ] + }, + { + "time": 0.5333, + "value": -24.74, + "curve": [ 0.706, -24.74, 0.961, -20.97 ] + }, + { + "time": 1.1333, + "value": -20.97, + "curve": [ 1.355, -20.97, 1.567, -22.28 ] + }, + { "time": 1.6667, "value": -22.88 } + ] + }, + "neck": { + "rotate": [ + { + "value": 3.78, + "curve": [ 0.167, 3.78, 0.5, 5.45 ] + }, + { + "time": 0.6667, + "value": 5.45, + "curve": [ 0.917, 5.45, 1.417, 3.78 ] + }, + { "time": 1.6667, "value": 3.78 } + ] + }, + "torso2": { + "rotate": [ + { + "curve": [ 0.067, 0.33, 0.341, 2.54 ] + }, + { + "time": 0.5333, + "value": 2.54, + "curve": [ 0.734, 2.55, 0.982, -0.94 ] + }, + { + "time": 1.1333, + "value": -0.93, + "curve": [ 1.365, -0.91, 1.549, -0.56 ] + }, + { "time": 1.6667 } + ] + }, + "torso3": { + "rotate": [ + { + "value": -2.15, + "curve": [ 0.052, -1.9, 0.384, -0.15 ] + }, + { + "time": 0.5333, + "value": -0.14, + "curve": [ 0.762, -0.13, 0.895, -3.1 ] + }, + { + "time": 1.1333, + "value": -3.1, + "curve": [ 1.348, -3.1, 1.592, -2.46 ] + }, + { "time": 1.6667, "value": -2.15 } + ] + }, + "hair1": { + "rotate": [ + { + "curve": [ 0.067, 0, 0.213, 2.86 ] + }, + { + "time": 0.2667, + "value": 3.65, + "curve": [ 0.358, 4.99, 0.535, 7.92 ] + }, + { + "time": 0.6667, + "value": 7.92, + "curve": [ 0.809, 7.92, 1.067, 5.49 ] + }, + { + "time": 1.1333, + "value": 4.7, + "curve": [ 1.245, 3.34, 1.525, 0 ] + }, + { "time": 1.6667 } + ] + }, + "hair2": { + "rotate": [ + { + "curve": [ 0.067, 0, 0.225, -7.97 ] + }, + { + "time": 0.2667, + "value": -9.75, + "curve": [ 0.316, -11.84, 0.519, -16.66 ] + }, + { + "time": 0.6667, + "value": -16.66, + "curve": [ 0.817, -16.66, 1.029, -11.43 ] + }, + { + "time": 1.1333, + "value": -9.14, + "curve": [ 1.25, -6.56, 1.525, 0 ] + }, + { "time": 1.6667 } + ] + }, + "hair3": { + "rotate": [ + { + "curve": [ 0.1, 0, 0.3, 1.32 ] + }, + { + "time": 0.4, + "value": 1.32, + "curve": [ 0.55, 1.32, 0.866, 0.93 ] + }, + { + "time": 1, + "value": 0.73, + "curve": [ 1.189, 0.46, 1.5, 0 ] + }, + { "time": 1.6667 } + ] + }, + "hair4": { + "rotate": [ + { + "curve": [ 0.118, -0.44, 0.3, -8.52 ] + }, + { + "time": 0.4, + "value": -8.52, + "curve": [ 0.55, -8.52, 0.85, 1.96 ] + }, + { + "time": 1, + "value": 1.96, + "curve": [ 1.167, 1.96, 1.577, 0.38 ] + }, + { "time": 1.6667 } + ] + }, + "head-control": { + "translate": [ + { + "curve": [ 0.098, 1.46, 0.3, 4.49, 0.17, 0.13, 0.316, -3.28 ] + }, + { + "time": 0.4, + "x": 4.55, + "y": -5.95, + "curve": [ 0.53, 4.64, 0.776, 2.59, 0.492, -8.89, 0.668, -14.21 ] + }, + { + "time": 0.8667, + "x": 1.42, + "y": -14.26, + "curve": [ 0.966, 0.15, 1.109, -2.91, 0.994, -14.26, 1.144, -10.58 ] + }, + { + "time": 1.2333, + "x": -3.02, + "y": -8.26, + "curve": [ 1.342, -3.02, 1.568, -1.48, 1.317, -6.1, 1.558, 0 ] + }, + { "time": 1.6667 } + ] + }, + "front-shoulder": { + "translate": [ + { + "curve": [ 0.21, 0, 0.525, -1.72, 0.21, 0, 0.525, 4.08 ] + }, + { + "time": 0.8333, + "x": -1.72, + "y": 4.08, + "curve": [ 1.15, -1.72, 1.46, 0, 1.15, 4.08, 1.46, 0 ] + }, + { "time": 1.6667 } + ] + } + } + }, + "idle-turn": { + "slots": { + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + } + }, + "bones": { + "front-upper-arm": { + "rotate": [ + { + "value": -302.77, + "curve": [ 0, -406.9, 0.125, -420.87 ] + }, + { "time": 0.2667, "value": -420.87 } + ], + "translate": [ + { + "x": 2.24, + "y": -4.98, + "curve": [ 0.067, 2.24, 0.111, 0, 0.067, -4.98, 0.111, 0 ] + }, + { "time": 0.2667 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 248.56, + "curve": [ 0, 371.28, 0.062, 399.2 ] + }, + { "time": 0.1333, "value": 399.2 } + ], + "translate": [ + { + "x": -2.84, + "y": 37.28, + "curve": [ 0.033, -2.84, 0.069, 0, 0.033, 37.28, 0.069, 0 ] + }, + { "time": 0.1333 } + ] + }, + "gun": { + "rotate": [ + { + "value": -3.95, + "curve": [ 0, -10.4, 0.019, -20.43 ] + }, + { + "time": 0.0333, + "value": -20.45, + "curve": [ 0.044, -20.47, 0.125, 0 ] + }, + { "time": 0.2 } + ] + }, + "neck": { + "rotate": [ + { + "value": 17.2, + "curve": [ 0, 6.27, 0.125, 3.78 ] + }, + { "time": 0.2667, "value": 3.78 } + ] + }, + "hip": { + "translate": [ + { + "x": -2.69, + "y": -6.79, + "curve": [ 0.067, -2.69, 0.2, -11.97, 0.067, -6.79, 0.2, -23.15 ] + }, + { "time": 0.2667, "x": -11.97, "y": -23.15 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -15.54, + "curve": [ 0, -3.08, 0.034, 18.44 ] + }, + { + "time": 0.0667, + "value": 19.02, + "curve": [ 0.108, 19.75, 0.169, 0 ] + }, + { "time": 0.2667 } + ], + "scale": [ + { + "x": 0.94, + "curve": [ 0, 0.962, 0.024, 1.237, 0, 1, 0.026, 0.947 ] + }, + { + "time": 0.0667, + "x": 1.236, + "y": 0.947, + "curve": [ 0.117, 1.235, 0.189, 1, 0.117, 0.947, 0.189, 1 ] + }, + { "time": 0.2667 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 11.75, + "curve": [ 0, -7.97, 0.017, -33.4 ] + }, + { + "time": 0.0333, + "value": -33.39, + "curve": [ 0.049, -33.37, 0.131, 0 ] + }, + { "time": 0.2 } + ] + }, + "torso": { + "rotate": [ + { + "value": -18.25, + "curve": [ 0, -10.59, 0.125, -22.88 ] + }, + { "time": 0.2667, "value": -22.88 } + ], + "scale": [ + { + "y": 1.03, + "curve": [ 0.067, 1, 0.132, 1, 0.067, 1.03, 0.132, 1 ] + }, + { "time": 0.2667 } + ] + }, + "head": { + "rotate": [ + { + "value": 5.12, + "curve": [ 0, -6.34, 0.125, -6.75 ] + }, + { "time": 0.2667, "value": -6.75 } + ], + "scale": [ + { + "y": 1.03, + "curve": [ 0.067, 1, 0.107, 1, 0.067, 1.03, 0.107, 1 ] + }, + { "time": 0.2667 } + ] + }, + "rear-foot-target": { + "translate": [ + { + "x": -58.39, + "y": 30.48, + "curve": [ 0, -7.15, 0.047, 16.62, 0, 12.71, 0.039, 0.22 ] + }, + { + "time": 0.1, + "x": 34.14, + "y": -0.19, + "curve": [ 0.136, 45.79, 0.163, 48.87, 0.133, -0.41, 0.163, 0 ] + }, + { "time": 0.2, "x": 48.87 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 6.69, + "curve": [ 0, 19.76, 0.039, 56.53 ] + }, + { + "time": 0.0667, + "value": 56.63, + "curve": [ 0.114, 56.79, 0.189, 42.46 ] + }, + { "time": 0.2667, "value": 42.46 } + ] + }, + "front-foot-target": { + "rotate": [ + { + "value": -1.85, + "curve": [ 0.014, -8.91, 0.047, -28.4 ] + }, + { + "time": 0.1, + "value": -28.89, + "curve": [ 0.144, -29.29, 0.262, -21.77 ] + }, + { "time": 0.2667 } + ], + "translate": [ + { + "x": 9.97, + "y": 0.82, + "curve": [ 0, -54.41, 0.078, -69.06, 0, 0.15, 0.078, 0 ] + }, + { "time": 0.1667, "x": -69.06 } + ] + }, + "hair3": { + "rotate": [ + { + "value": -9.01, + "curve": [ 0.044, -9.01, 0.072, 7.41 ] + }, + { + "time": 0.1333, + "value": 10.08, + "curve": [ 0.166, 11.47, 0.208, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -16.49, + "curve": [ 0.044, -16.49, 0.101, -5.98 ] + }, + { + "time": 0.1333, + "value": -2.95, + "curve": [ 0.162, -0.34, 0.208, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair1": { + "rotate": [ + { + "value": -3.85, + "curve": [ 0.044, -3.85, 0.072, 6.91 ] + }, + { + "time": 0.1333, + "value": 8.05, + "curve": [ 0.166, 8.65, 0.208, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair2": { + "rotate": [ + { + "value": 1.25, + "curve": [ 0.044, 1.25, 0.072, 8.97 ] + }, + { + "time": 0.1333, + "value": 8.6, + "curve": [ 0.166, 8.4, 0.208, 0 ] + }, + { "time": 0.2667 } + ] + }, + "front-thigh": { + "translate": [ + { + "x": 12.21, + "y": 1.89, + "curve": [ 0.033, 12.21, 0.1, 0, 0.033, 1.89, 0.1, 0 ] + }, + { "time": 0.1333 } + ] + }, + "rear-thigh": { + "translate": [ + { + "x": -16.11, + "y": -1.38, + "curve": [ 0.033, -16.11, 0.1, 0, 0.033, -1.38, 0.1, 0 ] + }, + { "time": 0.1333 } + ] + }, + "torso3": { + "rotate": [ + { "time": 0.2667, "value": -2.15 } + ] + }, + "head-control": { + "translate": [ + { + "x": -13.72, + "y": -34.7, + "curve": [ 0.067, -13.72, 0.2, 0, 0.067, -34.7, 0.2, 0 ] + }, + { "time": 0.2667 } + ] + }, + "front-shoulder": { + "translate": [ + { + "x": 1.13, + "y": -14.31, + "curve": [ 0.067, 1.13, 0.2, 0, 0.067, -14.31, 0.2, 0 ] + }, + { "time": 0.2667 } + ] + } + } + }, + "jump": { + "slots": { + "front-fist": { + "attachment": [ + { "name": "front-fist-open" }, + { "time": 0.1, "name": "front-fist-closed" }, + { "time": 0.8333, "name": "front-fist-open" } + ] + } + }, + "bones": { + "front-thigh": { + "rotate": [ + { + "value": 55.08, + "curve": [ 0.007, 46.66, 0.043, 26.3 ] + }, + { + "time": 0.0667, + "value": 22.84, + "curve": [ 0.1, 17.99, 0.165, 15.78 ] + }, + { + "time": 0.2333, + "value": 15.71, + "curve": [ 0.309, 15.63, 0.408, 46.67 ] + }, + { + "time": 0.5, + "value": 63.6, + "curve": [ 0.56, 74.72, 0.762, 91.48 ] + }, + { + "time": 0.9667, + "value": 91.81, + "curve": [ 1.068, 92.01, 1.096, 22.05 ] + }, + { + "time": 1.1667, + "value": 22.25, + "curve": [ 1.18, 22.29, 1.176, 56.17 ] + }, + { + "time": 1.2, + "value": 56.16, + "curve": [ 1.246, 56.15, 1.263, 54.94 ] + }, + { "time": 1.3333, "value": 55.08 } + ], + "translate": [ + { "x": -5.13, "y": 11.55 } + ] + }, + "torso": { + "rotate": [ + { + "value": -45.57, + "curve": [ 0.022, -44.61, 0.03, -39.06 ] + }, + { + "time": 0.0667, + "value": -35.29, + "curve": [ 0.12, -29.77, 0.28, -19.95 ] + }, + { + "time": 0.4333, + "value": -19.95, + "curve": [ 0.673, -19.95, 0.871, -22.38 ] + }, + { + "time": 0.9667, + "value": -27.08, + "curve": [ 1.094, -33.33, 1.176, -44.93 ] + }, + { "time": 1.3333, "value": -45.57 } + ], + "translate": [ + { "x": -3.79, "y": -0.77 } + ] + }, + "rear-thigh": { + "rotate": [ + { + "value": 12.81, + "curve": [ 0.067, 12.81, 0.242, 67.88 ] + }, + { + "time": 0.2667, + "value": 74.11, + "curve": [ 0.314, 86.02, 0.454, 92.23 ] + }, + { + "time": 0.5667, + "value": 92.24, + "curve": [ 0.753, 92.26, 0.966, 67.94 ] + }, + { + "time": 1, + "value": 61.32, + "curve": [ 1.039, 53.75, 1.218, 12.68 ] + }, + { "time": 1.3333, "value": 12.81 } + ] + }, + "rear-shin": { + "rotate": [ + { + "value": -115.64, + "curve": [ 0.067, -117.17, 0.125, -117.15 ] + }, + { + "time": 0.1667, + "value": -117.15, + "curve": [ 0.225, -117.15, 0.332, -108.76 ] + }, + { + "time": 0.4, + "value": -107.15, + "curve": [ 0.48, -105.26, 0.685, -103.49 ] + }, + { + "time": 0.7667, + "value": -101.97, + "curve": [ 0.826, -100.87, 0.919, -92.3 ] + }, + { + "time": 1, + "value": -92.28, + "curve": [ 1.113, -92.26, 1.297, -114.22 ] + }, + { "time": 1.3333, "value": -115.64 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -40.21, + "curve": [ 0.054, -35.46, 0.15, -31.12 ] + }, + { + "time": 0.2, + "value": -31.12, + "curve": [ 0.308, -31.12, 0.547, -80.12 ] + }, + { + "time": 0.6333, + "value": -96.56, + "curve": [ 0.697, -108.56, 0.797, -112.54 ] + }, + { + "time": 0.8667, + "value": -112.6, + "curve": [ 1.137, -112.84, 1.274, -49.19 ] + }, + { "time": 1.3333, "value": -40.21 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 20.54, + "curve": [ 0.054, 32.23, 0.192, 55.84 ] + }, + { + "time": 0.2333, + "value": 62.58, + "curve": [ 0.29, 71.87, 0.375, 79.28 ] + }, + { + "time": 0.4333, + "value": 79.18, + "curve": [ 0.555, 78.98, 0.684, 27.54 ] + }, + { + "time": 0.7333, + "value": 13.28, + "curve": [ 0.786, -1.85, 0.874, -24.76 ] + }, + { + "time": 1, + "value": -25.45, + "curve": [ 1.165, -26.36, 1.303, 9.1 ] + }, + { "time": 1.3333, "value": 20.54 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -36.16, + "curve": [ 0.114, -39.59, 0.3, -45.61 ] + }, + { + "time": 0.4, + "value": -45.61, + "curve": [ 0.442, -45.61, 0.537, -21.54 ] + }, + { + "time": 0.5667, + "value": -15.4, + "curve": [ 0.592, -10.23, 0.692, 11.89 ] + }, + { + "time": 0.7333, + "value": 11.73, + "curve": [ 0.783, 11.54, 0.831, 1.8 ] + }, + { + "time": 0.8667, + "value": -5.78, + "curve": [ 0.897, -12.22, 0.901, -14.22 ] + }, + { + "time": 0.9333, + "value": -14.51, + "curve": [ 0.974, -14.89, 0.976, 10.38 ] + }, + { + "time": 1, + "value": 10.55, + "curve": [ 1.027, 10.74, 1.023, -8.44 ] + }, + { + "time": 1.0333, + "value": -8.42, + "curve": [ 1.059, -8.36, 1.074, 10.12 ] + }, + { + "time": 1.1, + "value": 10.22, + "curve": [ 1.168, 10.48, 1.27, -36.07 ] + }, + { "time": 1.3333, "value": -36.16 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 40.5, + "curve": [ 0.048, 36.1, 0.168, 20.45 ] + }, + { + "time": 0.3, + "value": 20.45, + "curve": [ 0.476, 20.45, 0.571, 33.76 ] + }, + { + "time": 0.6, + "value": 38.67, + "curve": [ 0.642, 45.8, 0.681, 57.44 ] + }, + { + "time": 0.7333, + "value": 62.91, + "curve": [ 0.829, 72.8, 0.996, 77.61 ] + }, + { + "time": 1.0333, + "value": 80.37, + "curve": [ 1.082, 83.94, 1.148, 90.6 ] + }, + { + "time": 1.2, + "value": 90.6, + "curve": [ 1.248, 90.46, 1.317, 53.07 ] + }, + { "time": 1.3333, "value": 49.06 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 28.28, + "curve": [ 0.022, 25.12, 0.187, -0.89 ] + }, + { + "time": 0.2, + "value": -2.52, + "curve": [ 0.257, -9.92, 0.372, -17.38 ] + }, + { + "time": 0.4333, + "value": -17.41, + "curve": [ 0.54, -17.47, 0.659, -16.91 ] + }, + { + "time": 0.7667, + "value": -12.1, + "curve": [ 0.907, -5.79, 1.025, 14.58 ] + }, + { + "time": 1.1, + "value": 20.58, + "curve": [ 1.191, 27.85, 1.283, 29.67 ] + }, + { "time": 1.3333, "value": 29.67 } + ] + }, + "neck": { + "rotate": [ + { + "value": 11.88, + "curve": [ 0.104, 11.82, 0.179, 11.15 ] + }, + { + "time": 0.2, + "value": 10.08, + "curve": [ 0.255, 7.29, 0.405, -8.15 ] + }, + { + "time": 0.4333, + "value": -9.35, + "curve": [ 0.508, -12.48, 0.595, -13.14 ] + }, + { + "time": 0.6667, + "value": -12.61, + "curve": [ 0.714, -12.26, 0.815, -5.57 ] + }, + { + "time": 0.8333, + "value": -4.08, + "curve": [ 0.883, -0.07, 1.045, 12.77 ] + }, + { + "time": 1.1, + "value": 15.06, + "curve": [ 1.208, 19.6, 1.279, 20.64 ] + }, + { "time": 1.3333, "value": 20.73 } + ] + }, + "head": { + "rotate": [ + { + "value": 13.14, + "curve": [ 0.008, 12.19, 0.197, -23.53 ] + }, + { + "time": 0.3333, + "value": -23.95, + "curve": [ 0.509, -23.95, 0.667, -2.66 ] + }, + { + "time": 0.7333, + "value": -2.66, + "curve": [ 0.792, -2.66, 0.908, -13.32 ] + }, + { + "time": 0.9667, + "value": -13.32, + "curve": [ 1.158, -13.11, 1.241, -1.58 ] + }, + { "time": 1.3333, "value": -1.58 } + ], + "scale": [ + { + "curve": [ 0.041, 1, 0.052, 0.962, 0.041, 1, 0.052, 1.137 ] + }, + { + "time": 0.1, + "x": 0.954, + "y": 1.137, + "curve": [ 0.202, 0.962, 0.318, 1, 0.202, 1.137, 0.252, 1.002 ] + }, + { "time": 0.4667 }, + { + "time": 1.0667, + "x": 1.002, + "curve": [ 1.092, 1.002, 1.126, 1.143, 1.092, 1, 1.128, 0.975 ] + }, + { + "time": 1.1667, + "x": 1.144, + "y": 0.973, + "curve": [ 1.204, 1.145, 1.233, 0.959, 1.206, 0.972, 1.227, 1.062 ] + }, + { + "time": 1.2667, + "x": 0.958, + "y": 1.063, + "curve": [ 1.284, 0.958, 1.292, 1.001, 1.288, 1.063, 1.288, 1.001 ] + }, + { "time": 1.3333 } + ] + }, + "hip": { + "translate": [ + { + "y": -45.46, + "curve": [ 0.042, -0.09, 0.15, 15.22, 0.031, 44.98, 0.123, 289.73 ] + }, + { + "time": 0.2, + "x": 15.22, + "y": 415.85, + "curve": [ 0.332, 15.22, 0.539, -34.52, 0.271, 532.93, 0.483, 720.5 ] + }, + { + "time": 0.7667, + "x": -34.52, + "y": 721.6, + "curve": [ 0.888, -34.52, 1.057, -21.95, 1.049, 721.17, 1.098, 379.84 ] + }, + { + "time": 1.1333, + "x": -15.67, + "y": 266.77, + "curve": [ 1.144, -14.77, 1.188, -10.53, 1.15, 213.72, 1.172, -61.32 ] + }, + { + "time": 1.2333, + "x": -6.53, + "y": -61.34, + "curve": [ 1.272, -3.22, 1.311, 0.05, 1.291, -61.36, 1.296, -44.8 ] + }, + { "time": 1.3333, "y": -45.46 } + ] + }, + "front-shin": { + "rotate": [ + { + "value": -74.19, + "curve": [ 0, -51.14, 0.042, -12.54 ] + }, + { + "time": 0.1667, + "value": -12.28, + "curve": [ 0.285, -12.32, 0.37, -74.44 ] + }, + { + "time": 0.4333, + "value": -92.92, + "curve": [ 0.498, -111.86, 0.617, -140.28 ] + }, + { + "time": 0.9, + "value": -140.84, + "curve": [ 1.004, -141.04, 1.09, -47.87 ] + }, + { + "time": 1.1, + "value": -37.44, + "curve": [ 1.108, -29.83, 1.14, -21.18 ] + }, + { + "time": 1.1667, + "value": -21.08, + "curve": [ 1.18, -21.03, 1.191, -50.65 ] + }, + { + "time": 1.2, + "value": -53.17, + "curve": [ 1.22, -58.53, 1.271, -73.38 ] + }, + { "time": 1.3333, "value": -74.19 } + ] + }, + "front-foot": { + "rotate": [ + { + "value": 7.35, + "curve": [ 0, 4.8, 0.05, -26.64 ] + }, + { + "time": 0.0667, + "value": -26.64, + "curve": [ 0.192, -26.64, 0.442, -11.77 ] + }, + { + "time": 0.5667, + "value": -11.77, + "curve": [ 0.692, -11.77, 0.942, -19.36 ] + }, + { + "time": 1.0667, + "value": -19.36, + "curve": [ 1.133, -19.36, 1.32, 3.82 ] + }, + { "time": 1.3333, "value": 7.35 } + ] + }, + "rear-foot": { + "rotate": [ + { "value": -7.14 } + ] + }, + "gun": { + "rotate": [ + { + "value": 12.36, + "curve": [ 0.022, 16.28, 0.15, 30.81 ] + }, + { + "time": 0.2, + "value": 30.81, + "curve": [ 0.258, 30.81, 0.375, 13.26 ] + }, + { + "time": 0.4333, + "value": 13.26, + "curve": [ 0.508, 13.26, 0.658, 15.05 ] + }, + { + "time": 0.7333, + "value": 14.98, + "curve": [ 0.789, 14.94, 0.828, 13.62 ] + }, + { + "time": 0.8667, + "value": 12.72, + "curve": [ 0.887, 12.25, 0.984, 9.83 ] + }, + { + "time": 1.0333, + "value": 8.6, + "curve": [ 1.045, 8.31, 1.083, 7.55 ] + }, + { + "time": 1.1333, + "value": 7.13, + "curve": [ 1.175, 6.78, 1.283, 6.18 ] + }, + { "time": 1.3333, "value": 6.18 } + ] + }, + "front-leg-target": { + "translate": [ + { "x": -13.95, "y": -30.34 } + ] + }, + "rear-leg-target": { + "rotate": [ + { "value": -38.43 } + ], + "translate": [ + { "x": 85, "y": -33.59 } + ] + }, + "front-foot-target": { + "rotate": [ + { "value": -62.54 } + ], + "translate": [ + { "x": 16.34, "y": 0.18 } + ] + }, + "rear-foot-target": { + "rotate": [ + { "value": 18.55 } + ], + "translate": [ + { "x": -176.39, "y": 134.12 } + ] + }, + "back-foot-tip": { + "rotate": [ + { + "value": -143.73, + "curve": [ 0.083, -144.24, 0.167, -74.26 ] + }, + { + "time": 0.2667, + "value": -52.76, + "curve": [ 0.342, -36.57, 0.513, -36.57 ] + }, + { + "time": 0.6333, + "value": -30.97, + "curve": [ 0.724, -26.78, 0.848, -17.06 ] + }, + { + "time": 0.9667, + "value": -16.74, + "curve": [ 1.167, -16.2, 1.272, -144.17 ] + }, + { "time": 1.3333, "value": -143.73 } + ] + }, + "front-foot-tip": { + "rotate": [ + { + "value": -1.57, + "curve": [ 0, -24.71, 0.162, -60.88 ] + }, + { + "time": 0.2667, + "value": -60.83, + "curve": [ 0.342, -60.8, 0.582, -43.5 ] + }, + { + "time": 0.7, + "value": -39.45, + "curve": [ 0.773, -36.94, 0.832, -36.78 ] + }, + { + "time": 0.9667, + "value": -36.6, + "curve": [ 1.054, -36.49, 1.092, -37.37 ] + }, + { + "time": 1.1667, + "value": -33.26, + "curve": [ 1.237, -29.37, 1.147, -1.41 ] + }, + { "time": 1.2, "value": -1.57 } + ] + }, + "hair3": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0, 13.59, 0.117, 18.21 ] + }, + { + "time": 0.1333, + "value": 18.21, + "curve": [ 0.167, 18.21, 0.26, 12.95 ] + }, + { + "time": 0.3, + "value": 11.56, + "curve": [ 0.382, 8.7, 0.55, 9.43 ] + }, + { + "time": 0.6667, + "value": 9.32, + "curve": [ 0.843, 9.15, 0.918, -7.34 ] + }, + { "time": 1.3333, "value": -6.81 } + ], + "translate": [ + { + "time": 0.6667, + "curve": [ 0.781, 0, 0.972, 16.03, 0.781, 0, 0.972, 0.92 ] + }, + { + "time": 1.1333, + "x": 16.03, + "y": 0.92, + "curve": [ 1.211, 16.03, 1.281, 0, 1.211, 0.92, 1.281, 0 ] + }, + { "time": 1.3333 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.001, -3.88, 0.063, 16.18 ] + }, + { + "time": 0.1667, + "value": 16.14, + "curve": [ 0.242, 16.1, 0.249, 16.07 ] + }, + { + "time": 0.3333, + "value": 13.46, + "curve": [ 0.442, 10.09, 0.573, -2.2 ] + }, + { + "time": 0.6, + "value": -6.04, + "curve": [ 0.614, -8.05, 0.717, -33.44 ] + }, + { + "time": 0.7667, + "value": -33.44, + "curve": [ 0.809, -33.44, 0.835, -31.32 ] + }, + { + "time": 0.8667, + "value": -27.36, + "curve": [ 0.874, -26.47, 0.903, -14.28 ] + }, + { + "time": 0.9333, + "value": -14.47, + "curve": [ 0.956, -14.62, 0.944, -25.91 ] + }, + { + "time": 1, + "value": -25.96, + "curve": [ 1.062, -26.02, 1.051, -1.87 ] + }, + { + "time": 1.0667, + "value": -1.87, + "curve": [ 1.096, -1.87, 1.096, -16.09 ] + }, + { + "time": 1.1333, + "value": -16.08, + "curve": [ 1.169, -16.08, 1.153, -3.38 ] + }, + { + "time": 1.2, + "value": -3.38, + "curve": [ 1.234, -3.38, 1.271, -6.07 ] + }, + { "time": 1.3333, "value": -6.07 } + ] + }, + "hair2": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0, -3.17, 0.042, 16.33 ] + }, + { + "time": 0.0667, + "value": 16.33, + "curve": [ 0.21, 15.74, 0.208, -12.06 ] + }, + { + "time": 0.3333, + "value": -12.21, + "curve": [ 0.417, -12.3, 0.552, -3.98 ] + }, + { + "time": 0.6667, + "value": 1.52, + "curve": [ 0.726, 4.35, 0.817, 4.99 ] + }, + { + "time": 0.8667, + "value": 4.99, + "curve": [ 0.901, 4.99, 0.912, -29.05 ] + }, + { + "time": 0.9667, + "value": -27.45, + "curve": [ 0.987, -26.83, 1.018, -5.42 ] + }, + { + "time": 1.0667, + "value": -5.46, + "curve": [ 1.107, -5.22, 1.095, -33.51 ] + }, + { + "time": 1.1333, + "value": -33.28, + "curve": [ 1.162, -33.57, 1.192, 8.04 ] + }, + { + "time": 1.2667, + "value": 7.86, + "curve": [ 1.302, 7.77, 1.313, 2.7 ] + }, + { "time": 1.3333, "value": 2.7 } + ] + }, + "hair1": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.001, -3.12, 0.074, 14.66 ] + }, + { + "time": 0.1333, + "value": 14.66, + "curve": [ 0.188, 14.8, 0.293, 9.56 ] + }, + { + "time": 0.3333, + "value": 5.99, + "curve": [ 0.381, 1.72, 0.55, -11.11 ] + }, + { + "time": 0.6667, + "value": -11.11, + "curve": [ 0.833, -11.11, 0.933, 22.54 ] + }, + { + "time": 1.1, + "value": 22.54, + "curve": [ 1.158, 22.54, 1.275, -6.81 ] + }, + { "time": 1.3333, "value": -6.81 } + ] + }, + "torso2": { + "rotate": [ + { + "value": 4.52, + "curve": [ 0.013, 2.33, 0.092, -9.75 ] + }, + { + "time": 0.1333, + "value": -9.75, + "curve": [ 0.175, -9.75, 0.291, -1.26 ] + }, + { + "time": 0.3333, + "value": 0.96, + "curve": [ 0.359, 2.3, 0.543, 4.25 ] + }, + { + "time": 0.6, + "value": 4.68, + "curve": [ 0.683, 5.3, 0.771, 5.92 ] + }, + { + "time": 0.8333, + "value": 6.48, + "curve": [ 0.871, 6.82, 1.083, 11.37 ] + }, + { + "time": 1.1667, + "value": 11.37, + "curve": [ 1.208, 11.37, 1.317, 6.18 ] + }, + { "time": 1.3333, "value": 4.52 } + ], + "translate": [ + { + "curve": [ 0, 0, 0.082, -2.24, 0, 0, 0.082, -0.42 ] + }, + { + "time": 0.1667, + "x": -2.98, + "y": -0.56, + "curve": [ 0.232, -2.24, 0.298, 0, 0.232, -0.42, 0.298, 0 ] + }, + { "time": 0.3333, "curve": "stepped" }, + { + "time": 0.8667, + "curve": [ 0.889, 0, 0.912, 0.26, 0.889, 0, 0.912, 0.06 ] + }, + { + "time": 0.9333, + "x": 0.68, + "y": 0.23, + "curve": [ 1.016, 2.22, 1.095, 5.9, 1.023, 0.97, 1.095, 1.99 ] + }, + { + "time": 1.1667, + "x": 6.47, + "y": 2.18, + "curve": [ 1.23, 5.75, 1.286, 0, 1.23, 1.94, 1.286, 0 ] + }, + { "time": 1.3333 } + ] + }, + "torso3": { + "rotate": [ + { + "value": 4.52, + "curve": [ 0.025, 4.52, 0.075, -6.17 ] + }, + { + "time": 0.1, + "value": -6.17, + "curve": [ 0.175, -6.17, 0.381, -0.71 ] + }, + { + "time": 0.4, + "value": -0.25, + "curve": [ 0.447, 0.87, 0.775, 4.84 ] + }, + { + "time": 0.9, + "value": 4.84, + "curve": [ 1.008, 4.84, 1.225, 4.52 ] + }, + { "time": 1.3333, "value": 4.52 } + ] + }, + "head-control": { + "translate": [ + { + "curve": [ 0.138, -2.4, 0.227, -10.44, 0.123, 1.05, 0.227, 2.7 ] + }, + { + "time": 0.3667, + "x": -10.44, + "y": 2.7, + "curve": [ 0.484, -10.44, 0.585, -5.63, 0.484, 2.7, 0.629, -23.62 ] + }, + { + "time": 0.7333, + "x": -2.29, + "y": -26.61, + "curve": [ 0.818, -0.39, 0.962, 1.21, 0.858, -30.17, 0.972, -28.75 ] + }, + { + "time": 1.1, + "x": 1.25, + "y": -28.75, + "curve": [ 1.192, 1.28, 1.234, 0.98, 1.224, -28.75, 1.235, -2.15 ] + }, + { "time": 1.3333 } + ] + }, + "front-shoulder": { + "translate": [ + { + "curve": [ 0.031, -2.22, 0.065, -3.73, 0.02, -3.25, 0.065, -14.74 ] + }, + { + "time": 0.1, + "x": -3.73, + "y": -14.74, + "curve": [ 0.216, -3.73, 0.384, -0.17, 0.216, -14.74, 0.402, -12.51 ] + }, + { + "time": 0.5, + "x": 1.63, + "y": -9.51, + "curve": [ 0.632, 3.69, 0.935, 7.41, 0.585, -6.91, 0.909, 10.86 ] + }, + { + "time": 1.1, + "x": 7.45, + "y": 10.99, + "curve": [ 1.18, 7.46, 1.265, 2.86, 1.193, 11.05, 1.294, 3.38 ] + }, + { "time": 1.3333 } + ] + } + }, + "ik": { + "front-foot-ik": [ + { + "mix": 0, + "curve": [ 0.3, 0, 0.9, 1, 0.3, 0, 0.9, 0 ] + }, + { "time": 1.2 } + ], + "front-leg-ik": [ + { + "mix": 0, + "bendPositive": false, + "curve": [ 0.3, 0, 0.9, 1, 0.3, 0, 0.9, 0 ] + }, + { "time": 1.2, "bendPositive": false } + ], + "rear-foot-ik": [ + { "mix": 0 } + ], + "rear-leg-ik": [ + { "mix": 0, "bendPositive": false } + ] + }, + "events": [ + { "time": 1.2, "name": "footstep" } + ] + }, + "portal": { + "slots": { + "clipping": { + "attachment": [ + { "name": "clipping" } + ] + }, + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + }, + "mouth": { + "attachment": [ + { "time": 0.9, "name": "mouth-grind" }, + { "time": 2.2667, "name": "mouth-smile" } + ] + }, + "portal-bg": { + "attachment": [ + { "name": "portal-bg" }, + { "time": 3 } + ] + }, + "portal-flare1": { + "attachment": [ + { "time": 1.1, "name": "portal-flare1" }, + { "time": 1.1333, "name": "portal-flare2" }, + { "time": 1.1667, "name": "portal-flare3" }, + { "time": 1.2, "name": "portal-flare1" }, + { "time": 1.2333, "name": "portal-flare2" }, + { "time": 1.2667, "name": "portal-flare1" }, + { "time": 1.3333 } + ] + }, + "portal-flare2": { + "attachment": [ + { "time": 1.1, "name": "portal-flare2" }, + { "time": 1.1333, "name": "portal-flare3" }, + { "time": 1.1667, "name": "portal-flare1" }, + { "time": 1.2, "name": "portal-flare2" }, + { "time": 1.2333, "name": "portal-flare3" }, + { "time": 1.2667 } + ] + }, + "portal-flare3": { + "attachment": [ + { "time": 1.2, "name": "portal-flare3" }, + { "time": 1.2333, "name": "portal-flare2" }, + { "time": 1.2667 } + ] + }, + "portal-flare4": { + "attachment": [ + { "time": 1.2, "name": "portal-flare2" }, + { "time": 1.2333, "name": "portal-flare1" }, + { "time": 1.2667, "name": "portal-flare2" }, + { "time": 1.3333 } + ] + }, + "portal-flare5": { + "attachment": [ + { "time": 1.2333, "name": "portal-flare3" }, + { "time": 1.2667, "name": "portal-flare1" }, + { "time": 1.3333 } + ] + }, + "portal-flare6": { + "attachment": [ + { "time": 1.2667, "name": "portal-flare3" }, + { "time": 1.3333 } + ] + }, + "portal-flare7": { + "attachment": [ + { "time": 1.1333, "name": "portal-flare2" }, + { "time": 1.1667 } + ] + }, + "portal-flare8": { + "attachment": [ + { "time": 1.2, "name": "portal-flare3" }, + { "time": 1.2333, "name": "portal-flare2" }, + { "time": 1.2667 } + ] + }, + "portal-flare9": { + "attachment": [ + { "time": 1.2, "name": "portal-flare2" }, + { "time": 1.2333, "name": "portal-flare3" }, + { "time": 1.2667, "name": "portal-flare1" }, + { "time": 1.3 } + ] + }, + "portal-flare10": { + "attachment": [ + { "time": 1.2, "name": "portal-flare2" }, + { "time": 1.2333, "name": "portal-flare1" }, + { "time": 1.2667, "name": "portal-flare3" }, + { "time": 1.3 } + ] + }, + "portal-shade": { + "attachment": [ + { "name": "portal-shade" }, + { "time": 3 } + ] + }, + "portal-streaks1": { + "attachment": [ + { "name": "portal-streaks1" }, + { "time": 3 } + ] + }, + "portal-streaks2": { + "attachment": [ + { "name": "portal-streaks2" }, + { "time": 3 } + ] + } + }, + "bones": { + "portal-root": { + "translate": [ + { + "x": -458.35, + "y": 105.19, + "curve": [ 0.333, -458.22, 0.669, -457.86, 0.934, 105.19, 0.671, 105.19 ] + }, + { + "time": 1, + "x": -456.02, + "y": 105.19, + "curve": [ 1.339, -454.14, 2.208, -447.28, 1.35, 105.19, 2.05, 105.19 ] + }, + { + "time": 2.4, + "x": -439.12, + "y": 105.19, + "curve": [ 2.463, -436.44, 2.502, -432.92, 2.487, 105.19, 2.512, 105.09 ] + }, + { + "time": 2.6, + "x": -432.58, + "y": 105.09, + "curve": [ 2.784, -431.94, 2.978, -446.6, 2.772, 105.09, 2.933, 105.19 ] + }, + { "time": 3.0333, "x": -457.42, "y": 105.19 } + ], + "scale": [ + { + "x": 0.003, + "y": 0.006, + "curve": [ 0.329, 0.044, 0.347, 0.117, 0.329, 0.097, 0.37, 0.249 ] + }, + { + "time": 0.4, + "x": 0.175, + "y": 0.387, + "curve": [ 0.63, 0.619, 0.663, 0.723, 0.609, 1.338, 0.645, 1.524 ] + }, + { + "time": 0.7333, + "x": 0.724, + "y": 1.52, + "curve": [ 0.798, 0.725, 0.907, 0.647, 0.797, 1.517, 0.895, 1.424 ] + }, + { + "time": 1, + "x": 0.645, + "y": 1.426, + "curve": [ 1.095, 0.643, 1.139, 0.688, 1.089, 1.428, 1.115, 1.513 ] + }, + { + "time": 1.2333, + "x": 0.685, + "y": 1.516, + "curve": [ 1.325, 0.683, 1.508, 0.636, 1.343, 1.518, 1.467, 1.4 ] + }, + { + "time": 1.6, + "x": 0.634, + "y": 1.401, + "curve": [ 1.728, 0.631, 1.946, 0.687, 1.722, 1.402, 1.924, 1.522 ] + }, + { + "time": 2.0667, + "x": 0.688, + "y": 1.522, + "curve": [ 2.189, 0.69, 2.289, 0.649, 2.142, 1.522, 2.265, 1.417 ] + }, + { + "time": 2.4, + "x": 0.65, + "y": 1.426, + "curve": [ 2.494, 0.651, 2.504, 0.766, 2.508, 1.434, 2.543, 1.566 ] + }, + { + "time": 2.6, + "x": 0.766, + "y": 1.568, + "curve": [ 2.73, 0.765, 3.006, 0.098, 2.767, 1.564, 2.997, 0.1 ] + }, + { "time": 3.0333, "x": 0.007, "y": 0.015 } + ] + }, + "portal-streaks1": { + "rotate": [ + {}, + { "time": 3.1667, "value": 1200 } + ], + "translate": [ + { + "x": 15.15, + "curve": [ 0.162, 15.15, 0.432, 12.6, 0.162, 0, 0.432, -3.86 ] + }, + { + "time": 0.6667, + "x": 10.9, + "y": -6.44, + "curve": [ 0.794, 9.93, 0.912, 9.21, 0.794, -7.71, 0.912, -8.66 ] + }, + { + "time": 1, + "x": 9.21, + "y": -8.66, + "curve": [ 1.083, 9.21, 1.25, 21.53, 1.083, -8.66, 1.265, -4.9 ] + }, + { + "time": 1.3333, + "x": 21.53, + "y": -3.19, + "curve": [ 1.5, 21.53, 1.939, 12.3, 1.446, -0.37, 1.9, 6.26 ] + }, + { + "time": 2.0667, + "x": 11.26, + "y": 6.26, + "curve": [ 2.239, 9.85, 2.389, 9.68, 2.208, 6.26, 2.523, 0.51 ] + }, + { + "time": 2.5667, + "x": 9.39, + "y": -0.8, + "curve": [ 2.657, 9.24, 2.842, 9.21, 2.646, -3.2, 2.842, -8.91 ] + }, + { "time": 2.9333, "x": 9.21, "y": -8.91 } + ], + "scale": [ + { + "curve": [ 0.167, 1, 0.5, 1.053, 0.167, 1, 0.5, 1.053 ] + }, + { + "time": 0.6667, + "x": 1.053, + "y": 1.053, + "curve": [ 0.833, 1.053, 1.167, 0.986, 0.833, 1.053, 1.167, 0.986 ] + }, + { + "time": 1.3333, + "x": 0.986, + "y": 0.986, + "curve": [ 1.5, 0.986, 1.833, 1.053, 1.5, 0.986, 1.833, 1.053 ] + }, + { "time": 2, "x": 1.053, "y": 1.053 } + ] + }, + "portal-streaks2": { + "rotate": [ + {}, + { "time": 3.1667, "value": 600 } + ], + "translate": [ + { "x": -2.11 }, + { "time": 1, "x": -2.11, "y": 6.63 }, + { "time": 1.9333, "x": -2.11 } + ], + "scale": [ + { + "x": 1.014, + "y": 1.014, + "curve": [ 0.229, 0.909, 0.501, 0.755, 0.242, 0.892, 0.502, 0.768 ] + }, + { + "time": 0.8667, + "x": 0.745, + "y": 0.745, + "curve": [ 1.282, 0.733, 2.021, 0.699, 1.27, 0.719, 2.071, 0.709 ] + }, + { + "time": 2.2, + "x": 0.7, + "y": 0.704, + "curve": [ 2.315, 0.7, 2.421, 0.794, 2.311, 0.701, 2.485, 0.797 ] + }, + { + "time": 2.5667, + "x": 0.794, + "y": 0.794, + "curve": [ 2.734, 0.794, 2.99, 0.323, 2.714, 0.789, 3.019, 0.341 ] + }, + { "time": 3.1667, "x": 0, "y": 0 } + ] + }, + "portal-shade": { + "translate": [ + { "x": -29.68 } + ], + "scale": [ + { "x": 0.714, "y": 0.714 } + ] + }, + "portal": { + "rotate": [ + {}, + { "time": 3.1667, "value": 600 } + ] + }, + "clipping": { + "translate": [ + { "x": -476.55, "y": 2.27 } + ], + "scale": [ + { "x": 0.983, "y": 1.197 } + ] + }, + "hip": { + "rotate": [ + { + "time": 1.0667, + "value": 22.74, + "curve": [ 1.163, 18.84, 1.77, 8.77 ] + }, + { + "time": 1.9, + "value": 7.82, + "curve": [ 2.271, 5.1, 2.89, 0 ] + }, + { "time": 3.1667 } + ], + "translate": [ + { "x": -899.41, "y": 4.47, "curve": "stepped" }, + { + "time": 1.0667, + "x": -694.16, + "y": 183.28, + "curve": [ 1.091, -602.08, 1.138, -427.59, 1.115, 185.6, 1.171, 133.18 ] + }, + { + "time": 1.2333, + "x": -316.97, + "y": 55.29, + "curve": [ 1.317, -220.27, 1.512, -123.21, 1.271, 8.68, 1.461, -83.18 ] + }, + { + "time": 1.6, + "x": -95.53, + "y": -112.23, + "curve": [ 1.718, -58.25, 2.037, -22.54, 1.858, -166.17, 2.109, -31.4 ] + }, + { + "time": 2.1667, + "x": -14.82, + "y": -31.12, + "curve": [ 2.294, -7.28, 2.442, -7.2, 2.274, -30.6, 2.393, -36.76 ] + }, + { + "time": 2.6, + "x": -7.2, + "y": -36.96, + "curve": [ 2.854, -7.2, 3.071, -11.87, 2.786, -36.27, 3.082, -22.98 ] + }, + { "time": 3.1667, "x": -11.97, "y": -23.15 } + ] + }, + "rear-foot-target": { + "rotate": [ + { "time": 1.0667, "value": 41.6, "curve": "stepped" }, + { + "time": 1.2333, + "value": 41.6, + "curve": [ 1.258, 41.6, 1.379, 35.46 ] + }, + { + "time": 1.4, + "value": 30.09, + "curve": [ 1.412, 27.04, 1.433, 10.65 ] + }, + { "time": 1.4333, "value": -0.28 }, + { "time": 1.6, "value": 2.44 } + ], + "translate": [ + { "x": -899.41, "y": 4.47, "curve": "stepped" }, + { + "time": 1.0667, + "x": -591.13, + "y": 438.46, + "curve": [ 1.076, -539.77, 1.206, -268.1, 1.117, 418.44, 1.21, 333.18 ] + }, + { + "time": 1.2333, + "x": -225.28, + "y": 304.53, + "curve": [ 1.265, -175.22, 1.393, -74.21, 1.296, 226.52, 1.401, 49.61 ] + }, + { + "time": 1.4333, + "x": -52.32, + "y": 0.2, + "curve": [ 1.454, -40.85, 1.616, 40.87, 1.466, 0.17, 1.614, 0.04 ] + }, + { "time": 1.6667, "x": 45.87, "y": 0.01 }, + { "time": 1.9333, "x": 48.87 } + ] + }, + "front-foot-target": { + "rotate": [ + { + "time": 1.0667, + "value": 32.08, + "curve": [ 1.108, 32.08, 1.192, 35.16 ] + }, + { + "time": 1.2333, + "value": 35.16, + "curve": [ 1.258, 35.16, 1.317, 2.23 ] + }, + { + "time": 1.3333, + "value": -4.74, + "curve": [ 1.351, -12.14, 1.429, -34.96 ] + }, + { + "time": 1.6, + "value": -34.77, + "curve": [ 1.765, -34.58, 1.897, -17.25 ] + }, + { "time": 1.9333 } + ], + "translate": [ + { "x": -899.41, "y": 4.47, "curve": "stepped" }, + { + "time": 1.0667, + "x": -533.93, + "y": 363.75, + "curve": [ 1.074, -480.85, 1.18, -261.31, 1.094, 362.3, 1.195, 267.77 ] + }, + { + "time": 1.2333, + "x": -201.23, + "y": 199.93, + "curve": [ 1.269, -161.38, 1.294, -140.32, 1.274, 126.67, 1.308, 77.12 ] + }, + { + "time": 1.3333, + "x": -124.08, + "y": 0.2, + "curve": [ 1.426, -85.6, 1.633, -69.06, 1.45, 0.48, 1.633, 0 ] + }, + { "time": 1.7333, "x": -69.06 } + ] + }, + "torso": { + "rotate": [ + { + "time": 1.0667, + "value": 27.02, + "curve": [ 1.187, 26.86, 1.291, 7.81 ] + }, + { + "time": 1.3333, + "value": -2.62, + "curve": [ 1.402, -19.72, 1.429, -48.64 ] + }, + { + "time": 1.4667, + "value": -56.31, + "curve": [ 1.509, -64.87, 1.62, -77.14 ] + }, + { + "time": 1.7333, + "value": -77.34, + "curve": [ 1.837, -76.89, 1.895, -71.32 ] + }, + { + "time": 2, + "value": -57.52, + "curve": [ 2.104, -43.83, 2.189, -28.59 ] + }, + { + "time": 2.3, + "value": -29.03, + "curve": [ 2.413, -29.48, 2.513, -36.79 ] + }, + { + "time": 2.6667, + "value": -36.79, + "curve": [ 2.814, -36.95, 2.947, -22.88 ] + }, + { "time": 3.1667, "value": -22.88 } + ] + }, + "neck": { + "rotate": [ + { + "time": 1.0667, + "value": -3.57, + "curve": [ 1.146, -3.66, 1.15, -13.5 ] + }, + { + "time": 1.2333, + "value": -13.5, + "curve": [ 1.428, -13.5, 1.443, 11.58 ] + }, + { + "time": 1.5667, + "value": 11.42, + "curve": [ 1.658, 11.3, 1.775, 3.78 ] + }, + { + "time": 1.8667, + "value": 3.78, + "curve": [ 1.92, 3.78, 2.036, 8.01 ] + }, + { + "time": 2.1, + "value": 7.93, + "curve": [ 2.266, 7.72, 2.42, 3.86 ] + }, + { + "time": 2.5333, + "value": 3.86, + "curve": [ 2.783, 3.86, 3.004, 3.78 ] + }, + { "time": 3.1667, "value": 3.78 } + ] + }, + "head": { + "rotate": [ + { + "time": 1.0667, + "value": 16.4, + "curve": [ 1.133, 9.9, 1.207, 1.87 ] + }, + { + "time": 1.3333, + "value": 1.67, + "curve": [ 1.46, 1.56, 1.547, 47.54 ] + }, + { + "time": 1.7333, + "value": 47.55, + "curve": [ 1.897, 47.56, 2.042, 5.68 ] + }, + { + "time": 2.0667, + "value": 0.86, + "curve": [ 2.074, -0.61, 2.086, -2.81 ] + }, + { + "time": 2.1, + "value": -5.31, + "curve": [ 2.145, -13.07, 2.216, -23.65 ] + }, + { + "time": 2.2667, + "value": -23.71, + "curve": [ 2.334, -23.79, 2.426, -13.43 ] + }, + { + "time": 2.4667, + "value": -9.18, + "curve": [ 2.498, -5.91, 2.604, 2.53 ] + }, + { + "time": 2.6667, + "value": 2.52, + "curve": [ 2.738, 2.24, 2.85, -8.76 ] + }, + { + "time": 2.9333, + "value": -8.67, + "curve": [ 3.036, -8.55, 3.09, -7.09 ] + }, + { "time": 3.1667, "value": -6.75 } + ], + "scale": [ + { + "time": 1.3333, + "curve": [ 1.392, 1, 1.526, 1, 1.392, 1, 1.508, 1.043 ] + }, + { + "time": 1.5667, + "x": 0.992, + "y": 1.043, + "curve": [ 1.598, 0.985, 1.676, 0.955, 1.584, 1.043, 1.672, 1.04 ] + }, + { + "time": 1.7333, + "x": 0.954, + "y": 1.029, + "curve": [ 1.843, 0.954, 1.933, 1, 1.825, 1.013, 1.933, 1 ] + }, + { "time": 2 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "time": 0.9, + "value": 39.24, + "curve": [ 0.968, 39.93, 1.267, 85.31 ] + }, + { + "time": 1.4667, + "value": 112.27, + "curve": [ 1.555, 124.24, 1.576, 126.44 ] + }, + { + "time": 1.6333, + "value": 126.44, + "curve": [ 1.782, 126.44, 1.992, 94.55 ] + }, + { + "time": 2.1, + "value": 79.96, + "curve": [ 2.216, 64.26, 2.407, 34.36 ] + }, + { + "time": 2.5667, + "value": 33.38, + "curve": [ 2.815, 31.87, 3.1, 39.2 ] + }, + { "time": 3.1667, "value": 39.2 } + ] + }, + "back-foot-tip": { + "rotate": [ + { + "time": 1.0667, + "value": 56.07, + "curve": [ 1.138, 59.21, 1.192, 59.65 ] + }, + { + "time": 1.2333, + "value": 59.46, + "curve": [ 1.295, 59.17, 1.45, 22.54 ] + }, + { "time": 1.4667, "value": -0.84 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "time": 1.0667, + "value": 118.03, + "curve": [ 1.075, 93.64, 1.358, -34.03 ] + }, + { + "time": 1.6667, + "value": -33.94, + "curve": [ 1.808, -33.89, 1.879, -25 ] + }, + { + "time": 1.9667, + "value": -25.19, + "curve": [ 2.09, -25.46, 2.312, -34.58 ] + }, + { + "time": 2.3667, + "value": -38.36, + "curve": [ 2.465, -45.18, 2.557, -60.1 ] + }, + { + "time": 2.8333, + "value": -61.1, + "curve": [ 2.843, -61.06, 3.16, -60.87 ] + }, + { "time": 3.1667, "value": -60.87 } + ] + }, + "front-bracer": { + "rotate": [ + { + "time": 1.0667, + "value": 0.66, + "curve": [ 1.108, 0.66, 1.221, 44.95 ] + }, + { + "time": 1.2333, + "value": 49.25, + "curve": [ 1.263, 59.42, 1.342, 68.06 ] + }, + { + "time": 1.3667, + "value": 68.34, + "curve": [ 1.409, 68.8, 1.476, 4.9 ] + }, + { + "time": 1.5, + "value": -2.05, + "curve": [ 1.529, -10.3, 1.695, -15.95 ] + }, + { + "time": 1.7333, + "value": -17.38, + "curve": [ 1.807, -20.1, 1.878, -21.19 ] + }, + { + "time": 1.9333, + "value": -21.08, + "curve": [ 2.073, -20.8, 2.146, -7.63 ] + }, + { + "time": 2.1667, + "value": -3.64, + "curve": [ 2.186, 0.12, 2.275, 15.28 ] + }, + { + "time": 2.3333, + "value": 21.78, + "curve": [ 2.392, 28.31, 2.575, 37.66 ] + }, + { + "time": 2.7, + "value": 39.43, + "curve": [ 2.947, 42.93, 3.02, 42.46 ] + }, + { "time": 3.1667, "value": 42.46 } + ] + }, + "front-thigh": { + "translate": [ + { "time": 1.1, "x": -6.41, "y": 18.23, "curve": "stepped" }, + { "time": 1.1333, "x": -6.41, "y": 18.23 }, + { "time": 1.2, "x": 1.61, "y": 3.66 }, + { "time": 1.2333, "x": 4.5, "y": -3.15 }, + { "time": 1.3667, "x": -3.79, "y": 2.94 }, + { "time": 1.4, "x": -8.37, "y": 8.72 }, + { "time": 1.4333, "x": -11.26, "y": 16.99 }, + { "time": 1.4667, "x": -9.89, "y": 24.73, "curve": "stepped" }, + { "time": 1.8667, "x": -9.89, "y": 24.73 }, + { "time": 2.1 } + ] + }, + "front-foot-tip": { + "rotate": [ + { "time": 1.0667, "value": 42.55, "curve": "stepped" }, + { "time": 1.1333, "value": 42.55 }, + { "time": 1.2333, "value": 17.71 }, + { "time": 1.3667, "value": 3.63 }, + { "time": 1.4333 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "time": 1.0667, + "value": 108.71, + "curve": [ 1.082, 108.29, 1.437, 50.73 ] + }, + { + "time": 1.5667, + "value": 24.87, + "curve": [ 1.62, 14.2, 1.66, -11.74 ] + }, + { + "time": 1.7333, + "value": -11.74, + "curve": [ 1.961, -11.73, 2.172, 1.66 ] + }, + { + "time": 2.2667, + "value": 7.88, + "curve": [ 2.331, 12.13, 2.439, 18.65 ] + }, + { + "time": 2.5333, + "value": 18.72, + "curve": [ 2.788, 18.91, 3.145, -0.3 ] + }, + { "time": 3.1667 } + ] + }, + "front-fist": { + "rotate": [ + { + "time": 1.1, + "value": 6.32, + "curve": [ 1.11, 3.31, 1.153, -5.07 ] + }, + { + "time": 1.2333, + "value": -5.13, + "curve": [ 1.311, -5.19, 1.364, 34.65 ] + }, + { + "time": 1.4667, + "value": 34.53, + "curve": [ 1.574, 34.41, 1.547, -55.78 ] + }, + { + "time": 1.8667, + "value": -54.7, + "curve": [ 1.947, -54.7, 2.03, -53.94 ] + }, + { + "time": 2.1333, + "value": -42.44, + "curve": [ 2.215, -33.42, 2.358, -4.43 ] + }, + { + "time": 2.4, + "value": 0.03, + "curve": [ 2.444, 4.66, 2.536, 8.2 ] + }, + { + "time": 2.6333, + "value": 8.2, + "curve": [ 2.733, 8.19, 2.804, -0.67 ] + }, + { + "time": 2.9, + "value": -0.82, + "curve": [ 3.127, -1.16, 3.093, 0 ] + }, + { "time": 3.1667 } + ] + }, + "gun": { + "rotate": [ + { + "time": 1.2667, + "curve": [ 1.35, 0, 1.549, 7.49 ] + }, + { + "time": 1.6, + "value": 9.5, + "curve": [ 1.663, 12.02, 1.846, 19.58 ] + }, + { + "time": 1.9333, + "value": 19.43, + "curve": [ 1.985, 19.4, 2.057, 2.98 ] + }, + { + "time": 2.2, + "value": 2.95, + "curve": [ 2.304, 3.55, 2.458, 10.8 ] + }, + { + "time": 2.5, + "value": 10.8, + "curve": [ 2.642, 10.8, 2.873, -2.54 ] + }, + { + "time": 2.9333, + "value": -2.55, + "curve": [ 3.09, -2.57, 3.08, 0 ] + }, + { "time": 3.1667 } + ] + }, + "hair2": { + "rotate": [ + { + "time": 1.0667, + "value": 26.19, + "curve": [ 1.158, 26.19, 1.368, 26 ] + }, + { + "time": 1.4333, + "value": 24.43, + "curve": [ 1.534, 22.03, 2, -29.14 ] + }, + { + "time": 2.2, + "value": -29.14, + "curve": [ 2.292, -29.14, 2.475, 6.71 ] + }, + { + "time": 2.5667, + "value": 6.71, + "curve": [ 2.675, 6.71, 2.814, -5.06 ] + }, + { + "time": 2.9, + "value": -5.06, + "curve": [ 2.973, -5.06, 3.123, 0 ] + }, + { "time": 3.1667 } + ] + }, + "hair4": { + "rotate": [ + { + "time": 1.0667, + "value": 5.21, + "curve": [ 1.108, 5.21, 1.192, 26.19 ] + }, + { + "time": 1.2333, + "value": 26.19, + "curve": [ 1.317, 26.19, 1.483, 10.63 ] + }, + { + "time": 1.5667, + "value": 10.63, + "curve": [ 1.627, 10.63, 1.642, 17.91 ] + }, + { + "time": 1.7, + "value": 17.94, + "curve": [ 1.761, 17.97, 1.774, 8.22 ] + }, + { + "time": 1.8, + "value": 3.33, + "curve": [ 1.839, -4.21, 1.95, -22.67 ] + }, + { + "time": 2, + "value": -22.67, + "curve": [ 2.025, -22.67, 2.123, -21.86 ] + }, + { + "time": 2.1667, + "value": -18.71, + "curve": [ 2.228, -14.31, 2.294, -0.3 ] + }, + { + "time": 2.3667, + "value": 6.36, + "curve": [ 2.433, 12.45, 2.494, 19.21 ] + }, + { + "time": 2.6, + "value": 19.21, + "curve": [ 2.729, 19.21, 2.854, 6.75 ] + }, + { + "time": 2.9333, + "value": 4.62, + "curve": [ 3.09, 0.45, 3.062, 0 ] + }, + { "time": 3.1667 } + ] + }, + "hair3": { + "rotate": [ + { + "time": 1.4333, + "curve": [ 1.45, 0, 1.452, 11.29 ] + }, + { + "time": 1.5, + "value": 11.21, + "curve": [ 1.596, 11.06, 1.573, -14.17 ] + }, + { + "time": 1.7333, + "value": -20.4, + "curve": [ 1.851, -24.98, 1.943, -28.45 ] + }, + { + "time": 2.2, + "value": -28.75, + "curve": [ 2.317, -28.75, 2.55, 7.04 ] + }, + { + "time": 2.6667, + "value": 7.04, + "curve": [ 2.792, 7.04, 2.885, -5.19 ] + }, + { + "time": 2.9667, + "value": -5.19, + "curve": [ 3.037, -5.19, 3.096, 0 ] + }, + { "time": 3.1667 } + ] + }, + "hair1": { + "rotate": [ + { + "time": 1.2333, + "curve": [ 1.283, 0, 1.349, 3.99 ] + }, + { + "time": 1.4333, + "value": 6.58, + "curve": [ 1.497, 8.54, 1.683, 9.35 ] + }, + { + "time": 1.7667, + "value": 9.35, + "curve": [ 1.825, 9.35, 1.945, -8.71 ] + }, + { + "time": 2, + "value": -11.15, + "curve": [ 2.058, -13.71, 2.2, -14.97 ] + }, + { + "time": 2.2667, + "value": -14.97, + "curve": [ 2.367, -14.97, 2.567, 18.77 ] + }, + { + "time": 2.6667, + "value": 18.77, + "curve": [ 2.733, 18.77, 2.817, 8.29 ] + }, + { + "time": 2.8667, + "value": 6.51, + "curve": [ 2.988, 2.17, 3.058, 0 ] + }, + { "time": 3.1667 } + ] + }, + "flare1": { + "rotate": [ + { "time": 1.1, "value": 8.2 } + ], + "translate": [ + { "time": 1.1, "x": -19.97, "y": 149.68 }, + { "time": 1.2, "x": 3.85, "y": 152.43 }, + { "time": 1.2333, "x": -15.42, "y": 152.29 } + ], + "scale": [ + { + "time": 1.1, + "x": 0.805, + "y": 0.805, + "curve": [ 1.119, 0.763, 1.16, 1.162, 1.117, 0.805, 1.15, 0.605 ] + }, + { + "time": 1.1667, + "x": 1.279, + "y": 0.605, + "curve": [ 1.177, 1.47, 1.192, 2.151, 1.175, 0.605, 1.192, 0.911 ] + }, + { + "time": 1.2, + "x": 2.151, + "y": 0.911, + "curve": [ 1.208, 2.151, 1.231, 1.668, 1.208, 0.911, 1.227, 0.844 ] + }, + { + "time": 1.2333, + "x": 1.608, + "y": 0.805, + "curve": [ 1.249, 1.205, 1.283, 0.547, 1.254, 0.685, 1.283, 0.416 ] + }, + { "time": 1.3, "x": 0.547, "y": 0.416 } + ], + "shear": [ + { "time": 1.1, "y": 4.63 }, + { "time": 1.2333, "x": -5.74, "y": 4.63 } + ] + }, + "flare2": { + "rotate": [ + { "time": 1.1, "value": 12.29 } + ], + "translate": [ + { "time": 1.1, "x": -8.63, "y": 132.96 }, + { "time": 1.2, "x": 4.35, "y": 132.93 } + ], + "scale": [ + { "time": 1.1, "x": 0.864, "y": 0.864 }, + { "time": 1.1667, "x": 0.945, "y": 0.945 }, + { "time": 1.2, "x": 1.511, "y": 1.081 } + ], + "shear": [ + { "time": 1.1, "y": 24.03 } + ] + }, + "flare3": { + "rotate": [ + { "time": 1.1667, "value": 2.88 } + ], + "translate": [ + { "time": 1.1667, "x": 3.24, "y": 114.81 } + ], + "scale": [ + { "time": 1.1667, "x": 0.668, "y": 0.668 } + ], + "shear": [ + { "time": 1.1667, "y": 38.59 } + ] + }, + "flare4": { + "rotate": [ + { "time": 1.1667, "value": -8.64 } + ], + "translate": [ + { "time": 1.1667, "x": -3.82, "y": 194.06 }, + { "time": 1.2667, "x": -1.82, "y": 198.47, "curve": "stepped" }, + { "time": 1.3, "x": -1.94, "y": 187.81 } + ], + "scale": [ + { "time": 1.1667, "x": 0.545, "y": 0.545 }, + { "time": 1.2667, "x": 0.757, "y": 0.757 } + ], + "shear": [ + { "time": 1.1667, "x": 7.42, "y": -22.04 } + ] + }, + "flare5": { + "translate": [ + { "time": 1.2, "x": -11.17, "y": 176.42 }, + { "time": 1.2333, "x": -8.56, "y": 179.04, "curve": "stepped" }, + { "time": 1.3, "x": -14.57, "y": 168.69 } + ], + "scale": [ + { "time": 1.2333, "x": 1.146 }, + { "time": 1.3, "x": 0.703, "y": 0.61 } + ], + "shear": [ + { "time": 1.2, "x": 6.9 } + ] + }, + "flare6": { + "rotate": [ + { "time": 1.2333, "value": -5.36 }, + { "time": 1.2667, "value": -0.54 } + ], + "translate": [ + { "time": 1.2333, "x": 14.52, "y": 204.67 }, + { "time": 1.2667, "x": 19.16, "y": 212.9, "curve": "stepped" }, + { "time": 1.3, "x": 9.23, "y": 202.85 } + ], + "scale": [ + { "time": 1.2333, "x": 0.777, "y": 0.49 }, + { "time": 1.2667, "x": 0.777, "y": 0.657 }, + { "time": 1.3, "x": 0.475, "y": 0.401 } + ] + }, + "flare7": { + "rotate": [ + { "time": 1.1, "value": 5.98 }, + { "time": 1.1333, "value": 32.82 } + ], + "translate": [ + { "time": 1.1, "x": -6.34, "y": 112.98 }, + { "time": 1.1333, "x": 2.66, "y": 111.6 } + ], + "scale": [ + { "time": 1.1, "x": 0.588, "y": 0.588 } + ], + "shear": [ + { "time": 1.1333, "x": -19.93 } + ] + }, + "flare8": { + "rotate": [ + { "time": 1.2333, "value": -6.85 } + ], + "translate": [ + { "time": 1.1667, "x": 66.67, "y": 125.52, "curve": "stepped" }, + { "time": 1.2, "x": 58.24, "y": 113.53, "curve": "stepped" }, + { "time": 1.2333, "x": 40.15, "y": 114.69 } + ], + "scale": [ + { "time": 1.1667, "x": 1.313, "y": 1.203 }, + { "time": 1.2333, "x": 1.038, "y": 0.95 } + ], + "shear": [ + { "time": 1.2, "y": -13.01 } + ] + }, + "flare9": { + "rotate": [ + { "time": 1.1667, "value": 2.9 } + ], + "translate": [ + { "time": 1.1667, "x": 28.45, "y": 151.35, "curve": "stepped" }, + { "time": 1.2, "x": 48.8, "y": 191.09, "curve": "stepped" }, + { "time": 1.2333, "x": 52, "y": 182.52, "curve": "stepped" }, + { "time": 1.2667, "x": 77.01, "y": 195.96 } + ], + "scale": [ + { "time": 1.1667, "x": 0.871, "y": 1.073 }, + { "time": 1.2, "x": 0.927, "y": 0.944 }, + { "time": 1.2333, "x": 1.165, "y": 1.336 } + ], + "shear": [ + { "time": 1.1667, "x": 7.95, "y": 25.48 } + ] + }, + "flare10": { + "rotate": [ + { "time": 1.1667, "value": 2.18 } + ], + "translate": [ + { "time": 1.1667, "x": 55.64, "y": 137.64, "curve": "stepped" }, + { "time": 1.2, "x": 90.49, "y": 151.07, "curve": "stepped" }, + { "time": 1.2333, "x": 114.06, "y": 153.05, "curve": "stepped" }, + { "time": 1.2667, "x": 90.44, "y": 164.61 } + ], + "scale": [ + { "time": 1.1667, "x": 2.657, "y": 0.891 }, + { "time": 1.2, "x": 3.314, "y": 1.425 }, + { "time": 1.2333, "x": 2.871, "y": 0.924 }, + { "time": 1.2667, "x": 2.317, "y": 0.775 } + ], + "shear": [ + { "time": 1.1667, "x": -1.35 } + ] + }, + "torso2": { + "rotate": [ + { + "time": 1, + "curve": [ 1.117, 0, 1.255, 24.94 ] + }, + { + "time": 1.4, + "value": 24.94, + "curve": [ 1.477, 24.94, 1.59, -17.62 ] + }, + { + "time": 1.6333, + "value": -19.48, + "curve": [ 1.717, -23.1, 1.784, -26.12 ] + }, + { + "time": 1.9333, + "value": -26.14, + "curve": [ 2.067, -26.15, 2.158, 4.3 ] + }, + { + "time": 2.3, + "value": 4.22, + "curve": [ 2.45, 4.13, 2.579, -1.76 ] + }, + { + "time": 2.7333, + "value": -1.8, + "curve": [ 2.816, -1.82, 2.857, -2.94 ] + }, + { + "time": 2.9333, + "value": -2.99, + "curve": [ 3.056, -3.08, 3.09, 0 ] + }, + { "time": 3.1667 } + ] + }, + "torso3": { + "rotate": [ + { + "time": 1.3, + "curve": [ 1.352, 0, 1.408, 6.47 ] + }, + { + "time": 1.4667, + "value": 6.43, + "curve": [ 1.55, 6.39, 1.723, -5.05 ] + }, + { + "time": 1.7333, + "value": -5.53, + "curve": [ 1.782, -7.72, 1.843, -16.94 ] + }, + { + "time": 1.9667, + "value": -16.86, + "curve": [ 2.111, -16.78, 2.259, -3.97 ] + }, + { + "time": 2.4, + "value": -2.43, + "curve": [ 2.525, -1.12, 2.639, -0.5 ] + }, + { + "time": 2.7333, + "value": -0.49, + "curve": [ 2.931, -0.47, 2.999, -2.15 ] + }, + { "time": 3.1667, "value": -2.15 } + ] + }, + "head-control": { + "translate": [ + { + "time": 1.2333, + "curve": [ 1.25, 0, 1.474, 6.89, 1.25, 0, 1.496, 0.98 ] + }, + { + "time": 1.6667, + "x": 11.99, + "y": -6.42, + "curve": [ 1.743, 14.01, 1.86, 14.33, 1.785, -11.55, 1.86, -27.1 ] + }, + { + "time": 1.9667, + "x": 13.91, + "y": -26.88, + "curve": [ 2.074, 13.49, 2.244, 8.13, 2.074, -26.65, 2.215, -21.78 ] + }, + { + "time": 2.3, + "x": 6.07, + "y": -16.64, + "curve": [ 2.416, 1.84, 2.497, -1.41, 2.417, -9.57, 2.526, -1.72 ] + }, + { + "time": 2.5667, + "x": -3.78, + "y": -1.71, + "curve": [ 2.661, -6.98, 2.76, -8.76, 2.692, -1.68, 2.821, -15.75 ] + }, + { + "time": 2.9, + "x": -8.32, + "y": -16.7, + "curve": [ 2.962, -8.12, 3.082, -0.04, 2.958, -17.39, 3.089, 0 ] + }, + { "time": 3.1667 } + ] + }, + "front-shoulder": { + "translate": [ + { + "time": 1.3333, + "curve": [ 1.488, 0, 1.717, 0.21, 1.488, 0, 1.688, -30.29 ] + }, + { + "time": 1.9, + "x": 0.83, + "y": -30.29, + "curve": [ 2.078, 1.43, 2.274, 2.88, 2.071, -30.29, 2.289, 4.48 ] + }, + { + "time": 2.4333, + "x": 2.89, + "y": 4.59, + "curve": [ 2.604, 2.89, 2.677, -0.68, 2.57, 4.7, 2.694, -2.43 ] + }, + { + "time": 2.7667, + "x": -0.67, + "y": -2.47, + "curve": [ 2.866, -0.67, 2.986, -0.07, 2.882, -2.47, 3.036, -0.06 ] + }, + { "time": 3.1667 } + ] + } + }, + "ik": { + "rear-leg-ik": [ + { "time": 3.1667, "softness": 10, "bendPositive": false } + ] + } + }, + "run": { + "slots": { + "mouth": { + "attachment": [ + { "name": "mouth-grind" } + ] + } + }, + "bones": { + "front-thigh": { + "translate": [ + { + "x": -5.14, + "y": 11.13, + "curve": [ 0.033, -7.77, 0.112, -9.03, 0.034, 11.13, 0.108, 9.74 ] + }, + { + "time": 0.1667, + "x": -9.03, + "y": 7.99, + "curve": [ 0.23, -9.05, 0.314, -1.34, 0.236, 5.93, 0.28, 3.22 ] + }, + { + "time": 0.3333, + "x": 0.41, + "y": 3.19, + "curve": [ 0.352, 2.09, 0.449, 11.16, 0.384, 3.16, 0.449, 4.98 ] + }, + { + "time": 0.5, + "x": 11.17, + "y": 6.76, + "curve": [ 0.571, 10.79, 0.621, -1.83, 0.542, 8.21, 0.625, 11.13 ] + }, + { "time": 0.6667, "x": -5.14, "y": 11.13 } + ] + }, + "torso": { + "rotate": [ + { + "value": -37.66, + "curve": [ 0.034, -37.14, 0.107, -36.21 ] + }, + { + "time": 0.1333, + "value": -36.21, + "curve": [ 0.158, -36.21, 0.209, -38.8 ] + }, + { + "time": 0.2333, + "value": -38.79, + "curve": [ 0.259, -38.78, 0.313, -38.03 ] + }, + { + "time": 0.3333, + "value": -37.66, + "curve": [ 0.357, -37.21, 0.4, -36.21 ] + }, + { + "time": 0.4333, + "value": -36.21, + "curve": [ 0.458, -36.21, 0.539, -38.8 ] + }, + { + "time": 0.5667, + "value": -38.8, + "curve": [ 0.592, -38.8, 0.645, -38 ] + }, + { "time": 0.6667, "value": -37.66 } + ] + }, + "rear-thigh": { + "translate": [ + { + "x": -16.41, + "y": 1.55, + "curve": [ 0.013, -15.67, 0.183, -8.55, 0.03, 2.39, 0.183, 6.17 ] + }, + { + "time": 0.2333, + "x": -8.55, + "y": 6.17, + "curve": [ 0.308, -8.55, 0.492, -19.75, 0.308, 6.17, 0.492, 0.61 ] + }, + { + "time": 0.5667, + "x": -19.75, + "y": 0.61, + "curve": [ 0.592, -19.75, 0.641, -18.06, 0.592, 0.61, 0.632, 0.78 ] + }, + { "time": 0.6667, "x": -16.41, "y": 1.55 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -39.03, + "curve": [ 0.051, -0.1, 0.145, 88.36 ] + }, + { + "time": 0.2333, + "value": 88.36, + "curve": [ 0.28, 88.76, 0.324, 59.52 ] + }, + { + "time": 0.3333, + "value": 51.13, + "curve": [ 0.358, 30.2, 0.445, -74.91 ] + }, + { + "time": 0.5667, + "value": -75.82, + "curve": [ 0.599, -76.06, 0.642, -55.72 ] + }, + { "time": 0.6667, "value": -39.03 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 20.54, + "curve": [ 0.052, 11.42, 0.089, 0.13 ] + }, + { + "time": 0.1333, + "value": 0.15, + "curve": [ 0.186, 0.17, 0.221, 26.29 ] + }, + { + "time": 0.2333, + "value": 32.37, + "curve": [ 0.247, 39.19, 0.286, 61.45 ] + }, + { + "time": 0.3333, + "value": 61.58, + "curve": [ 0.371, 61.69, 0.42, 55.79 ] + }, + { "time": 0.4667, "value": 49.68 }, + { "time": 0.6667, "value": 20.54 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -36.16, + "curve": [ 0.014, -38.8, 0.036, -43.27 ] + }, + { + "time": 0.0667, + "value": -43.37, + "curve": [ 0.102, -43.49, 0.182, -28.46 ] + }, + { + "time": 0.2, + "value": -23.04, + "curve": [ 0.23, -13.87, 0.264, 3.86 ] + }, + { + "time": 0.3333, + "value": 3.7, + "curve": [ 0.38, 3.64, 0.535, -16.22 ] + }, + { "time": 0.5667, "value": -21.29 }, + { "time": 0.6667, "value": -36.16 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 40.5, + "curve": [ 0.028, 23.74, 0.128, -79.86 ] + }, + { + "time": 0.2333, + "value": -79.87, + "curve": [ 0.38, -79.88, 0.403, 63.25 ] + }, + { + "time": 0.5667, + "value": 64.13, + "curve": [ 0.607, 64.35, 0.644, 53.1 ] + }, + { "time": 0.6667, "value": 40.5 } + ], + "translate": [ + { + "x": -3.79, + "y": -0.77, + "curve": [ 0.044, -4.58, 0.169, -5.48, 0.044, 0.93, 0.169, 2.85 ] + }, + { + "time": 0.2333, + "x": -5.48, + "y": 2.85, + "curve": [ 0.346, -5.48, 0.475, -2.68, 0.346, 2.85, 0.475, -3.13 ] + }, + { + "time": 0.5667, + "x": -2.68, + "y": -3.13, + "curve": [ 0.611, -2.68, 0.642, -3.34, 0.611, -3.13, 0.642, -1.73 ] + }, + { "time": 0.6667, "x": -3.79, "y": -0.77 } + ] + }, + "rear-bracer": { + "rotate": [ + { "value": 28.28 }, + { + "time": 0.2333, + "value": -11.12, + "curve": [ 0.252, -14.12, 0.297, -19.37 ] + }, + { + "time": 0.3333, + "value": -19.38, + "curve": [ 0.435, -19.41, 0.522, 38.96 ] + }, + { + "time": 0.5667, + "value": 38.87, + "curve": [ 0.619, 38.76, 0.644, 32.01 ] + }, + { "time": 0.6667, "value": 28.28 } + ] + }, + "neck": { + "rotate": [ + { + "value": 11.88, + "curve": [ 0.024, 11.4, 0.075, 9.74 ] + }, + { + "time": 0.1, + "value": 9.74, + "curve": [ 0.125, 9.74, 0.208, 13.36 ] + }, + { + "time": 0.2333, + "value": 13.36, + "curve": [ 0.258, 13.36, 0.321, 12.2 ] + }, + { + "time": 0.3333, + "value": 11.88, + "curve": [ 0.365, 11.06, 0.408, 9.72 ] + }, + { + "time": 0.4333, + "value": 9.72, + "curve": [ 0.458, 9.72, 0.542, 13.36 ] + }, + { + "time": 0.5667, + "value": 13.36, + "curve": [ 0.592, 13.36, 0.636, 12.48 ] + }, + { "time": 0.6667, "value": 11.88 } + ] + }, + "head": { + "rotate": [ + { + "value": 13.14, + "curve": [ 0.02, 11.99, 0.039, 8.94 ] + }, + { + "time": 0.0667, + "value": 8.93, + "curve": [ 0.122, 8.9, 0.232, 15.8 ] + }, + { + "time": 0.2667, + "value": 15.81, + "curve": [ 0.325, 15.82, 0.357, 8.95 ] + }, + { + "time": 0.4, + "value": 8.93, + "curve": [ 0.444, 8.91, 0.568, 15.8 ] + }, + { + "time": 0.6, + "value": 15.77, + "curve": [ 0.632, 15.74, 0.649, 14.05 ] + }, + { "time": 0.6667, "value": 13.14 } + ], + "scale": [ + { + "curve": [ 0.014, 0.996, 0.068, 0.991, 0.027, 1.005, 0.083, 1.012 ] + }, + { + "time": 0.1, + "x": 0.991, + "y": 1.012, + "curve": [ 0.128, 0.991, 0.205, 1.018, 0.128, 1.012, 0.197, 0.988 ] + }, + { + "time": 0.2333, + "x": 1.018, + "y": 0.988, + "curve": [ 0.272, 1.018, 0.305, 1.008, 0.262, 0.988, 0.311, 0.995 ] + }, + { + "time": 0.3333, + "curve": [ 0.351, 0.995, 0.417, 0.987, 0.359, 1.006, 0.417, 1.013 ] + }, + { + "time": 0.4333, + "x": 0.987, + "y": 1.013, + "curve": [ 0.467, 0.987, 0.533, 1.02, 0.467, 1.013, 0.533, 0.989 ] + }, + { + "time": 0.5667, + "x": 1.02, + "y": 0.989, + "curve": [ 0.592, 1.02, 0.652, 1.004, 0.592, 0.989, 0.644, 0.996 ] + }, + { "time": 0.6667 } + ] + }, + "gun": { + "rotate": [ + { + "value": 12.36, + "curve": [ 0.022, 16.28, 0.087, 20.25 ] + }, + { + "time": 0.1333, + "value": 20.19, + "curve": [ 0.168, 20.32, 0.254, -8.82 ] + }, + { + "time": 0.2667, + "value": -11.88, + "curve": [ 0.291, -17.91, 0.344, -24.11 ] + }, + { + "time": 0.4, + "value": -23.88, + "curve": [ 0.448, -23.69, 0.533, -15.47 ] + }, + { "time": 0.5667, "value": -8.69 }, + { "time": 0.6667, "value": 12.36 } + ] + }, + "hip": { + "rotate": [ + { "value": -8.24 } + ], + "translate": [ + { + "x": -3.6, + "y": -34.1, + "curve": [ 0.042, -3.84, 0.118, 7.62, 0.042, -33.74, 0.112, 20.55 ] + }, + { + "time": 0.1667, + "x": 7.61, + "y": 20.36, + "curve": [ 0.194, 7.6, 0.21, 5.06, 0.204, 20.65, 0.217, -8.69 ] + }, + { + "time": 0.2333, + "x": 1.68, + "y": -18.48, + "curve": [ 0.279, -4.99, 0.297, -5.64, 0.254, -31.08, 0.292, -34.55 ] + }, + { + "time": 0.3333, + "x": -5.76, + "y": -35, + "curve": [ 0.379, -5.9, 0.451, 6.8, 0.384, -35.56, 0.428, 17.6 ] + }, + { + "time": 0.5, + "x": 6.61, + "y": 17.01, + "curve": [ 0.536, 6.47, 0.545, 3.56, 0.533, 16.75, 0.548, -8.71 ] + }, + { + "time": 0.5667, + "x": 0.35, + "y": -18.81, + "curve": [ 0.597, -4.07, 0.642, -3.45, 0.584, -28.58, 0.642, -34.32 ] + }, + { "time": 0.6667, "x": -3.6, "y": -34.1 } + ] + }, + "front-foot-target": { + "rotate": [ + { + "value": -62.54, + "curve": [ 0.015, -74.19, 0.056, -103.19 ] + }, + { + "time": 0.0667, + "value": -111.08, + "curve": [ 0.092, -129.44, 0.189, -146.55 ] + }, + { + "time": 0.2333, + "value": -146.32, + "curve": [ 0.285, -146.06, 0.32, -125.1 ] + }, + { "time": 0.3333, "value": -117.24 }, + { + "time": 0.5, + "value": -35.07, + "curve": [ 0.522, -28.64, 0.546, -24.84 ] + }, + { + "time": 0.5667, + "value": -24.9, + "curve": [ 0.595, -25, 0.623, -40.82 ] + }, + { "time": 0.6667, "value": -62.54 } + ], + "translate": [ + { "x": 16.34, "y": 0.18 }, + { + "time": 0.0667, + "x": -101.43, + "y": 8.04, + "curve": [ 0.085, -131.35, 0.129, -207.69, 0.08, 14.9, 0.124, 113.28 ] + }, + { + "time": 0.1667, + "x": -207.92, + "y": 145.81, + "curve": [ 0.196, -208.13, 0.21, -202.91, 0.186, 160.26, 0.206, 163.48 ] + }, + { + "time": 0.2333, + "x": -189.94, + "y": 163.85, + "curve": [ 0.27, -169.94, 0.31, -126.19, 0.269, 164.35, 0.316, 85.97 ] + }, + { + "time": 0.3333, + "x": -90.56, + "y": 78.57, + "curve": [ 0.355, -57.99, 0.376, -29.14, 0.35, 71.55, 0.376, 66.4 ] + }, + { + "time": 0.4, + "x": 2.87, + "y": 66.38, + "curve": [ 0.412, 19.24, 0.469, 90.73, 0.429, 66.37, 0.469, 70.66 ] + }, + { + "time": 0.5, + "x": 117.18, + "y": 70.46, + "curve": [ 0.522, 136.24, 0.542, 151.33, 0.539, 70.2, 0.555, 38.25 ] + }, + { + "time": 0.5667, + "x": 151.49, + "y": 25.29, + "curve": [ 0.578, 146.76, 0.586, 133.13, 0.572, 19.7, 0.582, 12.23 ] + }, + { "time": 0.6, "x": 115.02, "y": 0.1 }, + { "time": 0.6667, "x": 16.34, "y": 0.18 } + ] + }, + "front-leg-target": { + "translate": [ + { "x": -13.95, "y": -30.34 } + ] + }, + "rear-foot-target": { + "rotate": [ + { "value": 18.55 }, + { + "time": 0.2333, + "value": 167.84, + "curve": [ 0.246, 153.66, 0.256, 129.74 ] + }, + { + "time": 0.2667, + "value": 124.32, + "curve": [ 0.296, 124.43, 0.313, 129.93 ] + }, + { + "time": 0.3667, + "value": 129.87, + "curve": [ 0.421, 128.32, 0.519, 0.98 ] + }, + { + "time": 0.5667, + "curve": [ 0.6, 0.27, 0.642, 4.73 ] + }, + { "time": 0.6667, "value": 18.55 } + ], + "translate": [ + { + "x": -176.39, + "y": 134.12, + "curve": [ 0.018, -142.26, 0.054, -94.41, 0.01, 120.96, 0.044, 84.08 ] + }, + { + "time": 0.0667, + "x": -73.56, + "y": 76.68, + "curve": [ 0.086, -42.82, 0.194, 101.2, 0.098, 66.73, 0.198, 60.88 ] + }, + { "time": 0.2333, "x": 98.32, "y": 32.17 }, + { "time": 0.2667, "x": 49.13, "y": -0.63 }, + { + "time": 0.4, + "x": -147.9, + "y": 0.32, + "curve": [ 0.414, -168.78, 0.478, -284.76, 0.43, 30.09, 0.478, 129.14 ] + }, + { + "time": 0.5, + "x": -283.37, + "y": 167.12, + "curve": [ 0.526, -285.66, 0.548, -280.54, 0.516, 194.84, 0.55, 216.53 ] + }, + { + "time": 0.5667, + "x": -266.98, + "y": 216.12, + "curve": [ 0.581, -256.27, 0.643, -206.54, 0.61, 214.82, 0.65, 145.33 ] + }, + { "time": 0.6667, "x": -176.39, "y": 134.12 } + ] + }, + "rear-leg-target": { + "translate": [ + { "x": 85, "y": -33.59 } + ] + }, + "back-foot-tip": { + "rotate": [ + { + "value": -147.04, + "curve": [ 0.033, -113.4, 0.161, 44.34 ] + }, + { + "time": 0.2333, + "value": 43.48, + "curve": [ 0.24, 43.41, 0.282, 35.72 ] + }, + { + "time": 0.3, + "value": 0.29, + "curve": [ 0.347, 0.28, 0.396, 4.27 ] + }, + { + "time": 0.4, + "curve": [ 0.424, -23.8, 0.525, -181.39 ] + }, + { + "time": 0.5667, + "value": -181.39, + "curve": [ 0.592, -181.39, 0.642, -169.09 ] + }, + { "time": 0.6667, "value": -147.04 } + ] + }, + "front-foot-tip": { + "rotate": [ + { + "value": -0.25, + "curve": [ 0.008, -0.25, 0.056, 1.73 ] + }, + { + "time": 0.0667, + "value": -7.68, + "curve": [ 0.075, -43.13, 0.15, -130.44 ] + }, + { + "time": 0.2, + "value": -130.08, + "curve": [ 0.239, -129.79, 0.272, -126.8 ] + }, + { + "time": 0.3, + "value": -116.24, + "curve": [ 0.333, -103.91, 0.348, -86.1 ] + }, + { + "time": 0.3667, + "value": -71.08, + "curve": [ 0.386, -55.25, 0.415, -32.44 ] + }, + { + "time": 0.4333, + "value": -21.63, + "curve": [ 0.47, -0.01, 0.542, 33.42 ] + }, + { + "time": 0.5667, + "value": 33.2, + "curve": [ 0.622, 32.7, 0.569, 0.64 ] + }, + { "time": 0.6667, "value": -0.25 } + ] + }, + "hair1": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.087, -6.81, 0.143, -5.75 ] + }, + { + "time": 0.1667, + "value": -4.3, + "curve": [ 0.183, -3.28, 0.209, 2.79 ] + }, + { + "time": 0.2333, + "value": 2.78, + "curve": [ 0.262, 2.77, 0.305, -6.63 ] + }, + { + "time": 0.3333, + "value": -6.64, + "curve": [ 0.419, -6.68, 0.49, -4.84 ] + }, + { + "time": 0.5, + "value": -4.38, + "curve": [ 0.518, -3.56, 0.574, 2.32 ] + }, + { + "time": 0.6, + "value": 2.33, + "curve": [ 0.643, 2.35, 0.633, -6.81 ] + }, + { "time": 0.6667, "value": -6.81 } + ] + }, + "hair2": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.014, -3.17, 0.109, 43.93 ] + }, + { + "time": 0.1333, + "value": 43.95, + "curve": [ 0.177, 43.97, 0.192, -13.76 ] + }, + { + "time": 0.2667, + "value": -13.83, + "curve": [ 0.302, -13.72, 0.322, -8.86 ] + }, + { + "time": 0.3333, + "value": -6.6, + "curve": [ 0.349, -3.5, 0.436, 41.1 ] + }, + { + "time": 0.4667, + "value": 41.05, + "curve": [ 0.51, 40.99, 0.549, -14.06 ] + }, + { + "time": 0.6, + "value": -14.18, + "curve": [ 0.63, -14.26, 0.656, -9.04 ] + }, + { "time": 0.6667, "value": -6.81 } + ] + }, + "hair3": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.079, -6.83, 0.108, 0.3 ] + }, + { + "time": 0.1333, + "value": 1.96, + "curve": [ 0.177, 4.89, 0.208, 6.28 ] + }, + { + "time": 0.2333, + "value": 6.29, + "curve": [ 0.313, 6.31, 0.383, 3.49 ] + }, + { + "time": 0.4, + "value": 2.58, + "curve": [ 0.442, 0.28, 0.523, -6.81 ] + }, + { "time": 0.6, "value": -6.81 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -6.81, + "curve": [ 0.011, -4.06, 0.108, 24.92 ] + }, + { + "time": 0.1333, + "value": 24.92, + "curve": [ 0.158, 24.92, 0.208, -10.62 ] + }, + { + "time": 0.2333, + "value": -10.62, + "curve": [ 0.254, -10.62, 0.312, -9.73 ] + }, + { + "time": 0.3333, + "value": -6.4, + "curve": [ 0.356, -2.95, 0.438, 24.93 ] + }, + { + "time": 0.4667, + "value": 24.93, + "curve": [ 0.492, 24.93, 0.575, -9.78 ] + }, + { + "time": 0.6, + "value": -9.78, + "curve": [ 0.617, -9.78, 0.655, -8.63 ] + }, + { "time": 0.6667, "value": -6.81 } + ] + }, + "torso2": { + "rotate": [ + { + "value": 3.5, + "curve": [ 0.07, 3.51, 0.075, 8.69 ] + }, + { + "time": 0.1, + "value": 8.69, + "curve": [ 0.139, 8.69, 0.214, 6.9 ] + }, + { + "time": 0.2333, + "value": 6.33, + "curve": [ 0.266, 5.34, 0.285, 3.48 ] + }, + { + "time": 0.3333, + "value": 3.48, + "curve": [ 0.398, 3.48, 0.408, 8.68 ] + }, + { + "time": 0.4333, + "value": 8.68, + "curve": [ 0.458, 8.68, 0.551, 6.8 ] + }, + { + "time": 0.5667, + "value": 6.26, + "curve": [ 0.598, 5.17, 0.642, 3.49 ] + }, + { "time": 0.6667, "value": 3.5 } + ] + }, + "torso3": { + "rotate": [ + { + "value": 4.52, + "curve": [ 0.067, 4.54, 0.075, -7.27 ] + }, + { + "time": 0.1, + "value": -7.27, + "curve": [ 0.125, -7.27, 0.227, 0.84 ] + }, + { + "time": 0.2333, + "value": 1.24, + "curve": [ 0.254, 2.5, 0.301, 4.51 ] + }, + { + "time": 0.3333, + "value": 4.52, + "curve": [ 0.386, 4.54, 0.408, -7.35 ] + }, + { + "time": 0.4333, + "value": -7.35, + "curve": [ 0.458, -7.35, 0.549, -0.14 ] + }, + { + "time": 0.5667, + "value": 0.95, + "curve": [ 0.586, 2.18, 0.632, 4.54 ] + }, + { "time": 0.6667, "value": 4.52 } + ] + }, + "aim-constraint-target": { + "rotate": [ + { "value": 30.57 } + ] + }, + "rear-foot": { + "rotate": [ + { "value": -6.5 } + ] + }, + "front-foot": { + "rotate": [ + { "value": 4.5 } + ] + }, + "head-control": { + "translate": [ + { + "y": -9.94, + "curve": [ 0.058, 0, 0.175, -15.32, 0.044, -4.19, 0.175, 5 ] + }, + { + "time": 0.2333, + "x": -15.32, + "y": 5, + "curve": [ 0.317, -15.32, 0.429, -9.74, 0.317, 5, 0.382, -31.71 ] + }, + { + "time": 0.4667, + "x": -7.81, + "y": -31.59, + "curve": [ 0.507, -5.76, 0.617, 0, 0.549, -31.47, 0.628, -13.33 ] + }, + { "time": 0.6667, "y": -9.94 } + ] + }, + "front-shoulder": { + "translate": [ + { + "x": -0.74, + "y": 11.22, + "curve": [ 0.061, -0.74, 0.144, 1.17, 0.061, 11.22, 0.143, -17.93 ] + }, + { + "time": 0.2333, + "x": 1.19, + "y": -17.9, + "curve": [ 0.54, 1.25, 0.558, -0.74, 0.545, -17.8, 0.558, 11.22 ] + }, + { "time": 0.6667, "x": -0.74, "y": 11.22 } + ] + }, + "back-shoulder": { + "translate": [ + { + "curve": [ 0.083, 0, 0.25, 0, 0.083, 0, 0.25, 8.93 ] + }, + { + "time": 0.3333, + "y": 8.93, + "curve": [ 0.417, 0, 0.583, 0, 0.417, 8.93, 0.583, 0 ] + }, + { "time": 0.6667 } + ] + } + }, + "ik": { + "front-leg-ik": [ + { "softness": 10, "bendPositive": false }, + { "time": 0.5667, "softness": 14.8, "bendPositive": false }, + { "time": 0.6, "softness": 48.2, "bendPositive": false }, + { "time": 0.6667, "softness": 10, "bendPositive": false } + ], + "rear-leg-ik": [ + { "bendPositive": false }, + { "time": 0.1667, "softness": 22.5, "bendPositive": false }, + { "time": 0.3, "softness": 61.4, "bendPositive": false }, + { "time": 0.6667, "bendPositive": false } + ] + }, + "events": [ + { "time": 0.2333, "name": "footstep" }, + { "time": 0.5667, "name": "footstep" } + ] + }, + "run-to-idle": { + "slots": { + "front-fist": { + "attachment": [ + { "name": "front-fist-open" } + ] + } + }, + "bones": { + "front-foot-target": { + "translate": [ + { + "x": -16.5, + "y": 3.41, + "curve": [ 0.033, -16.5, 0.1, -69.06, 0.033, 3.41, 0.1, 0 ] + }, + { "time": 0.1333, "x": -69.06 } + ] + }, + "hip": { + "translate": [ + { + "x": -28.78, + "y": -72.96, + "curve": [ 0.036, -28.63, 0.2, -10.85, 0.135, -62.35, 0.2, -23.15 ] + }, + { "time": 0.2667, "x": -11.97, "y": -23.15 } + ] + }, + "rear-foot-target": { + "translate": [ + { + "x": 33.15, + "y": 31.61, + "curve": [ 0.017, 33.15, 0.05, 24.41, 0.017, 31.61, 0.041, 20.73 ] + }, + { + "time": 0.0667, + "x": 24.41, + "y": 0.19, + "curve": [ 0.117, 24.41, 0.217, 48.87, 0.117, 0.19, 0.217, 0 ] + }, + { "time": 0.2667, "x": 48.87 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -80.61, + "curve": [ 0.067, -80.61, 0.2, -60.87 ] + }, + { "time": 0.2667, "value": -60.87 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 8.79, + "curve": [ 0.041, 8.79, 0.115, 6.3 ] + }, + { + "time": 0.1667, + "value": 6.41, + "curve": [ 0.201, 6.48, 0.241, 42.46 ] + }, + { "time": 0.2667, "value": 42.46 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 55.3, + "curve": [ 0.067, 55.3, 0.2, 39.2 ] + }, + { "time": 0.2667, "value": 39.2 } + ] + }, + "head": { + "rotate": [ + { + "curve": [ 0.05, 0, 0.083, 2.67 ] + }, + { + "time": 0.1333, + "value": 2.67, + "curve": [ 0.15, 2.67, 0.25, -6.75 ] + }, + { "time": 0.2667, "value": -6.75 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": 38.26, + "curve": [ 0.041, 38.26, 0.127, -2.19 ] + }, + { + "time": 0.1667, + "value": -3, + "curve": [ 0.209, -3.84, 0.241, 0 ] + }, + { "time": 0.2667 } + ], + "scale": [ + { + "x": 0.844, + "curve": [ 0.067, 0.844, 0.2, 1, 0.067, 1, 0.2, 1 ] + }, + { "time": 0.2667 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 57.24, + "curve": [ 0.067, 57.24, 0.2, 0 ] + }, + { "time": 0.2667 } + ] + }, + "gun": { + "rotate": [ + { + "value": 2.28, + "curve": [ 0.041, 2.28, 0.105, 15.34 ] + }, + { + "time": 0.1667, + "value": 15.32, + "curve": [ 0.205, 15.31, 0.241, 0 ] + }, + { "time": 0.2667 } + ] + }, + "torso": { + "rotate": [ + { + "value": -12.98, + "curve": [ 0.033, -12.98, 0.103, -14.81 ] + }, + { + "time": 0.1333, + "value": -16.63, + "curve": [ 0.168, -18.69, 0.233, -22.88 ] + }, + { "time": 0.2667, "value": -22.88 } + ], + "scale": [ + { + "x": 0.963, + "y": 1.074, + "curve": [ 0.067, 0.963, 0.132, 1, 0.067, 1.074, 0.132, 1 ] + }, + { "time": 0.2667 } + ] + }, + "neck": { + "rotate": [ + {}, + { "time": 0.2667, "value": 3.78 } + ] + }, + "hair3": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.1, 0.88 ] + }, + { + "time": 0.1333, + "value": 0.88, + "curve": [ 0.167, 0.88, 0.233, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair4": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.1, 15.97 ] + }, + { + "time": 0.1333, + "value": 15.97, + "curve": [ 0.167, 15.97, 0.233, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair1": { + "rotate": [ + { + "curve": [ 0.033, 0, 0.1, 10.76 ] + }, + { + "time": 0.1333, + "value": 10.76, + "curve": [ 0.167, 10.76, 0.233, 0 ] + }, + { "time": 0.2667 } + ] + }, + "hair2": { + "rotate": [ + { + "curve": [ 0.014, -2.28, 0.042, -7.84 ] + }, + { + "time": 0.0667, + "value": -7.82, + "curve": [ 0.108, -7.79, 0.166, 6.57 ] + }, + { + "time": 0.2, + "value": 6.67, + "curve": [ 0.222, 6.73, 0.255, 1.98 ] + }, + { "time": 0.2667 } + ] + }, + "torso2": { + "rotate": [ + { + "curve": [ 0.041, 0, 0.107, 3.03 ] + }, + { + "time": 0.1667, + "value": 3.03, + "curve": [ 0.205, 3.03, 0.241, 0 ] + }, + { "time": 0.2667 } + ] + }, + "torso3": { + "rotate": [ + { + "curve": [ 0.049, 0, 0.166, 0.66 ] + }, + { + "time": 0.2, + "value": 0.66, + "curve": [ 0.232, 0.65, 0.249, -2.15 ] + }, + { "time": 0.2667, "value": -2.15 } + ] + }, + "head-control": { + "translate": [ + { "x": -10.12, "y": 8.71 }, + { "time": 0.2667 } + ] + }, + "front-shoulder": { + "translate": [ + { "x": 4.91, "y": 11.54 }, + { "time": 0.2667 } + ] + } + } + }, + "shoot": { + "slots": { + "muzzle": { + "rgba": [ + { "time": 0.1333, "color": "ffffffff" }, + { "time": 0.2, "color": "ffffff62" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle01" }, + { "time": 0.0667, "name": "muzzle02" }, + { "time": 0.1, "name": "muzzle03" }, + { "time": 0.1333, "name": "muzzle04" }, + { "time": 0.1667, "name": "muzzle05" }, + { "time": 0.2 } + ] + }, + "muzzle-glow": { + "rgba": [ + { "color": "ff0c0c00" }, + { + "time": 0.0333, + "color": "ffc9adff", + "curve": [ 0.255, 1, 0.273, 1, 0.255, 0.76, 0.273, 0.4, 0.255, 0.65, 0.273, 0.22, 0.255, 1, 0.273, 1 ] + }, + { "time": 0.3, "color": "ff400cff" }, + { "time": 0.6333, "color": "ff0c0c00" } + ], + "attachment": [ + { "name": "muzzle-glow" } + ] + }, + "muzzle-ring": { + "rgba": [ + { + "time": 0.0333, + "color": "d8baffff", + "curve": [ 0.202, 0.85, 0.214, 0.84, 0.202, 0.73, 0.214, 0.73, 0.202, 1, 0.214, 1, 0.202, 1, 0.214, 0.21 ] + }, + { "time": 0.2333, "color": "d7baff00" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle-ring" }, + { "time": 0.2333 } + ] + }, + "muzzle-ring2": { + "rgba": [ + { + "time": 0.0333, + "color": "d8baffff", + "curve": [ 0.174, 0.85, 0.184, 0.84, 0.174, 0.73, 0.184, 0.73, 0.174, 1, 0.184, 1, 0.174, 1, 0.184, 0.21 ] + }, + { "time": 0.2, "color": "d7baff00" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle-ring" }, + { "time": 0.2 } + ] + }, + "muzzle-ring3": { + "rgba": [ + { + "time": 0.0333, + "color": "d8baffff", + "curve": [ 0.174, 0.85, 0.184, 0.84, 0.174, 0.73, 0.184, 0.73, 0.174, 1, 0.184, 1, 0.174, 1, 0.184, 0.21 ] + }, + { "time": 0.2, "color": "d7baff00" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle-ring" }, + { "time": 0.2 } + ] + }, + "muzzle-ring4": { + "rgba": [ + { + "time": 0.0333, + "color": "d8baffff", + "curve": [ 0.174, 0.85, 0.184, 0.84, 0.174, 0.73, 0.184, 0.73, 0.174, 1, 0.184, 1, 0.174, 1, 0.184, 0.21 ] + }, + { "time": 0.2, "color": "d7baff00" } + ], + "attachment": [ + { "time": 0.0333, "name": "muzzle-ring" }, + { "time": 0.2 } + ] + } + }, + "bones": { + "gun": { + "rotate": [ + { + "time": 0.0667, + "curve": [ 0.094, 25.89, 0.112, 45.27 ] + }, + { + "time": 0.1333, + "value": 45.35, + "curve": [ 0.192, 45.28, 0.18, -0.09 ] + }, + { "time": 0.6333 } + ] + }, + "muzzle": { + "translate": [ + { "x": -11.02, "y": 25.16 } + ] + }, + "rear-upper-arm": { + "translate": [ + { + "time": 0.0333, + "curve": [ 0.045, 0.91, 0.083, 3.46, 0.044, 0.86, 0.083, 3.32 ] + }, + { + "time": 0.1, + "x": 3.46, + "y": 3.32, + "curve": [ 0.133, 3.46, 0.176, -0.1, 0.133, 3.32, 0.169, 0 ] + }, + { "time": 0.2333 } + ] + }, + "rear-bracer": { + "translate": [ + { + "time": 0.0333, + "curve": [ 0.075, -3.78, 0.083, -4.36, 0.08, -2.7, 0.083, -2.88 ] + }, + { + "time": 0.1, + "x": -4.36, + "y": -2.88, + "curve": [ 0.133, -4.36, 0.168, 0.18, 0.133, -2.88, 0.167, 0 ] + }, + { "time": 0.2333 } + ] + }, + "gun-tip": { + "translate": [ + {}, + { "time": 0.3, "x": 3.15, "y": 0.39 } + ], + "scale": [ + { "x": 0.366, "y": 0.366 }, + { "time": 0.0333, "x": 1.453, "y": 1.453 }, + { "time": 0.3, "x": 0.366, "y": 0.366 } + ] + }, + "muzzle-ring": { + "translate": [ + { "time": 0.0333 }, + { "time": 0.2333, "x": 64.47 } + ], + "scale": [ + { "time": 0.0333 }, + { "time": 0.2333, "x": 5.951, "y": 5.951 } + ] + }, + "muzzle-ring2": { + "translate": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 172.57 } + ], + "scale": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 4, "y": 4 } + ] + }, + "muzzle-ring3": { + "translate": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 277.17 } + ], + "scale": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 2, "y": 2 } + ] + }, + "muzzle-ring4": { + "translate": [ + { "time": 0.0333 }, + { "time": 0.2, "x": 392.06 } + ] + } + } + }, + "walk": { + "bones": { + "rear-foot-target": { + "rotate": [ + { + "value": -32.82, + "curve": [ 0.035, -42.69, 0.057, -70.49 ] + }, + { + "time": 0.1, + "value": -70.59, + "curve": [ 0.236, -70.78, 0.335, -9.87 ] + }, + { + "time": 0.3667, + "value": -1.56, + "curve": [ 0.393, 5.5, 0.477, 13.96 ] + }, + { + "time": 0.5, + "value": 13.96, + "curve": [ 0.519, 13.96, 0.508, 0.13 ] + }, + { "time": 0.5667, "value": -0.28 }, + { + "time": 0.7333, + "value": -0.28, + "curve": [ 0.827, -0.06, 0.958, -21.07 ] + }, + { "time": 1, "value": -32.82 } + ], + "translate": [ + { + "x": -167.32, + "y": 0.58, + "curve": [ 0.022, -180.55, 0.075, -235.51, 0.045, 0.58, 0.075, 30.12 ] + }, + { + "time": 0.1, + "x": -235.51, + "y": 39.92, + "curve": [ 0.142, -235.51, 0.208, -201.73, 0.138, 54.94, 0.18, 60.78 ] + }, + { + "time": 0.2333, + "x": -176.33, + "y": 61.48, + "curve": [ 0.272, -136.61, 0.321, -45.18, 0.275, 62.02, 0.321, 56.6 ] + }, + { + "time": 0.3667, + "x": 8.44, + "y": 49.67, + "curve": [ 0.403, 51.03, 0.486, 66.86, 0.401, 44.37, 0.48, 23.11 ] + }, + { "time": 0.5, "x": 66.57, "y": 14.22 }, + { "time": 0.5333, "x": 52.58, "y": 0.6 }, + { "time": 1, "x": -167.32, "y": 0.58 } + ] + }, + "front-foot-target": { + "rotate": [ + { + "value": 18.19, + "curve": [ 0.01, 11.17, 0.043, 1.37 ] + }, + { "time": 0.1, "value": 0.47 }, + { + "time": 0.2333, + "value": 0.55, + "curve": [ 0.364, 0.3, 0.515, -80.48 ] + }, + { + "time": 0.7333, + "value": -80.78, + "curve": [ 0.788, -80.38, 0.921, 17.42 ] + }, + { "time": 1, "value": 18.19 } + ], + "translate": [ + { + "x": 139.21, + "y": 22.94, + "curve": [ 0.025, 139.21, 0.069, 111.46, 0.031, 3.25, 0.075, 0.06 ] + }, + { "time": 0.1, "x": 96.69, "y": 0.06 }, + { + "time": 0.5, + "x": -94.87, + "y": -0.03, + "curve": [ 0.518, -106.82, 0.575, -152.56, 0.534, 5.42, 0.557, 38.46 ] + }, + { + "time": 0.6, + "x": -152.56, + "y": 57.05, + "curve": [ 0.633, -152.56, 0.688, -128.05, 0.643, 75.61, 0.7, 84.14 ] + }, + { + "time": 0.7333, + "x": -109.42, + "y": 84.14, + "curve": [ 0.771, -93.91, 0.832, -30.64, 0.787, 84.14, 0.799, 89.65 ] + }, + { + "time": 0.8667, + "x": 17, + "y": 75.25, + "curve": [ 0.903, 66.18, 0.967, 139.21, 0.932, 61.53, 0.967, 44.02 ] + }, + { "time": 1, "x": 139.21, "y": 22.94 } + ] + }, + "hip": { + "rotate": [ + { "value": -4.35 } + ], + "translate": [ + { + "x": -2.86, + "y": -13.86, + "curve": [ 0.025, -2.84, 0.067, -2.82, 0.028, -19.14, 0.054, -24.02 ] + }, + { + "time": 0.1, + "x": -2.61, + "y": -24.19, + "curve": [ 0.143, -2.34, 0.202, -1.79, 0.152, -23.98, 0.213, -14.81 ] + }, + { + "time": 0.2667, + "x": -1.21, + "y": -7.12, + "curve": [ 0.308, -0.86, 0.345, -0.51, 0.306, -1.63, 0.341, 3.15 ] + }, + { + "time": 0.3667, + "x": -0.33, + "y": 3.15, + "curve": [ 0.41, 0.02, 0.458, 0.26, 0.427, 3.3, 0.481, -6.75 ] + }, + { + "time": 0.5, + "x": 0.26, + "y": -10.59, + "curve": [ 0.553, 0.26, 0.559, 0.2, 0.519, -14.41, 0.548, -23.88 ] + }, + { + "time": 0.6, + "x": -0.17, + "y": -23.71, + "curve": [ 0.663, -0.72, 0.798, -2.09, 0.702, -23.36, 0.802, 3.53 ] + }, + { + "time": 0.8667, + "x": -2.46, + "y": 3.48, + "curve": [ 0.901, -2.63, 0.967, -2.87, 0.913, 3.45, 0.967, -7.64 ] + }, + { "time": 1, "x": -2.86, "y": -13.86 } + ] + }, + "front-foot-tip": { + "rotate": [ + { + "value": 28.96, + "curve": [ 0.056, 28.74, 0.049, 19.6 ] + }, + { "time": 0.0667, "value": 1.68 }, + { + "time": 0.5, + "value": -10, + "curve": [ 0.525, -10, 0.592, -54.69 ] + }, + { + "time": 0.6, + "value": -59.66, + "curve": [ 0.623, -74.54, 0.674, -101.78 ] + }, + { + "time": 0.7333, + "value": -101.78, + "curve": [ 0.812, -101.78, 0.855, -84.67 ] + }, + { + "time": 0.8667, + "value": -63.53, + "curve": [ 0.869, -58.38, 0.975, 28.96 ] + }, + { "time": 1, "value": 28.96 } + ] + }, + "torso": { + "rotate": [ + { + "value": -20.72, + "curve": [ 0.025, -20.57, 0.071, -20.04 ] + }, + { + "time": 0.1333, + "value": -20.04, + "curve": [ 0.187, -20.04, 0.285, -21.16 ] + }, + { + "time": 0.3667, + "value": -21.16, + "curve": [ 0.405, -21.16, 0.47, -20.9 ] + }, + { + "time": 0.5, + "value": -20.71, + "curve": [ 0.518, -20.6, 0.582, -20.03 ] + }, + { + "time": 0.6333, + "value": -20.04, + "curve": [ 0.709, -20.05, 0.815, -21.18 ] + }, + { + "time": 0.8667, + "value": -21.18, + "curve": [ 0.908, -21.18, 0.971, -20.93 ] + }, + { "time": 1, "value": -20.72 } + ] + }, + "neck": { + "rotate": [ + { + "value": 17.78, + "curve": [ 0.025, 17.93, 0.071, 18.46 ] + }, + { + "time": 0.1333, + "value": 18.46, + "curve": [ 0.187, 18.46, 0.285, 17.34 ] + }, + { + "time": 0.3667, + "value": 17.34, + "curve": [ 0.405, 17.34, 0.47, 17.6 ] + }, + { + "time": 0.5, + "value": 17.79, + "curve": [ 0.518, 17.9, 0.582, 18.47 ] + }, + { + "time": 0.6333, + "value": 18.46, + "curve": [ 0.709, 18.45, 0.815, 17.32 ] + }, + { + "time": 0.8667, + "value": 17.32, + "curve": [ 0.908, 17.32, 0.971, 17.57 ] + }, + { "time": 1, "value": 17.78 } + ] + }, + "head": { + "rotate": [ + { + "value": -12.23, + "curve": [ 0.061, -12.23, 0.191, -7.45 ] + }, + { + "time": 0.2667, + "value": -7.43, + "curve": [ 0.341, -7.42, 0.421, -12.23 ] + }, + { + "time": 0.5, + "value": -12.23, + "curve": [ 0.567, -12.26, 0.694, -7.46 ] + }, + { + "time": 0.7667, + "value": -7.47, + "curve": [ 0.853, -7.49, 0.943, -12.23 ] + }, + { "time": 1, "value": -12.23 } + ], + "scale": [ + { + "curve": [ 0.039, 1, 0.084, 0.991, 0.039, 1, 0.084, 1.019 ] + }, + { + "time": 0.1333, + "x": 0.991, + "y": 1.019, + "curve": [ 0.205, 0.991, 0.318, 1.019, 0.205, 1.019, 0.337, 0.992 ] + }, + { + "time": 0.4, + "x": 1.019, + "y": 0.992, + "curve": [ 0.456, 1.019, 0.494, 1.001, 0.483, 0.991, 0.493, 0.999 ] + }, + { + "time": 0.5, + "curve": [ 0.508, 0.998, 0.584, 0.991, 0.51, 1.002, 0.584, 1.019 ] + }, + { + "time": 0.6333, + "x": 0.991, + "y": 1.019, + "curve": [ 0.705, 0.991, 0.818, 1.019, 0.705, 1.019, 0.837, 0.992 ] + }, + { + "time": 0.9, + "x": 1.019, + "y": 0.992, + "curve": [ 0.956, 1.019, 0.955, 1, 0.983, 0.991, 0.955, 1 ] + }, + { "time": 1 } + ] + }, + "back-foot-tip": { + "rotate": [ + { "value": 4.09 }, + { "time": 0.0333, "value": 3.05 }, + { + "time": 0.1, + "value": -59.01, + "curve": [ 0.124, -72.97, 0.169, -100.05 ] + }, + { + "time": 0.2333, + "value": -99.71, + "curve": [ 0.326, -99.21, 0.349, -37.4 ] + }, + { + "time": 0.3667, + "value": -17.85, + "curve": [ 0.388, 4.74, 0.451, 32.35 ] + }, + { + "time": 0.5, + "value": 32.4, + "curve": [ 0.537, 32.44, 0.566, 6.43 ] + }, + { "time": 0.5667, "value": 2 }, + { "time": 1, "value": 4.09 } + ] + }, + "front-thigh": { + "translate": [ + { + "x": 17.15, + "y": -0.09, + "curve": [ 0.178, 17.14, 0.295, -4.26, 0.009, -0.09, 0.475, 0.02 ] + }, + { + "time": 0.5, + "x": -4.26, + "y": 0.02, + "curve": [ 0.705, -4.27, 0.848, 17.15, 0.525, 0.02, 0.975, -0.09 ] + }, + { "time": 1, "x": 17.15, "y": -0.09 } + ] + }, + "rear-thigh": { + "translate": [ + { + "x": -17.71, + "y": -4.63, + "curve": [ 0.036, -19.81, 0.043, -20.86, 0.036, -4.63, 0.05, -7.03 ] + }, + { + "time": 0.1, + "x": -20.95, + "y": -7.06, + "curve": [ 0.162, -21.05, 0.4, 7.79, 0.2, -7.13, 0.4, -1.9 ] + }, + { + "time": 0.5, + "x": 7.79, + "y": -1.94, + "curve": [ 0.612, 7.69, 0.875, -10.49, 0.592, -1.97, 0.917, -3.25 ] + }, + { "time": 1, "x": -17.71, "y": -4.63 } + ] + }, + "torso2": { + "rotate": [ + { + "value": 1, + "curve": [ 0.006, 1.2, 0.084, 2.88 ] + }, + { + "time": 0.1333, + "value": 2.88, + "curve": [ 0.205, 2.88, 0.284, -1.17 ] + }, + { + "time": 0.3667, + "value": -1.17, + "curve": [ 0.411, -1.17, 0.481, 0.57 ] + }, + { + "time": 0.5, + "value": 1, + "curve": [ 0.515, 1.33, 0.59, 2.83 ] + }, + { + "time": 0.6333, + "value": 2.85, + "curve": [ 0.683, 2.86, 0.796, -1.2 ] + }, + { + "time": 0.8667, + "value": -1.2, + "curve": [ 0.916, -1.2, 0.984, 0.62 ] + }, + { "time": 1, "value": 1 } + ] + }, + "torso3": { + "rotate": [ + { "value": -1.81 } + ] + }, + "front-upper-arm": { + "rotate": [ + { + "value": -9.51, + "curve": [ 0.021, -13.32, 0.058, -19.4 ] + }, + { + "time": 0.1, + "value": -19.4, + "curve": [ 0.238, -19.69, 0.337, 7.78 ] + }, + { + "time": 0.3667, + "value": 16.2, + "curve": [ 0.399, 25.42, 0.497, 60.19 ] + }, + { + "time": 0.6, + "value": 60.26, + "curve": [ 0.719, 60.13, 0.845, 27.61 ] + }, + { + "time": 0.8667, + "value": 22.45, + "curve": [ 0.892, 16.38, 0.979, -3.27 ] + }, + { "time": 1, "value": -9.51 } + ] + }, + "front-bracer": { + "rotate": [ + { + "value": 13.57, + "curve": [ 0.022, 9.71, 0.147, -3.78 ] + }, + { + "time": 0.3667, + "value": -3.69, + "curve": [ 0.457, -3.66, 0.479, 0.83 ] + }, + { + "time": 0.5, + "value": 4.05, + "curve": [ 0.513, 6.08, 0.635, 30.8 ] + }, + { + "time": 0.8, + "value": 30.92, + "curve": [ 0.974, 31, 0.98, 18.35 ] + }, + { "time": 1, "value": 13.57 } + ] + }, + "front-fist": { + "rotate": [ + { + "value": -28.72, + "curve": [ 0.024, -31.74, 0.176, -43.4 ] + }, + { + "time": 0.3667, + "value": -43.6, + "curve": [ 0.403, -43.65, 0.47, -40.15 ] + }, + { + "time": 0.5, + "value": -35.63, + "curve": [ 0.547, -28.59, 0.624, -4.57 ] + }, + { + "time": 0.7333, + "value": -4.59, + "curve": [ 0.891, -4.62, 0.954, -24.28 ] + }, + { "time": 1, "value": -28.48 } + ] + }, + "rear-upper-arm": { + "rotate": [ + { + "value": 28.28, + "curve": [ 0.034, 30.94, 0.068, 32.05 ] + }, + { + "time": 0.1, + "value": 31.88, + "curve": [ 0.194, 31.01, 0.336, -0.11 ] + }, + { + "time": 0.3667, + "value": -7.11, + "curve": [ 0.421, -19.73, 0.53, -46.21 ] + }, + { + "time": 0.6, + "value": -45.75, + "curve": [ 0.708, -45.03, 0.844, -13.56 ] + }, + { + "time": 0.8667, + "value": -6.48, + "curve": [ 0.909, 6.59, 0.958, 24.21 ] + }, + { "time": 1, "value": 28.28 } + ] + }, + "hair2": { + "rotate": [ + { + "value": -2.79, + "curve": [ 0.074, -2.84, 0.121, 25.08 ] + }, + { + "time": 0.2333, + "value": 24.99, + "curve": [ 0.35, 24.89, 0.427, -2.86 ] + }, + { + "time": 0.5, + "value": -2.8, + "curve": [ 0.575, -2.73, 0.652, 24.5 ] + }, + { + "time": 0.7333, + "value": 24.55, + "curve": [ 0.828, 24.6, 0.932, -2.69 ] + }, + { "time": 1, "value": -2.79 } + ] + }, + "hair4": { + "rotate": [ + { + "value": -6.01, + "curve": [ 0.106, -5.97, 0.151, 18.62 ] + }, + { + "time": 0.2333, + "value": 18.72, + "curve": [ 0.336, 18.7, 0.405, -11.37 ] + }, + { + "time": 0.5, + "value": -11.45, + "curve": [ 0.626, -11.46, 0.629, 18.94 ] + }, + { + "time": 0.7333, + "value": 18.92, + "curve": [ 0.833, 18.92, 0.913, -6.06 ] + }, + { "time": 1, "value": -6.01 } + ], + "translate": [ + { "x": 0.03, "y": 1.35 } + ] + }, + "rear-bracer": { + "rotate": [ + { + "value": 10.06, + "curve": [ 0.044, 11.16, 0.063, 11.49 ] + }, + { + "time": 0.1, + "value": 11.49, + "curve": [ 0.215, 11.49, 0.336, 2.92 ] + }, + { + "time": 0.3667, + "value": 0.84, + "curve": [ 0.416, -2.52, 0.498, -10.84 ] + }, + { + "time": 0.6, + "value": -10.83, + "curve": [ 0.762, -10.71, 0.845, -3.05 ] + }, + { + "time": 0.8667, + "value": -1.34, + "curve": [ 0.917, 2.54, 0.977, 8.81 ] + }, + { "time": 1, "value": 10.06 } + ] + }, + "gun": { + "rotate": [ + { + "value": -14.67, + "curve": [ 0.086, -14.67, 0.202, 8.31 ] + }, + { + "time": 0.2333, + "value": 12.14, + "curve": [ 0.279, 17.71, 0.391, 25.79 ] + }, + { + "time": 0.5, + "value": 25.77, + "curve": [ 0.631, 25.74, 0.694, 4.53 ] + }, + { + "time": 0.7333, + "value": -0.65, + "curve": [ 0.768, -5.21, 0.902, -14.4 ] + }, + { "time": 1, "value": -14.67 } + ] + }, + "front-leg-target": { + "translate": [ + { + "x": -2.83, + "y": -8.48, + "curve": [ 0.008, -2.83, 0.058, 0.09, 0.001, 4.97, 0.058, 6.68 ] + }, + { + "time": 0.0667, + "x": 0.09, + "y": 6.68, + "curve": [ 0.3, 0.09, 0.767, -2.83, 0.3, 6.68, 0.767, -8.48 ] + }, + { "time": 1, "x": -2.83, "y": -8.48 } + ] + }, + "hair1": { + "rotate": [ + { + "curve": [ 0.028, 1.24, 0.016, 3.46 ] + }, + { + "time": 0.1, + "value": 3.45, + "curve": [ 0.159, 3.45, 0.189, 0.23 ] + }, + { + "time": 0.2333, + "value": -2.29, + "curve": [ 0.265, -4.32, 0.305, -5.92 ] + }, + { + "time": 0.3667, + "value": -5.94, + "curve": [ 0.446, -5.96, 0.52, 3.41 ] + }, + { + "time": 0.6, + "value": 3.42, + "curve": [ 0.717, 3.42, 0.772, -5.93 ] + }, + { + "time": 0.8667, + "value": -5.97, + "curve": [ 0.933, -5.99, 0.982, -0.94 ] + }, + { "time": 1 } + ] + }, + "hair3": { + "rotate": [ + { + "curve": [ 0.067, 0, 0.159, -10.48 ] + }, + { + "time": 0.2333, + "value": -10.49, + "curve": [ 0.334, -10.5, 0.439, -0.09 ] + }, + { + "time": 0.5, + "value": -0.09, + "curve": [ 0.569, -0.09, 0.658, -10.75 ] + }, + { + "time": 0.7333, + "value": -10.7, + "curve": [ 0.833, -10.63, 0.947, 0 ] + }, + { "time": 1 } + ] + }, + "gun-tip": { + "rotate": [ + { "time": 0.2333, "value": 0.11 } + ] + }, + "muzzle-ring": { + "rotate": [ + { "time": 0.2333, "value": 0.11 } + ] + }, + "muzzle-ring2": { + "rotate": [ + { "time": 0.2667, "value": 0.11 } + ] + }, + "muzzle-ring3": { + "rotate": [ + { "time": 0.2667, "value": 0.11 } + ] + }, + "muzzle-ring4": { + "rotate": [ + { "time": 0.2667, "value": 0.11 } + ] + }, + "back-shoulder": { + "translate": [ + { + "x": -0.18, + "y": -4.49, + "curve": [ 0.133, -0.18, 0.333, 7.69, 0.133, -4.49, 0.333, 2.77 ] + }, + { + "time": 0.4667, + "x": 7.69, + "y": 2.77, + "curve": [ 0.6, 7.69, 0.858, -0.18, 0.6, 2.77, 0.858, -4.49 ] + }, + { "time": 1, "x": -0.18, "y": -4.49 } + ] + }, + "front-shoulder": { + "translate": [ + { + "x": 1.46, + "y": 9.37, + "curve": [ 0.162, 1.41, 0.333, -1.66, 0.162, 9.37, 0.301, -7.23 ] + }, + { + "time": 0.5, + "x": -1.6, + "y": -7.27, + "curve": [ 0.735, -1.5, 0.847, 1.46, 0.723, -7.31, 0.838, 9.32 ] + }, + { "time": 1, "x": 1.46, "y": 9.37 } + ] + }, + "head-control": { + "translate": [ + { + "x": -6.46, + "y": -8.4, + "curve": [ 0.053, -5.31, 0.167, -3.64, 0.093, -8.4, 0.196, -3.81 ] + }, + { + "time": 0.2333, + "x": -3.64, + "y": -1.32, + "curve": [ 0.309, -3.64, 0.436, -5.84, 0.275, 1.43, 0.38, 10.3 ] + }, + { + "time": 0.5, + "x": -7.03, + "y": 10.29, + "curve": [ 0.538, -7.75, 0.66, -10.54, 0.598, 10.27, 0.694, 1.56 ] + }, + { + "time": 0.7333, + "x": -10.54, + "y": -1.26, + "curve": [ 0.797, -10.54, 0.933, -7.91, 0.768, -3.79, 0.875, -8.4 ] + }, + { "time": 1, "x": -6.46, "y": -8.4 } + ] + } + }, + "ik": { + "front-leg-ik": [ + { + "softness": 25.7, + "bendPositive": false, + "curve": [ 0.008, 1, 0.025, 1, 0.008, 25.7, 0.025, 9.9 ] + }, + { + "time": 0.0333, + "softness": 9.9, + "bendPositive": false, + "curve": [ 0.15, 1, 0.383, 1, 0.15, 9.9, 0.383, 43.2 ] + }, + { + "time": 0.5, + "softness": 43.2, + "bendPositive": false, + "curve": [ 0.625, 1, 0.875, 1, 0.625, 43.2, 0.846, 45.57 ] + }, + { "time": 1, "softness": 25.7, "bendPositive": false } + ], + "rear-leg-ik": [ + { "softness": 5, "bendPositive": false }, + { "time": 0.4333, "softness": 4.9, "bendPositive": false }, + { "time": 0.5, "softness": 28.81, "bendPositive": false }, + { "time": 0.6, "softness": 43.8, "bendPositive": false }, + { "time": 1, "softness": 5, "bendPositive": false } + ] + }, + "events": [ + { "name": "footstep" }, + { "time": 0.5, "name": "footstep" } + ] + } + } + } \ No newline at end of file diff --git a/e2e/.dev/public/spineboy.png b/e2e/.dev/public/spineboy.png new file mode 100644 index 0000000000000000000000000000000000000000..0ea9737f30707915e0bf495cdf987c6511dc0500 GIT binary patch literal 245321 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 z
  • sZ@kf@gL7X)y;k|NZYN7CI4}AlT7Z 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

    $ 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%0DxD_{VYG+f0?szS)DJk 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 kPx# literal 0 HcmV?d00001 diff --git a/e2e/.dev/public/tank-pro.atlas b/e2e/.dev/public/tank-pro.atlas new file mode 100644 index 0000000000..0c1a46580e --- /dev/null +++ b/e2e/.dev/public/tank-pro.atlas @@ -0,0 +1,65 @@ +tank-pro.png +size:870,638 +filter:Linear,Linear +scale:0.5 +antenna +bounds:647,376,11,152 +cannon +bounds:2,359,466,29 +cannon-connector +bounds:812,448,56,68 +ground +bounds:2,16,512,177 +guntower +bounds:641,4,365,145 +rotate:90 +machinegun +bounds:470,359,166,29 +machinegun-mount +bounds:823,518,36,48 +rock +bounds:690,371,265,61 +offsets:7,1,290,64 +rotate:90 +smoke-glow +bounds:817,371,50,50 +smoke-puff01-bg +bounds:753,371,92,62 +rotate:90 +smoke-puff01-fg +bounds:753,465,86,57 +offsets:1,1,88,59 +rotate:90 +smoke-puff02-fg +bounds:788,127,85,53 +offsets:4,6,92,62 +rotate:90 +smoke-puff03-fg +bounds:788,214,85,53 +offsets:4,6,92,62 +rotate:90 +smoke-puff04-fg +bounds:788,47,78,48 +rotate:90 +tank-bottom +bounds:2,390,643,138 +tank-bottom-shadow +bounds:2,195,638,162 +offsets:1,8,646,171 +tank-top +bounds:2,530,687,106 +offsets:13,5,704,111 +tread +bounds:817,423,48,15 +tread-inside +bounds:611,81,13,14 +wheel-big +bounds:516,97,96,96 +wheel-big-overlay +bounds:516,2,93,93 +wheel-mid +bounds:753,553,68,68 +wheel-mid-overlay +bounds:788,301,68,68 +wheel-small +bounds:788,9,36,36 diff --git a/e2e/.dev/public/tank-pro.json b/e2e/.dev/public/tank-pro.json new file mode 100644 index 0000000000..324d313149 --- /dev/null +++ b/e2e/.dev/public/tank-pro.json @@ -0,0 +1 @@ +{"skeleton":{"hash":"nW5DL2uoEfA","spine":"4.2.40","x":-5852.65,"y":-348.5,"width":7202.61,"height":1298.88,"images":"./images/","audio":""},"bones":[{"name":"root"},{"name":"tank-root","parent":"root","y":146.79},{"name":"tank-treads","parent":"tank-root","y":48.35},{"name":"tank-body","parent":"tank-treads","y":10},{"name":"guntower","parent":"tank-body","x":-21.72,"y":245.48},{"name":"antenna-root","parent":"guntower","x":164.61,"y":202.53},{"name":"antenna1","parent":"antenna-root","length":40,"rotation":90,"y":40,"color":"ffee00ff"},{"name":"antenna2","parent":"antenna1","length":42,"x":42,"color":"ffee00ff"},{"name":"antenna3","parent":"antenna2","length":42,"x":42,"color":"ffee00ff"},{"name":"antenna4","parent":"antenna3","length":42,"x":42,"color":"ffee00ff"},{"name":"antenna5","parent":"antenna4","length":42,"x":42,"color":"ffee00ff"},{"name":"antenna6","parent":"antenna5","length":42,"x":42,"color":"ffee00ff"},{"name":"cannon-connector","parent":"guntower","x":-235.05,"y":96.07},{"name":"cannon-target","parent":"tank-root","x":-2276.67,"y":400.17,"color":"0096ffff","icon":"arrows"},{"name":"cannon","parent":"cannon-connector","length":946.68,"rotation":180,"color":"ff4000ff"},{"name":"machinegun-mount","parent":"guntower","length":90.98,"rotation":90,"x":-123.73,"y":218.33,"color":"15ff00ff"},{"name":"machinegun-target","parent":"tank-root","x":-2272.76,"y":607.77,"color":"0096ffff","icon":"ik"},{"name":"machinegun","parent":"machinegun-mount","length":208.95,"rotation":90,"x":91.52,"y":-1.03,"color":"15ff00ff"},{"name":"machinegun-tip","parent":"machinegun","x":210.43,"y":-12.21},{"name":"rock","parent":"root","x":-1925.2,"y":33.17},{"name":"smoke-root","parent":"tank-root","x":-1200.38,"y":405.76,"scaleX":-6.5,"scaleY":6.5,"color":"ff4000ff","icon":"particles"},{"name":"smoke-glow","parent":"smoke-root","x":62.92,"y":-0.71,"color":"ff4000ff","icon":"particles"},{"name":"smoke1","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke10","parent":"smoke-root","rotation":-103.52,"x":25.45,"y":2.48,"scaleX":3.9011,"scaleY":2.8523,"color":"ff4000ff","icon":"particles"},{"name":"smoke11","parent":"smoke-root","rotation":-103.52,"x":25.45,"y":2.48,"scaleX":3.9011,"scaleY":2.8523,"color":"ff4000ff","icon":"particles"},{"name":"smoke12","parent":"smoke-root","rotation":-103.52,"x":25.45,"y":2.48,"scaleX":3.9011,"scaleY":2.8523,"color":"ff4000ff","icon":"particles"},{"name":"smoke13","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke14","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke15","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke16","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke17","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke18","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke2","parent":"smoke-root","rotation":-84.14,"x":45.06,"y":29.7,"scaleX":3.3345,"scaleY":3.3345,"color":"ff4000ff","icon":"particles"},{"name":"smoke20","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke21","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke22","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke23","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke24","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke25","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke26","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke27","parent":"smoke-root","rotation":-179.99,"x":14.75,"y":-1.55,"scaleX":1.6484,"scaleY":1.6484,"color":"ff4000ff","icon":"particles"},{"name":"smoke3","parent":"smoke-root","rotation":-87.91,"x":55.15,"y":-17.5,"scaleX":3.0415,"scaleY":4.157,"color":"ff4000ff","icon":"particles"},{"name":"smoke4","parent":"smoke-root","rotation":-87.91,"x":69.25,"y":8.01,"scaleX":2.1808,"scaleY":2.9807,"color":"ff4000ff","icon":"particles"},{"name":"smoke5","parent":"smoke-root","rotation":-87.91,"x":80.63,"y":59.88,"scaleX":4.5119,"scaleY":2.9725,"color":"ff4000ff","icon":"particles"},{"name":"smoke6","parent":"smoke-root","rotation":-87.91,"x":96.19,"y":25.65,"scaleX":3.7912,"scaleY":3.0552,"color":"ff4000ff","icon":"particles"},{"name":"smoke7","parent":"smoke-root","rotation":153.68,"x":85.65,"y":-50.47,"scaleX":4.8523,"scaleY":3.6528,"color":"ff4000ff","icon":"particles"},{"name":"smoke8","parent":"smoke-root","rotation":67.58,"x":47.85,"y":-42.55,"scaleX":4.0006,"scaleY":3.4796,"color":"ff4000ff","icon":"particles"},{"name":"smoke9","parent":"smoke-root","rotation":150.05,"x":104.02,"y":-8.73,"scaleX":4.2074,"scaleY":3.0762,"color":"ff4000ff","icon":"particles"},{"name":"tank-glow","parent":"tank-root","x":-247.72,"y":404.37,"scaleX":1.0582,"scaleY":0.6785},{"name":"tread","parent":"tank-root","length":82,"rotation":180,"x":-22.9,"y":213.86,"scaleX":0.9933,"color":"e64344ff"},{"name":"wheel-mid-center","parent":"tank-root","y":-66.21},{"name":"tread-collider1","parent":"wheel-mid-center","x":-329.58,"y":-85.44,"color":"ff00fbff"},{"name":"tread-collider2","parent":"wheel-mid-center","x":-165.95,"y":-85.44,"color":"ff00fbff"},{"name":"tread-collider3","parent":"wheel-mid-center","y":-85.44,"color":"ff00fbff"},{"name":"tread-collider4","parent":"wheel-mid-center","x":163.56,"y":-85.44,"color":"ff00fbff"},{"name":"tread-collider5","parent":"wheel-mid-center","x":329.12,"y":-85.44,"color":"ff00fbff"},{"name":"tread-gravity1","parent":"tank-root","rotation":180,"x":-175.35,"y":149.31,"color":"ff00fbff"},{"name":"tread-gravity2","parent":"tank-root","rotation":180,"x":177.89,"y":144.78,"color":"ff00fbff"},{"name":"tread10","parent":"tread","length":82,"rotation":48.85,"x":662.9,"y":-120.35,"color":"e64344ff"},{"name":"tread11","parent":"tread","length":82,"rotation":97.99,"x":651.5,"y":-39.69,"color":"e64344ff"},{"name":"tread12","parent":"tread","length":82,"rotation":113.79,"x":618.43,"y":34.83,"color":"e64344ff"},{"name":"tread13","parent":"tread","length":82,"rotation":122.96,"x":573.82,"y":103.18,"color":"e64344ff"},{"name":"tread14","parent":"tread","length":82,"rotation":142.01,"x":509.19,"y":153.3,"color":"e64344ff"},{"name":"tread15","parent":"tread","length":82,"rotation":157.84,"x":433.25,"y":184.02,"color":"e64344ff"},{"name":"tread16","parent":"tread","length":82,"rotation":157.37,"x":357.56,"y":215.37,"color":"e64344ff"},{"name":"tread17","parent":"tread","length":82,"rotation":157.29,"x":281.92,"y":246.8,"color":"e64344ff"},{"name":"tread18","parent":"tread","length":82,"rotation":157.19,"x":206.33,"y":278.38,"color":"e64344ff"},{"name":"tread19","parent":"tread","length":82,"rotation":157.14,"x":130.77,"y":310.02,"color":"e64344ff"},{"name":"tread2","parent":"tread","length":82,"x":82,"color":"e64344ff"},{"name":"tread20","parent":"tread","length":82,"rotation":157.34,"x":55.1,"y":341.41,"color":"e64344ff"},{"name":"tread21","parent":"tread","length":82,"rotation":158.11,"x":-20.99,"y":371.77,"color":"e64344ff"},{"name":"tread22","parent":"tread","length":82,"rotation":157.99,"x":-97.02,"y":402.28,"color":"e64344ff"},{"name":"tread23","parent":"tread","length":82,"rotation":157.59,"x":-172.83,"y":433.33,"color":"e64344ff"},{"name":"tread24","parent":"tread","length":82,"rotation":156.86,"x":-248.23,"y":465.34,"color":"e64344ff"},{"name":"tread25","parent":"tread","length":82,"rotation":177.94,"x":-330.17,"y":468.27,"color":"e64344ff"},{"name":"tread26","parent":"tread","length":82,"rotation":-169.55,"x":-410.81,"y":453.5,"color":"e64344ff"},{"name":"tread27","parent":"tread","length":82,"rotation":-163.86,"x":-489.58,"y":430.86,"color":"e64344ff"},{"name":"tread28","parent":"tread","length":82,"rotation":-139.13,"x":-551.59,"y":377.57,"color":"e64344ff"},{"name":"tread29","parent":"tread","length":82,"rotation":-89.04,"x":-550.21,"y":296.14,"color":"e64344ff"},{"name":"tread3","parent":"tread","length":82,"rotation":-8.91,"x":163.01,"y":-12.61,"color":"e64344ff"},{"name":"tread30","parent":"tread","length":82,"rotation":-38.99,"x":-486.48,"y":244.89,"color":"e64344ff"},{"name":"tread31","parent":"tread","length":82,"rotation":-20.04,"x":-409.45,"y":216.98,"color":"e64344ff"},{"name":"tread32","parent":"tread","length":82,"rotation":-46.24,"x":-352.74,"y":158.15,"color":"e64344ff"},{"name":"tread33","parent":"tread","length":82,"rotation":-27.95,"x":-280.3,"y":119.98,"color":"e64344ff"},{"name":"tread34","parent":"tread","length":82,"rotation":10.46,"x":-199.66,"y":134.77,"color":"e64344ff"},{"name":"tread35","parent":"tread","length":82,"rotation":-17.9,"x":-121.63,"y":109.73,"color":"e64344ff"},{"name":"tread36","parent":"tread","length":82,"rotation":-36.82,"x":-55.99,"y":60.92,"color":"fbff00ff"},{"name":"tread4","parent":"tread","length":82,"rotation":-29.27,"x":234.55,"y":-52.43,"color":"e64344ff"},{"name":"tread5","parent":"tread","length":82,"rotation":-45.26,"x":292.26,"y":-110.28,"color":"e64344ff"},{"name":"tread6","parent":"tread","length":82,"rotation":-15.29,"x":371.36,"y":-131.76,"color":"e64344ff"},{"name":"tread7","parent":"tread","length":82,"rotation":-5.49,"x":452.98,"y":-139.55,"color":"e64344ff"},{"name":"tread8","parent":"tread","length":82,"rotation":-24.99,"x":527.31,"y":-173.95,"color":"e64344ff"},{"name":"tread9","parent":"tread","length":82,"rotation":-5.44,"x":608.94,"y":-181.68,"color":"e64344ff"},{"name":"wheel-big-root1","parent":"tank-treads","x":-549.6,"y":14.4,"color":"abe323ff"},{"name":"wheel-big-root2","parent":"tank-treads","x":547.34,"y":14.4},{"name":"wheel-big1","parent":"wheel-big-root1","x":-0.02,"color":"abe323ff"},{"name":"wheel-big2","parent":"wheel-big-root2"},{"name":"wheel-mid-root1","parent":"wheel-mid-center","x":-410.57,"color":"abe323ff"},{"name":"wheel-mid-root2","parent":"wheel-mid-center","x":-246.95},{"name":"wheel-mid-root3","parent":"wheel-mid-center","x":-82.73},{"name":"wheel-mid-root4","parent":"wheel-mid-center","x":80.89},{"name":"wheel-mid-root5","parent":"wheel-mid-center","x":244.51},{"name":"wheel-mid-root6","parent":"wheel-mid-center","x":408.74},{"name":"wheel-mid1","parent":"wheel-mid-root1","color":"abe323ff"},{"name":"wheel-mid2","parent":"wheel-mid-root2"},{"name":"wheel-mid3","parent":"wheel-mid-root3"},{"name":"wheel-mid4","parent":"wheel-mid-root4"},{"name":"wheel-mid5","parent":"wheel-mid-root5"},{"name":"wheel-mid6","parent":"wheel-mid-root6"},{"name":"wheel-small-root1","parent":"tank-treads","x":-337.39,"y":109.43},{"name":"wheel-small-root2","parent":"tank-treads","x":0.09,"y":109.43},{"name":"wheel-small-root3","parent":"tank-treads","x":334.69,"y":109.43},{"name":"wheel-small1","parent":"wheel-small-root1","color":"abe323ff"},{"name":"wheel-small2","parent":"wheel-small-root2"},{"name":"wheel-small3","parent":"wheel-small-root3"}],"slots":[{"name":"rock","bone":"rock","attachment":"rock"},{"name":"ground","bone":"root","attachment":"ground"},{"name":"ground2","bone":"root","attachment":"ground"},{"name":"ground3","bone":"root","attachment":"ground"},{"name":"ground4","bone":"root","attachment":"ground"},{"name":"ground5","bone":"root","attachment":"ground"},{"name":"ground6","bone":"root","attachment":"ground"},{"name":"ground7","bone":"root","attachment":"ground"},{"name":"tank-body-shadow","bone":"tank-body","color":"ffffffb9","attachment":"tank-bottom-shadow"},{"name":"bottom","bone":"tank-body","attachment":"tank-bottom"},{"name":"tread-inside1","bone":"tread","attachment":"tread-inside"},{"name":"tread-inside53","bone":"tread27","attachment":"tread-inside"},{"name":"tread-inside27","bone":"tread14","attachment":"tread-inside"},{"name":"tread-inside3","bone":"tread2","attachment":"tread-inside"},{"name":"tread-inside55","bone":"tread28","attachment":"tread-inside"},{"name":"tread-inside29","bone":"tread15","attachment":"tread-inside"},{"name":"tread-inside5","bone":"tread3","attachment":"tread-inside"},{"name":"tread-inside57","bone":"tread29","attachment":"tread-inside"},{"name":"tread-inside31","bone":"tread16","attachment":"tread-inside"},{"name":"tread-inside7","bone":"tread4","attachment":"tread-inside"},{"name":"tread-inside59","bone":"tread30","attachment":"tread-inside"},{"name":"tread-inside33","bone":"tread17","attachment":"tread-inside"},{"name":"tread-inside9","bone":"tread5","attachment":"tread-inside"},{"name":"tread-inside61","bone":"tread31","attachment":"tread-inside"},{"name":"tread-inside35","bone":"tread18","attachment":"tread-inside"},{"name":"tread-inside11","bone":"tread6","attachment":"tread-inside"},{"name":"tread-inside63","bone":"tread32","attachment":"tread-inside"},{"name":"tread-inside37","bone":"tread19","attachment":"tread-inside"},{"name":"tread-inside13","bone":"tread7","attachment":"tread-inside"},{"name":"tread-inside65","bone":"tread33","attachment":"tread-inside"},{"name":"tread-inside39","bone":"tread20","attachment":"tread-inside"},{"name":"tread-inside15","bone":"tread8","attachment":"tread-inside"},{"name":"tread-inside67","bone":"tread34","attachment":"tread-inside"},{"name":"tread-inside69","bone":"tread35","attachment":"tread-inside"},{"name":"tread-inside71","bone":"tread36","attachment":"tread-inside"},{"name":"tread-inside41","bone":"tread21","attachment":"tread-inside"},{"name":"tread-inside17","bone":"tread9","attachment":"tread-inside"},{"name":"tread-inside43","bone":"tread22","attachment":"tread-inside"},{"name":"tread-inside19","bone":"tread10","attachment":"tread-inside"},{"name":"tread-inside45","bone":"tread23","attachment":"tread-inside"},{"name":"tread-inside21","bone":"tread11","attachment":"tread-inside"},{"name":"tread-inside47","bone":"tread24","attachment":"tread-inside"},{"name":"tread-inside23","bone":"tread12","attachment":"tread-inside"},{"name":"tread-inside49","bone":"tread25","attachment":"tread-inside"},{"name":"tread-inside25","bone":"tread13","attachment":"tread-inside"},{"name":"tread-inside51","bone":"tread26","attachment":"tread-inside"},{"name":"tread-inside2","bone":"tread","attachment":"tread-inside"},{"name":"tread-inside54","bone":"tread27","attachment":"tread-inside"},{"name":"tread-inside28","bone":"tread14","attachment":"tread-inside"},{"name":"tread-inside4","bone":"tread2","attachment":"tread-inside"},{"name":"tread-inside56","bone":"tread28","attachment":"tread-inside"},{"name":"tread-inside30","bone":"tread15","attachment":"tread-inside"},{"name":"tread-inside6","bone":"tread3","attachment":"tread-inside"},{"name":"tread-inside58","bone":"tread29","attachment":"tread-inside"},{"name":"tread-inside32","bone":"tread16","attachment":"tread-inside"},{"name":"tread-inside8","bone":"tread4","attachment":"tread-inside"},{"name":"tread-inside60","bone":"tread30","attachment":"tread-inside"},{"name":"tread-inside34","bone":"tread17","attachment":"tread-inside"},{"name":"tread-inside10","bone":"tread5","attachment":"tread-inside"},{"name":"tread-inside62","bone":"tread31","attachment":"tread-inside"},{"name":"tread-inside36","bone":"tread18","attachment":"tread-inside"},{"name":"tread-inside12","bone":"tread6","attachment":"tread-inside"},{"name":"tread-inside64","bone":"tread32","attachment":"tread-inside"},{"name":"tread-inside38","bone":"tread19","attachment":"tread-inside"},{"name":"tread-inside14","bone":"tread7","attachment":"tread-inside"},{"name":"tread-inside66","bone":"tread33","attachment":"tread-inside"},{"name":"tread-inside40","bone":"tread20","attachment":"tread-inside"},{"name":"tread-inside16","bone":"tread8","attachment":"tread-inside"},{"name":"tread-inside68","bone":"tread34","attachment":"tread-inside"},{"name":"tread-inside70","bone":"tread35","attachment":"tread-inside"},{"name":"tread-inside72","bone":"tread36","attachment":"tread-inside"},{"name":"tread-inside42","bone":"tread21","attachment":"tread-inside"},{"name":"tread-inside18","bone":"tread9","attachment":"tread-inside"},{"name":"tread-inside44","bone":"tread22","attachment":"tread-inside"},{"name":"tread-inside20","bone":"tread10","attachment":"tread-inside"},{"name":"tread-inside46","bone":"tread23","attachment":"tread-inside"},{"name":"tread-inside22","bone":"tread11","attachment":"tread-inside"},{"name":"tread-inside48","bone":"tread24","attachment":"tread-inside"},{"name":"tread-inside24","bone":"tread12","attachment":"tread-inside"},{"name":"tread-inside50","bone":"tread25","attachment":"tread-inside"},{"name":"tread-inside26","bone":"tread13","attachment":"tread-inside"},{"name":"tread-inside52","bone":"tread26","attachment":"tread-inside"},{"name":"wheel-big","bone":"wheel-big1","color":"dbdbdbff","attachment":"wheel-big"},{"name":"wheel-big2","bone":"wheel-big2","color":"dbdbdbff","attachment":"wheel-big"},{"name":"wheel-mid","bone":"wheel-mid1","attachment":"wheel-mid"},{"name":"wheel-mid2","bone":"wheel-mid2","attachment":"wheel-mid"},{"name":"wheel-mid3","bone":"wheel-mid3","attachment":"wheel-mid"},{"name":"wheel-mid4","bone":"wheel-mid4","attachment":"wheel-mid"},{"name":"wheel-mid5","bone":"wheel-mid5","attachment":"wheel-mid"},{"name":"wheel-mid6","bone":"wheel-mid6","attachment":"wheel-mid"},{"name":"wheel-small","bone":"wheel-small1","attachment":"wheel-small"},{"name":"wheel-small2","bone":"wheel-small2","attachment":"wheel-small"},{"name":"wheel-small3","bone":"wheel-small3","attachment":"wheel-small"},{"name":"wheel-mid-overlay","bone":"wheel-mid-root1","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay2","bone":"wheel-mid-root2","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay3","bone":"wheel-mid-root3","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay4","bone":"wheel-mid-root4","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay5","bone":"wheel-mid-root5","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-mid-overlay6","bone":"wheel-mid-root6","color":"ffffffec","attachment":"wheel-mid-overlay","blend":"multiply"},{"name":"wheel-big-overlay1","bone":"wheel-big-root1","color":"ffffffe9","attachment":"wheel-big-overlay","blend":"multiply"},{"name":"wheel-big-overlay2","bone":"wheel-big-root2","color":"ffffffe9","attachment":"wheel-big-overlay","blend":"multiply"},{"name":"treads-path","bone":"tank-root","attachment":"treads-path"},{"name":"tread","bone":"tread","attachment":"tread"},{"name":"tread27","bone":"tread27","color":"adc9b8ff","attachment":"tread"},{"name":"tread14","bone":"tread14","attachment":"tread"},{"name":"tread2","bone":"tread2","attachment":"tread"},{"name":"tread28","bone":"tread28","attachment":"tread"},{"name":"tread15","bone":"tread15","color":"adc9b8ff","attachment":"tread"},{"name":"tread3","bone":"tread3","color":"adc9b8ff","attachment":"tread"},{"name":"tread29","bone":"tread29","color":"adc9b8ff","attachment":"tread"},{"name":"tread16","bone":"tread16","attachment":"tread"},{"name":"tread4","bone":"tread4","attachment":"tread"},{"name":"tread30","bone":"tread30","attachment":"tread"},{"name":"tread17","bone":"tread17","color":"adc9b8ff","attachment":"tread"},{"name":"tread5","bone":"tread5","color":"adc9b8ff","attachment":"tread"},{"name":"tread31","bone":"tread31","color":"adc9b8ff","attachment":"tread"},{"name":"tread18","bone":"tread18","attachment":"tread"},{"name":"tread6","bone":"tread6","attachment":"tread"},{"name":"tread32","bone":"tread32","attachment":"tread"},{"name":"tread19","bone":"tread19","color":"adc9b8ff","attachment":"tread"},{"name":"tread7","bone":"tread7","color":"adc9b8ff","attachment":"tread"},{"name":"tread33","bone":"tread33","color":"adc9b8ff","attachment":"tread"},{"name":"tread20","bone":"tread20","attachment":"tread"},{"name":"tread8","bone":"tread8","attachment":"tread"},{"name":"tread34","bone":"tread34","attachment":"tread"},{"name":"tread35","bone":"tread35","color":"adc9b8ff","attachment":"tread"},{"name":"tread36","bone":"tread36","color":"adc9b8ff","attachment":"tread"},{"name":"tread21","bone":"tread21","color":"adc9b8ff","attachment":"tread"},{"name":"tread9","bone":"tread9","color":"adc9b8ff","attachment":"tread"},{"name":"tread22","bone":"tread22","attachment":"tread"},{"name":"tread10","bone":"tread10","attachment":"tread"},{"name":"tread23","bone":"tread23","color":"adc9b8ff","attachment":"tread"},{"name":"tread11","bone":"tread11","color":"adc9b8ff","attachment":"tread"},{"name":"tread24","bone":"tread24","attachment":"tread"},{"name":"tread12","bone":"tread12","attachment":"tread"},{"name":"tread25","bone":"tread25","color":"adc9b8ff","attachment":"tread"},{"name":"tread13","bone":"tread13","color":"adc9b8ff","attachment":"tread"},{"name":"tread26","bone":"tread26","attachment":"tread"},{"name":"machinegun","bone":"machinegun","attachment":"machinegun"},{"name":"machinegun-mount","bone":"machinegun-mount","attachment":"machinegun-mount"},{"name":"tank-top","bone":"tank-body","attachment":"tank-top"},{"name":"guntower","bone":"guntower","attachment":"guntower"},{"name":"cannon","bone":"cannon","attachment":"cannon"},{"name":"cannon-connector","bone":"cannon-connector","attachment":"cannon-connector"},{"name":"antenna","bone":"antenna-root","attachment":"antenna"},{"name":"smoke-puff1-bg","bone":"smoke1","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg13","bone":"smoke13","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg14","bone":"smoke14","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg15","bone":"smoke15","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg16","bone":"smoke16","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg17","bone":"smoke17","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg18","bone":"smoke18","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg20","bone":"smoke20","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg21","bone":"smoke21","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg22","bone":"smoke22","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg23","bone":"smoke23","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg24","bone":"smoke24","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg25","bone":"smoke25","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg26","bone":"smoke26","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg27","bone":"smoke27","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg2","bone":"smoke2","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg5","bone":"smoke5","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg6","bone":"smoke6","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg7","bone":"smoke7","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg10","bone":"smoke10","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg11","bone":"smoke11","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg12","bone":"smoke12","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg8","bone":"smoke8","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg9","bone":"smoke9","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg4","bone":"smoke4","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-bg3","bone":"smoke3","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg","bone":"smoke1","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg13","bone":"smoke13","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg14","bone":"smoke14","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg15","bone":"smoke15","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg16","bone":"smoke16","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg17","bone":"smoke17","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg18","bone":"smoke18","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg20","bone":"smoke20","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg21","bone":"smoke21","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg22","bone":"smoke22","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg23","bone":"smoke23","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg24","bone":"smoke24","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg25","bone":"smoke25","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg26","bone":"smoke26","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg27","bone":"smoke27","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg2","bone":"smoke2","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg5","bone":"smoke5","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg6","bone":"smoke6","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg7","bone":"smoke7","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg10","bone":"smoke10","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg11","bone":"smoke11","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg12","bone":"smoke12","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg8","bone":"smoke8","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg9","bone":"smoke9","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg4","bone":"smoke4","color":"ecececff","dark":"000000"},{"name":"smoke-puff1-fg3","bone":"smoke3","color":"ecececff","dark":"000000"},{"name":"smoke-glow","bone":"smoke-glow","blend":"additive"},{"name":"clipping","bone":"tank-body","attachment":"clipping"},{"name":"tank-glow","bone":"tank-glow","color":"fcdc6da7","blend":"additive"}],"ik":[{"name":"cannon-ik","bones":["cannon"],"target":"cannon-target"},{"name":"machinegun-ik","order":1,"bones":["machinegun"],"target":"machinegun-target","mix":0}],"transform":[{"name":"wheel-big-transform","order":8,"bones":["wheel-big2"],"target":"wheel-big1","rotation":65.6,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-mid1-transform","order":3,"bones":["wheel-mid2","wheel-mid4"],"target":"wheel-mid1","rotation":93,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-mid2-transform","order":4,"bones":["wheel-mid3","wheel-mid5"],"target":"wheel-mid1","rotation":-89,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-mid3-transform","order":5,"bones":["wheel-mid6"],"target":"wheel-mid1","rotation":-152.6,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-small1-transform","order":6,"bones":["wheel-small2"],"target":"wheel-small1","rotation":87,"mixX":0,"mixScaleX":0,"mixShearY":0},{"name":"wheel-small2-transform","order":7,"bones":["wheel-small3"],"target":"wheel-small1","rotation":54.9,"mixX":0,"mixScaleX":0,"mixShearY":0}],"path":[{"name":"treads-path","order":2,"bones":["tread","tread2","tread3","tread4","tread5","tread6","tread7","tread8","tread9","tread10","tread11","tread12","tread13","tread14","tread15","tread16","tread17","tread18","tread19","tread20","tread21","tread22","tread23","tread24","tread25","tread26","tread27","tread28","tread29","tread30","tread31","tread32","tread33","tread34","tread35","tread36"],"target":"treads-path","rotateMode":"chain"}],"skins":[{"name":"default","attachments":{"antenna":{"antenna":{"type":"mesh","uvs":[0.64286,0.07876,0.65354,0.1535,0.66325,0.22138,0.67367,0.29433,0.68383,0.36543,0.6936,0.43374,0.70311,0.5003,0.71311,0.57031,0.72327,0.64139,0.73406,0.71689,0.74441,0.7893,0.75614,0.87141,0.76905,0.94311,1,0.94311,1,1,0,1,0,0.94311,0.20106,0.94311,0.20106,0.87094,0.21461,0.78847,0.22651,0.71607,0.23886,0.64099,0.25036,0.57105,0.26206,0.49983,0.27306,0.43291,0.2843,0.36454,0.29593,0.29382,0.308,0.22038,0.319,0.15345,0.33142,0.07796,0.34423,0,0.63161,0],"triangles":[30,31,0,29,30,0,29,0,1,28,29,1,28,1,2,27,28,2,27,2,3,26,3,4,25,26,4,25,4,5,26,27,3,24,5,6,23,24,6,7,23,6,24,25,5,22,7,8,21,22,8,21,8,9,7,22,23,20,9,10,19,20,10,20,21,9,19,10,11,18,19,11,17,18,11,17,11,12,15,16,17,12,13,14,15,17,12,14,15,12],"vertices":[2,10,65.38,-3.14,0.3125,11,23.38,-3.14,0.6875,2,10,42.73,-3.38,0.66667,11,0.73,-3.38,0.33333,2,9,64.17,-3.59,0.33333,10,22.17,-3.59,0.66667,2,9,42.06,-3.82,0.66667,10,0.06,-3.82,0.33333,2,8,62.52,-4.04,0.33333,9,20.52,-4.04,0.66667,2,8,41.82,-4.26,0.66667,9,-0.18,-4.26,0.33333,2,7,63.65,-4.47,0.33333,8,21.65,-4.47,0.66667,2,7,42.44,-4.69,0.66667,8,0.44,-4.69,0.33333,2,6,62.9,-4.91,0.33333,7,20.9,-4.91,0.66667,2,6,40.03,-5.15,0.66667,7,-1.97,-5.15,0.33333,2,5,5.38,58.09,0.4,6,18.09,-5.38,0.6,1,5,5.64,33.21,1,1,5,5.92,11.48,1,1,5,11,11.48,1,1,5,11,-5.76,1,1,5,-11,-5.76,1,1,5,-11,11.48,1,1,5,-6.58,11.48,1,1,5,-6.58,33.35,1,2,5,-6.28,58.34,0.4,6,18.34,6.28,0.6,2,6,40.27,6.02,0.66667,7,-1.73,6.02,0.33333,2,6,63.03,5.75,0.33333,7,21.03,5.75,0.66667,2,7,42.22,5.49,0.66667,8,0.22,5.49,0.33333,2,7,63.8,5.23,0.33333,8,21.8,5.23,0.66667,2,8,42.07,4.99,0.66667,9,0.07,4.99,0.33333,2,8,62.79,4.75,0.33333,9,20.79,4.75,0.66667,2,9,42.22,4.49,0.66667,10,0.22,4.49,0.33333,2,9,64.47,4.22,0.33333,10,22.47,4.22,0.66667,2,10,42.75,3.98,0.66667,11,0.75,3.98,0.33333,2,10,65.62,3.71,0.3125,11,23.62,3.71,0.6875,1,11,47.24,3.43,1,1,11,47.24,-2.9,1],"hull":32,"edges":[28,30,28,26,30,32,26,24,24,22,32,34,34,24,34,36,36,22,60,62,38,36,20,22,38,20,40,38,18,20,40,18,42,40,16,18,42,16,44,42,14,16,44,14,46,44,12,14,46,12,48,46,10,12,48,10,50,48,8,10,50,8,52,50,6,8,52,6,54,52,4,6,54,4,56,54,2,4,56,2,60,58,58,56,62,0,0,2,58,0],"width":22,"height":303}},"bottom":{"tank-bottom":{"x":-16.67,"y":9.89,"width":1285,"height":276}},"cannon":{"cannon":{"x":481.95,"y":-0.03,"rotation":180,"width":931,"height":58}},"cannon-connector":{"cannon-connector":{"type":"mesh","uvs":[1,0.03237,1,0.10603,0.90988,0.32859,0.81975,0.55116,0.72963,0.77373,0.6395,0.9963,0.42157,0.9963,0.20364,0.9963,0,0.85434,0,0.69902,0.02268,0.52884,0,0.31444,0.21602,0.12998,0.43368,0,0.63547,0.0037,0.48408,0.77059,0.31496,0.52497,0.64133,0.19648,0.21516,0.76766,0.58346,0.56471,0.68444,0.40146,0.46758,0.36649,0.28935,0.34604],"triangles":[7,18,6,6,18,15,7,8,18,8,9,18,18,16,15,15,16,19,9,10,18,18,10,16,16,21,19,19,21,20,10,22,16,10,11,22,16,22,21,21,17,20,21,12,13,17,13,14,17,21,13,11,12,22,21,22,12,6,15,5,5,15,4,15,19,4,4,19,3,19,20,3,3,20,2,20,17,2,2,17,1,17,14,1,14,0,1],"vertices":[1,12,35.91,69.08,1,1,12,35.91,59.14,1,1,12,25.82,29.09,1,1,12,15.72,-0.95,1,1,12,5.63,-31,1,1,12,-4.46,-61.05,1,2,12,-28.87,-61.05,0.33333,14,28.87,61.03,0.66667,1,14,53.28,61.02,1,1,14,76.09,41.84,1,1,14,71.17,21.63,1,1,14,72.83,-1.62,1,1,14,70.38,-29.12,1,1,14,50.67,-56.14,1,2,12,-28.43,74.38,0.41,14,28.43,-74.4,0.59,2,12,-4.92,72.95,0.52,14,4.92,-72.95,0.48,2,12,-21.87,-30.58,0.49,14,21.87,30.57,0.51,1,14,40.81,-2.6,1,2,12,-4.26,46.93,0.49,14,4.26,-46.93,0.51,1,14,51.99,30.15,1,2,12,-10.74,-2.78,0.49,14,10.74,2.78,0.51,2,12,0.57,19.25,0.49,14,-0.57,-19.25,0.51,1,14,23.72,-23.99,1,1,14,43.68,-26.76,1],"hull":15,"edges":[0,2,2,4,4,6,6,8,8,10,10,12,12,14,14,16,16,18,18,20,20,22,22,24,24,26,26,28,28,0],"width":112,"height":135}},"clipping":{"clipping":{"type":"clipping","end":"tank-glow","vertexCount":32,"vertices":[1,3,165.84,455.67,1,1,3,114.21,493.01,1,1,3,-38.53,492.23,1,1,3,-193.4,464.18,1,2,3,-280.85,415.48,0.752,14,24.09,-73.93,0.248,1,14,70.34,-27.32,1,1,14,412.56,-22.02,1,1,14,412.82,-29.21,1,1,14,539.26,-29.34,1,1,14,539.52,-17.09,1,1,14,894.02,-16.8,1,1,14,902.99,-28.89,1,1,14,942.06,-28.58,1,1,14,948.14,-16.64,1,1,14,947.9,14.29,1,1,14,539.3,14.55,1,1,14,539,29.22,1,1,14,412.51,29.88,1,1,14,412.51,21.73,1,1,14,74.24,27.28,1,1,3,-296.64,281.2,1,1,3,-316.06,225.71,1,1,3,-521.69,190.74,1,1,3,-610.03,141.02,1,1,3,-671.84,87.13,1,1,3,-652.23,-11.24,1,1,3,-618.53,-71.36,1,1,3,-478.77,-114.21,1,1,3,-274.11,-116.26,1,1,3,1.38,-45.75,1,1,3,189.67,148.78,1,1,3,215.75,276.59,1],"color":"ce3a3aff"}},"ground":{"ground":{"x":837.96,"y":-172,"width":1024,"height":353}},"ground2":{"ground":{"x":-179.89,"y":-172,"width":1024,"height":353}},"ground3":{"ground":{"x":-1213.48,"y":-172,"scaleX":1.035,"width":1024,"height":353}},"ground4":{"ground":{"x":-2268.51,"y":-172,"scaleX":1.04,"width":1024,"height":353}},"ground5":{"ground":{"x":-3306.54,"y":-172,"width":1024,"height":353}},"ground6":{"ground":{"x":-4322.71,"y":-172,"width":1024,"height":353}},"ground7":{"ground":{"x":-5340.65,"y":-172,"width":1024,"height":353}},"guntower":{"guntower":{"x":77.22,"y":122.59,"width":730,"height":289}},"machinegun":{"machinegun":{"x":44.85,"y":-5.72,"rotation":-180,"width":331,"height":57}},"machinegun-mount":{"machinegun-mount":{"x":47.42,"y":-1.54,"rotation":-90,"width":72,"height":96}},"rock":{"rock":{"x":25.24,"y":27.35,"width":580,"height":127}},"smoke-glow":{"smoke-glow":{"type":"mesh","uvs":[1,0.24906,1,0.51991,1,0.73165,0.70776,1,0.49012,1,0.24373,1,0,0.71158,0,0.50308,0,0.26235,0.28107,0,0.47435,0,0.73345,0,0.48858,0.51759],"triangles":[12,7,8,12,10,11,12,11,0,9,10,12,12,8,9,12,0,1,6,7,12,12,1,2,5,6,12,3,4,12,5,12,4,2,3,12],"vertices":[49.99,25.1,50,-1.98,50.01,-23.15,20.79,-50,-0.98,-50,-25.62,-50.01,-50,-21.17,-50,-0.32,-50.01,23.75,-21.9,50,-2.58,50,23.33,50.01,-1.14,-1.76],"hull":12,"edges":[2,24,24,14,20,24,24,8,2,0,20,22,0,22,18,20,14,16,18,16,12,14,8,10,12,10,6,8,2,4,6,4],"width":100,"height":100}},"smoke-puff1-bg":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg2":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg3":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg4":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg5":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg6":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg7":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg8":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg9":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg10":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg11":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg12":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg13":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg14":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg15":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg16":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg17":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg18":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg20":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg21":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg22":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg23":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg24":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg25":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg26":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-bg27":{"smoke-puff01-bg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123}},"smoke-puff1-fg":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg2":{"smoke-puff01-fg":{"x":-1.01,"y":-0.07,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.82,"y":-0.39,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.82,"y":-0.39,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.82,"y":-0.39,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg3":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.02,"y":-0.25,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1145,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.03,"y":-0.43,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg4":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.63,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.63,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.63,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg5":{"smoke-puff01-fg":{"x":-1.21,"y":-0.08,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.89,"y":-0.04,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.89,"y":-0.04,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.89,"y":-0.04,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg6":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.48,"y":-0.07,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.48,"y":-0.07,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.48,"y":-0.07,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg7":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-0.7,"y":-0.36,"scaleX":0.1216,"scaleY":0.1214,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-0.56,"y":-0.15,"scaleX":0.1224,"scaleY":0.1224,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-0.56,"y":-0.15,"scaleX":0.1224,"scaleY":0.1224,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg8":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-0.65,"y":0.01,"scaleX":0.1226,"scaleY":0.1226,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-0.75,"y":-0.15,"scaleX":0.1211,"scaleY":0.1211,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-0.75,"y":-0.15,"scaleX":0.1211,"scaleY":0.1211,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg9":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.99,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.99,"y":-0.09,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-0.95,"y":-0.48,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg10":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg11":{"smoke-puff04-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg12":{"smoke-puff04-fg":{"x":-1.27,"y":-0.37,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg13":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg14":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg15":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg16":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg17":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg18":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg20":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg21":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg22":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg23":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg24":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg25":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg26":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"smoke-puff1-fg27":{"smoke-puff01-fg":{"x":-0.5,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":175,"height":118},"smoke-puff02-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff03-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":184,"height":123},"smoke-puff04-fg":{"x":-1.38,"y":-0.12,"scaleX":0.1106,"scaleY":0.1106,"rotation":88.58,"width":155,"height":96}},"tank-body-shadow":{"tank-bottom-shadow":{"x":-11.44,"y":-42.89,"width":1291,"height":341}},"tank-glow":{"smoke-glow":{"type":"mesh","uvs":[1,1,0,1,1,0],"triangles":[1,2,0],"vertices":[469.64,-738.08,-1660.32,-738.08,469.64,1391.88],"hull":3,"edges":[0,2,0,4,2,4],"width":100,"height":100}},"tank-top":{"tank-top":{"x":6.8,"y":168.71,"width":1407,"height":222}},"tread":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread-inside1":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside2":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside3":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside4":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside5":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside6":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside7":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside8":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside9":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside10":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside11":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside12":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside13":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside14":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside15":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside16":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside17":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside18":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside19":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside20":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside21":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside22":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside23":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside24":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside25":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside26":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside27":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside28":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside29":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside30":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside31":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside32":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside33":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside34":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside35":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside36":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside37":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside38":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside39":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside40":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside41":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside42":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside43":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside44":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside45":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside46":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside47":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside48":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside49":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside50":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside51":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside52":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside53":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside54":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside55":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside56":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside57":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside58":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside59":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside60":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside61":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside62":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside63":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside64":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside65":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside66":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside67":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside68":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside69":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside70":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside71":{"tread-inside":{"x":60.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread-inside72":{"tread-inside":{"x":20.1,"y":12.56,"rotation":-180,"width":25,"height":28}},"tread2":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread3":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread4":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread5":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread6":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread7":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread8":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread9":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread10":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread11":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread12":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread13":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread14":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread15":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread16":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread17":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread18":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread19":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread20":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread21":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread22":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread23":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread24":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread25":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread26":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread27":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread28":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread29":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread30":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread31":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread32":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread33":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread34":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread35":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"tread36":{"tread":{"x":45.47,"y":-8.28,"rotation":-180,"width":96,"height":30}},"treads-path":{"treads-path":{"type":"path","closed":true,"lengths":[185.21,354.53,478.3,608.52,786,1058.49,1138.97,1223.96,1303.87,1388.23,1471.11,1551.64,1633.55,1713.27,1799.89,1882.28,2164.2,2326.85,2444.07,2584.91,2754.15,2940.62],"vertexCount":66,"vertices":[1,110,11.23,41.87,1,1,110,0.79,41.95,1,1,110,-34.72,42.24,1,1,56,-104.22,0.41,1,1,56,0.07,0.55,1,1,56,68.8,0.65,1,1,109,20.5,43.47,1,1,109,1.14,40.82,1,1,109,-27.38,36.85,1,1,93,147.07,105.01,1,1,93,96.21,96.63,1,1,93,43.87,87.72,1,1,93,16.18,103.35,1,1,93,-33.67,94.21,1,1,93,-99.36,81.25,1,1,93,-122.05,-1.7,1,1,93,-83.58,-47.93,1,1,93,-33.53,-109.37,1,1,97,-83.57,-66.1,1,1,97,-2.17,-67.9,1,2,97,56.68,-41.49,0.68,51,-24.31,-41.49,0.32,1,51,-26.59,16.7,1,1,51,-2.69,16.7,1,1,51,13.52,16.7,1,2,98,-52.42,-46.51,0.744,51,30.21,-46.52,0.256,1,98,-0.32,-68.92,1,2,98,52.09,-44.73,0.712,52,-28.91,-44.73,0.288,1,52,-22.81,16.24,1,1,52,-1.42,16.24,1,1,52,20.48,16.24,1,2,99,-47.21,-47.46,0.744,52,36.01,-47.46,0.256,1,99,-0.29,-69.66,1,2,99,45.24,-47.26,0.736,53,-37.49,-47.26,0.264,1,53,-23.76,15.28,1,1,53,-0.14,15.28,1,1,53,24.45,15.28,1,2,100,-47.37,-48.7,0.744,53,33.53,-48.7,0.256,1,100,-0.5,-70.4,1,2,100,49.09,-48.34,0.744,54,-33.58,-48.34,0.256,1,54,-20.89,15.84,1,1,54,-1.26,15.84,1,1,54,15.78,15.84,1,2,101,-52.5,-48.21,0.76,54,28.45,-48.22,0.24,1,101,-2.5,-68.92,1,2,101,55.72,-47.82,0.752,55,-28.88,-47.83,0.248,1,55,-21.64,16.7,1,1,55,-0.48,16.7,1,1,55,20.74,16.7,1,2,102,-53.65,-48.9,0.76,55,25.97,-48.9,0.24,1,102,2.28,-69.66,1,1,102,44.95,-69.74,1,1,94,76.03,-85.61,1,1,94,93.58,-42.24,1,1,94,118.67,19.75,1,1,94,78.59,76.62,1,1,94,37.27,95.07,1,1,94,31.45,97.67,1,1,94,-15.16,87.48,1,1,94,-79.8,92.52,1,1,94,-119.06,95.58,1,1,111,47.07,42.29,1,1,111,0.25,42.75,1,1,111,-29.64,43.29,1,1,57,-86.65,1.35,1,1,57,0.49,0.26,1,1,57,92.42,-0.9,1],"color":"ff8819ff"}},"wheel-big":{"wheel-big":{"width":191,"height":191}},"wheel-big-overlay1":{"wheel-big-overlay":{"width":186,"height":186}},"wheel-big-overlay2":{"wheel-big-overlay":{"width":186,"height":186}},"wheel-big2":{"wheel-big":{"width":191,"height":191}},"wheel-mid":{"wheel-mid":{"width":136,"height":136}},"wheel-mid-overlay":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay2":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay3":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay4":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay5":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid-overlay6":{"wheel-mid-overlay":{"width":136,"height":136}},"wheel-mid2":{"wheel-mid":{"width":136,"height":136}},"wheel-mid3":{"wheel-mid":{"width":136,"height":136}},"wheel-mid4":{"wheel-mid":{"width":136,"height":136}},"wheel-mid5":{"wheel-mid":{"width":136,"height":136}},"wheel-mid6":{"wheel-mid":{"width":136,"height":136}},"wheel-small":{"wheel-small":{"width":71,"height":71}},"wheel-small2":{"wheel-small":{"width":71,"height":71}},"wheel-small3":{"wheel-small":{"width":71,"height":71}}}}],"animations":{"drive":{"bones":{"tank-root":{"rotate":[{"time":2},{"time":2.0667,"value":1.99},{"time":2.5,"value":-15.63},{"time":2.6667,"value":-10.37,"curve":[2.718,-10.37,2.78,-8.34]},{"time":2.8333,"value":-6.13,"curve":[2.909,-2.8,2.974,0.8]},{"time":3,"value":1.84},{"time":3.0667,"value":5.32},{"time":3.1667,"value":10.99},{"time":3.2333,"value":9.73},{"time":3.4333,"value":-4.52,"curve":[3.474,-3.99,3.608,0.01]},{"time":3.6667,"value":0.01}],"translate":[{"curve":[1.019,0,1.608,-582.83,1.019,0,1.608,0]},{"time":2,"x":-1209.75},{"time":2.3333,"x":-1652.84,"y":26.05},{"time":2.5,"x":-1877.69,"y":71.5},{"time":2.6667,"x":-2053.37,"y":100.44},{"time":2.8333,"x":-2183.86,"y":97.42},{"time":3,"x":-2312.32,"y":74.12},{"time":3.0667,"x":-2340.68,"y":45.94},{"time":3.1333,"x":-2403.04,"y":17.04},{"time":3.1667,"x":-2439.84,"y":5.45},{"time":3.2333,"x":-2523.34,"y":-3.31},{"time":3.4333,"x":-2728.27,"y":-12.73},{"time":3.5,"x":-2795.65,"y":-6.14,"curve":[3.538,-2829.89,3.583,-2878.59,3.538,-4.93,3.583,-3.21]},{"time":3.6333,"x":-2938.53,"y":-1.09,"curve":[3.89,-3218.84,4.404,-3972.02,3.89,-0.79,4.404,0]},{"time":4.8333,"x":-3972.02},{"time":5,"x":-3991.31},{"time":5.3667,"x":-3973.94}]},"tread-collider1":{"translate":[{"time":2},{"time":2.0667,"y":9.99},{"time":2.1667,"y":37.69},{"time":2.3333,"y":53.45},{"time":2.5,"y":30.97},{"time":2.6667,"y":-2.89},{"time":2.8333,"y":-0.71},{"time":3.0667,"y":-13.64},{"time":3.1667,"y":59.3},{"time":3.2333,"y":48.2},{"time":3.4333,"y":-11.27},{"time":3.6333,"y":4.15}]},"tread-collider2":{"translate":[{"time":2},{"time":2.0667,"y":-2.83},{"time":2.1667,"y":-17.44},{"time":2.3333,"y":46.07},{"time":2.5,"y":19.45},{"time":2.6667,"y":13.46},{"time":2.8333,"y":-1.92,"curve":"stepped"},{"time":2.9667,"y":-1.92},{"time":3,"y":-13.17},{"time":3.0667,"y":-23.25},{"time":3.1667,"y":28.13},{"time":3.2333,"y":25.63},{"time":3.4333,"y":-1.52},{"time":3.6333,"y":1.15}]},"tread-collider3":{"translate":[{"time":2},{"time":2.0667,"y":-7.76},{"time":2.1667,"y":-16.61},{"time":2.5,"y":29.05},{"time":2.6667,"y":30.12},{"time":2.8333,"y":5.3},{"time":3,"y":-0.38},{"time":3.1667,"y":2.6},{"time":3.4333,"y":15.41},{"time":3.6333,"y":1.44}]},"tread-collider4":{"translate":[{"time":2},{"time":2.1667,"y":-6.72},{"time":2.3333,"y":-0.92},{"time":2.5,"y":18.37},{"time":2.6667,"y":38.77},{"time":2.8333,"y":30.6},{"time":3.1667,"y":12.61},{"time":3.2333,"y":-16},{"time":3.4333,"y":25.62},{"time":3.6333,"y":-0.68}]},"tread-collider5":{"translate":[{"time":2},{"time":2.1667,"y":3.35},{"time":2.3333,"y":22.17},{"time":2.6667,"y":13.35},{"time":2.8333,"y":39},{"time":3,"y":39.88},{"time":3.1667,"y":26.57},{"time":3.2333,"y":-10.15},{"time":3.4333,"y":35.98},{"time":3.6333,"y":-1.36}]},"wheel-mid-root6":{"translate":[{"time":2},{"time":2.1667,"y":5.61},{"time":2.3333,"y":27.21},{"time":2.5,"y":30.28},{"time":2.6667,"y":-2.81},{"time":2.8333,"y":19.59},{"time":3,"y":29.11},{"time":3.1667,"y":32.55},{"time":3.2333,"y":3.55},{"time":3.4333,"y":40.54},{"time":3.6333}]},"wheel-mid-root5":{"translate":[{"time":2},{"time":2.1667,"y":-7.46},{"time":2.3333,"y":9.53},{"time":2.6667,"y":36.78},{"time":2.8333,"y":46.11},{"time":3.1667,"y":7.55},{"time":3.2333,"y":-16.28},{"time":3.4333,"y":26.21},{"time":3.6333}]},"wheel-mid-root4":{"translate":[{"time":2},{"time":2.1667,"y":-13.98},{"time":2.3333,"y":-8.26},{"time":2.5,"y":24.27},{"time":2.6667,"y":34.42},{"time":2.8333,"y":8.88},{"time":3.1667,"y":10.32},{"time":3.2333,"y":-7.63},{"time":3.4333,"y":19.69},{"time":3.6333}]},"wheel-mid-root3":{"translate":[{"time":2},{"time":2.1667,"y":-21.14},{"time":2.3333,"y":22.83},{"time":2.5,"y":23.34},{"time":2.6667,"y":18.07},{"time":2.8333,"y":1.2},{"time":3.0667,"y":-13.36},{"time":3.1667,"y":15.48},{"time":3.2333,"y":13.34},{"time":3.4333,"y":6.4},{"time":3.6333}]},"wheel-mid-root2":{"translate":[{"time":2},{"time":2.0667,"y":-4.39},{"time":2.1667,"y":3.13},{"time":2.3333,"y":53.56},{"time":2.5,"y":16.65},{"time":2.6667,"y":8.39},{"time":3.0667,"y":-19.16},{"time":3.1667,"y":43.25},{"time":3.2333,"y":39.04},{"time":3.4333,"y":-8.61},{"time":3.6333}]},"wheel-mid-root1":{"translate":[{"time":2},{"time":2.0333,"y":22.64},{"time":2.0667,"y":53.65},{"time":2.1667,"y":71.18},{"time":2.5,"y":46.83},{"time":2.6667,"y":8.38},{"time":3.0667,"y":-10.03},{"time":3.1667,"y":72.71},{"time":3.2333,"y":64.74},{"time":3.4333,"y":-17.65},{"time":3.6333}]},"tank-body":{"rotate":[{"curve":[0.208,0,0.625,-4.39]},{"time":0.8333,"value":-4.39},{"time":2},{"time":2.1667,"value":-1.34},{"time":2.3333,"value":-6.23},{"time":2.5,"value":-5.45},{"time":2.9667,"value":-5.07},{"time":3.0667,"value":-2.39},{"time":3.1667,"value":-0.98},{"time":3.2333,"value":-1.1},{"time":3.4,"value":0.43,"curve":[3.433,0.43,3.483,-1.56]},{"time":3.5333,"value":-3.55,"curve":[3.675,-3.47,3.754,1.49]},{"time":3.8333,"value":1.93},{"time":4,"value":0.48},{"time":4.3333,"curve":[4.476,0.59,4.833,3.8]},{"time":5,"value":3.8,"curve":[5.286,3.8,5.35,-2.17]},{"time":5.4667,"value":-2.17},{"time":5.6,"value":-0.61}]},"wheel-big-root1":{"translate":[{"time":2},{"time":2.0667,"y":20.07},{"time":2.3333,"y":67.24},{"time":2.6667,"y":21.04},{"time":3,"y":10.28},{"time":3.1,"y":11.28},{"time":3.1667,"y":29.43},{"time":3.2333,"y":35.31},{"time":3.4333,"y":18.38},{"time":3.5}]},"tank-treads":{"rotate":[{},{"time":0.8333,"value":-2.4},{"time":2},{"time":2.0667,"value":1.72},{"time":2.4333,"value":-0.37},{"time":2.8},{"time":3,"value":-1.41},{"time":3.1667,"value":0.54},{"time":3.2667,"value":2.22,"curve":[3.348,2.22,3.392,-1.31]},{"time":3.4333,"value":-1.31},{"time":3.7333,"value":-1.14},{"time":4.3333,"curve":[4.476,0.35,4.833,2.24]},{"time":5,"value":2.24,"curve":[5.286,2.24,5.35,0]},{"time":5.4667}]},"cannon-target":{"translate":[{},{"time":0.8333,"y":121.95},{"time":2,"y":45.73}]},"wheel-big-root2":{"translate":[{"time":3.4333,"y":13.01}]},"wheel-big1":{"rotate":[{"curve":[0.51,0,0.804,57.81]},{"time":1,"value":120},{"time":1.2667,"value":240},{"time":1.5333,"value":360},{"time":1.7667,"value":480},{"time":2.0333,"value":600},{"time":2.2,"value":720},{"time":2.4,"value":840},{"time":2.5667,"value":960},{"time":2.7333,"value":1080},{"time":2.9333,"value":1200},{"time":3.1333,"value":1320},{"time":3.3333,"value":1440},{"time":3.5,"value":1560},{"time":3.6667,"value":1680},{"time":3.8667,"value":1800},{"time":4.0667,"value":1920},{"time":4.2667,"value":2040},{"time":4.5,"value":2160,"curve":[4.563,2194.34,4.695,2225.3]},{"time":4.8333,"value":2247.67}]},"wheel-mid1":{"rotate":[{"curve":[0.459,0,0.724,57.81]},{"time":0.9,"value":120},{"time":1.1667,"value":240},{"time":1.4333,"value":360},{"time":1.6333,"value":480},{"time":1.8333,"value":600},{"time":2,"value":720},{"time":2.1333,"value":840},{"time":2.2667,"value":960},{"time":2.4,"value":1080},{"time":2.5333,"value":1200},{"time":2.6667,"value":1320},{"time":2.8333,"value":1440},{"time":2.9667,"value":1560},{"time":3.1,"value":1680},{"time":3.2333,"value":1800},{"time":3.3667,"value":1920},{"time":3.5,"value":2040},{"time":3.6333,"value":2160},{"time":3.7667,"value":2280},{"time":3.9,"value":2400},{"time":4.0333,"value":2520},{"time":4.1667,"value":2640},{"time":4.3,"value":2760},{"time":4.4667,"value":2880,"curve":[4.538,2949.2,4.742,3000]},{"time":4.8333,"value":3000}]},"wheel-small1":{"rotate":[{"curve":[0.34,0,0.536,57.81]},{"time":0.6667,"value":120},{"time":0.8667,"value":240},{"time":1.0333,"value":360},{"time":1.1667,"value":480},{"time":1.3,"value":600},{"time":1.4333,"value":720},{"time":1.5333,"value":840},{"time":1.6333,"value":960},{"time":1.7333,"value":1080},{"time":1.8333,"value":1200},{"time":1.9333,"value":1320},{"time":2.0333,"value":1440},{"time":2.1333,"value":1560},{"time":2.2333,"value":1680},{"time":2.3333,"value":1800},{"time":2.4333,"value":1920},{"time":2.5333,"value":2040},{"time":2.6333,"value":2160},{"time":2.7333,"value":2280},{"time":2.8333,"value":2400},{"time":2.9333,"value":2520},{"time":3.0333,"value":2640},{"time":3.1333,"value":2760},{"time":3.2333,"value":2880},{"time":3.3333,"value":3000},{"time":3.4333,"value":3120},{"time":3.5333,"value":3240},{"time":3.6333,"value":3360},{"time":3.7333,"value":3480},{"time":3.8333,"value":3600},{"time":3.9333,"value":3720},{"time":4.0333,"value":3840},{"time":4.1333,"value":3960},{"time":4.2333,"value":4080},{"time":4.3333,"value":4200},{"time":4.4333,"value":4320},{"time":4.6667,"value":4440},{"time":4.9,"value":4490}]},"wheel-small-root1":{"translate":[{"time":2},{"time":2.1333,"y":12.37},{"time":2.4667,"y":32.37},{"time":2.7333,"y":-5.27},{"time":2.9667,"y":14.31},{"time":3.1667,"y":19.54},{"time":3.4667,"y":7.5},{"time":4.3667,"y":-2.4}]},"wheel-small-root2":{"translate":[{"time":2},{"time":2.9,"y":5.26},{"time":3.1667,"y":10.67},{"time":3.4667,"y":-4.71}]},"wheel-small-root3":{"translate":[{"time":2},{"time":2.4667,"y":-10.56},{"time":2.9,"y":-16.08},{"time":3.1667,"y":10.12},{"time":3.4667,"y":4.1},{"time":4.3667,"y":-0.03}]},"antenna1":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna2":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna3":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna4":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna5":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"antenna6":{"rotate":[{},{"time":0.3333,"value":1.05,"curve":[0.731,1.05,1.008,-4.55]},{"time":1.2333,"value":-4.55,"curve":[1.35,-4.55,1.583,0.53]},{"time":1.7,"value":0.53},{"time":2,"value":-2.13},{"time":2.2,"value":4.71},{"time":2.3667,"value":-7.26},{"time":2.5667,"value":2.26},{"time":2.7333,"value":-3.39},{"time":3.0667,"value":-5.53},{"time":3.2333,"value":7.78},{"time":3.4667,"value":-5.99},{"time":3.7,"value":3.11},{"time":3.9,"value":-3.05},{"time":4.1,"value":0.31},{"time":4.3,"value":-3.06},{"time":4.5333,"value":0.36},{"time":4.8667,"value":4.94,"curve":[4.925,4.94,5.042,-2.38]},{"time":5.1,"value":-2.38},{"time":5.2667,"value":3.65},{"time":5.4,"value":-3.04},{"time":5.5,"value":1.49},{"time":5.6,"value":-1.86},{"time":5.7,"value":0.42}]},"machinegun":{"rotate":[{"value":8.07,"curve":"stepped"},{"time":2.0667,"value":8.07},{"time":2.1667,"value":3.11},{"time":2.5667,"value":-10.99,"curve":"stepped"},{"time":3.1333,"value":-10.99},{"time":3.2667,"value":18.18},{"time":3.4333,"value":2.75,"curve":"stepped"},{"time":4.7,"value":2.75},{"time":4.9,"value":8.07}]}},"path":{"treads-path":{"position":[{"curve":[0.984,0,1.588,0.1788]},{"time":2,"value":0.385,"curve":[2.023,0.3916,2.045,0.3983]},{"time":2.0667,"value":0.405},{"time":2.3333,"value":0.555},{"time":2.5,"value":0.605},{"time":2.6667,"value":0.685},{"time":2.8333,"value":0.745},{"time":3,"value":0.785},{"time":3.0667,"value":0.8},{"time":3.1333,"value":0.825},{"time":3.1667,"value":0.835},{"time":3.2333,"value":0.87},{"time":3.5,"value":0.98,"curve":[3.726,1.0474,4.335,1.4]},{"time":4.8333,"value":1.4}]}}},"shoot":{"slots":{"rock":{"attachment":[{}]},"smoke-glow":{"rgba":[{"time":0.1333,"color":"ffffffff"},{"time":0.1667,"color":"ffbc8af4"},{"time":0.2,"color":"fc8e8e90"},{"time":0.2667,"color":"fa3e3e1e"}],"attachment":[{"time":0.0667,"name":"smoke-glow"},{"time":0.3}]},"smoke-puff1-bg":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":1.0333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.0667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg2":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg3":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg4":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":0.9,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg5":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":0.9,"light":"ffd50c00","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg6":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg7":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3333,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg8":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4333,"light":"ffd50cff","dark":"604b3f"},{"time":0.9333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg9":{"rgba2":[{"time":0.1333,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.5333,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg10":{"rgba2":[{"time":0.1333,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.5333,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg11":{"rgba2":[{"time":0.1333,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7667,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg12":{"rgba2":[{"time":0.3333,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":0.8667,"light":"ffd50c00","dark":"604a3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg13":{"rgba2":[{"time":0.3667,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":1,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg14":{"rgba2":[{"time":0.4333,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":1.0667,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.4333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg15":{"rgba2":[{"time":0.4,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.4,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg16":{"rgba2":[{"time":0.4,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.4,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg17":{"rgba2":[{"time":0.2333,"light":"ffd50cff","dark":"534035"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4,"light":"ffd50cff","dark":"604b3f"},{"time":0.6667,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.2333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg18":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.5,"light":"ffd50cff","dark":"604b3f"},{"time":0.8,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.2333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg20":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.8,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3333,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg21":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}]},"smoke-puff1-bg22":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}]},"smoke-puff1-bg23":{"rgba2":[{"time":0.0667,"light":"ffd50cff","dark":"3b2c23"},{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.5,"light":"ffd50cff","dark":"604b3f"},{"time":0.7667,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg24":{"rgba2":[{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg25":{"rgba2":[{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":1,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg26":{"rgba2":[{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.6,"light":"ffd50cff","dark":"604b3f"},{"time":0.9333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-bg27":{"rgba2":[{"time":0.3,"light":"ffd50cff","dark":"604b3f","curve":"stepped"},{"time":0.4667,"light":"ffd50cff","dark":"604b3f"},{"time":0.7333,"light":"ffd50c00","dark":"604b3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff01-bg"}]},"smoke-puff1-fg":{"rgba2":[{"time":0.0667,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1333,"light":"fde252ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":1.0333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.0667,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg2":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg3":{"rgba2":[{"time":0.1333,"light":"ffe457ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg4":{"rgba2":[{"time":0.1333,"light":"fae781ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":0.9,"light":"ac8c7500","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg5":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":0.9,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg6":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg7":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3333,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ac8c7500","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg8":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4333,"light":"ab764cff","dark":"ac8d75"},{"time":0.9333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg9":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.5333,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg10":{"rgba2":[{"time":0.1333,"light":"fce35dff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.5333,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ac8c7500","dark":"604a3f"}],"attachment":[{"time":0.1333,"name":"smoke-puff01-fg"},{"time":0.1667,"name":"smoke-puff02-fg"},{"time":0.2,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg11":{"rgba2":[{"time":0.3333,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg12":{"rgba2":[{"time":0.3667,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":0.8667,"light":"ac8c7500","dark":"604a3f"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg13":{"rgba2":[{"time":0.3667,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":1,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg14":{"rgba2":[{"time":0.4333,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":1.0667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.4333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg15":{"rgba2":[{"time":0.4,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.4,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg16":{"rgba2":[{"time":0.4,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.4,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg17":{"rgba2":[{"time":0.2333,"light":"e3c05eff","dark":"ab7e59"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4,"light":"ab764cff","dark":"ac8d75"},{"time":0.6667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.2333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg18":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.5,"light":"ab764cff","dark":"ac8d75"},{"time":0.7667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.2333,"name":"smoke-puff03-fg"},{"time":0.2667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg20":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.8,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3333,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg21":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}]},"smoke-puff1-fg22":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}]},"smoke-puff1-fg23":{"rgba2":[{"time":0.1333,"light":"ffdf31ff","dark":"ff0000"},{"time":0.1667,"light":"ffe568ff","dark":"e26425"},{"time":0.2,"light":"ffe568ff","dark":"ab774c"},{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.5,"light":"ab764cff","dark":"ac8d75"},{"time":0.7667,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg24":{"rgba2":[{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg25":{"rgba2":[{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":1,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg26":{"rgba2":[{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.6,"light":"ab764cff","dark":"ac8d75"},{"time":0.9333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"smoke-puff1-fg27":{"rgba2":[{"time":0.3,"light":"ab764cff","dark":"ac8d75","curve":"stepped"},{"time":0.4667,"light":"ab764cff","dark":"ac8d75"},{"time":0.7333,"light":"ab764c00","dark":"ac8d75"}],"attachment":[{"time":0.3667,"name":"smoke-puff04-fg"}]},"tank-glow":{"rgba":[{"time":0.0667,"color":"fc994d84"},{"time":0.1333,"color":"f5b16bc8","curve":[0.221,0.96,0.252,0.98,0.221,0.69,0.252,0.62,0.221,0.42,0.252,0.33,0.221,0.78,0.252,0.32]},{"time":0.2667,"color":"fc994c30"}],"attachment":[{"time":0.0667,"name":"smoke-glow"},{"time":0.2667}]}},"bones":{"cannon":{"translate":[{"time":0.0667},{"time":0.1667,"x":34.77,"y":0.9},{"time":0.2667,"x":1.3}]},"tank-body":{"rotate":[{"time":0.0667},{"time":0.1667,"value":-4.29,"curve":[0.2,-4.29,0.267,2.37]},{"time":0.3,"value":2.37,"curve":[0.333,2.37,0.4,0]},{"time":0.4333}],"translate":[{"time":0.0667},{"time":0.1667,"x":31.04,"y":1.67,"curve":[0.2,31.04,0.267,-12.05,0.2,1.67,0.267,-0.23]},{"time":0.3,"x":-12.05,"y":-0.23},{"time":0.3667}]},"tank-treads":{"rotate":[{"time":0.0667},{"time":0.1667,"value":-3.08},{"time":0.3,"value":-0.42}]},"smoke1":{"rotate":[{"time":0.0667},{"time":0.1333,"value":2.88},{"time":0.1667,"value":2.34},{"time":0.2,"value":124.36},{"time":0.2667,"value":142.26},{"time":0.3333,"value":86.78},{"time":0.4667,"value":128.79},{"time":0.6333,"value":146.22},{"time":1.0333,"value":210.7}],"translate":[{"time":0.0667,"x":-9.69,"y":1.05},{"time":0.1333,"x":7.53,"y":1.21},{"time":0.1667,"x":3.26,"y":4.07},{"time":0.2,"x":29.64,"y":-17.46},{"time":0.2667,"x":86.97,"y":17.83},{"time":0.3333,"x":193.74,"y":-38.98},{"time":0.4,"x":341.67,"y":-39.52},{"time":0.6333,"x":393.24,"y":-4.01},{"time":1.0333,"x":410.76,"y":6.35}],"scale":[{"time":0.0667},{"time":0.1333,"x":3.171,"y":0.756},{"time":0.1667,"x":3.488,"y":1.279},{"time":0.2,"x":5.151,"y":2.369},{"time":0.2667,"x":4.735,"y":3.622},{"time":0.3,"x":4.735,"y":4.019},{"time":0.3333,"x":4.613,"y":3.339},{"time":0.3667,"x":4.918,"y":3.561},{"time":0.4,"x":4.6,"y":4.263},{"time":0.6333,"x":4.449,"y":2.62},{"time":1.0333,"x":3.09,"y":1.447}]},"smoke2":{"rotate":[{"time":0.1667,"value":31.55},{"time":0.3,"value":-22.63},{"time":0.4667,"value":142.89},{"time":0.6,"value":253.78},{"time":0.8333,"value":299.28}],"translate":[{"time":0.1667,"x":17.26,"y":4.86},{"time":0.2333,"x":141.22,"y":27.27},{"time":0.3,"x":178.86,"y":56.63},{"time":0.3667,"x":200.46,"y":71.05},{"time":0.4333,"x":213.12,"y":78.39},{"time":0.6333,"x":221.44,"y":73.1},{"time":0.8333,"x":223.32,"y":73.74}],"scale":[{"time":0.1667,"x":1.34,"y":1.34},{"time":0.2333,"x":2.81,"y":1.317},{"time":0.3,"x":2.932,"y":1.374},{"time":0.4667,"x":1.247,"y":0.639},{"time":0.8333,"x":0.778,"y":0.515}]},"smoke3":{"rotate":[{"time":0.1667,"value":-5.54},{"time":0.2333,"value":0.2},{"time":0.3333,"value":20.27},{"time":0.4,"value":31.36},{"time":0.4667,"value":68.52},{"time":0.5333,"value":99.74},{"time":0.6333,"value":145.8},{"time":0.8333,"value":193.28}],"translate":[{"time":0.1333,"x":1.17,"y":8.53},{"time":0.1667,"x":37.53,"y":4.84},{"time":0.2,"x":67.99,"y":9.85},{"time":0.2333,"x":134.14,"y":-13.5},{"time":0.2667,"x":181.31,"y":-19.93},{"time":0.3,"x":238.28,"y":-8.82},{"time":0.3333,"x":268.51,"y":-25.75},{"time":0.3667,"x":359.06,"y":-28.49},{"time":0.4,"x":432.96,"y":-24.11},{"time":0.4667,"x":452.16,"y":-16.73},{"time":0.6333,"x":456.28,"y":-0.41},{"time":0.8333,"x":454.14,"y":16.41}],"scale":[{"time":0.1333,"x":2.258,"y":1.366},{"time":0.1667,"x":2.656,"y":1.47},{"time":0.2,"x":3.202,"y":1.772},{"time":0.2333,"x":3.202,"y":1.93},{"time":0.2667,"x":3.124,"y":1.896},{"time":0.3,"x":3.593,"y":1.896},{"time":0.3333,"x":2.363,"y":1.247},{"time":0.3667,"x":1.845,"y":0.973},{"time":0.4,"x":1.754,"y":0.926},{"time":0.4333,"x":1.448,"y":0.695},{"time":0.4667,"x":1.441,"y":0.688},{"time":0.5333,"x":0.865,"y":0.456},{"time":0.7,"x":0.86,"y":0.454},{"time":0.8333,"x":0.211,"y":0.111}]},"smoke4":{"rotate":[{"time":0.1667,"value":-20.35},{"time":0.2333,"value":18.5},{"time":0.3,"value":57.77},{"time":0.4,"value":105.85},{"time":0.6,"value":161.28},{"time":0.9,"value":208.43}],"translate":[{"time":0.1667,"x":35.95,"y":25.54},{"time":0.2333,"x":34.17,"y":1.87},{"time":0.3,"x":136.7,"y":21.5},{"time":0.4,"x":138.61,"y":34.8},{"time":0.6,"x":160.38,"y":37.13},{"time":0.9,"x":196.41,"y":30.36}],"scale":[{"time":0.1667,"x":2.751,"y":1.754},{"time":0.2333,"x":3.486,"y":2.224},{"time":0.2667,"x":3.486,"y":2.586},{"time":0.3,"x":3.847,"y":2.109},{"time":0.4,"x":1.96,"y":1.074},{"time":0.9,"x":0.825,"y":0.452}]},"smoke5":{"rotate":[{"time":0.2,"value":23.09},{"time":0.2667,"value":12.24},{"time":0.3333,"value":36.92},{"time":0.4333,"value":-37.33},{"time":0.5333,"value":-0.66},{"time":0.9,"value":64.02}],"translate":[{"time":0.1333},{"time":0.2333,"x":123.76,"y":19.44},{"time":0.3,"x":239.08,"y":-49.72},{"time":0.3667,"x":280.23,"y":-51.46},{"time":0.7,"x":340.62,"y":-20.09},{"time":0.9,"x":349.18,"y":-5.25}],"scale":[{"time":0.1333},{"time":0.1667,"x":1.718,"y":1.718},{"time":0.2,"x":2.109,"y":2.109},{"time":0.2333,"x":1.781,"y":2.183},{"time":0.2667,"x":2.148,"y":2.633},{"time":0.3333,"x":2.234,"y":2.738},{"time":0.3667,"x":1.366,"y":2.148},{"time":0.4,"x":0.97,"y":1.524},{"time":0.4333,"x":1.078,"y":1.157},{"time":0.4667,"x":1.126,"y":1.005},{"time":0.7,"x":1.241,"y":1.301},{"time":0.9,"x":0.709,"y":0.893}]},"smoke6":{"rotate":[{"time":0.1667,"value":-37.43},{"time":0.2333,"value":-18.36},{"time":0.3333,"value":28.58},{"time":0.4,"value":150.54},{"time":0.7,"value":301.59}],"translate":[{"time":0.1333},{"time":0.2,"x":68.04,"y":16.15},{"time":0.2667,"x":214.52,"y":13.25},{"time":0.3333,"x":285.4,"y":17.95},{"time":0.4,"x":202.91,"y":101.43},{"time":0.4667,"x":189.25,"y":116.39},{"time":0.7,"x":182.77,"y":137.4}],"scale":[{"time":0.1333},{"time":0.1667,"x":1.152,"y":1.288},{"time":0.2,"x":1.939,"y":2.168},{"time":0.2333,"x":2.278,"y":2.223},{"time":0.2667,"x":2.023,"y":1.974},{"time":0.3,"x":2.644,"y":1.974},{"time":0.4,"x":1.539,"y":1.425},{"time":0.4667,"x":1.14,"y":0.939},{"time":0.7,"x":0.215,"y":0.161}]},"smoke7":{"rotate":[{"time":0.1667,"value":-243.11},{"time":0.4,"value":-182.02},{"time":0.8333,"value":-83.02}],"translate":[{"time":0.1333,"x":3.19,"y":-6.53},{"time":0.1667,"x":44.54,"y":1.12},{"time":0.2,"x":65.84,"y":6.02},{"time":0.2333,"x":173.84,"y":97.51},{"time":0.4,"x":167.39,"y":74.58},{"time":0.8333,"x":227.77,"y":84.64}],"scale":[{"time":0.1333,"x":0.878,"y":0.878},{"time":0.1667,"x":1.235,"y":1.235},{"time":0.2,"x":1.461,"y":1.461},{"time":0.2333,"x":1.114,"y":1.114},{"time":0.3333,"x":1.067,"y":1.067},{"time":0.4667,"x":0.81,"y":0.753},{"time":0.8333,"x":0.52,"y":0.484}]},"smoke8":{"rotate":[{"time":0.1667,"value":-156.52},{"time":0.2667,"value":-154.05},{"time":0.3333,"value":-108.35},{"time":0.6,"value":-93.14},{"time":0.9333,"value":-70.89}],"translate":[{"time":0.1667,"x":20.72,"y":0.25},{"time":0.2333,"x":46.1,"y":-10.06},{"time":0.3,"x":149.77,"y":0.92},{"time":0.3667,"x":241.21,"y":49.01},{"time":0.5333,"x":276,"y":58.76},{"time":0.7,"x":292.02,"y":65.91},{"time":0.9333,"x":308.7,"y":69.51}],"scale":[{"time":0.1333,"y":1.174},{"time":0.1667,"x":1.813,"y":1.438},{"time":0.2,"x":1.813,"y":1.878},{"time":0.2333,"x":1.211,"y":1.878},{"time":0.2667,"x":1.584,"y":1.596},{"time":0.3,"x":1.958,"y":1.878},{"time":0.4667,"x":1.139,"y":0.958},{"time":0.9333,"x":0.839,"y":0.591}]},"smoke9":{"rotate":[{"time":0.1333,"value":-44.34},{"time":0.1667,"value":14.73},{"time":0.2333,"value":116.07},{"time":0.2667,"value":118.29},{"time":0.3333,"value":148.13},{"time":0.3667,"value":172.74},{"time":0.4,"value":235.69},{"time":0.4333,"value":283.36},{"time":0.7667,"value":358.76}],"translate":[{"time":0.1333,"x":-3.49,"y":0.04},{"time":0.2,"x":87.4,"y":-7.97},{"time":0.2667,"x":233.69,"y":-33.86},{"time":0.3333,"x":296.44,"y":-30.87},{"time":0.4,"x":390.8,"y":4},{"time":0.4667,"x":391.42,"y":13.17},{"time":0.6333,"x":413.3,"y":36.13},{"time":0.7667,"x":408.59,"y":40.75}],"scale":[{"time":0.1333,"x":1.289,"y":1.501},{"time":0.2,"x":1.751,"y":2.039},{"time":0.2667,"x":2.064,"y":2.347},{"time":0.3333,"x":1.822,"y":2.072},{"time":0.4,"x":1.296,"y":1.045},{"time":0.4667,"x":1.872,"y":1.526},{"time":0.6333,"x":1.181,"y":1.037},{"time":0.7667,"x":0.716,"y":0.615}]},"smoke10":{"rotate":[{"time":0.1333,"value":12.16},{"time":0.2,"value":49.19},{"time":0.2667,"value":33.17},{"time":0.3333,"value":42.23},{"time":0.4,"value":11.69},{"time":0.4667,"value":41.83},{"time":0.5333,"value":54.86},{"time":0.6333,"value":75.25},{"time":0.8333,"value":126.4}],"translate":[{"time":0.1333,"x":7.74,"y":10.25},{"time":0.2,"x":42.9,"y":72.89},{"time":0.2667,"x":221.58,"y":82.27},{"time":0.3333,"x":297.31,"y":85.39},{"time":0.4,"x":322.91,"y":81.04},{"time":0.4667,"x":346.62,"y":76.68},{"time":0.6667,"x":377.46,"y":81.85},{"time":0.8333,"x":402.18,"y":101.03}],"scale":[{"time":0.1333,"x":0.537,"y":1.062},{"time":0.1667,"x":1.042,"y":0.841},{"time":0.2,"x":1.937,"y":1.563},{"time":0.2333,"x":1.937,"y":2.176},{"time":0.2667,"x":2.254,"y":2.532},{"time":0.3,"x":2.24,"y":2.516},{"time":0.5333,"x":1.731,"y":1.882},{"time":0.8333,"x":0.855,"y":0.867}]},"smoke-glow":{"translate":[{"time":0.0667,"x":-57.08,"y":0.01},{"time":0.1,"x":-49.68,"y":-1.46},{"time":0.1333,"x":6.3,"y":-2.92},{"time":0.1667,"x":31.57,"y":0.44},{"time":0.2,"x":34.04,"y":0.27},{"time":0.2333,"x":109.29,"y":1.02},{"time":0.4,"x":119.89,"y":1.01},{"time":0.4333,"x":135.2,"y":1.03},{"time":0.4667,"x":152.86,"y":1.06},{"time":0.5333,"x":164.64,"y":1.07},{"time":0.6,"x":179.94,"y":1.09},{"time":0.6333,"x":190.54,"y":1.1}],"scale":[{"time":0.0667,"x":0.233,"y":0.233},{"time":0.1,"x":0.42,"y":0.288},{"time":0.1333,"x":1.669,"y":1.072},{"time":0.1667,"x":1.669,"y":1.785,"curve":"stepped"},{"time":0.2,"x":1.669,"y":1.785},{"time":0.2333,"x":2.544,"y":1.785},{"time":0.4333,"x":3.48,"y":2.22},{"time":0.4667,"x":4.337,"y":2.655}]},"smoke11":{"rotate":[{"time":0.4,"value":47.07},{"time":0.4333,"value":109.71},{"time":0.4667,"value":164.62},{"time":0.8333,"value":276.93}],"translate":[{"time":0.3333,"x":280.31,"y":126.85},{"time":0.4,"x":296.27,"y":125.62},{"time":0.4667,"x":312.45,"y":131.57},{"time":0.6667,"x":310.5,"y":149.67},{"time":0.8333,"x":307.08,"y":153.94}],"scale":[{"time":0.3333,"x":1.491,"y":1.491},{"time":0.4667,"x":1.144,"y":0.948},{"time":0.5667,"x":0.491,"y":0.491},{"time":0.8333,"x":0.985,"y":0.91}]},"smoke12":{"rotate":[{"time":0.3667,"value":-37.96},{"time":0.4333,"value":28.55},{"time":0.5333,"value":108.53},{"time":0.8667,"value":191.85}],"translate":[{"time":0.3667,"x":390.22,"y":-1.06},{"time":0.4333,"x":411.78,"y":26.39},{"time":0.5333,"x":428.12,"y":56.28},{"time":0.8667,"x":444.34,"y":68.06}],"scale":[{"time":0.3667,"x":2.006,"y":1.821},{"time":0.5333,"x":1.719,"y":1.293},{"time":0.7333,"x":1.562,"y":1.304},{"time":0.8667,"x":0.727,"y":0.637}]},"smoke13":{"rotate":[{"time":0.3667,"value":305.8},{"time":0.4,"value":478.49},{"time":0.4333,"value":537.45},{"time":0.4667,"value":573.84},{"time":0.5333,"value":596.4},{"time":0.7,"value":622.3},{"time":1,"value":657.95}],"translate":[{"time":0.3667,"x":331.84,"y":-25.82},{"time":0.4,"x":417.88,"y":-42.62},{"time":0.4667,"x":451.61,"y":-42.21},{"time":0.5333,"x":453.81,"y":-37.03},{"time":0.6,"x":451.86,"y":-31.89},{"time":0.7,"x":453.37,"y":-27.28},{"time":1,"x":454.04,"y":-17.89}],"scale":[{"time":0.3667,"x":4.509,"y":3.114},{"time":0.4,"x":3.673,"y":2.537},{"time":0.4333,"x":4.201,"y":2.638},{"time":0.4667,"x":4.27,"y":2.399},{"time":0.6,"x":2.798,"y":1.932},{"time":0.8333,"x":2.316,"y":1.599},{"time":1,"x":1.081,"y":0.746}]},"smoke14":{"rotate":[{"time":0.4333,"value":271.03},{"time":0.7,"value":299.97},{"time":1.0667,"value":331.16}],"translate":[{"time":0.4333,"x":371.68,"y":-29.8},{"time":0.7667,"x":400.59,"y":-44.36},{"time":1.0667,"x":432.26,"y":-44.79}],"scale":[{"time":0.4333,"x":4.011,"y":3.366},{"time":0.7667,"x":2.071,"y":1.624},{"time":1.0667,"x":1.798,"y":1.111}]},"smoke15":{"rotate":[{"time":0.4,"value":111.75},{"time":0.4667,"value":171.93},{"time":0.6,"value":256.95},{"time":0.8333,"value":299.15}],"translate":[{"time":0.4,"x":266.71,"y":-53.04},{"time":0.4333,"x":290.84,"y":-51.43},{"time":0.5333,"x":305.65,"y":-44.32},{"time":0.6667,"x":318.96,"y":-38.95},{"time":0.8333,"x":342.65,"y":-27.33}],"scale":[{"time":0.4,"x":2.749,"y":2.095},{"time":0.4333,"x":3.302,"y":2.289},{"time":0.4667,"x":2.591,"y":1.895},{"time":0.5333,"x":1.777,"y":1.354},{"time":0.7,"x":1.932,"y":1.267},{"time":0.8333,"x":1.002,"y":1.546}]},"smoke16":{"rotate":[{"time":0.4,"value":89.78},{"time":0.4667,"value":137.83},{"time":0.5333,"value":193.49},{"time":0.6,"value":235.26},{"time":0.6333,"value":286.8}],"translate":[{"time":0.4,"x":217.23,"y":-21.45},{"time":0.4667,"x":249.95,"y":-13.73},{"time":0.5333,"x":264.96,"y":-9.87},{"time":0.6,"x":278.95,"y":6.37},{"time":0.6333,"x":245.65,"y":11.77}],"scale":[{"time":0.4,"x":2.265,"y":1.859},{"time":0.4333,"x":2.621,"y":1.955},{"time":0.4667,"x":1.953,"y":1.538},{"time":0.6,"x":1.005,"y":0.825},{"time":0.6333,"x":0.387,"y":0.318}]},"smoke17":{"rotate":[{"time":0.2333,"value":99.02},{"time":0.3,"value":58.06},{"time":0.3333,"value":34.05},{"time":0.3667,"value":-17.34},{"time":0.6667,"value":-62.36}],"translate":[{"time":0.2333,"x":18.91,"y":-62.91},{"time":0.3,"x":2.43,"y":-61.54},{"time":0.3333,"x":1.89,"y":-36.55},{"time":0.3667,"x":6.97,"y":-29.52},{"time":0.4333,"x":10.78,"y":-20.55},{"time":0.6667,"x":18.65,"y":-13.19}],"scale":[{"time":0.2333,"x":1.915,"y":1.915},{"time":0.3,"x":1.509,"y":1.509},{"time":0.3333,"x":1.01,"y":1.01},{"time":0.3667,"x":0.715,"y":0.715},{"time":0.4333,"x":0.949,"y":0.721},{"time":0.5667,"x":0.785,"y":0.74}]},"smoke18":{"rotate":[{"time":0.2333,"value":141.75},{"time":0.2667,"value":134.51},{"time":0.3333,"value":249.12},{"time":0.5,"value":363.82},{"time":0.7333,"value":450.54}],"translate":[{"time":0.2333,"x":60.81,"y":56.17},{"time":0.2667,"x":68.74,"y":69.4},{"time":0.3333,"x":76.85,"y":69.07},{"time":0.5,"x":101.49,"y":89.87},{"time":0.7333,"x":118.58,"y":101.16}],"scale":[{"time":0.2333,"x":2.288,"y":2.288},{"time":0.2667,"x":2.288,"y":1.628},{"time":0.3,"x":1.524,"y":1.308},{"time":0.5,"x":1.757,"y":1.385},{"time":0.5333,"x":2.08,"y":1.51},{"time":0.7333,"x":1.405,"y":0.896}]},"smoke20":{"rotate":[{"time":0.3333,"value":95.16},{"time":0.3667,"value":130.42},{"time":0.4,"value":170.7},{"time":0.4333,"value":266.75},{"time":0.4667,"value":299.82},{"time":0.5333,"value":326.88},{"time":0.6,"value":350.8},{"time":0.9,"value":403.14}],"translate":[{"time":0.3333,"x":124.61,"y":-46.55},{"time":0.5333,"x":173.8,"y":-36.62},{"time":0.7,"x":186.5,"y":-35.41},{"time":0.9,"x":188.56,"y":-37.75}],"scale":[{"time":0.3333,"x":3.346,"y":2.654},{"time":0.3667,"x":2.661,"y":2.111},{"time":0.4333,"x":2.751,"y":1.984},{"time":0.4667,"x":3.059,"y":2.21},{"time":0.5333,"x":2.159,"y":1.712},{"time":0.7,"x":1.601,"y":1.27},{"time":0.9,"x":1.679,"y":0.856}]},"smoke23":{"rotate":[{"time":0.3,"value":115.12},{"time":0.3667,"value":79.01},{"time":0.7667,"value":6.96}],"translate":[{"time":0.3,"x":75.15,"y":-50.92},{"time":0.3667,"x":59.33,"y":-53.52},{"time":0.7667,"x":39.68,"y":-48.64}],"scale":[{"time":0.3,"x":3.331,"y":2.096},{"time":0.4333,"x":2.4,"y":2.006},{"time":0.5,"x":2.555,"y":2.094},{"time":0.7667,"x":1.35,"y":1.241}]},"antenna1":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna2":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna3":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna4":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna5":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"antenna6":{"rotate":[{"time":0.0667},{"time":0.2,"value":11.78},{"time":0.3,"value":-9.52},{"time":0.4,"value":8.07},{"time":0.5,"value":-4.45},{"time":0.6,"value":1.54},{"time":0.7,"value":-0.34}]},"smoke24":{"rotate":[{"time":0.3,"value":71.32},{"time":0.3667,"value":112.39},{"time":0.4667,"value":159.56},{"time":0.7,"value":224.21}],"translate":[{"time":0.3,"x":90.72,"y":-18.79},{"time":0.3667,"x":149.69,"y":-7.78},{"time":0.4667,"x":176.26,"y":12.31},{"time":0.7,"x":184.07,"y":31.75}],"scale":[{"time":0.3,"x":2.906,"y":2.311},{"time":0.4333,"x":3.567,"y":2.58},{"time":0.4667,"x":3.157,"y":2.41},{"time":0.7,"x":1.705,"y":1.356}]},"smoke25":{"rotate":[{"time":0.3667,"value":91.25},{"time":0.4333,"value":117.56},{"time":0.6333,"value":150.9},{"time":1,"value":189.47}],"translate":[{"time":0.3667,"x":187.21,"y":-51.18},{"time":0.5333,"x":245.48,"y":-46.28},{"time":0.6667,"x":277.36,"y":-43.12},{"time":1,"x":313.27,"y":-38.14}],"scale":[{"time":0.3667,"x":3.606,"y":2.657},{"time":0.4333,"x":4.166,"y":2.792},{"time":0.5333,"x":3.09,"y":2.091},{"time":1,"x":3.062,"y":1.801}]},"smoke26":{"rotate":[{"time":0.3667,"value":10.64},{"time":0.4,"value":60.85},{"time":0.4667,"value":89.45},{"time":0.7,"value":125.01},{"time":0.9333,"value":155.24}],"translate":[{"time":0.3667,"x":442.07,"y":-13.19},{"time":0.4,"x":453.7,"y":0.81},{"time":0.4667,"x":443.57,"y":-6.95},{"time":0.7,"x":460.97,"y":15.79},{"time":0.9333,"x":465.22,"y":20.92}],"scale":[{"time":0.3667,"x":2.726,"y":2.726},{"time":0.4333,"x":3.729,"y":2.822},{"time":0.4667,"x":3.398,"y":2.441},{"time":0.7,"x":4.324,"y":3.159},{"time":0.9,"x":1.977,"y":1.48}]},"smoke27":{"rotate":[{"time":0.3667,"value":24.75},{"time":0.4333,"value":-5.43},{"time":0.5333,"value":-39.76},{"time":0.8333,"value":-56.25}],"translate":[{"time":0.3667,"x":92.98,"y":-49.06},{"time":0.5333,"x":129.81,"y":-33.09},{"time":0.8333,"x":143.68,"y":-25.27}],"scale":[{"time":0.3667,"x":3.633,"y":2.223},{"time":0.4333,"x":2.745,"y":2.283},{"time":0.4667,"x":2.962,"y":2.122},{"time":0.5333,"x":2.007,"y":1.266}]},"cannon-target":{"translate":[{"time":0.1333},{"time":0.2,"y":128.38,"curve":[0.4,0,0.8,0,0.4,128.38,0.8,0]},{"time":1}],"scale":[{"time":0.4333,"x":0.632,"y":1.244},{"time":0.4667,"x":0.477,"y":1.487}]},"machinegun-target":{"scale":[{"time":0.4333,"x":0.632,"y":1.244},{"time":0.4667,"x":0.477,"y":1.487}]},"machinegun":{"rotate":[{"value":8.07,"curve":"stepped"},{"time":0.0667,"value":8.07},{"time":0.2333,"value":-18.67,"curve":[0.894,-18.44,0.832,7.5]},{"time":0.9,"value":8.07}]},"tank-root":{"translate":[{"time":0.0667},{"time":0.1667,"x":46.59,"curve":[0.192,46.59,0.242,0,0.192,0,0.242,0]},{"time":0.2667}]},"tank-glow":{"translate":[{"time":0.1333,"x":198.14,"curve":[0.199,190.76,0.222,-255.89,0.199,0,0.222,0]},{"time":0.2333,"x":-390}],"scale":[{"time":0.0667},{"time":0.1333,"x":1.185,"y":0.945,"curve":[0.199,1.182,0.222,1.048,0.199,0.939,0.222,0.579]},{"time":0.2333,"x":1.008,"y":0.471}]}},"attachments":{"default":{"clipping":{"clipping":{"deform":[{"time":0.0667,"offset":54,"vertices":[4.59198,-4.59192]},{"time":0.1333,"offset":8,"vertices":[-8.97369,-1.88211,9.11177,1.02258,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-14.73321,-45.16878,-30.31448,-84.4631,-32.24969,-108.78421,70.26825,-36.90201]},{"time":0.1667,"offset":8,"vertices":[-11.32373,-1.65065,11.42179,0.53259,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-15.36503,-69.18713,-4.45626,-121.90839,5.46554,-115.23274,71.78526,-33.85687]},{"time":0.2,"offset":8,"vertices":[-8.70522,1.02196,8.65102,-1.4101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4.59198,-4.59192]},{"time":0.2333,"offset":8,"vertices":[-5.23146,0.85796,5.23882,-0.81519]},{"time":0.2667,"offset":54,"vertices":[4.59198,-4.59192]}]}},"smoke-glow":{"smoke-glow":{"deform":[{"time":0.1333,"vertices":[-14.17073,19.14352,0,0,-10.97961,-15.09065,-5.79558,-24.82121,0.68117,-17.78759,-1.1179,-5.4463,0,0,0,0,17.52957,6.89397,-0.33841,-2.21582,5.51004,18.88118,-6.80153,20.91101]},{"time":0.1667,"vertices":[-4.34264,39.78125,5.6649,-2.42686,-8.39346,-22.52338,-2.66431,5.08595,-19.28093,3.98568,-11.21397,10.2879,4.56749,4.1329,-19.50706,-2.28786,11.35747,4.55941,9.04341,-11.72194,2.15381,5.14344,-12.82158,16.08209,-23.19814,1.81836]},{"time":0.2,"vertices":[-3.95581,36.12203,37.20779,-0.87419,21.29579,-15.76854,-2.02438,6.16526,-5.92201,4.19709,-1.39027,9.92793,7.70584,-0.7169,-6.69733,-2.62048,17.91826,7.77333,-12.2858,3.25454,-12.75876,3.71516,9.67891,15.48546]},{"time":0.2333,"vertices":[-11.9371,26.01078,2.91821,-0.27533,7.69899,-17.45375,-2.02438,6.16526,-5.92201,4.19709,-1.39027,9.92793,7.70584,-0.7169,-6.69733,-2.62048,17.91826,7.77333,-4.30551,-6.01406,-12.75876,3.71516,-5.10017,17.59191]},{"time":0.2667,"vertices":[0.5959,23.58176,20.74303,0.93943,7.69899,-17.45375,-2.02438,6.16526,-5.92201,4.19709,-1.39027,9.92793,20.51733,2.52203,13.35544,2.64274,24.32408,-1.94308,8.50604,-20.99353,13.14276,5.73959,6.31876,19.2114,16.98909,0.80981]}]}}}},"drawOrder":[{"time":0.3,"offsets":[{"slot":"smoke-puff1-bg2","offset":24},{"slot":"smoke-puff1-bg8","offset":19},{"slot":"smoke-puff1-bg9","offset":22},{"slot":"smoke-puff1-bg3","offset":17},{"slot":"smoke-puff1-fg17","offset":13},{"slot":"smoke-puff1-fg2","offset":2},{"slot":"smoke-puff1-fg5","offset":8},{"slot":"smoke-puff1-fg6","offset":4},{"slot":"smoke-puff1-fg7","offset":-4},{"slot":"smoke-puff1-fg4","offset":-4}]},{"time":0.3333,"offsets":[{"slot":"smoke-puff1-bg2","offset":8},{"slot":"smoke-puff1-bg8","offset":5},{"slot":"smoke-puff1-bg9","offset":3},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg5","offset":-14},{"slot":"smoke-puff1-fg6","offset":-21},{"slot":"smoke-puff1-fg7","offset":-19},{"slot":"smoke-puff1-fg10","offset":-21}]},{"time":0.3667,"offsets":[{"slot":"smoke-puff1-bg2","offset":7},{"slot":"smoke-puff1-bg9","offset":4},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg2","offset":5},{"slot":"smoke-puff1-fg6","offset":-22},{"slot":"smoke-puff1-fg7","offset":-18},{"slot":"smoke-puff1-fg10","offset":-20}]},{"time":0.4,"offsets":[{"slot":"smoke-puff1-bg2","offset":5},{"slot":"smoke-puff1-bg4","offset":0},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg2","offset":5},{"slot":"smoke-puff1-fg6","offset":-21},{"slot":"smoke-puff1-fg7","offset":-18},{"slot":"smoke-puff1-fg10","offset":-22}]},{"time":0.4333,"offsets":[{"slot":"smoke-puff1-bg2","offset":4},{"slot":"smoke-puff1-bg9","offset":4},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg2","offset":5},{"slot":"smoke-puff1-fg6","offset":-17},{"slot":"smoke-puff1-fg7","offset":-19},{"slot":"smoke-puff1-fg10","offset":-23}]},{"time":0.5333,"offsets":[{"slot":"smoke-puff1-bg2","offset":9},{"slot":"smoke-puff1-bg12","offset":0},{"slot":"smoke-puff1-fg","offset":24},{"slot":"smoke-puff1-fg2","offset":6},{"slot":"smoke-puff1-fg6","offset":-20},{"slot":"smoke-puff1-fg7","offset":-19},{"slot":"smoke-puff1-fg10","offset":-23},{"slot":"smoke-puff1-fg4","offset":-5}]}]}}} \ No newline at end of file diff --git a/e2e/.dev/public/tank-pro.png b/e2e/.dev/public/tank-pro.png new file mode 100644 index 0000000000000000000000000000000000000000..badc613ca27b7fdd5739135163fdcd3c638b358c GIT binary patch literal 533353 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

    duM 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,SJ6*an%MM8J!|cRA>-_O&@mBJm#j%?%3lW^~N? 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%0DGcXOZ3Y50=zYk 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

    ~TMaV=bn+wVmNV)iL-v;m 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 z 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

    Rxb0k$D4R;#f&poRLNgZhO|;ch zr)aXV`g6-TsFZBu`5fc?crZPSJz7)#WWmml)HMM^QaMbMt&+0)Z<(|U7YZuv9u24! z&}j69WfThVmf5<;fID_&61)-w6NZc|gHItKJ1Xmv9hzQz#vr-RS|9Tl7A$~EoNXZ) zqWOu_rgWMQ8b{vc6g_$jL|%T`OMOaoa3poxNL^XZvav_;Pn_H$W4wuy|NKA+NpQIjX5 zlk_P?Dj;uvq`fvj&>{joQVfi|S*9~9~{`Cnfibe=>k z*VbsnNe2L>J@`7AUO9l`zXV$+CZZJ!Mf2ma1GdF4s1vrWvDD?*QFc-bCf{=aB_yRz z`j|2YMm)hWp1<9n)P{6Yfb(>qo;FyS9u!T?mVI+qK)v0B=;T+fYB?f|Pe?eDemU%x z#j`y(@4FZ*Jq{*YT>8ehObI?rf&5P#x_H6jZeZ=}t~$!MH1_)uklWv{^KVfunp#}Z zc64d?H7XsIndZo~9}B^a?wF40rf@Vi&bY_JxJ<8Twy8jr)wxlc;%E{8`F5yDVYAu>!!AFyGyPhgM5qSjgf>&XCKd@=fF8fc(PT|o0YQMff;nOlU zOpOvz_@@wz(#(3c9w-{{mB+MZmmy_3wHw+%p=Qj%xVTqJ!LA_jd6@KEu~6`dbnTX| zyW?Tvg-N|4?;MXHmfjNs3B4Nt>f*L zYv!lF)hnj5xRtH1A7&Z`V`n5`OiZ=5UIU)WQ?Z}WL}<7KvunP=L_7ki|PqwngaNI zsF2c}=t}p`bAmC+JhmSUjKKp*ZAAD=DdK6hST_C{bPh}(<^3-m{=^0JX$auT7yJzZ z3Lt|4{kW(YfVrW&4Q^^@bWJT`XlwGKmm@DyhDXb=6X0)L$N2mNP| z!eFi4S*g|dOVo@cz_Fhkx0i}o3*Y!SX(8=Wmim>+*w}dbV({X8Ku#S&E^DQRmq~^3 zYj%FFSHGW2u$2Gh>RF*XwYF}`o&7j9>+fz>3*+Ouct@35Ws4lUTP-Uz=(`m2q`N-& zS#{4O&9ZmTZ>k~tC-PXlM6NnCsvk>{1fDfuK6n6S6}f>fgE+zX}CgVywB3`33=iV3P#= zU-B$~b>6hVtr`~7&0njto2BD{j*oGcucj?#nu1d?F#oTPhFFY3q?WbO=jfcDvc9gY zC_qc?n0J@QM0`{r`%7#V{M+(1558@iW+9GBQ465->Qvy4#Zcg8K^S(+TT}DVpT*? zD6vk)eY^%)`<|blmOpG9tvmSiBiUALre=shgmFlo;Oz(wLQJKq`!Ww19nk9*y}q(( z17zaj;IPQ=^ztA_Dt;H_CARw_b|ij-k-2AYFfw;5je^;fSjf#P3dhK;-ItYy_s5C+(@BLSyACUyqHmi zgB8dJs)~&Jem4=D+E-5(?>lbK+2+0z*n@QM=32^q4LDioPW;@>NxuYgztw>hZ}&nK zFf$*Qk(26_v0LrWSSx0FbBH~c-XtrM%SJtlQkLv9v`}e#H8>amY3KLFYPbm%+Vbt~ zi!x>8KDX`MV`tlPPtPJQTGIAkj=JP&-qG`&Pf1PplEpC=e-rhlZ*i^QsQ*2ln9h$! zjIDE-V3ZW7fL9crZbppVbH&S%QYb;r%_ykEZZpn1lhyi0Z`M zrj~PZmU=NSZQ*geKhy0d+1l{Y&Mv^QOM?`n8?2p6p~tXUkS6Xn&bY`+r=nhGxF+A8 zq?meC0%|<^v+wview1UESrxn8Gp)V6$Is43SnxL-d|cmlmdCC)B#h3SN()JJ_C?aJ zhJJXyY(j{@n3+9&w(LX)H@WGTxz&aTr%_lM0{$%qD`USmpL1Gs7#M8CK7KMMnfEa7 zFG#Zp%PN*6uAfGB<;S}=Ti1z3jn{US3g9ujIt`ZUBL7J7_z zl49xZ<$r#zxmkX5v%-m(zK{EdoNB+CsM_3}u0t;_YS-7-CwukU4y3C}ta`t#yZ^u< zyi7@b`1s$MvcC5yZQwCjE~LG<@`?q|K4g3Q0aQ}9p&_u4Oj!3e#-DjW&ht~}I#D{e zr;hb@%=NyB50x(Mhr{rFGAGxwD8-(aNt8s$P6w=h)yGKhK;U>!WuVqDd9{n`=IH&btpo@IQ>QBFuwTALx=>h2`+o+fz`mdhA>AhSsj=a^W!EjuWDKLF~Q#UF`#h|%y_w20jfN$%e;A-+|*ci7!m-CtEX1_Wfzxi+3a)HO9J zCN!#qo3-4|n4@I>|3}q#)V=m)OmoYUT#Tp-)UCek!^c;6NoMyDnnDfZr1$CX7r+?_ zaro5`XOD#FUerZ`i%;(1gOQF{+5~mcn#ZlMX>Zkjcsg% zFL6%)9HIFg#2w(8Q9`~52y=(?{FQ6KK_?j|yS){0-Yr_Tdb!@*Ex5R6hqM_93jp>w zTYEFQWYZP}V+faL39m6P>$5jR`hCCOdIpZy_HM|QO@mJ`pQgs?So+@MuRaQ*@o`Zc z=meTB6S(7-f^_~4RA--J=Br4-`-1|X>df%ahIJaaYnX{4o6Z2#(41(~khnjO9D^dB zh>vIU4y&srOT4?yolK8@j!*^|`?HeQN~zus3<(_C>$29?>Vi)MSv`(X`5GLn@pPs5 zdZm%yvCW~oxrJ#Ts36F-2dSM;+VlG{HwrY1@fGXzclRE_IpZZnsWo&7a^_#8Nd zFQ>50kSdm~$J16}9UfZ!%Jz;(;m$qdLVC&Hs z73T8GrFpnuT*a)C!>e+GROYPs&hnpZ^-z9I$CZ65Lh)aHZjooWPLsZ>(wU-w`s#F% zp_9=D`T`7iBIc64_aVe#xq*!km!_MIaVx=LU0RBgvd1}K@8MpE12MgHO+=@2_~fw9 zc*Fc-uPb9>6vKRX^_2GGlzE{>mpHbmqf4bMj-PUr&%aLUK(0R215jl@`=IMiN`uA~ zCq+*CP|Ys$PilAOxgpa(r7hwjO!P%EP|Aw73oprsj5u{#km|5nm7=k0vcnE0JB2E z1O>3sCDQgotJ@bCijikbrV>?GHr(=_!3nh{a9wGD&G}T>Ei*e(|m^Yp_6o^Q^E^!73pt zssE+^+Z_^7b;&myG)19!fCBHYuI8k~#8KV;WW@|w)V;VU>RP)EpyMl{r#@GM@}I>Z z&kbn7`!E1)ZuA6(v8yvx-g*G6+Y4lK+$+|aIp0*0IG)1_$n4zx`S@6PMY=lv5`)7QRYmuIeL2h#Yb~Z>(8WJB{?V{uCW>?TBj5OaZnaV9RBudp0a_BxKP zM8cbCIR!^NOBK^pa+P^I%PL5OwSAOY2^{rgD^4V>;hl0vMg|rr6cH$!kC6oO$ss2K zv7yFC80L53Rnz-|^9du%+i~rgS64armpNO$)+G87=VbS-ispwSV zi@y)rBW#{UOkenAiSc8jVlD$`_Z0Q@^!kaGY2az-?tl;rZCa6&vCWsruo8Kgi>qsB z!#=eQe}uJ2ZditCScdjQ9x+LjV@Mw^<^s#uNt3)uqgCkHu4PxKh}@1{V0ZN4^af*I z9i2AaZ12G&%j9*6(@g2>4s{oEH}$uX!S#g@U~>O&6pXSY9Wh?rwEx#z&nPi!rg_f# z57slKMykJAw&*3!q9()>ReNquC3w#Ea%7u%3Dr1GYd4e;I8Ng93I34STcc>aEOaKa z4Tf#sGmUM(MzNWL-yBfnQ5sM!`r^RqAv4jeu{~0zpULv6V6yxxxAOK}H6w;Fb&2)1gA!SN z;M1%;PS$l~Ej%mRN!DZ=x8_{Uzw9e>!iIoMoCQBBmHqOv7twWnNO`~MqLCco0%yV!cr0D|lQ7&q!X>D8j z+}8&BA9@dCphv9nhtf-b{fgf0Mh#=G*MB@;Xur9;_H+I6UhloEy*!O+x-m9pMi3{P zf{KbDX<{<}9247ZiEsh7NIn{s0wdGM; zRu;E)Qc45`Nmk5?v12q`=j%@sLUlzslY?GFanfDTGn8lINJ<}Lp|Ddk81+L)W#Tu}>$n;WSa9M(|Ve&z4o|TtTRbtlwbiIHfWMR4}Q?V;=bWfhM_~V3G*sIgavLJDg*Pc=9kDAV$P@<)&>VhLp#Z;!!V;Mk8eNk|$P;QX!a!M2pwVTiyc z7myUEzS*W3*~aP>#>;<)^^=As2K-a)@|*aI^YkCy4W|nIuQVOT@ffP(M#UCw4z|M8 zwMcuGg!frd;Oe0#>BMIcM{I2l5Mz_|3fk5rekVk(Enl+(BA*lRN2ry|u5CEx5}s05 z#x+?}x_n@C`Jsa%3s{X8B{Bczw%kR1)zp7+&_%lS#qySuUd%0Yvcgo^ zX|kN`ane8b$l3lPUzxF?A?5HhWBt0FSGeIJc9U+TG6{wJwg62Z#}{JE(Yq>I44N&zh?nkVx`9^28l^=%D-2W-vf<+&Xi8%22PE zgOyd-R?*seC3uR4%c!D7wkx%bbKJSV`jsi2v3PoXQ2Bj|P3J3wwcVql%JTfTR7(^s z)d(DBjI;TRlu02+FiZszzs`1aF-LUsG2|E1Icreg#r|!xh|gd8&!we4h$v1@_R!H6 z0tN;GFrie{hCDu&!_e6{uq+~BU2al*I@4n_v+C3X6cmdS783Q3Y`MNppbo5nW}xCO|L|!-1P!We7K^ zv2KGi;X7(ig>}9^;grK5-Au%1rzO8Qfg=u;F)9?eUU}a|0&2x+2QC?S9!8K*dn|n~ zXbK(HlaB|PK{vMRJ-=sC6Vj@`5WqpoxBfVF!z^uZ_#%&9qK4WVL~QlLX$>S!Bp`(> zoY#Spa!Gk0IiMeIHwV5lVD1SrO`azOI9`@4j24^^BU@+1EQP>E!(Kk&Naz>5pTdD4 z-FImR7WqV%EB(POdDL zDd%6@t9zJxTRAqLR=M|&8L7MPq7&T{7A@uWsN{B`t1kneDI4P+G z8pH;0rq&y2SoQbMVpa5;Dqw)yHNoDzsA}_>7N>*hJX`S4a~Hsj#8H4#DI?4WE*gB; zv+9LhcHSXbcr#2sICMUaN|y;ZD*cEl1BMvhSbqw@$3~AYYV6nrl)nC{{V`ZmR~fho zcQpVqVP;Z}fKJ^K?X3mL!&}7-3BV)#WS^J?$Fx7Eoq)o)>`;i@dw>b}jc6gm6HUrI z05PXY^v67ck)9+QK81=8YAodyd{)q?F@v7-7s6d1%+dUX9FvojI^LShjP2RO=3E@9 z&~7)2-{-4AO)-c7J~QQx{7~c8aLDg9$)&l?pHv-XULA5T8oL{n=onaqYxGOup}qOm zv1&Htks5;rFs&;j*3w9hGa2gP^t8(8TayOl%)6^P0;KUMA?DN4lB$v67+V@wJ)nK( zcT5Vx90VF(clhRaxbhTd;!R>6W>pC0tj-&GPft(u{JbXSD=2=1sw(*(Yxe4&ld8iq z*W6w1Yo|a+;15lP!xX{)@k72H5dEJ9En%M=0ru{BTF|LWsQ~7={c_Y$Y#24bA_1 z_WGl>$`hYE5bQU}!wCA2;D`vSAvde;wvs4Mo>YU>*3QM{^F#BM_Qm}pv4fvNF28>d zPEB_b`A}Cg%~vH+UHEwD8|j*x6XU##VE);|B%-7@{t-PBhjfQ7(T;4+XoX->C@WHts9os z_-AG}kv_ZNu9NAMG}3WTgOnI_U1Hy%PCp+Xl*klcgZSYGGSXu*V+o!7k?0^nFE{R( z1DDt%WuHZ+co0h7Db*(WkRScCsBHT=$qB{4hO*rI_)bh^ zqJ@twj7C*IaJma%w*nbKdq+B&k`oynO4kCx9G^C;3_BCQP4$;{J~$;4xfG{CDstG> z^RtFs;%9^Ny4Unj>m;-g+~+`tMxjKCise z-yLYkR3P>Cq8F|+3_H^?-G@OPVJc34dOR+# zL1FjN8w=6<5ARz;ruqsJD~iAfCUk_Y{l?J`(M0ElCkTinAA+l-q(tMBXrrIQ_0WVa z&g5j}_Rog>?7~~ps)As;W78;yn9lO5D&y(Ns8PS>1Vo?7)}QItlNELCamJuaYWg7S zPYiS7@;f4@Bk6kuUfFrXK9`?!#O3fArrBX4LUfcp@cV{^Ot2$+5Y#SQmp;YI1fgDoOn*23$#q7l2tLbdtwq|L)fZ5s*)_<{ahdGtz!= zF6bfasU@@yPV{2$NZ}*Vl={V`l%@rqKko;%j1$4QTE4kGeKwRM`)1JF?*4%EUheq#n4Ba^5nN|dVSNz>90$~PcfTm*i?F@( z-nK{3)Mb4tqm$+MmpDT!!k`ia!&4Yyz(9iH2!;}!{9HW$Wkzc`1$q#>r$^TkrS`Ys#eY9Bot`v6wJqN@1!Vd&R|U%yri!C$BLP7{Am=W3 zca1IO^|HLCCQe{29PSL4>wm)cZUFY;2y57zcAf5C+Vz9`!9hUgG@1*p<>SM^7mD%Q zk5*bTIXSog)O`IKQxzQQvzuwhzhlL}y7PNG|6^eBR!7&4<2}oc<6WnKs^{(w(ZTB# z3_gi-U+E3vVvq<4pb9=+(>uA84SG)^Bhx1*qC~rKwb@B;<8^~(07BU9NXNsJ?H$~n zqDIxmH#0<5KDS|!diy{QyhI!@`o~RyxEa@0d{`2ko%kUUe{SdV_9BYgwf>SaLf4o` zYM~D4|R`kNc z(79D)$twWH@sCjRSqwgZkSfp9OyU^#&R1OUvFp*hoYa3|9mMpQy7xLJ#$~_L4}id7 zJ$nRqcp?44R9{FgRj7nA$%Bv6oQJTiP$s^!qF{VDlyWIDP!3j}nCA?uP7Am;DM?v; zET$lxRj3B!e3x$*|9Ldo}Mo*mcw+c302}A7_~w3F6Vwvh(Wei z=PH?b?;+gS)fJ0^>cg@1w=g;qDg(nwNcO~mFlfvbSW~|fP+EYaM9Vu<<0&kC^Bb4| z#uT{WY;2MxRqYv7tyw0fv>epstBZY_$~iOgd?9<}x684gHble?z5@N{k+amyKVZDqy3$g#TnIQpAtg}WU0OqI<>S(aAJ!4 zp*6qL7YfBPUHm#ubON(x4RooziIODV0Y=gsNV4ZI90Bflpp_7>S@g%_LyQD))5+=e zQedS{rs=h5+0Q964nn%zhm?0bF5cG9$9k{dn$l8VeOWVeL%fh+(Iv*R11PcEIY`9o zD5LP{fe5U3x&we$0gRA|S&h@ffgusS03{m6=64!w>~b3oa`6J=RaC9xx3gSIlKQg@p=1C8xTj zDBl$R*GNDAk9V`OuA8vmV0Ly?ur`?gkD5dpMXXZRX?SbP2@B#YUR9MUQw1xo=R-hd znY@C@697@YBcSSxMAlm{TV8(WFR|{T+&j#WvN_b194Q|eK|@9GJUAj5KTau=U@H}5 zLnQWDm~R7ndu-%_#@jhk@%uYp|H|cy%SEl#k!0epj(SyJc`$?Pm|Lu6m98s_?l=mH zJoP@fj#CScCXIO?u<(+0-e1-Bkq98Ao1!sUCwF4pJm@w8r1NcEn2L zs#=Cza!Y%-LH6b#9KDRQSazS!$A0ONj@IRb#F0q1WhnK|I5-r>8&>TVhFQs;5?@j` znFnb->#~yWvJl3bB?mcL@{sY8+zxfBZr38Z5Tm?14qb{pk2rHBlB z6&oxQkVEl-^RtprDkan7jUL4bjnq1`LM}TvM|{5sqig(F7l@=poD!>UY;1)9$9avCiL8(^V9dCRBuk!uBosbCztMp9TwHFdrw(DhL73 z^h-%RMZDJr5}dqZ=EOvy>>a6mfNwuobeRlFB!uKl3rR}k8QZgw#@gXp)sZU@LLAeKXfxYYat-nl5A%e*wZil ztd9+5s9#84=fLk|&p%uDI&*UPg!2IIJt`9tymOgIN?TKl9yJ*kxu+Tk=d}1hYhF>mX%z>mQLNOj%^~_X|m45|*@@ ztgGaHF>J{oDn`9hJBd(viU<#2K_XK}d88%e8=gs-{w-yfk+*kr;j{dEB%GXBh3h?e zUq2_iBfME^MOYR;E^iXF6)#4@#D}V$nB6a8NSGYLFwlph3d9lD){%15>)2fPNhrMd z?@k;KOCCWUiis0q?Df)7Vw^`6hh8o21%b`bjpP^q)1Tk@r!qAbI*cPPr(u#TR zMV%14!c2aeT78osyB!pruSZRRbPO%#z`!}UKVgLLKWBqSN9OGYFmy5i3`adp)4{28 z7`S;#j5;X6F};zFl&{%}GsLXXh^6ZLPte$KG8k;Tp5Gsvsdu_uI61g+4jOvu$X@Z8 z*Fu?HA$#o`WSK4TLiJyR_;Z-W$E%v^+QEqBjT}P4@{PfCO#AFYd3$Sn&fV;(xDwMs z!DWPN#9Afs-c{|sE_*J!ork^uO*$rzzU_SrRBp$nSzhDiNFktY%Ex;l=x0P-K}r|n?P2HP&@_U7@|W9Cp}#=YOt^4gz%t7^;C$9}@>YLEzU#;-7* zQf9HCUzrgTTzE1RXWpu5T|T-nUji%d>3nGTjF3jZj*oOtq&&a6c=TIqy>3nn&5Wu#mPJv+-Y$}}}&w)(q#n5x{t4Psy23+2yg zf8XxQsQPvfX5S$J7w?dv z4k1@C57el5`VY>ebJH|_E-2Bj0&B*av%mwck3qN=KrzM^xTsXF0>S%{PK^nSIh6iB zKUX8F{vwbt!g?MI5$hK-d2<+RN#~!VEEH<)c=P9(Y~FmcFXT^yn%>sQiB~C~0?n#7 zqSlkn=0Rh}yBy@d>I_XOSV?>YUjUrnTvzPRyqL%W~27xuKjX-;6#cFxC4NdDaUlBj0OZNu)as~Mjq-^&4; zTwo}SVlYvswYw@UnB{9lo$XnwMJvj@j2=k->8Q)59w7}Ku+sVvVL#y8si;w%Ez z`~wc1K7&!kiUP9@PT>h3=QM|duB-z7LK4TKQzRYcfy5}0^YKl6B4Rjfa(>lX%u|3` z{1<89&BN^l8eDX4e!X`G@hJ0FETDWl^HCWPIl<&z^HM$ohc`a9AFMA-|MQ8OiEUn) zWC|2U37X=MYr;jDTuS951>Ep5u`CQgc@9DGcfJ%Wpa;jwMcvEa8&9K*`5zb~ zp5`#dOj~1yi=#IWz(yJurZTbV>-)DIhR3QlbJ9BHcYhcXxLRC=4AEqI3yTN`vq3dfxlGpMRV$ ze4UxYIeVYA)?UkDr+TgtLDb9p$#OgGg&iwkK4Xb>`0cT##qBBn^~S4iS%Gf9RN^Pi z004JW{Z!(eH*?mLJJKu(-#}IunyMd@0Ehlp;+pfe%NL7 z9jiOO**@$I4Wo5NgqQugYM{-ivAsm) z=td>lfXz>V#y+2Ju?V0nyjd9hA})ee&*LtA)tJ5791TXu`~d4N@2yHx1{|stI{V$1oBrP$!S^Iq5b1(q4##Q`!f3em$(cm~_Ja*P ziZ{#v5oLE%RzXoWDE_1G!>U9=uY*Z%ttVcwbEMzp#Q&ib$)+UVE9pAk$ zAL}$JTgCYLwmmhdLOEGT)>!ftE(<2wMA$O8eSQEq#Z!;KEVZL5>X@y<<z{XJ z;XlitbPqA4lnORHb%8pc5fU_De~yft^9-b=6e)Fj8As9-U7+naihdcyT!rkz!v+;= z$XMd#C)e9=LEMU$w+3$6UI^;$(<&JI5EK=0I$1X3*#hsUcE{#Kwv1#2>#r3x-Pipp z@t>p-odB!tb$M*He{@7M2fWP$A%ulUxZtszSfqWBX2aob7aF|JH1HA}> zKa>Usk-^MR3_xYdy$4PKw@(-)+Ee-}!n~z~IUJ$udkr_cpL8Yd53gU)(YhA>fxj0P zoswU9@A@3=qir`dHHG^|Igq^aECoYVDp?3zm)<@d&JBiD$kAEzfvSw)H?mS`m<>v7 z@$&d(d==VFwt>R#ZkrqUfv;r+E7Q(GX-AgYSKE#FJnfI1EiFCO0z?+}ALBCWT+IJC zq8Nz(^&(CqL_)^!Iu)b;^z1p$&~XY0y&sO<%PfIkQ9g%@50F-WKVb66m|VS%?WMm)rWo>a zBUEh_3--`xJsd>hQYoG0&)0S*j}3n3D$O1E$5O`n5&z?Q0S-8C$hq4Bnh}94nE?N_ z=_2*1C)7|vLc-sNuL3d<2n6kK1w8|UAv8K7T?Xug?vXj za4|hb>h~6n)-=zLTi*A7;<5`$a^8g_c5v-4x*aftgwy=5(f$(mv;1`%M|)1ib50Uv z0iRW*uwM8R!8~3g%sW!%LOT|LW-HozclKM zTNb_I(f(sgHmIRxVb(8dVTjI6%?mJ=lOqlgRiT6z!iRVeAi*wTJ`=&2jAv}?uPS%t z2+*sOr57TA*(Yn*&jAt{1|SML;QH_au-6Ui0f_y-M?7sJr@mh22gd1{nd~_e!(R&& zS0U$DZVs~<8d#IRC*}AE=EprdYovN~Wcf^o-RFGt{%>*lPo3Fl5ve`yB-F}&yDtX_ zx%4O-4i3zS&ha+TE59kVIEE!vQziz)C4sg|Ne1!hPu-H1=_$^gPy7Ky z2d5%~5baRCA=$~NEARb%);!~8Mt@vF-XH#)m?G?X6Ap|clD`?6nPAF={{uveKYP*z zOuI8AOEjH)5#6W%9l>Kg0n&rF2&cRLbfm;p1Q`ExkXBJqX$N=wtfH9UIvGK_nq9P- z)yFH`0px7lo|-Y>lX>2EF2q+vCN&JQ1pcRq==TSg+UZKWDqesz9MMabzwp4Giroe5@*;~27#ciIH(TBDME>#!^1F5f%!R_+ILJYpG9u8-lrYCzzkjTRo^Ab}h zovkJ>v+oO;WpRcd9^lyYJWxa3F8N~Cn~Gp$aufon8ufx5a1cDaZv2;>o&1fM@^4l$@Gt>=4xTp4c&rHaXD$(-Qo*JNWuO zS2GYv&mbkOWxB0Gi{5`{<;qbZB7=#DiD7lLw@2g#T`BWOfnm!M=;JxwE;N&jkcw8$ zqLLO0z zXHk99vFyR=TKv||fS|$?83g*WiI5Vx2!Ubs_Hm7(w7MnoF6o+KMXqI-7%)F|) z&7m0jKYrr3BRn;J^i@$UH)W{6E(v=coT&*T*=p4kE+x%#7R9E~bo4ud&PW zzGyYQw$7%#kOwbPU zu>JnTeaH<`_|T+kfiaa^au_7NAa1a|ak~6@M}^W~8i)2XW*_G1TbEludK}Cva#W2s zk}7^8Z8&c0=C*B4+?Me`kdbN4JK#SH>?56COfmtBX8F`aS$cYpubUIad zRO*8U#5(fJfpO%4;DEGHqo5iJZcA7dJycX@M#qmbqtgb<23tRbBZin7c6T4$^VZu8 z@g^`+OjtH!ST;C8hAp6pjp$njWF8R5S`p<2RR7B$4(G0!|*e)b9o0 zejIB$C!%t(4X!d=T#dmI|YJdDY&&}j6*XU zu|+(Gv_pRqpyy96Qn)0?6B9SM_+7iO13tM`r5(rN!8Uwqq!33OCLbFn;v=Wx9&BZ{ zwMhV>%563OXf9s~tS{(bjAsYjzw`M$rQ+b|7}@N+C=YPW7 zE=VViULVE)oZQV-Sp+twF7tGz!^hs~ z6{C#kqpIkpHJ$RZ*HKOeKaBY6F5}++5t2G-;`zN=U>9RB#OJlgv;}x)Nh;tH$7l}^ z(UsfAJ2_O4hE<9lB>q-HC*f2uZ1M=?3k6_MEz!`(?Q9kc*3xzi69^SKUqLs|H`OKc z`7GKT7jJ3JQd>Q-c@)bFBlHqLg=oVK1DvuHiI;`xB1_uO3d z-ctHqXWQoE6`}6rW@Uac=X;N>+IMT2?_6#>UwX88eBR!YZm>3)PBo znH`lZ-eLV-(EZAOyOF0dHPRaL5YfJSTV&ksF=3T1DXxSY2T|Yj_f)6q@eY^h;3;cj zV`(r!3KGt>;Q&VA^%G?? zOGJ{qTX=X$7b8(Aye?7r_-?ctTY#&lb}uiP zC1M=eH7a;>=Zp4Fvv)AvP^%WJeo~!I^WyE=WRyVn<>*EF<0wj|0T|~ROTfjO0M4oF z(tzud0Qu#OjDJE9FXA0hA0;pL{)vIQ+l-{*nH=INEByBWnwT(`)DCDcG(@F&Pk*-W z2sBm(95wKB^R>+?viVr)7#s`6GMs3W9qoU;W!BViE%XjF4o-WvBBhvCgPv?VleEA1 z&PRSfwKQTm4+0(-mmh8dwgP;0``SgEoWuN!3o(eMwt4!hhSyzoqW_w zluV8zbTd8~nOdDl&`=4=^>Y8dni}#`{mOSNsab`cF5Mv-Z~;frN5qLo*lc;gZLUVo zuHJ$go}efO(1z7~O1*?~>~HycY4eZkSS0xP40LD?V|ho`hEv4OF6-V2(c`8;ToS~* zvkPJQ`p=u>){*5Ri9ZCe($VRQlcz}K#l;2jVshH&Ww%l#w1QW~PQ@Q&V4lTleWLTM6YlIc=Kk$jrmq)pN}NLm-5}!= zT9XT(luxf2ld}{0dlS_8ADdjLIcy9$&SftcbHy$uSYy?zwih&{@6*5cI-M0W?`#hP zdYxjFxm2Sq52oY?J&Pl|T2DcwIBjVrL{2&#wW6%$fA_*?pZ-FXXQR)0Kv?*@e98E) z&85M>=3rt*#k_^uM;V|HkSKy@ET5GE@l9O+O;mrwe!p z;WfsGd~krg5Ixo3jIs`e)}qna{;v75)Fw<){u!N2mi#fSTvCi=7x<_C4VC!ozf#12 zn-7KsdN_KBw1x-E*u+BMj%^X-zgTZZ*sGqmAp!-^(HOm}NUj|SX}FN33oB%k+A`GM zuONktB{iY1CM8B(ln1Ca$A=M7mZ_bz_d(vuGlB|b$8q{cglLw)L=25>Oh1fx>_v*zo`dJnv#){t3U zVwxLCjC$MpmZ=u@MGr!=g#^64-vNfdvTHs$y$jz?(?0NRe!WKV}i?_%x)JvB>cRhfu>SO>hcn&{UVBU_i`m^R^}U^ zYC6o-wey~u+Mw|Aa?NaZcFIjP0QY5+Vp-vcG+jT^J9-%KF0a)bNy8)0g13H@(D}$E zd=K-1TjViPr$&=ORF)FawZH<8dO>Ig*_9QW# zN!L5`?ML0HZHU%?n&o+{1(Xfn=Is7{Ej~@(%RAF#Cw4U?*4Y=Vm z&aC}o#pN~m=zD>HtI+|Oo5hLlzd+M+|3U(*x9j(`de1%JCwcrCK-}ma%+aBGn~T*BGmAX(t?iAUu}OM zNnOSGwpLH2F^j9y)EpG_)gjV5d_Vk>Zpnzv%Cvb)pO@_tQ*YT`Vt6Q60Y{8F%JaMT zf$WhS3WRJ+#C36%pY0t!28U9sSSSUx+6G0+zv~PODqBe40_pF6dDz2zA$fWK5~J>S zG<#@)jr($ay}q+_wKX-Qqkr|SxLQ4}5sVYTLBl+-H~uh;qQ!QE0ZO19x=a9F0@-YL z3-TD!VKIFZYkkqmc$|`z#8ORh3l| z8DCC4$PtSIEv`#9e1^hXT~#B!(zeffe66qn+hz3!oWal!N~G-h3~+XMxxrBBqbAGi zU`#k(t&MCF6#Q~%m&Q~7A0psx;>j7BqUUl@!2dks$L%&Fc0leR84j)Q&#D&J&fC+` zn!iBvadi$vba%wPsu0YMvPPcSiiv^%!ux+u9qG%{uO`vQaaV-8!s2E9GVAN)MTto> zTx@J<9-dJ)ImX`JtRee64&D|rL@`Zp7!EY~iI$V+P$_^?TKNXRoe=e?}qBA{UpbGhBIu;P zh=G3lrZfm&Bo}!}4l2UC@2&e>qRz|9W=fO@+>~tp;K+RFO4{|dy*^kFb z=o=mT24x_!_40p)0`k=*uV z3t9HrFL1H_wPPC88~I+_61;*rJ~@y4oHDv)g+~a_{Uh`?3jw;xnUK2p_QT_SM$n<@ zKbfvQw}1@ZasRO8hwIvK{`B>U%NH&{Ffb!{#D7t&>BZ@gMtd~-t8?vxPnnwunFY@ zT$5!*ANtpXB1US;J*JEKtkQcV)Gd6DDc^57UAUPS3CMVa^1oOSh+V9skldZ!Gxs`0 zLl`Bc<+|?pW~Yo5x`JaUUEPu@D5KH|IW!8(i$CV!of)I*Rq3jN-Wl=W zQrMQd>s50hUm}oN3Y(qycF($rhLwB@(2h?Da|J5~fv`qyNka~f^j_@8JOU)FjP@5c z+HhG&5QETP+XF**kU9tF&{IMHKJA#Do~fhJ6t{KjE3}oP z5?80=n+a(lIxPZ=^stdin3=7%e)u%C$Q}!or?HJeONY16)>AzlTWQ&up|r)1SqQ>d zU?_w2);depm>r37Yu@BMc>f4<9F2lEvJBD2+irNcr+D^}RXpZK>wnQ7(*Kn5g-VOy z0QB}@^4=m4@$!FHl`UJKF{NSSptEgePVQ9QGlFDE2uyt|>Bd>wszukmE>2Btq^$I_ z`X3^Z#v@B+BI1_ko~9hZX<@NU^!ZUVcDu7c=}KSZbqODXAYMZdb0S50#(T-1?>uSV z%-oS==5qFUYc4^_?%hw8{PF*a`Miu?bLL!m@BKJStSRZbxkbnBjcVZdUOz9*5;ffB zA^PV8&87VE0|LRTKp-k)IcCM`iqevW5S!4ZhZZrWmKm798^nacS^YqTT6|@Hf$*l= z<||DqtYT}{7*qSHjZz2eHvx3e7H0D7x3pH{ArA*xs%fe%cZiL#86M>6vqhE=@%m4N ze=re?;`T`zq$~qxQ>Wn|vQFwmT2B{(W9N5)ac zIkgvDCYj1LULU|E-=PH z{`*d1?jX6sC;f}?f@7+14z*=lpW>-;WY98=9UAt-fY%8}h=Yv0WA*@IkJ+eQ2&qfQ8wYW*g9{d0qbZ@8XCet4666?KK}a5yCVnSWKshc@p>z}baZIq zU`X@M!d_>`_AnoxpQS~7vU%}oa$gjvQl9O9XUM;*@ZHNBo|w|L=t%<}M+7T3R~uf} z-}nKBEgvfk$U5yKlkj&;kInf8Tv21Q-;IIfwa~J*F_kr+$02lT9JXYmdHNdC--KGF z2qyjRG5xDvhSopf{5S3> z6qb0Lw}~7xr)G+4!@8LX60{9gSqDE8Lq<=2a&bXr!K$Xi7Ans%Ertbk2BmZ-Q%8=$ zvJ`>pv1FHOnjtsTn(ppm^Agg(Ozxl5`SPXxt8rLyU@MYs;BBs`nN-6>$pxfy0M;{#i_xJu=-S3CWaVmN@FsS|y z>Z&aLcg)g2LrG~wFxYc`5*zljpfAq=qhTps#~r}*mXvn*H6{Dn9qpn8nN-9Il~g{eT6AMa7u%xIG*z1bj!?jdl1MhMuH%P)1dICMyi(Bv5D@B=WeTMvJQ1 zE(d-N?hIKD1#WxL9Q)Qn|G{?#Pj2GYjx59xAh_OadsEa6S{V87o5aE4?z#Me3V~s7 zej-$9XZy|Mu^ZvlD-56k%^c6gYzU>3sI5s04b>Q~Ox}(qnJTJi5Du^i5A0=o_1&zj4=YTzod)jNU43n!Xp~{D$blIJ~bH|qj8rbu18UH?fAMwj*>2vlmn}ipY|y z3g?-LRGOf-DWTPY;-Dw1!zW2T-O{@nBMkCCUHOijmNuf5UE?(Sb+OSP>t|+mFwe91 zuAxR?!MGMxaReB-2t|{o9l25BDZc#0PaXRp;Ng@Y!3euhfK;#Zn7q(Nzt#dSdjZju zvz72N?Q=xM1GPkIwqinIEZ#KY$s2u0f(1K-&=ZZbBRQe-3K zolqcj30haeOZ+T;Ew##SH;|{1mD(i=Vgs>4k_795A1MrStTzAg@a6qMY^e*v1;I@i zXW~E6TDQ6kp`m61(hpzITZZJ|yPc-te&fL1#jL^5Uc$2MzS~ z{7?qIUW)UYyw_73W1Mkuqe&B%p&}Tfp?s}2q1M~r z8Cp_TSLDCPL}<=V>&T_d@kOSV6*uPk9sMb^Wdd#h{i>5Fh#O+LBM4uYl5rr5j+*(K_! z`LJFR3bcpI9wuvw2#1jAHefw%pwj9X2t8m?7|mVcpyuggbn5}Bqfn~K8Y!Z(?q5Ff z0l!7tu~;B#FCAmjW(eX6U80SFk$wZi0Ji62i0ZCNW}_zN_%SVrfuZ3|SO9zJXTR>) zMPPqH1a;@2XJV$8bgp#0gqGm1Xbo*j0FE3brCdM3dpB>$MM8+ZqeZ%Aet zs$&`2dfPA0V{5~%?kC+ZQ8(+LC~XUCmOBR@W*QXy!DeWVY+_*J`IOo7!bHZCOqr-nW%fUM8fjdiAxUL~<1P8r3jp3M2}l}Laaq;7odZ&cwT zEy<(V^##MYd;KX{Uf%w=nnz0-rp+Hq%gX%v1PKkq$|^ciw*|(g6(9&LL1Y%{hk~S? z=+v0xDTVxU-`50UQh9TwacXT>f_SL(2O){xsFMB-H+25GbD4ZZM2n6;20`8f zv#W)@qLEZwQbpTiyL@ikl%#RDl%ZVQFu{<-BnsM>FKr(3C_T8Gi#G8P+FlY>RcDt~ zYqLdqVr5zhn9xyC+8q0iJdn(U7jmj;t27^&Ds=km=v1~Et|Os@g;V|E6BrGznV3x^ z2$QXcw87d~nJEOw#Zqh_XccZbcBs4<&4_T6q3eF*2VO@mo_)$o+M50!-hgY^Ic5#n zB?H5vgt}p?V)CMVM?tz@3cZu9ER2Jmx5h5(s2=hu*hn+I8rsGB?v``a=~w+kr({I@LK_4Qww@fy+?Vtg$bljOI^cqtaCVXJ2u ziZes3BA)Z@D~y)~vnSE}@UZxBnZ7Kdud;qfQzedQ@^%nk;vM3nRYitR^T)EbwoFF_ zCISNqE?VBa7<#8&^x(?x&lEem6#?$dS=l*T!>+}7bp}i0mZJ3q!5FV})=%TmF*8He zq#^W}v)}dk9HNwn8~KZ$@^&Bm;2z0O)Zx*emnf6?RMQt2dS}ZIj^*yIsw&77btRro z{qDHWSYEUi;nU`PM4-J>%+m8lAY@-{tFF(HQ-Qwx<65<%93vLDG+|Cp_=sPKP=LOP zi6z>0OTF^R2e_JAybn_I3gRxlxOa=93E-V~#I zMUJ3TNHxfa+wdV5p65RiV?lr&Z&P+o@XqVo>#sf4Ys+Rs?(Tk_#4jei##1@YJ{eOY zn>=leA5^;j+Y467s9V)}i5ilm&E;>8Dbn!APVK%W%vr@kCtIhcf?+(VThqnX1{6RL zi}Pb5=U4f~+aP^><#9QfkZ# z&J52-g&YrH3Rd5pkAhg!CA*U_Ts|yGR+QDD`pRp9(oL~8Pj;T};yyTadAEnjcyGz7 z|5f}-O1!LoNv@Au5=#$bP9$||{+?990!#CkV_uhv==5#VbDz}{DPiFk!z@NB%V5Vc zC-r!_s!S8;dipYe+s$bajTzxj53--QI{jmi1<1p8rZb*G#- zy~H)LvJ%BIqzmvqfmjMo6Ss-(^Or3wJaV59{Zys@%nQ2 z>@M~&8Bd-8lZRAi9Ei=@Te>q_#{{nrAAR0J8AdzbLA>{-Ps58AJR(A7>b>Y(Y1jWG zY=%>+Y&9E0r{gEXkNS27dKxryd2>e1U@->Dve5I6yeu*vocsc++$sxZU?<;1hSTh(RlE+#)3rp3=J>%aR(4e=f7vP0{*t`Wc!|6?c@Y(4Ff1$!;h=+H}}^+ z?hk*&!V2d={l{t+fz6Np-cJ0z6|->Nw8q*c zf67;^Y|6I8nO5W!8WL<(?P4}&_Jtb9ZuD2o|K1RC{cUDkUk$4D)UlCSS3_kwJeZwk*tnBJcp7nbjzzG>&Gp6 zGtKu8rBee~W5`0hmBke1tT@x69Nmpg`;W$0Ee(Dg5B*a#NJ` zd-LxTjgE^q(+k+atE!AWt}(5SSt|sSwLDctV-qzywza$k*UBVw^JzMo3u7PxLt@KN za|hy3Xh*LGszv)eu+LrIUisNGFbBu5y5vSE5hzW+F_uTdrwOxRp$XHl%t6)gTkk66 zaE`lBKr;$r>(*QgGwp*S#z3~)&WzN`);UNts)(xveb~ofoL#6@{vOblhC6!d5tisj zvYlra6_@|CH`QZ(fS;Ta!uaax>1F0OF9%zVwP7ln9YXZl-OF*vVRS+@v=>s1LZ{zF z#6nrZXElu3la1TH0ai$V7q7dH-XA>&*QNb4a7FKxMB5#ncc2+s)AM&K&R^JPV~7Z3 zvI~rK9#|eu~m~p-NFvh~Ik7XU@YfW)2X#@K*xc%PSCq zKypqO=IWBFuGh1(>vk+2Q(uy>yiWRQ#~6=FBFen?_sRqr;Amuuwo)xpf)>3k@C(Qi zL3dxr`0jld$j;r9k`jJ0uXYEq8`xm<0@O%Y-+2#Z#PS&`YI7ty980_V20$MQm2eP9 z@d{$`()MvyHv?-ussYrnq3Rz^0<An1oDpqKUuitX(EGRGo1G*c#c zFs$&DR*%mce5B!6ei)UcjvwTE!vj%b2*q(0WZqxzmF#?fyAf06clEmaNb~Gn@o^@w<1MFeX9fIE zqJG>S0vAOQjfEfC83G}>QI8G(=uheCvQ4+P-%$FV<|i*%fcezFMD=w8=`&Gxrmqgq z;RssOzGrZNUI9D&Y_3H!vVMBN!=uS9FroIn{^jD+Z4$B4`Vl#{f7*4T12cQF5bo1xv#Z@E1 z7){qgtS}YY@8>gn4IQOj4Uzl%ce6{zPY)p){94VtNun$D>2CN6sNiyWj&bu! z7K9su7y|%PE4nOuUu5LXUy4YrraR|W)Wdu zu5Y=4yx3{cdAvqAwT(#v+Rh;3?oVjzA@pA3)0>m?l|V>K!~67Rd>3*XlE-$dR9!y$?jc5={}^Q~W<7Awd6ru=C^Q`$hMk{fZibWQ!BgfSJ>B9r8c`~ zQI4KwmxNLO%i7Vf@P5x9-*NM&aryR8!rvoM^y1^Djf@nV5(rChUv*ny^SxDJ*|`-L&!*}qmL2@^V?eyBlm_HfYR%048WR?q*0@v9ggnoJAO^fnPBFk;E2nQiZM|_R z=LmR5HV}px{ZfTmf9?@&1@U58eY0Yltq8FLoL7m*nj(17QnlCF;|={7(|)0+q4F*v z$;7csOk+_`XnTu+j%ip9ooVO#c*xHc$QxI85Dy1LF(j~;V*rw7(hXs;ruE!TSV`c4 z8JJ}bq>N1_k7?VcfZ_*jln9nr&{(6bRm%CT62-kHZ+SxCMM;$Vwo@Ut7BW@TF&E6#)?qK+H!;d%MK_VtIaKBthB!z9TS_Ndpjoo^Nl7 zKu`rbsjJN-VR7-NjEs!4lankHCq7+}Y2y2<@vS3x$X}P2u8K-Zcj_#pvL+k#S%Sel zq4R@|)rV%1Z4deO1$f_4&+WG!k5>MdCaer`r>s<<+brLStXIq&Wkmffsdi--Si1Zu zt(2?Li%w9Xl6x#7f^&&YTb&#g8cJ^)TUqvnEp`#rU=7hr^4hhRiHU7_u0=wvaf5?~ z#cf&F?(LSRw%)2RW0KKD9;WD>o?))CfudI z>P8QKB=jQ_sRgDk@NV$t#0UAXka-{Gyr0@CIx!>A*MKcPE&3tC#2yiVyCNkco5oCA z5|m(q;&ECw;vY}%HBxPJNttsSW&p=SiBT=#YEn3ufCX?E+|YzNDpG$|`aA%YlJ?3L z3D_$flLXR!6G9_h7IIT{t>+=Ik9P>SiPgfqB0(DWtLdR|sK6F5NxB+KMOg&L=7uRa zvLQx5rR$+j2@BHs;6WNo&<5(2`oZHkF$ZhmU|Dps%BQu_10#)ObSi6tyMnXu)}~E_ zLh&}5G^55$WQI@Ive+AiF^%wRY>+6Ny zL9&xlQaEI(X=qe{+onXn!e9!JpnczN_rCt^Achzv9$~r7?}rYY_VJlfn8Al^Gn6<) zs%v*)r2lzrRLS734CxPv4CxOxEs?u3y;+?!aCEAmIyYHlizsjW14>+kGPFAJ4pSd-`l z05tVdaJ)fOvvQc?^5fhT_u5!xZxA}&0r5&QOKe7k=7_%ddypfFbAZ>lv^%-HKYY}V zMWGgViGOseIb2+*)dpu8_*+lTd7iyBvTg_Dn0HL9Cg+`=rG9|sI^SLEQ&PRAQJA1} zADut>8lIy9!^;Jyf!H$mLh^Vux5ogrb_O9Qgp`VBw9*}gLb3Fw4B8HDmE?tx$uWZj z*$&udG6mq=n!ONDTX(|IGR3aWnCyPK|(#`W4pXS2QT^D*mZL#x}#tLs%Uem0&4>Ni# zyR=N%NuNQlXG^62Jh4zI->A2x4>Sary6rfR!}Xs%-7mz#mb>gIni^BpotV7cZGjokxyW}V&fp~Zr>tHbgrB##a;y16GtP4x1s|y0 z?|wP-TAzg-F3qqBUT27gK!(vpmIb;{&(EC_A@)E#NeQd+pN#8c;Fo_$_4 zjMAdYY1It*9TVuuZNAs!X+PS^LM1vrK!RM2;}3{7b!|2Adz#hF;J_0Vga^z_*pc7& zS-sM5F6_OK2ypq>{M%61Gb4vCeKi%zZ(J0NDJOZwbFex1U8u(6 zyPDLM^3BWaKxk(DOWq=Wq25aECV_rFAO*z>P3t^g)e(iUO5EXMaU%VU=F zI?p&b-X$jjA9w~X^d&Vz<(VDy>MjL>cmWos2{t5ZYStG@eplMr|1>Q2_tYAY<(>Vj zI4DRNrSIen(Y7s$^79X649$#<(f9f_&2&9IV7D7l^P(t>xknZ^7E*pp0&ZXowH6K7 z17)@Pru#ID2tC0mYbr=Ik?_;|v0juoZFYKf`pD=ozLo@?svoNNz}mLSJXHg$l+I)o z8Iocxx*|W*;Ntar)dq}jt@&I8Xai`%t;BE z5KalTz5o$hCdcE!6h_#%(-RZNIA%sL;D9@!T`{cR7*5Dfmnry!lj_Wt{1yWiWlgG1 z&N4b8zZ!+vkiiH{=*vRJ7p}ug{XDy!?|g9Vw^va&;g4_j`QsP_(<|f&9j2{D z$|5md>f~I0;w(I#Z@pj1_t#1-B0Ubex4u1nU_b@vnHm(#aQV0?u`l`vzFM*T%c+d& zifO4gl&+GXk&ArX5&?2EGv>1kq`5j;d3BZ&DO$%3P*H}>lUW@lC7zVj(9-ty56vz_ zfQgy5H`40e@9&FzUde0DK0a(jL?VL7IUU`k9PVI#2WxC3FtM|TzU99@EwI~*s4=&d zl~jTIwmko9$>zfBPz>ZN4L}e({09YX8x)Q2)Y;}j{bm=<`6P!-mg3RK(PKRpVne0Z zg8HntYE20G(j(-b7jB3E(xsWt<1aQpEy@e-ljLT!DJ_?~xW#>vM zpB_n$MSCkZ3Lp^a+QkeFUSJV`dhl%Ez_uYn&%=IKit0e;H0b`(K0FA>{;m#2vPPVE z>pYD{nb}Ur{Jlj3*kL!!G}O3Gh53DQv+D}?nYBTMJ_8Iyo6AdHzv_pI2dkjH2Hog? z!R62&Ycni)>;0{t2Xq>x_FEWQdhVF$`H+|iH40&l#CkB+meSr~Mn!(LXr9mcS0Ni< zokvKpb@mjQd;Au zO}rx?B|$nygBlp&Rl@M)XT!ggc{XZ2&`a;v%jZ5)BKt%XR>LOnZh@o-YPH?4$RS)L z>S;7$7k5=dZqc_1P<+oawyt?1b{gLmUWJ6ANRW_wrJBxmMhhU%*=Rj&{qU;$YJ2VtqyrKW2^febi~08a z5b%6yZ>6~WAeXR~3g4vo#`c3rApY&nTDj~obN=?fu&*s^=0qRkfElzshjc%bw1q<)L(;qPxp|ghDu=)aW8$1l$;)PL zZXUwtPw|&1?q*kF#Qb_1xiQh+V_q}B1iF-OyMtF_h>wpWO>bp1MoA@a+|px%=|!L+ z>r>I1DbRXBK77!dU==a)ctD1ji7}5bjb!@Um#o zZD&-V@dN%jKj=5111eoPrgFaLZI<@diiB*kPE&joxMR`dkGCf_>!yFgDdKX$#vdiks8qU-4CZz*!x zgkro$!MdUIijBO=@=#)@`7zf8W_+83KNHk{I_h{s-Xj(~*Xl~Wl|OA4WSzjix>qMr z<2Y(k&n1vX+2G$`}LF-{0??&Z$2; z_ukj*^?W|&mC`^zyec$`b4vP$aIN;5mS>P~!(;;#eXGpP+o+ywAl+I;?6zZC15(@3 z{f14e66Ergz$C)?)~fq@XjUctAGBUr$qXt zOIfjv`3m@5gvX?8&P$Dz8z>)1#m2@O>7PBxY_b_@jReNAI5Og32JO`q1IkL}n(SGf zI#H9ntNY^76L+uyngz(;b%Mn!Y-2)|O~83BEG8>(_@CPR$*WGT{;EtC0uq8O@W{X9 zN{5e)lhNi^g77OTeefo+VSQH}-lq9bu_@X7*iDmkFf**L)Dw$8cVXyvOQy?jXRiL= zQ|kGAVxRkG%=;3qqIwJ$)&+a@+E=Oo)-m=Pk#(mKxf$r}Y`TiL^kVX6M11_GdAEqA z!B0j(vFv59cR#hGkZSmd(DD#s7^LNKkOym+nb3%IV@YcsPSx4%ohK4oBdOd@(tV;v zPr6do8`H5;A2JU2;4CQ`J@>lawbo8FSyQi7(LUXuA1?%yybFSec0DT?YsV`%Z;DZ; zGHAW(euvT;eF3!>S&UQQ?Qo}3J8MNjCMQBg^5#TQ(9nk$^x)45?@_Eqd-s!GpveuA zI`M*@uq2lL*JQRsL}wLCmLZndI!!V}d-`lP9nP_=6HzhlQk^e)4c5VZ8}1QeS(9(&7;{0rlggu>< z{U0O}De&zw*=5h48fkl9g~jEp0>P)pS2zB7MwB^ofQfepg2G{9 ze;*71hiV*z>%NU%*$C?xlC86&6bFHWf&`HffxYu=;|#r!lYfyqO9+m9aTT1oo3502 zp)oy`$PTEVjdwr84%rygGo8-ywh~~dv@P_c$yH+JhcI7Xk|S%R#h+#@WuyBW7bwy?}WOToK;|8S(yvXjgO}#8hTlprp;7efb_XLsgr-d5=K6D%69ph zGIGmW@W+Abz1m;WqM4N?Z-ytI2gxfdtJ0hU&8(r1T;0wXn$<=>2Aa8SQ)?}O7j4%? z;>Io7e9k|aFSl4)e$wk~@5&4+56K2+@UwNv#ZJyZ+r$1?N^0~*W?ji0Outx=@N3ar zlo_L0giZn2z~NoDs)diOjA8ol%xCCPou(aFY0U09v8OQQg#{S$S+HoxIM)QxSsoLv zDUu%oOND9m9amoz@nr6WhENMOZ4O*aa;GhJubn+*9{N!QR&0!ij9x{O^tcKqNc6>$ z&Y`83bD}|92E35xYA7X*Vjg9hSd$uVF$tX~>;$~n5cK>QZ;cXjOb-IfEt72&={~&>F0R2ovE}O? zbdSM6SN!zGPn+k%GItDdn>6>7_K$W}8XtW;@j0Ya`xtH{|MU=FZ?l6ldQuas&Uj~7RMrXsoB3|Yi5t8mpdW=Pnl2D_XO*>tPUxm@T=S(ER z(hDSYMDo9ali_sgR>qxgoN+kS1y?PM{p{Zc<;k2sFlOUmHe6{Sfe+Bdp7Q6@h75F|gGT`YPb2JlNLZ0FcQ=Tc^{61P=G zXCzBcCqUUEvV#tmu1*7n@7JMpKuQY&4qvP8{XV;!-0_-l3qw=_<0pU+MJ24s-Y}F# z1@s6V2moqua7xKySk~f(H`^dg#}gp8!Kv*yk|MiDY*Qq53s~B8)A&? zxc@9g9U&%^7{0~sSNEm$!bo+-!NHu;NH!{>?)yIP?*ZuZ_}FFk7T2DY=bOOCYq2;~ zo9~Ya;@bA`N|ihOwyIy_bOP!6de_Bq<&v`_d{bK&eMZevjSw&F#&N+@&@oaN_a=Th zL5=+S(~i&2ZD%9X=h3C5L|9*S4>F64 zlq7UjHhkmEP>fD-^HV69bl`KdNl7xKwz;XmZ&981W)`uofof6llaCpCO}p0B&XFfugWrF`2E>P!kbz5|#}#W_bUCH#_F#B+mp`Ga{o> z5X+Lvn7Xr4h6Y~1TRoeRM^zQWL;8^Wa~9nQBXW?tLu$*cx&U|tjT;x2!@Tf6)Z^-( zR#0sTtNkom5u@7yKHpZ_-jzEbFr)~b`d1v@G!g^rR0dB40|1@HjjBs5jn9si?IS`g zne5jRqaq!;DXAQ<36VQL+=n{uFFAw-_MoU${2Kh;XCylmAndb6KY8~jv10mIoJbem zq|?O-rVK>>Y1w>nr2g(=DN_A{Q7xZ>Wj-UF zF7}HX_L(Lry+gVQW8@K+&a#^EkL-TJ(MeLqoCG#t#L9lrZDMYII#$uVRkwxZYznw{ za`+qhb?M8eAofKsT*+g~;0J8(5XXR~SYo-3^Y5*5kM=}1x8}75h8p&dr0%;$<&|eQ zz8xK09Eg<)M1|bS%E&_lsuTMi3Y9i-3v8%i=hdi6&pV?Xb5Fl;Xq2`X6i~p!{HTlF zo}=r^Qvx^_QSe{Iy1Qph{(v+*Rhwfm4C!$=6cbL3zbIZLDemG(y6f!NER7e4LKn5W zSB3l70g*W2?4?m+rR4JttvUM*YV6AgLiEyH87@^?V<__W~q8;utf5%UnYS;;JFm5_G{Tr zWl9puPm<@wG=pgEn74^hy&r$jHv>y1bD}3KSbunU*!0ew{%NmsTuv=Dh#);r*fRCl zPqDr%+qDH!fTsFQ`)jj!jsA>T)}sWCRcVF(d!fI|G&}~d9<8GfTK^*xwXX6zeibVl zru*rZ+2GD1?YR*O%KFr1m?^$y7Ty1|6y$c2Cnoi#Iq+Q`*45pTc3M`$i`!$Pw9t{bLsDB3?57 zrY@aXYT58hy4})U_DdP^X(5&ZYL;fP^e4`)+eRne_?{~OcSnutvi>%TgIU2YWilR< z_4a2W{AlrHL_?6M@;kFrd4-5Tn!tit@jNL;8uJp3jzwse+H*deZmcZYXbVeK)f&=plxq|G7;>cpt$pc>e0rj!sBw&&Ef$)Bu?6|c5u-7QvdH?#ha|VeL;lQ zP!?BsJJbV9+veTq2i{r#yjaBN877tTSG6V@W>vl z4C}sJe~wQ5h{F7Bi39hN=R98H={V^i)&V8qPH8_ON>`TeHGQeU}(|qV(o#xON18di-z1n#;+Lm@JGNxBH zPj8`bO)gzb4HD^!*1HvlrqweGBpJ_9lDriau9{3%Q}qH<#>L~R3nartW#*bMJ&`Sr zLNnxgMg&G^uR91Sw7_y3>M5FsIFf7ABW+Y%dyX}LzNOwf#ilY)XUd8X`G z^XG4ub5R=4-~FZ_Zfp_?ZJ@4z{TSl2ZBVh@?~!hNG|j;&cU_&LOP?uC88HF`X0#`A zet@F?<&{)_*gDHXcO24WHoa)K=x6k>kp3@d%rxY_{oa-pfgcyT{oN2X3T~H42w~}n zQ|f9Usi(wCu!bS+bvsG>VhT}OO*lWkgY-+5gHZjLg7gM8K}oo;fH`xfh!2cVF^ZVT zzP71Ik_9TDsouc*1DKBG)YMzYhe~#m$cRa7?*=iEs5o^Or*|B;Jv}pKGYaY5$qHtk zf?P6{yy&|!>z zO>-VIK2+TJ8Op+{!^g{&$CUzgq?ranm}&K=B*c8>)h>G-|tgb2a8F*>Mo;Vbl+VfY8e#lphYb}`kzccan!a|?- zz^J3qPv=NT?}sZr#)BWWLbH>@U8i!~JvXK__x;uRgh{$ zhN*yJ9yga+U8O45VH>!Be@M~&V-r29r*R|!E*I35Gev3gm@D|HNh0lAJ^KyVd%zwq zkbWoN)M^JGGLi~X5D2U8g-fBA%|(Q@chc-CLFQ}n89Ckg-30BrL#cUhAG`gUN7?ic zvdD`pg~_j*RTe(O{eaJ3JJ!t4otBOfj(*>#pSJ5UkWpB0#n9bGO8Lqi4n^cS+kSk- zU3v`TK>9vGCE6oKq~5-Mqk%p9$hBoMN1rFaV&Z*befTYLQe-$-jSnKthjD?h~`JIuwaa`#ln33hV}}SV>zHQNW#~}V~GI(t|ZLF#|^-xWh{Vm^=z!Z z|4`-lU3s5J*ti$?3-Izvp~8wMv7blHZk!cUqPKGb;(f;BPe6d#?am-m?4WQ8@%cT9#g`6GC88^cszDwcIytSzg%T*ZqvP)Kd9z$%6(C^NpS$)z&lFZg zi!v+pvz1i)rTC>fQt6d)pI<=|(j4fV$rg-F%xEKhdl?QA-~EwJ&Z|{7z7idU{Biub zMsE8HloC5rzqhN+D%ZCVTucRL?iIa5_5<6!cK%ta=snW6DWY@8cyYsT{*<2Eo!3XV zK}N>08b9tv@U}&;#*-&chKThs97xKL>zI9pAT1^5^wpR}gUZ`r*MhMe^w|=B>>mmkH_^$e%CcM$^3e&q&nv!(JMOsN)v2G5D7Wwhrksl z_ihD!t=5VomYZQRekQ@#Ks?2ww0m4Bn2e$*B`57AcwN{;c=tcCd_eUuc=X@;Ii}kp zfY*>HkQBcI=d@h-2r1o{6iw-QY$=?!TRV35(!-^~w4kbI#GC#2vuDpHPXDpJg~XJ2 zS>b*)gQ$4>5HsGiwDSI!FD)1;pQdVl`SGSN@kH6l(&l>X6vo-*@TG=;{PM&9aIf=P za<)y$<&!T5`UgKrIT+pGUc5_=egVn?eU4X5Fq%vkzY<&?833U! z-N6u_lgo6+4WF(I>++J;(x*J-S=87GHc|jcJsJKiJJ|D?K|Ils!DfB}sEsCuhW zW~FGEy*=I4N%Xa#IS+SrAIKjRe|tK0fz6OsiVUwo9n#zfz@{l4Z>$voQ>fCvEPY>F zgb(u;qVNe(22t_x`Bf-jWP|i+2Yeqt8TUh~(xJvk8BNNN3l}iPRb4tT%jX-nL$W}4 z-3bJrLKDp#H#eV;Gs(7}Nr%`kZ*5X(07fMz(>)wC?)V;!5-{=A2L1y}!cT zATgyK7U%R7X3Wmaj1~}AzDDJ<4?yG1U*!cog@Gi!UQ)eOyZiQX69ma?HECNHClVpD zHR<>9&Jz%k{q3%*<10Yb45H%|dj@aUWnoB$)72$gLyi*lclJb}#!pbs`Q(GakR=s& z+mv6#ip4Gy{cWYd(dQeKE*64(_7t^e`rp+Sc5z??t~=^se#Rh#2@Cz0BRR76FR87kfUxt{laBguEj7_6-p8>CkSiFnB@YO+UGyMqzm|pJArACSUF6& zh2+eui*KPGF`p1_7x1tTF?6?OUO>l0Z*`9&pJ_X^k{LUjQQ|MMIwekh$O)e+Q&ri! zwotvalEuG4MK1^e54?Y)$6?D4BW}a@X8e6MQ_y?#q#=Rp?_&B4bb6o}%g!Y^Z z7R2YX0XlC}BJG-HsV<4O=baJMOkxIXq(z+XfT6Iykyy5PwG&)avY^pdfS=M6mQArp zj!U7bitg`$y9nf*&&7_Acbg7O+a^eW#4~KO|F~l+{(?wKcA|Qz3z!i|83TWWuJ9gG zpmzT5;y>B*ey6Tftt>i)-cMLnM45>HQ@-)7C9t%tEK2z;2X@36=su{PESuz5okQ(7 z4HIeeUiegKh}o*q(%K;1lmFC~Twa)#^}-%u&a=h;eShLN_`mY-0}0_nGR_Vz0hW8& z;mS$q3Zr6;7jJpqt@0`FvU9j`Dl00hoxlD-7UXYI9n7T}4A0zD@R(qKS(FMf8+jyj z0dw26EH`#aIw>jg+H2tOuB|CgheOhuqzqa}M*>B#6tN*#d|U=<{ncgI+Y ztY&#Q!Nbs_1{SSyxA+v6OY=okD$c~ETVDf1t=UAkQIdluH8RbKk)*@IfkcX*fM4JX zsN+qv`W{WHKE(iU2Xr-765TUMAqPAHzWpy9b6`o#NJ&NyuL2~dxP;Te#``>{c#S0c z{14$Oou;c#mEa<*w30Rl9;Q~QSKsa*LNOio4H9hr@LaNiYJeHea$RenJ-KNI*JXke zFivZK5gmFO3lt>7B6Ly6W-2Ox#)88Fp?14ZSE6O>MBEVeD?<5@XLY(QCnGcS!>sq3 zQ>y5uW6s~npNq*khx||Gm~-l3IHhIK7_Lpa%U6s#S8lo zStDuxZAG`oPZ{UH{g2E#!E|1`D4+C{h00-%;#!a1n+4j^2{x^chm%@m{VlWW6j~rQ zwfo3_c7hS8*eDMmR{FNCJ6H>p*2Xa^SFQEc_gA~HX&#`A4&T>^8;;|TMw7FY| zrN+fBdfrWnXw<@K3iJ6zx!1qkcl)>$%M$K>6pDZJrqqK~hCe7GC*HYA;ZWAvOVLoZ|y*}`s2SQJTG`SsO-PQHMj;t&!G zwPXQ8^2Y;rYkkKp2SbVivcYBnN_7D-ELIqv{1pz3(_{k~L0NGIHAZ?ig53gN1QP)Y z%2m2#G!&8c;*!WYm?xQI`uI7^5Cj#5Zpa`lGn1rjd*K>K9=Wsk$ERwC{99d@83V%6 zm{8EmQr_#{e;Ns(KL2Mv#e08a{?S;;A&7kp6~Ehjv!bGc>41@irRQUd^<*^&4V=2v zA=czMw;^UZ9}{V#(c)X0$HZCy3Q z7S9{8cZN@pUi(--_vR;SKhoDNzU_t%Z{@|`$=z)|;g8j?L1dW?w|do@tJ5p`Wy zs4uT`&Ywtpo*Y#5a^u4Dy4RIqeyREo_kC~)l)a)^JfayoWSEO(y{}5tyXW(Jp@lIt zSI97ejt<}$?i?GjvsWZY_OJ(4MlC~Ah=4wM=|H3c5Ez1Hmdo7c*b0S7K{U!^UQ;7g zzKnVZI!*FB#z-Oi;=F`= zNt8%0gJVl~a*Xf|rqY;H^iCrR(+_8c2d;Os{>lNI${#D#&i3oz7Y7FiAlf!Ebg*N( z&-zi{tHJoQGpTo2DT=130?(fshBZ*zZ8tfAd;! z)EnhZ+0Q}HOkGiq)ckGA4mY;It<e*tKt6gIBTJ`pAsD&+FG$*PlfSCmH4 z@7#zNwVcS!m(9%%t@9~4s-F;cN_zA8S?esutV{`STeG34hT;4 zj+k-OS<`yxm{AWZ=!D3H7RAD%aVclv(RY|huP!6ah!3_eBfYpJLdDzDE~d2gi8|u0 zVZL1!77vaWw%uZqGpX7Se4+@%h*tKf+^3S0a|A~ za~f1j9%0y&VuJ{t zXwK#AfAGw=E94LI`Y8h?I3mBJIF3%zv932dzO9PU5W>5{&g%AJEq*`v!!-3|+GlyC z)Q(%EfLL}NTu}K@#Or0HB6*EiW1o+My(y`2v+uPpU^a`{uyk7PZ~^}DLL8$C3uly|nUV(?4J#+q^%eg8SV^HV{8f|j{T6O6T%B)$im+|Cls<7$8kjpOZl z+MoV}%oJ9nO7iY?alluzi4QIUke?4#XC0dU%wc6x*k?aG+qJ#MmlOa5tX$`%`@mZ- z>A-Hdpmt0aNJ)gNvpvMQHkCMsr z(!po`>+cxVslq>)8Yc}5E-+kW=-?7Ws|)klZ{+9mnRk$)ZC{6gd9BwD8nrKhGn7xSgyeOqpi24veJ4n_J*g~$yV`#XOgy6u&KYqdUb!9m))~{?%M9$9f8j_Z()iaulUX&-vK8`$NfU-s&S2kViz55qcE+-2`H-^X)F zI6+7_+T$eOtyF>Mez?pu!LUMstu7AnSl@QsNd2Lpj$Tdsif~-|_YoAf(G@*uf(bG=y8ST)Mp6>lTXQCa`%ffGPn37;UF5i7$1u(HqVVq+_FsIJ4I8RFC~XTG(UZ2@*c z@_Da>P+!poJr$<*o`9*ft?N_R5Y=}!ys@CPq9&Pf#PBBfrCsM5FNj0-D0bnd zVpm>nezLK$do|^SogFrBgDq#rUveFkI_Q5jJM7&Lar2)0Z@#M3mxF6H@J?vI`Pg|w z?p(Le?YO$+)gJkNphUV{nq$3e-LO4C)%YGI(*(bH(QF)g7aZ{nA#Zb|@Vf+ZvbuNt z5ptM@eIZ<`r>Zf1yXaFUrj;STqDLfC2gRof{D*uB=ADYvn!@HC?yh->sa{J()_*SB z!_jNy{e~b9QV*+hRT$o9M2E$PW7Rd)cdi!q)L#SR6ruJJsku2o*XFoLU-rYQzUDEI)v6pWiO+lW$@6^b*h=I%M-~0qU7qKE%JGM_ z9ZUEvK!}Z>iI5K@EScQ}(7Z;Z{dJ=`&QM(%$^Xqm!E$S{c z?`^x}{4uCNtlM^`WmmD!qcB>F+u?mZDP6@CK}eiZWncHdd+C?_1!^s{GWLjN5+vX} z>i51KN8!)$=yl&W*C3ZSc8~I$<_|du{<}}KmJYi$*9E1X#;Pdxd29_M2WSA>id7l|WS;M>O$ocN+k4x3lV`8NUUvBn#Exok< z11o4fax}F}zA9+vAp>Zu2@QT`dH0`3|15XLyQ-~W+ zt;l?bXj<>Nw6}9b2(50J9}z)yO<~WxA3>38_ok*z*#;@W{C*~D|7LJ{V_Vt{;gB{1 zcpDb((>KQqa=&^xa707uG*JbuesQ&F7*-d(0Mp?y(>_fNskcf&V!wtt1i@ZztSc>C zEQmVukP2Uc4bE*Ntss!IdK^e*a0H~ozSwRwCXR{U4c35@B)uHOJj>Y=(!qqs_P%cERLX=o*lzg?A+r#*zw<`s)( zgo+Xwc?Du`vsGu=M$ViIu{?J+))aU>`oNS+MNKfd<3C$TwtPuAKWf?kIZ;Ab-;SYU zrJ8a{LM@oxenSkdwx(594kG z=;Y%*Y4uA`;5vBJOj}2ssNd>sXFN<6yB-n@IAehgw~k-h+BcSX@A8qGkm?_|>fh>i zJC|mxslM8U=-TW=sI>v46LW?ExVoMZ-QuI@3*W#;&>deJzx(C~f54657KKMJ|)r(!)dmiS0JbF$3zE7%VuXRa7VaUX8ijjqvG=&-8>Mx<< zYN9{!gMQKHt=sf;n^%>C-*x{b9!l1$L!`DeyWjD=NaPWde?~^UwFE4W@JkRpr!hql z8K!ptj-NUU7jc5?=2w?N(`{2D@`lRe(k+SaQK@8bHM&AKo7RiRi4b^TEqVb3S98g2 zJGm?5sIEM|ASQbTZwK13xA3bM2G=hX>!l#9!U`*lHA60NR72<>Mn3!XX9+wt6p9$T zL!~c;uaMU)uQ`Uiaaa=ZyE$#?OMjVQ;w>0L(&|?~V;ojCn_(CJY)?1Gl9D3*MTcmX zZQI3T9u!!*7bLq#dJgLB`NEFPI55A*cllD5&u;!A$M{7=uoq#P%x`^@yy5im<6!t#R%h3` zc&uh-No_tpLqVscumv~!DoQ?3r1TZ<3rW8@At8Z`r%!Qx6h8$={rmaFb6Ck$X0mzg z;PO}SlGXMlmDem5QaJI(KKqLZg@Cf?f$3}SQRc#)mf&|6GG2yG&3#0&(Sdy$OU+pL zjW4~7#7Q;%UDY+If_^NE89u+kLU3Z%S<&5&X8u3PDeJ<=pvwNjxN3yD@cNM zopu4Uo-A9mJvv~{*?Yz>iGB7AzPS4BJ!;4LyZrrrR1P0@#I7WCSgy)vRo3r3Mb$w+ z>TUisH1%)w`tt1XqO6((`w-W^o2llm*h+UDJCTzDAP$eT}2uJLwIetlzkMJOM;Kwjo1m>XBAvwZ1QUebmsQAF?M%Hae0 zw>0!6>YCKi=%Tr0pxa{7!bxeWQ)^rCpvo$REK($`P%_guwG3ACH}CJ7oCM({i;NyR zHu{2w5D=UMDx^Rp3@9V)5uwrRl8#OoB1(6V?9hXBytcMnLKc(N^Oz;Eu8Eo@OsJ$N zm>GpOt3d&Usa3mPp~TB)M%S!vnn+EF3wcoH^6Dd0vO4$FL(=cqAlhL{anH}D#50?K z8Fnj1Fx?MB_?!8L=BH7L4Eg7oH6BXJIQ|1-C)?*W#P8mB>d(>CyU^Do7fCiWdf!>% zE!*6vtJ=7KiUMK)s|1YxE>I`s{llNi!7U-$IV1S+xhVqk71C3=F$0+w~!K z#A|__+I~ATPiSOW){hgccBgB^L4RZHy+-^$2ZV>WUHr2;rmdat?U()vn_kK5on{x0 zogMyFot9y_=*7nFcGpr~N%ier8wa_}qV7UF`2hQvMmAF8fiQ^p$ZGssR_c|T5!E*= z*aio!6DES!q>Uj^f%_py1-09h(`Dg*UGsKzLM`LuFf_%>OM^BP`X7Osx8ThEGUFa! zAxAxCIJl$wfc^F5hGTsVIpMbeh|LQQ7$@1j&YuwZ zA@l+{DC|m5B0%O7wBJ2pqgKlQVHj#9sFvJ+nLF>Q&pNHr4ceRaBuU)_!wZ?vbZVe} zpn9pHJH(~g+)0S9o#fm9`KUYu z#=DtFVml4qkxA@W3Fb>-EChzMvv-M++c{WvgwGxN;KjYL-sezWOB3+ozLz#-dq46& zqv)HjK8;~P$ld?>y~ij`%Khix5(#J0psdQ1q%))*gZ>q%NqqIo+r@eP{qe!l{_hk5 zYthoq*Ym?-KQYdpK9Oc${kPc^oQ-A(}k(`$; z2wD#jr9|35dLS;|rvvwlb%>>+F(59Ix1|unf}o3p`S0T4i=)p?T&PrWMfcxvmZ?xj znGW|@Wu zv`gh0Y^U+0f8r8K2o0Q9x*TD`!p!{P&7%IdIs8K#GK_HSdYK;yf*C#ic~jqQ^cUJ; z1x*C3r)T+#aO`INQu1(Kd`>K~8Y`(Suw_@0Qmu6>bZL7oi~Ck2+W60e|&|9NbG zonH41=W?iD<)!^6WatwT5$3w64MB1|$j{+Q(V@dmhvagp~EN+@ew5G7nMI&8W0-`|$1$xGb!sVpBWp;3dOq98Q%? zGE~xRATnF@-SSv8+WZ3wlPW1LiGMMn_4Bvk=}%>YC>7)hO>1Q$&16`f?gZ415Li(C+!DMFfFb51kEz^pGRd(REI&tLs(mU}cM{p}}<^)FkkvOJRhI z=k5cRj>&^D^suFK?_MsBu{4HJ^6@1y?j_i{pw%i2U6GFSI}fJdv+dA7TcYoVd7tfd{xh^;%v$Gu-uosYEX#SzxJU{)j+dcWvAc0pp*ZAgdY`h@@>%A6}%;Z#E|}H7sLaqD;_N7=R8`e z>MhC5RhipBpdNwrpsnoP=7{4BiIb$`If>&=&KQ}_gMz9!8g?(Nw_Qj|+zl+=E!Dlm zG?}Ao+$*5=^vdz$I=$`3W`_mc-fjK|Qj}8s>c`UC5O$2@Ry56+9vvEec*YTO?~LRk zdr5_a*hj0#)%@;4o(bjAn_{copQkL;%xW7<&9l2gsB$5K0+P6K zp<$&`Pvvd-kM!@Fch_t zGSSGpyj+oXnUD_B5$>?%R;|mpdWsb6RQggExeGhOqYWYpycMV~M?6oa%w=$9fDu>NueIHOz5J<~gK)!#wCt?f{^~|MP&he~V+tVz-nVP8l48oI zvn0}&ATUPbOUZ`mNpatUh^S9E;1_M@AZ(^BA5XYn*+Fla$2m*X+@C#Nt=lYMU zy|IV_#6;(j9{fyd_(RMhmztE)!N*r2AayuHt#BPNw>ygiLiJqeEgSH$3XLHca?Wr6 z{F`@p4GKyKsa2BK&Qqp7XxK`>LxxcpBPUq&Oa2A%k%M4D|4MWDLC6i+)Bk|tR=Qnw zdYjLSGwsjv1_RUtgc?stfOoaiBt_`EWmz>g+FMWVf`L(o_g>roR{V@An0=wiGp0NG z>rH$msmbA=pQhYvcuAKd6E;b%n=s$g+uV6ii8-6|Vjzn(|l43VUK0wJz#3)_-eX&g7g`D2uqgw{@Vv!d(*Si`4;**tf$S34BK)#H_R;gAC0J*>#`zl zTA|=14mfA|+{Svnu%l@o`YoYX4}{?St!(pZl$7KmkW`P?B9!@0=D=V$^g8&+ZFy-a z@y75?r+3$pA~Ms<(gc=l*B7kq&|urK76$S;FF|ketcKmpc{bwRw^#$Pe%f(Y9%0Pc zB2N(kJisxH<{nI)3m*Qbh~WzlTU*7-|Ikcf+|m6aJL`R^gRA7Gs>t)SJR;{<1<~2o z!c5Ka-fta>FiW)GHR-ozgL6Z)oC-c6MGK83O|PR|FGZC$7A4HS3eh1B3tp9YPET(_ z6*Fh9CJRvsNmjw;s`v~*U?I$+1(Gs1h0y|>__F-ls z8!6&xf5%W+4C51jGKYBR1vmwcNRsi>So?mnv}O{a@?t)UDtzT4>nC!>t2OCeh5HR; zj}{SKht=Let2>blK8tGy9G}2(($aw?atncSgrX!^KhN+Lyj{a_t3I5N1Y=C+dN1-D zXkcbr(j;ty_PLH{x%gi&wqU&b1Ab3_fCLT;XW8Y|_)xTi@Y9PSV=ZgF@pf46{xyn< z5wBD?KsvgCEfmh+wpZiEMynK`{NJQ_KZ@adY?xO~2r7qCPECZC$ma7tHj00xiQ#St z#pXQ3g# zQ&({gU#KIDCJO_&t=|R=_(vOXl4bRex9~h8YeZAcK7^lvqxxRN)aB0H0uH3eA-a#{ zDn9R{d%WyvFKk#k7eKw!Vaf5{%*>>Mw@`tYW?tS4c%D$>?jK(r;$TCU%#!#fB`Nc{ zXV9;G`&4J}tI|2c{?kBkvWy7Zq=pj!My$TkYiAcagl1uc6+uAKwm#5%L!V&2_A4RX z?kwSNT*31|C_lsmHQR#(r<|=jjs8pbK@H*`mw+Hzjv{9^0K|;g}vkz z66)66c@Lq&^p8uyZ8Xr|Yo(O#7|=NVaz1vx^g?c0U-3xbwCs%@jFlV!fG^BKAQd|N zNv3PPDZuwBI#*U013GU|bzg_oRF&Thx?vr9?p*%rd^Ic`$cfT$NZ$99YK0a*)6xrL z*sgG19_XuFNY#)x^-A{(f-zkPqgDheE=*{=*;SoB zX}n@-aO=MGXXziWYwnwyoS=}>GG>FviJ)IzB7g2Ac-B{8XG%X)~f|??5si9;sOV&fY|O!J6CySVgnbgIiWOgRdJ4d#ED#pn_v9%m`q zCN9bf-#ou|5l)ei*S6AgX=I{i{|dMj0pP~JM}fl5oW~;DAUICpF%eOc1rytB#yB6m zNmNCL6+O2`hyX3&ChE`Hp2Cmcg2x+Inxr}MdI6m(D_pGC0n0V$*Cd=C!&DLi4euJG zxGdJI2T)Za+Z|mP==JgYgFF*BpJHLm6|^@?bGqrK!G@e29Mqqla=GIe2%rt6jxRSk z?nx0ZkvaXkJ-UZfOF7f#@x!iU@&7RO)lpHd-P=RgfPlmd9U=__14u}YG{TS)N(c-k zGNef7(1OwkNS9Iqf=Gkp&<#pTNQt!M_julOe(Te<{LAH1@8{n4-q*guFG&n&j+#f8 z$4gQ_VeU5mJ(Ldso~-%bXx03L;FPS5;04&31wDW?2_xmK4L*VZo*K=UC8l$ZA6CI| z075R#tuC}LEUte|q3xk-rYe36o)|y)iKzA4Vmm%E9B)I)9v=duK+(s3t$tyR!uuHR zB${sSr)nSnN*jJY;gM%yw@i5rRqo?mPD-%3{i3``B+)0L-~J|;FSuqjiXp?)J}$AUmY51;!3@yaYn=h?^^2=L7b_60Jd`2F)f&;hHIKTb|S}VEzJatl17OT z-vn8&Z)aM6G6WW21@V#zCEh)azZZ|3G1V_Csy2pm9vB7%P~R8ru% z0wXYt61!g2p^!pu2O)mL08}^=JDmSW$W!w=Q!s~Gzno?~%XNnHU=2<`igNbPm<%h- z)EiHN8qR^vV?pOa{)ZNa2eZ4F)Vw+ox6Ec+t``G?Q^CQOK>wgl zUY_WrHSh)Fh(_Q>^=?vXsC^V%y5I-$*RD{j@MDiN zZy8SyT0JXu>QIx9lBhB3l+-F|O9p<<#C8GBDJ|PtpiM>bakf1ppWcbqQ?Zz9HVY55 z>`QQfgSrNsA__z}gjm~I9lfC61lQ2&PoF~iAtF8CdK?-m+S;v@udp=S{-unsU)i@< zC<*Pf!UIYSRK?{KvTu!^<&yRT2|T1IZvhStV;mEkNCjhiV^H8QTY&T3v_H7m`?P5< zfu-4FIVN~&?h&aqg5~#(8F25e^E1CB*7eDm;$GM#f$Pa-ftcKIxYO zcfNb|Py=;sZ03Q?!=a;*ePejCp^cuEE}x-6p}voo7}s5(p~>HhAWbAd_C1B+Wfm6A z6HF|n8BU?}RCP!^Pg8Y&S*ADGUK{b2@2RG*R*MI9LjzIcI?f7>&+kAH@yw2xH1J3 zNDYYLEPFpWPTs2-dYNABPa3EQ4*(*6Y=-UK(%3gxZ$ns3Kh$NVF6I%e@`ozROPiy6 z0u3mO^RZO>fPm~*Oj1g>WREBTDQj!q^>qNR3z(Gj4R9tI?k9Pk-%~TMSfZrl`Fe7!I>l_txdh>Cz`DG|7_rLp z)pVNWve4R^Jd|^gAFtEd3EICNMxIsfBX^GWw__eM+5KT$C#qRo04i!-DK^QM?oy+JLYK*yJt&JcTA8 z>-_?i*zl4Jo6yZu8p~scpZ0tAWY-Mh<|Sp9rhXQY8r^S~LrMkFM;EOp6I7lAFK-Ye z+`WT2943X46_;$X-R^%3%sg1}#If$l&-PnRN>B36mXl^VL(ZgN^g_lt05ugocp?fo zD?QLq&nE@VJ~Uq!fKS?WTdwD}0e=MKHopX6QJX&Z zg|raxUB9D@(cD<)RvU@bu<_Ux3AJ9QzJ5*p6^0LkrxE}}j&cBjt=DQ#$Auft5l@5B zch_RUr~ozFBo;V8zZ)<}@@G?44ku2_Ef$iH2y3PP_7SZC1JbQ{=#orc|IzD)M1EA0 z|16BYTQj(nUCJWiI-(?EBdzv1{_>oe_{I`e6HXLkO~=g4&K0+33FNwxr>8(sGB~Jt zkf_s^W5sNuj@;}aki{Xd{oDs=2R64YZlvq#>eMOK&}i81Of^D}VR0p_dl<6ivf<1< zv(W&{xjVPm@Nsd+v9q|63yl90;lw50*Z~DmaBKayN}kTIl6v4t^-To-U2Ys+^9k_- z_g{jDWUBKcUH`BYDlE)wy2NjDBE9r{_D^u~?cxo5Z1=5WJ$EN@&*Mp?z7xsrc3D9q zM#*uTXCpqQq?RL#6oT#NABSXuG{CB*y5Kvys0Xr30!gB0zXb>Z?K+i^tQ8O}XubPH z8P`@8VJjy0U4eVp6R2d>Ng{N~MaTdv>a`#5m&ZVGSM0~vy#U4p@FYq9F3sG!dmp93 zSH!=v@s5@!jb>UAN+QmBPIlj_qtbD(e!#Qfpt8mv^hVMbE8MxiQNl)8V;Nu`bo=Q+ z=b10ZC1>mkwe!It?obF~@PW$owP=7GC=HY@g=S#o#K*UNwUU=Ei`x1_mFf~~k|2rF zeT5G22k`gzNB~}#R2+BeS~KG)cJflLgjO~I)|N*d0FC&1sl%27BLRpiZchvF@o9u8 zQ0BUDiLE%?V7G5!&dq%0NySGOFnkV2_cGDza(;8Ne|eFWw1g`e6W-q%L`$i7<4zgN zE~zTA{;OQacNAzTDs>6M+Tabg_N(YDv-LSMOz1NM#@ut_h(&vDlR%h^jETJXbiLbI zujHUGOF`~y|A~pm%Kmts6Me|3QA75_0cq)h()Yz7(F0>$BJWEFo_a?Ydn>5vbvc4m zGmp%|k#431N9iUwY-83&iNHTH3#ePIt+CyAo$P;h$u;gSurtURKP*h}w9ziIJ9Kv?9^S)We8o2~6_TVin^ zVC&Ny-i%t5SXdX-ty)^nFhSERlC|^i+673%GZ~}Xh=U1pA-;^!Wsgm^!cDY49r!J=u>vflq^rKW9ETo?%&G|J8 z{KNO``NDbtx8mAcP(`JhTv?1+bj!!P0TfN*9>9r*6q_&>IX@!_GKJu!@ja^b!4^Ez z1z}BAh`kYG);uBdclFq&>k95Dh}Ffw!av*o&^Dl!v6lK|dVbv5##-pwtYD z4;?Ggi7F}0)&GwE+bsS^qk(?Q;q)!1e=UMt$c9*6{-9!DWX$jAPzn&FfGRZG4$%1w z`z{<7Cy7<;pYJlne-Uc1mF<>Jy}AAz?xr&HGpGijxmJjSe>keg(Pf&b$5n`&rvb1j zqVqrO_m~|BJyP1_>I@*fV0a%kN_wfVMmZ3+x)FA?ei?UoBO=9zBP)0LDRv$AgY_<(^%UzVauZ0)x~yco^)a^ZnC1I76My>rvIjGbdAR&G5)9?|SthX88Z)eDm`KF{fuaG|l>xlQ`d^so7t*m{0B-MswTsZ~d+erSjjkrnb?(f#4V{ zpD6qk0Sc6K8WY$`ml^2Pc14LZIyO}16Y;Rw_ z%s{93{TwSS>tPM_OLy6TV6zDTyW5Mi9(irR`7-JCYxds&zdc)TERrojo_eU|>-Kx6 z`_IcCdHXLQIqxonvNcEqZVXCAlJ9#|E;IuN?Sz@ zWQdqV?`U&=1CvZtaK#!e5mA)78plO*)cWUK;rZS*U?qTTDfm3eKgjJ=?Pe@S5emPP zl+sMp@AN=7y(2uW7l9<}>sXM-k*0}{^|sS7FgWM*XD|W2qR;yB529`O1eBt3Lq%)y zxnAmqD7@(?sk)=9zy~xbY>6pQ1hcGit@np4?X2zGn)+L#Ur#BO~F6{;s$Ky*xAR)Z}eAZ9MsjUPRtAK02W z!bCq>m%0QJWHmb{>|KrA`FF6IGXBXyu03yoNf`N?>6%`tL*$X%uCJXK% zDbhnk{XXP?*8DNw4y905^2V-1F7V%(8}*4Sn6l`W4e5Nv&l zTbPY*HF%ftGd9;EP7%2EY0S+|iZ(sbZO&|+^wg|CV-r?J8TmE`g-rmC_Nkc;e z5_G#@5gP({Cgp8_gW0A*_BXaX>p`nK*HV(U@**_uA1(l~8dv5_4#^~+gI_r;!AafG zuN-nU9SVQ^&FlYUX^aSB#x^TB>SLY`%hdsTFok^b(RxP5rfElH+pV3LD)|m}uZrCB zlj>>H(-J>>n-A~nzI^$8&>n@u@NH2U4{YbWTi4_W3CXZWH>97$e;RA=6JlU-&TBkZ zDl5U}PMuJfx%8%FR~>A5@S!9-cdBD|BwdI4-;3HHJ(QxetnNkvq(*!1ri=yO8_Dl~ zT0$@V>L%dE8Z|M49+w{US<>SO&4DajOZ^)1k3}1dYpdyB?`Euuce3heO^&z0I!)|& z#UGwFDJexrioi-S{7_gXz-Y0N^TFccA^Yt^((AMoZUfR7iMcf# z*kQuL$0f=-O51oe1&B5XOp)GTGX}sa@km!f-4=U-pd~qwAeVSFbt07t^<5;lcKT5n z0cmVI?`@vMm$slLOWwq;w&&PzL?9CinAba=0eHD`G(a()ujxSm1doMfEt?|+P%p*7 z$w?Ec2m6LBlf9Hm)-H0CD3fcS?+S?58zu_4xmc_O4FMP-*EujkYvAGG89aV00K~L{ zZR5=9pC>C&HrQ3-*@J}9Ap8aIp*8nS-U~Z~Yfs}%dDEm4ls$+Tjd`PT^tSw;4t0}( z5Z8$nP56F6R^=wH1FD^~fAt^8KBYMWUI}6-hY$-Q$S_V8>Edb(S4T#qtEODBn}VHk zB(mo-s#cZZAA3Eg+oP^iQl_>Bh>(>0Z^2Rwrh8>=@7w3fGZ(GfNt9Z;yE7lh_T41vVrO7o&tIPf&yAT)gUHS{a$Y@~h z4^%�Ik_l3Qkw%^K?_+lcmEiibq0z0#8z>B*q|b4&`s) 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�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 { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 20); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + engine.resourceManager + .load({ + urls: ["/spineboy.json", "/spineboy.atlas", "/spineboy.png"], + type: "Spine" + }) + .then((resource: any) => { + const spineEntity = resource.instantiate(); + spineEntity.transform.setPosition(-0.5, -3.2, 0); + root.addChild(spineEntity); + spineEntity.getComponent(SpineAnimationRenderer).state.setAnimation(0, "idle", true); + + updateForE2E(engine); + initScreenshot(engine, camera); + }); +}); diff --git a/e2e/case/spine-tint-black.ts b/e2e/case/spine-tint-black.ts new file mode 100644 index 0000000000..b2c6c91504 --- /dev/null +++ b/e2e/case/spine-tint-black.ts @@ -0,0 +1,41 @@ +/** + * @title Spine TintBlack + * @category Spine + */ +import { Camera, Logger, Vector3, WebGLEngine } from "@galacean/engine"; +import { SpineAnimationRenderer } from "@galacean/engine-spine"; +import "@galacean/engine-spine-core-4.2"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +Logger.enable(); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 60); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + engine.resourceManager + .load({ + urls: ["/tank-pro.json", "/tank-pro.atlas", "/tank-pro.png"], + type: "Spine" + }) + .then((resource: any) => { + const spineEntity = resource.instantiate(); + const spine = spineEntity.getComponent(SpineAnimationRenderer); + // tank-pro ships with per-slot darkColor; tintBlack renders the two-color tint. + spine.tintBlack = true; + spineEntity.transform.setPosition(3, -5, 0); + root.addChild(spineEntity); + spine.state.setAnimation(0, "shoot", true); + + // Sample ~0.4s into "shoot": the muzzle-smoke darkColor makes the two-color tint + // clearly visible (tintBlack on/off differs ~7% here, vs ~0.01% on a static frame). + updateForE2E(engine, 40, 10); + initScreenshot(engine, camera); + }); +}); diff --git a/e2e/case/sprite-filled.ts b/e2e/case/sprite-filled.ts new file mode 100644 index 0000000000..dbe3415571 --- /dev/null +++ b/e2e/case/sprite-filled.ts @@ -0,0 +1,63 @@ +/** + * @title SpriteFilled + * @category Sprite + */ +import { + AssetType, + Camera, + Sprite, + SpriteDrawMode, + SpriteFilledMode, + SpriteRenderer, + Texture2D, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +// One baseline covering every fill mode (rows) across fill amounts 0/0.25/0.5/0.75/1 (columns). +// The amount steps hit the topology boundaries (45° quad↔triangle, 90° quadrant edges). +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 50); + const camera = cameraEntity.addComponent(Camera); + camera.isOrthographic = true; + camera.orthographicSize = 9; + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*rgNGR4Vb7lQAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture + }) + .then((texture) => { + const sprite = new Sprite(engine, texture); + const modes = [ + SpriteFilledMode.Horizontal, + SpriteFilledMode.Vertical, + SpriteFilledMode.Radial90, + SpriteFilledMode.Radial180, + SpriteFilledMode.Radial360 + ]; + const amounts = [0, 0.25, 0.5, 0.75, 1]; + + modes.forEach((mode, row) => { + amounts.forEach((amount, col) => { + const entity = rootEntity.createChild(`filled-${row}-${col}`); + entity.transform.setPosition((col - 2) * 3, (2 - row) * 3, 0); + const renderer = entity.addComponent(SpriteRenderer); + renderer.sprite = sprite; + renderer.width = 2.4; + renderer.height = 2.4; + renderer.drawMode = SpriteDrawMode.Filled; + renderer.filledMode = mode; + renderer.filledAmount = amount; + }); + }); + + updateForE2E(engine); + initScreenshot(engine, camera); + }); +}); diff --git a/e2e/config.ts b/e2e/config.ts index d7399ef8e1..75a973a570 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -1,4 +1,18 @@ export const E2E_CONFIG = { + Spine: { + spineboy: { + category: "Spine", + caseFileName: "spine-spineboy", + threshold: 0, + diffPercentage: 0.05 + }, + tintBlack: { + category: "Spine", + caseFileName: "spine-tint-black", + threshold: 0, + diffPercentage: 0.05 + } + }, Animator: { additive: { category: "Animator", @@ -578,5 +592,13 @@ export const E2E_CONFIG = { threshold: 0, diffPercentage: 0 } + }, + Sprite: { + filled: { + category: "Sprite", + caseFileName: "sprite-filled", + threshold: 0.1, + diffPercentage: 0.3 + } } }; diff --git a/e2e/fixtures/originImage/Spine_spine-spineboy.jpg b/e2e/fixtures/originImage/Spine_spine-spineboy.jpg new file mode 100644 index 0000000000..76065431eb --- /dev/null +++ b/e2e/fixtures/originImage/Spine_spine-spineboy.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58b5f1a156b82707ead037eb64d0b130534834c146d907b45be9b0cb16dbed49 +size 74591 diff --git a/e2e/fixtures/originImage/Spine_spine-tint-black.jpg b/e2e/fixtures/originImage/Spine_spine-tint-black.jpg new file mode 100644 index 0000000000..410a970ccf --- /dev/null +++ b/e2e/fixtures/originImage/Spine_spine-tint-black.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51fc0948e2ce8b819093526b5665b10e9975a810d4c1975fa661c091f2842e81 +size 122845 diff --git a/e2e/fixtures/originImage/Sprite_sprite-filled.jpg b/e2e/fixtures/originImage/Sprite_sprite-filled.jpg new file mode 100644 index 0000000000..79272e5169 --- /dev/null +++ b/e2e/fixtures/originImage/Sprite_sprite-filled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ef3c3796189e73349329df7cc1d0b35b5a5c47ef8d9e5757b5c68dd918b327a +size 215254 diff --git a/e2e/package.json b/e2e/package.json index 5aaec39214..c5a59ec567 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -19,6 +19,8 @@ "@galacean/engine-rhi-webgl": "workspace:*", "@galacean/engine-physics-physx": "workspace:*", "@galacean/engine-ui": "workspace:*", + "@galacean/engine-spine": "workspace:*", + "@galacean/engine-spine-core-4.2": "workspace:*", "dat.gui": "^0.7.9", "vite": "3.1.6", "sass": "^1.55.0" diff --git a/examples/package.json b/examples/package.json index 72baee02cd..48b483c7e0 100644 --- a/examples/package.json +++ b/examples/package.json @@ -20,6 +20,9 @@ "@galacean/engine-rhi-webgl": "workspace:*", "@galacean/engine-shader": "workspace:*", "@galacean/engine-shader-compiler": "workspace:*", + "@galacean/engine-spine": "workspace:*", + "@galacean/engine-spine-core-3.8": "workspace:*", + "@galacean/engine-spine-core-4.2": "workspace:*", "@galacean/engine-toolkit": "latest", "@galacean/engine-toolkit-stats": "latest", "@galacean/engine-ui": "workspace:*" diff --git a/examples/src/spine-keli-4.2.ts b/examples/src/spine-keli-4.2.ts new file mode 100644 index 0000000000..8ad862a4a0 --- /dev/null +++ b/examples/src/spine-keli-4.2.ts @@ -0,0 +1,60 @@ +/** + * @title Spine Keli (4.2) + * @category Spine + */ +import { Camera, Entity, Logger, Vector3, WebGLEngine } from "@galacean/engine"; +import { SpineAnimationRenderer, SpineResource } from "@galacean/engine-spine"; +import "@galacean/engine-spine-core-4.2"; +import * as dat from "dat.gui"; + +Logger.enable(); + +const gui = new dat.GUI(); +const state = { animation: "" }; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 2, 10); + cameraEntity.transform.lookAt(new Vector3(0, 2, 0)); + + engine.run(); + + engine.resourceManager + .load({ + urls: [ + "/spine/keli/keli.json", + "/spine/keli/keli.atlas", + "/spine/keli/images/arm_l.png", + "/spine/keli/images/arm_r.png", + "/spine/keli/images/head.png", + "/spine/keli/images/legs.png", + "/spine/keli/images/torso.png" + ], + type: "Spine" + }) + .then((resource) => { + const spineEntity: Entity = resource.instantiate(); + root.addChild(spineEntity); + + const animator = spineEntity.getComponent(SpineAnimationRenderer); + const animationNames = resource.skeletonData.animations.map((animation) => animation.name); + + state.animation = animationNames[0]; + animator.state.setAnimation(0, state.animation, true); + + gui + .add(state, "animation", animationNames) + .name("Animation") + .onChange((name: string) => { + animator.state.setAnimation(0, name, true); + }); + }) + .catch((error) => { + console.error("Failed to load keli (4.2):", error); + }); +}); diff --git a/examples/src/spine-otakugirl-3.8.ts b/examples/src/spine-otakugirl-3.8.ts new file mode 100644 index 0000000000..410db286dd --- /dev/null +++ b/examples/src/spine-otakugirl-3.8.ts @@ -0,0 +1,55 @@ +/** + * @title Spine Otakugirl (3.8) + * @category Spine + * @remarks + * A genuine spine 3.8.99 export (binary .skel), unlike the keli asset used in the 4.2 example — + * this verifies the 3.8 backend against data it's actually meant to parse. + */ +import { Camera, Entity, Logger, Vector3, WebGLEngine } from "@galacean/engine"; +import { SpineAnimationRenderer, SpineResource } from "@galacean/engine-spine"; +import "@galacean/engine-spine-core-3.8"; +import * as dat from "dat.gui"; + +Logger.enable(); + +const gui = new dat.GUI(); +const state = { animation: "" }; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 2, 10); + cameraEntity.transform.lookAt(new Vector3(0, 2, 0)); + + engine.run(); + + engine.resourceManager + .load({ + urls: ["/spine/otakugirl/otakugirl.skel", "/spine/otakugirl/otakugirl.atlas", "/spine/otakugirl/otakugirl.png"], + type: "Spine" + }) + .then((resource) => { + const spineEntity: Entity = resource.instantiate(); + root.addChild(spineEntity); + + const animator = spineEntity.getComponent(SpineAnimationRenderer); + const animationNames = resource.skeletonData.animations.map((animation) => animation.name); + + state.animation = animationNames[0]; + animator.state.setAnimation(0, state.animation, true); + + gui + .add(state, "animation", animationNames) + .name("Animation") + .onChange((name: string) => { + animator.state.setAnimation(0, name, true); + }); + }) + .catch((error) => { + console.error("Failed to load otakugirl (3.8):", error); + }); +}); diff --git a/examples/src/sprite-mask.ts b/examples/src/sprite-mask.ts new file mode 100644 index 0000000000..f8dc719105 --- /dev/null +++ b/examples/src/sprite-mask.ts @@ -0,0 +1,87 @@ +/** + * @title Sprite Mask + * @category 2D + */ +import { + AssetType, + Camera, + Sprite, + SpriteMask, + SpriteMaskInteraction, + SpriteMaskLayer, + SpriteRenderer, + Texture2D, + WebGLEngine +} from "@galacean/engine"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor.set(0.05, 0.05, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 50); + cameraEntity.addComponent(Camera); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*ApFPTZSqcMkAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D + }) + .then((texture) => { + const sprite = new Sprite(engine, texture); + const maskSprite = new Sprite(engine, createSolidTexture(engine)); + + const spriteWidth = sprite.width; + const spriteHeight = sprite.height; + // Mask covers ~half of the sprite so the cut is obvious. + const maskWidth = spriteWidth * 0.6; + const maskHeight = spriteHeight * 0.6; + // Lay the two characters out side by side based on sprite size. + const groupOffsetX = spriteWidth * 0.6; + + // --- Left: VisibleInsideMask -> only the part covered by the square mask is visible --- + const leftGroup = root.createChild("LeftGroup"); + leftGroup.transform.setPosition(-groupOffsetX, 0, 0); + + const leftMaskEntity = leftGroup.createChild("Mask"); + const leftMask = leftMaskEntity.addComponent(SpriteMask); + leftMask.sprite = maskSprite; + leftMask.width = maskWidth; + leftMask.height = maskHeight; + leftMask.influenceLayers = SpriteMaskLayer.Layer0; + + const leftSpriteEntity = leftGroup.createChild("Sprite"); + const leftSprite = leftSpriteEntity.addComponent(SpriteRenderer); + leftSprite.sprite = sprite; + leftSprite.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + leftSprite.maskLayer = SpriteMaskLayer.Layer0; + + // --- Right: VisibleOutsideMask -> character with a square hole punched out --- + const rightGroup = root.createChild("RightGroup"); + rightGroup.transform.setPosition(groupOffsetX, 0, 0); + + const rightMaskEntity = rightGroup.createChild("Mask"); + const rightMask = rightMaskEntity.addComponent(SpriteMask); + rightMask.sprite = maskSprite; + rightMask.width = maskWidth; + rightMask.height = maskHeight; + rightMask.influenceLayers = SpriteMaskLayer.Layer1; + + const rightSpriteEntity = rightGroup.createChild("Sprite"); + const rightSprite = rightSpriteEntity.addComponent(SpriteRenderer); + rightSprite.sprite = sprite; + rightSprite.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask; + rightSprite.maskLayer = SpriteMaskLayer.Layer1; + }); + + engine.run(); +}); + +function createSolidTexture(engine: WebGLEngine): Texture2D { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return texture; +} diff --git a/examples/src/ui-mask-alpha.ts b/examples/src/ui-mask-alpha.ts new file mode 100644 index 0000000000..9b5f340fd0 --- /dev/null +++ b/examples/src/ui-mask-alpha.ts @@ -0,0 +1,97 @@ +/** + * @title UI Mask Alpha Cutoff + * @category UI + */ +import * as dat from "dat.gui"; +import { Camera, Color, Sprite, SpriteMaskInteraction, Texture2D, TextureFormat, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, Mask, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + const circleSprite = createCircleSprite(engine, 256); + + const groupEntity = canvasEntity.createChild("Group"); + (groupEntity.transform).setPosition(0, 40, 0); + + // Circular mask + const maskEntity = groupEntity.createChild("Mask"); + (maskEntity.transform).size.set(300, 300); + const mask = maskEntity.addComponent(Mask); + mask.sprite = circleSprite; + mask.alphaCutoff = 0.5; + + // Background visible inside circle + const insideEntity = groupEntity.createChild("Inside"); + (insideEntity.transform).size.set(500, 500); + const inside = insideEntity.addComponent(Image); + inside.sprite = solidSprite; + inside.color.set(0.95, 0.61, 0.07, 1); + inside.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + + const labelEntity = canvasEntity.createChild("Label"); + (labelEntity.transform).size.set(800, 80); + (labelEntity.transform).setPosition(0, -260, 0); + const label = labelEntity.addComponent(Text); + label.text = "Drag the slider to change alphaCutoff"; + label.fontSize = 28; + label.color.set(0.85, 0.9, 1, 1); + + const gui = new dat.GUI(); + const state = { alphaCutoff: 0.5 }; + gui + .add(state, "alphaCutoff", 0.0, 1.0, 0.01) + .name("Mask Alpha Cutoff") + .onChange((value: number) => { + mask.alphaCutoff = value; + }); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} + +/** Soft circle: alpha falls off radially so alphaCutoff has visible effect. */ +function createCircleSprite(engine: WebGLEngine, size: number): Sprite { + const buffer = new Uint8Array(size * size * 4); + const cx = size * 0.5; + const cy = size * 0.5; + const radius = size * 0.5; + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const dx = x - cx; + const dy = y - cy; + const dist = Math.sqrt(dx * dx + dy * dy); + const t = Math.max(0, 1 - dist / radius); + const alpha = Math.min(255, Math.floor(t * 255)); + const i = (y * size + x) * 4; + buffer[i] = 255; + buffer[i + 1] = 255; + buffer[i + 2] = 255; + buffer[i + 3] = alpha; + } + } + const texture = new Texture2D(engine, size, size, TextureFormat.R8G8B8A8, false); + texture.setPixelBuffer(buffer); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-mask-overlay.ts b/examples/src/ui-mask-overlay.ts new file mode 100644 index 0000000000..e7fa6b5096 --- /dev/null +++ b/examples/src/ui-mask-overlay.ts @@ -0,0 +1,120 @@ +/** + * @title UI Mask Overlay + * @category UI + */ +import { Camera, Color, Sprite, SpriteMaskInteraction, Texture2D, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, Mask, RectMask2D, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + // Camera is required for default scene rendering even though overlay UI doesn't use it for projection. + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + + // ===== Left half: SpriteMask (Mask component) ===== + const maskGroup = canvasEntity.createChild("MaskGroup"); + (maskGroup.transform).setPosition(-300, 60, 0); + + const maskEntity = maskGroup.createChild("Mask"); + (maskEntity.transform).size.set(280, 280); + const mask = maskEntity.addComponent(Mask); + mask.sprite = solidSprite; + + const insideEntity = maskGroup.createChild("InsideImage"); + (insideEntity.transform).size.set(440, 440); + const inside = insideEntity.addComponent(Image); + inside.sprite = solidSprite; + inside.color.set(0.91, 0.3, 0.24, 1); + inside.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + + const maskLabelEntity = maskGroup.createChild("Label"); + (maskLabelEntity.transform).size.set(360, 60); + (maskLabelEntity.transform).setPosition(0, -260, 0); + const maskLabel = maskLabelEntity.addComponent(Text); + maskLabel.text = "Mask (Overlay)"; + maskLabel.fontSize = 32; + maskLabel.color.set(1, 1, 1, 1); + + // ===== Right half: RectMask2D ===== + const rectGroup = canvasEntity.createChild("RectGroup"); + (rectGroup.transform).setPosition(300, 60, 0); + + const viewportEntity = rectGroup.createChild("Viewport"); + (viewportEntity.transform).size.set(360, 280); + const viewport = viewportEntity.addComponent(Image); + viewport.sprite = solidSprite; + viewport.color.set(0.17, 0.18, 0.2, 1); + viewportEntity.addComponent(RectMask2D); + + const contentEntity = viewportEntity.createChild("Content"); + (contentEntity.transform).size.set(560, 480); + (contentEntity.transform).setPosition(60, -50, 0); + + const tileColors = [ + new Color(0.91, 0.3, 0.24, 1), + new Color(0.16, 0.5, 0.73, 1), + new Color(0.18, 0.8, 0.44, 1), + new Color(0.95, 0.61, 0.07, 1) + ]; + const tileSize = 220; + const gap = 20; + for (let row = 0; row < 2; row++) { + for (let col = 0; col < 2; col++) { + const i = row * 2 + col; + const tileEntity = contentEntity.createChild(`Tile_${i}`); + const t = tileEntity.transform; + t.size.set(tileSize, tileSize); + t.setPosition(col * (tileSize + gap) - (tileSize + gap) / 2, (tileSize + gap) / 2 - row * (tileSize + gap), 0); + + const tile = tileEntity.addComponent(Image); + tile.sprite = solidSprite; + tile.color = tileColors[i]; + + const labelEntity = tileEntity.createChild("Label"); + (labelEntity.transform).size.set(tileSize, tileSize); + const label = labelEntity.addComponent(Text); + label.text = `${i + 1}`; + label.fontSize = 64; + label.color.set(1, 1, 1, 1); + } + } + + const rectLabelEntity = rectGroup.createChild("Label"); + (rectLabelEntity.transform).size.set(360, 60); + (rectLabelEntity.transform).setPosition(0, -260, 0); + const rectLabel = rectLabelEntity.addComponent(Text); + rectLabel.text = "RectMask2D (Overlay)"; + rectLabel.fontSize = 32; + rectLabel.color.set(1, 1, 1, 1); + + // Top header + const headerEntity = canvasEntity.createChild("Header"); + (headerEntity.transform).size.set(900, 80); + (headerEntity.transform).setPosition(0, 320, 0); + const header = headerEntity.addComponent(Text); + header.text = "ScreenSpaceOverlay · Mask & RectMask2D"; + header.fontSize = 36; + header.color.set(0.85, 0.92, 1, 1); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-mask.ts b/examples/src/ui-mask.ts new file mode 100644 index 0000000000..260392e6e1 --- /dev/null +++ b/examples/src/ui-mask.ts @@ -0,0 +1,85 @@ +/** + * @title UI Mask + * @category UI + */ +import { Camera, Color, Sprite, SpriteMaskInteraction, Texture2D, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, Mask, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + + // --- Left group: VisibleInsideMask --- + const leftGroupEntity = canvasEntity.createChild("LeftGroup"); + (leftGroupEntity.transform).setPosition(-300, 0, 0); + + // Square mask + const leftMaskEntity = leftGroupEntity.createChild("Mask"); + (leftMaskEntity.transform).size.set(300, 300); + const leftMask = leftMaskEntity.addComponent(Mask); + leftMask.sprite = solidSprite; + + // Image clipped to inside the mask + const insideImageEntity = leftGroupEntity.createChild("InsideImage"); + (insideImageEntity.transform).size.set(500, 500); + const insideImage = insideImageEntity.addComponent(Image); + insideImage.sprite = solidSprite; + insideImage.color.set(0.91, 0.3, 0.24, 1); + insideImage.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + + const leftLabelEntity = leftGroupEntity.createChild("Label"); + (leftLabelEntity.transform).size.set(300, 60); + (leftLabelEntity.transform).setPosition(0, -210, 0); + const leftLabel = leftLabelEntity.addComponent(Text); + leftLabel.text = "VisibleInsideMask"; + leftLabel.fontSize = 30; + leftLabel.color.set(1, 1, 1, 1); + + // --- Right group: VisibleOutsideMask --- + const rightGroupEntity = canvasEntity.createChild("RightGroup"); + (rightGroupEntity.transform).setPosition(300, 0, 0); + + const rightMaskEntity = rightGroupEntity.createChild("Mask"); + (rightMaskEntity.transform).size.set(300, 300); + const rightMask = rightMaskEntity.addComponent(Mask); + rightMask.sprite = solidSprite; + + const outsideImageEntity = rightGroupEntity.createChild("OutsideImage"); + (outsideImageEntity.transform).size.set(500, 500); + const outsideImage = outsideImageEntity.addComponent(Image); + outsideImage.sprite = solidSprite; + outsideImage.color.set(0.16, 0.5, 0.73, 1); + outsideImage.maskInteraction = SpriteMaskInteraction.VisibleOutsideMask; + + const rightLabelEntity = rightGroupEntity.createChild("Label"); + (rightLabelEntity.transform).size.set(300, 60); + (rightLabelEntity.transform).setPosition(0, -210, 0); + const rightLabel = rightLabelEntity.addComponent(Text); + rightLabel.text = "VisibleOutsideMask"; + rightLabel.fontSize = 30; + rightLabel.color.set(1, 1, 1, 1); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-rect-mask-nested.ts b/examples/src/ui-rect-mask-nested.ts new file mode 100644 index 0000000000..0eae971bfb --- /dev/null +++ b/examples/src/ui-rect-mask-nested.ts @@ -0,0 +1,76 @@ +/** + * @title UI RectMask2D Nested + * @category UI + */ +import { Camera, Color, Sprite, Texture2D, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, RectMask2D, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + + // Outer mask (wide, short) + const outerEntity = canvasEntity.createChild("OuterMask"); + (outerEntity.transform).size.set(560, 240); + (outerEntity.transform).setPosition(0, 60, 0); + const outerImage = outerEntity.addComponent(Image); + outerImage.sprite = solidSprite; + outerImage.color.set(0.13, 0.18, 0.28, 1); + outerEntity.addComponent(RectMask2D); + + // Inner mask (tall, narrow), child of outer + const innerEntity = outerEntity.createChild("InnerMask"); + (innerEntity.transform).size.set(240, 480); + (innerEntity.transform).setPosition(0, 0, 0); + const innerImage = innerEntity.addComponent(Image); + innerImage.sprite = solidSprite; + innerImage.color.set(0.2, 0.3, 0.5, 1); + innerEntity.addComponent(RectMask2D); + + // Big colored content under both masks — only the intersection of outer ∩ inner remains visible + const contentEntity = innerEntity.createChild("Content"); + (contentEntity.transform).size.set(800, 800); + const content = contentEntity.addComponent(Image); + content.sprite = solidSprite; + content.color.set(0.95, 0.61, 0.07, 1); + + const labelTopEntity = canvasEntity.createChild("LabelTop"); + (labelTopEntity.transform).size.set(900, 60); + (labelTopEntity.transform).setPosition(0, 220, 0); + const labelTop = labelTopEntity.addComponent(Text); + labelTop.text = "Outer 560x240 ∩ Inner 240x480 → visible: 240x240"; + labelTop.fontSize = 28; + labelTop.color.set(1, 1, 1, 1); + + const labelBottomEntity = canvasEntity.createChild("LabelBottom"); + (labelBottomEntity.transform).size.set(900, 60); + (labelBottomEntity.transform).setPosition(0, -260, 0); + const labelBottom = labelBottomEntity.addComponent(Text); + labelBottom.text = "Nested RectMask2D takes the rect intersection of all ancestor masks."; + labelBottom.fontSize = 24; + labelBottom.color.set(0.77, 0.82, 0.89, 1); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-rect-mask.ts b/examples/src/ui-rect-mask.ts new file mode 100644 index 0000000000..92078d6c6a --- /dev/null +++ b/examples/src/ui-rect-mask.ts @@ -0,0 +1,135 @@ +/** + * @title UI RectMask2D + * @category UI + */ +import * as dat from "dat.gui"; +import { Camera, Color, Sprite, Texture2D, Vector2, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Image, RectMask2D, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.03, 0.04, 0.07, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1200, 800); + + const solidSprite = createSolidSprite(engine); + + // Frame card + const frameEntity = canvasEntity.createChild("Frame"); + (frameEntity.transform).size.set(560, 460); + (frameEntity.transform).setPosition(-180, 20, 0); + const frame = frameEntity.addComponent(Image); + frame.sprite = solidSprite; + frame.color.set(0.09, 0.11, 0.15, 1); + + // Viewport with RectMask2D + const viewportEntity = frameEntity.createChild("Viewport"); + (viewportEntity.transform).size.set(440, 320); + (viewportEntity.transform).setPosition(30, -10, 0); + const viewport = viewportEntity.addComponent(Image); + viewport.sprite = solidSprite; + viewport.color.set(0.17, 0.18, 0.2, 1); + const rectMask = viewportEntity.addComponent(RectMask2D); + + // 3x3 colored tiles overflow the viewport + const contentEntity = viewportEntity.createChild("Content"); + (contentEntity.transform).size.set(740, 560); + (contentEntity.transform).setPosition(80, -60, 0); + + const colors = [ + new Color(0.91, 0.3, 0.24, 1), + new Color(0.16, 0.5, 0.73, 1), + new Color(0.18, 0.8, 0.44, 1), + new Color(0.95, 0.61, 0.07, 1), + new Color(0.56, 0.27, 0.68, 1), + new Color(0.2, 0.6, 0.86, 1), + new Color(0.83, 0.33, 0.33, 1), + new Color(0.1, 0.74, 0.61, 1), + new Color(0.93, 0.78, 0.0, 1) + ]; + + const tileWidth = 180; + const tileHeight = 180; + const gap = 10; + for (let row = 0; row < 3; row++) { + for (let col = 0; col < 3; col++) { + const index = row * 3 + col; + const tileEntity = contentEntity.createChild(`Tile_${index}`); + const t = tileEntity.transform; + t.size.set(tileWidth, tileHeight); + t.setPosition(col * (tileWidth + gap) - 170, 170 - row * (tileHeight + gap), 0); + + const tile = tileEntity.addComponent(Image); + tile.sprite = solidSprite; + tile.color = colors[index]; + + const labelEntity = tileEntity.createChild("Label"); + (labelEntity.transform).size.set(tileWidth, tileHeight); + const label = labelEntity.addComponent(Text); + label.text = `${index + 1}`; + label.fontSize = 56; + label.color.set(1, 1, 1, 1); + } + } + + // Right info card + const noteEntity = canvasEntity.createChild("Note"); + (noteEntity.transform).size.set(360, 220); + (noteEntity.transform).setPosition(290, 20, 0); + const note = noteEntity.addComponent(Image); + note.sprite = solidSprite; + note.color.set(0.08, 0.09, 0.12, 1); + + const noteTextEntity = noteEntity.createChild("Copy"); + (noteTextEntity.transform).size.set(320, 180); + const noteText = noteTextEntity.addComponent(Text); + noteText.text = + "RectMask2D clips Image\nand Text by an axis-\naligned rectangle.\n\nUse the GUI to tweak\nsoftness / alphaClip."; + noteText.fontSize = 26; + noteText.color.set(0.77, 0.82, 0.89, 1); + + const gui = new dat.GUI(); + const state = { + softnessX: 0, + softnessY: 0, + alphaClip: false + }; + gui + .add(state, "softnessX", 0, 80, 1) + .name("softness.x") + .onChange((v: number) => { + rectMask.softness = new Vector2(v, state.softnessY); + }); + gui + .add(state, "softnessY", 0, 80, 1) + .name("softness.y") + .onChange((v: number) => { + rectMask.softness = new Vector2(state.softnessX, v); + }); + gui + .add(state, "alphaClip") + .name("alphaClip (discard)") + .onChange((v: boolean) => { + rectMask.alphaClip = v; + }); + + engine.run(); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/examples/src/ui-text-outline.ts b/examples/src/ui-text-outline.ts new file mode 100644 index 0000000000..fae8865a9f --- /dev/null +++ b/examples/src/ui-text-outline.ts @@ -0,0 +1,121 @@ +/** + * @title UI Text Outline + * @category UI + */ +import * as dat from "dat.gui"; +import { Camera, Color, WebGLEngine } from "@galacean/engine"; +import { CanvasRenderMode, Text, UICanvas, UITransform } from "@galacean/engine-ui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.background.solidColor = new Color(0.05, 0.07, 0.1, 1); + const root = scene.createRootEntity("Root"); + + const cameraEntity = root.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + + const canvasEntity = root.createChild("UICanvas"); + const uiCanvas = canvasEntity.addComponent(UICanvas); + uiCanvas.renderMode = CanvasRenderMode.ScreenSpaceCamera; + uiCanvas.camera = camera; + uiCanvas.referenceResolutionPerUnit = 100; + uiCanvas.referenceResolution.set(1280, 800); + + // ---------- Matrix grid ---------- + // Rows = outlineWidth (0 / 1 / 2 / 4 / 8 px) + // Cols = (text color, outline color) presets + const widthCases = [0, 1, 2, 4, 8]; + const colorCases: { name: string; fill: Color; outline: Color }[] = [ + { name: "white / black", fill: new Color(1, 1, 1, 1), outline: new Color(0, 0, 0, 1) }, + { name: "black / white", fill: new Color(0, 0, 0, 1), outline: new Color(1, 1, 1, 1) }, + { name: "yellow / red", fill: new Color(1, 0.85, 0.1, 1), outline: new Color(0.85, 0.1, 0.1, 1) } + ]; + + const cellW = 380; + const cellH = 110; + const startX = -cellW * (colorCases.length - 1) * 0.5; + const startY = 220; + + // Column headers + for (let c = 0; c < colorCases.length; c++) { + const headerEntity = canvasEntity.createChild(`header-${c}`); + const ht = headerEntity.transform; + ht.size.set(cellW, 32); + ht.setPosition(startX + c * cellW, startY + cellH * 0.5 + 20, 0); + const header = headerEntity.addComponent(Text); + header.text = `text/outline = ${colorCases[c].name}`; + header.fontSize = 18; + header.color.set(0.7, 0.78, 0.9, 1); + } + + for (let r = 0; r < widthCases.length; r++) { + const w = widthCases[r]; + + // Row label + const labelEntity = canvasEntity.createChild(`row-label-${r}`); + const lt = labelEntity.transform; + lt.size.set(120, cellH); + lt.setPosition(startX - cellW * 0.5 - 60, startY - r * cellH, 0); + const label = labelEntity.addComponent(Text); + label.text = `width = ${w}px`; + label.fontSize = 18; + label.color.set(0.7, 0.78, 0.9, 1); + + for (let c = 0; c < colorCases.length; c++) { + const { fill, outline } = colorCases[c]; + + const cell = canvasEntity.createChild(`cell-${r}-${c}`); + const ct = cell.transform; + ct.size.set(cellW, cellH); + ct.setPosition(startX + c * cellW, startY - r * cellH, 0); + const text = cell.addComponent(Text); + text.text = "Hello 描边 Outline"; + text.fontSize = 36; + text.color = fill; + text.outlineWidth = w; + text.outlineColor = outline; + } + } + + // ---------- Live preview controlled by dat.gui ---------- + const previewEntity = canvasEntity.createChild("preview"); + const previewTransform = previewEntity.transform; + previewTransform.size.set(900, 160); + previewTransform.setPosition(0, -300, 0); + const preview = previewEntity.addComponent(Text); + preview.text = "实时 Live 预览 Preview"; + preview.fontSize = 72; + preview.color = new Color(1, 1, 1, 1); + preview.outlineWidth = 3; + preview.outlineColor = new Color(0, 0, 0, 1); + + const state = { + fontSize: preview.fontSize, + outlineWidth: preview.outlineWidth, + fillColor: [255, 255, 255], + outlineColor: [0, 0, 0], + text: preview.text + }; + + const gui = new dat.GUI(); + gui.add(state, "text").onChange((v: string) => { + preview.text = v; + }); + gui.add(state, "fontSize", 12, 120, 1).onChange((v: number) => { + preview.fontSize = v; + }); + gui.add(state, "outlineWidth", 0, 8, 0.1).onChange((v: number) => { + preview.outlineWidth = v; + }); + gui.addColor(state, "fillColor").onChange((rgb: number[]) => { + preview.color.set(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1); + }); + gui.addColor(state, "outlineColor").onChange((rgb: number[]) => { + preview.outlineColor.set(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1); + }); + + engine.run(); +}); diff --git a/packages/core/src/2d/assembler/FilledSpriteAssembler.ts b/packages/core/src/2d/assembler/FilledSpriteAssembler.ts new file mode 100644 index 0000000000..4ecf56f104 --- /dev/null +++ b/packages/core/src/2d/assembler/FilledSpriteAssembler.ts @@ -0,0 +1,619 @@ +import { BoundingBox, Matrix, Vector2, Vector3 } from "@galacean/engine-math"; +import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { StaticInterfaceImplement } from "../../base/StaticInterfaceImplement"; +import { SpriteFilledMode } from "../enums/SpriteFilledMode"; +import { SpriteFilledOrigin } from "../enums/SpriteFilledOrigin"; +import { ISpriteAssembler } from "./ISpriteAssembler"; +import { ISpriteRenderer } from "./ISpriteRenderer"; + +/** + * Assemble vertex data for the sprite renderer in filled mode. + */ +@StaticInterfaceImplement() +export class FilledSpriteAssembler { + private static _matrix = new Matrix(); + private static _worldPositions = [ + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3(), + new Vector3() + ]; + private static _uvs = [ + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2(), + new Vector2() + ]; + private static _inPositions: Vector3[] = []; + private static _inUVs: Vector2[] = []; + private static _outPositions: Vector3[] = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; + private static _outUVs: Vector2[] = [new Vector2(), new Vector2(), new Vector2(), new Vector2()]; + private static _vertexOffset = 0; + private static _indicesOffset = 0; + // Fill amounts at or below this are treated as empty (render nothing), avoiding degenerate geometry. + private static readonly _fillAmountEpsilon = 0.001; + + static resetData(renderer: ISpriteRenderer): void { + const manager = renderer._getChunkManager(); + const lastSubChunk = renderer._subChunk; + lastSubChunk && manager.freeSubChunk(lastSubChunk); + // Allocate the maximum any fill mode can produce (Radial360 = 4 quadrants x 4 verts = 16). + // A fixed size avoids reallocating the sub-chunk when filledMode changes at runtime. + const subChunk = manager.allocateSubChunk(16); + subChunk.indices = []; + renderer._subChunk = subChunk; + } + + static updatePositions( + renderer: ISpriteRenderer, + worldMatrix: Matrix, + width: number, + height: number, + pivot: Vector2, + flipX: boolean, + flipY: boolean + ): void { + const { x: pivotX, y: pivotY } = pivot; + const modelMatrix = FilledSpriteAssembler._matrix; + const { elements: wE } = modelMatrix; + const { elements: pWE } = worldMatrix; + const sx = flipX ? -width : width; + const sy = flipY ? -height : height; + (wE[0] = pWE[0] * sx), (wE[1] = pWE[1] * sx), (wE[2] = pWE[2] * sx); + (wE[4] = pWE[4] * sy), (wE[5] = pWE[5] * sy), (wE[6] = pWE[6] * sy); + (wE[8] = pWE[8]), (wE[9] = pWE[9]), (wE[10] = pWE[10]); + wE[12] = pWE[12] - pivotX * wE[0] - pivotY * wE[4]; + wE[13] = pWE[13] - pivotX * wE[1] - pivotY * wE[5]; + wE[14] = pWE[14] - pivotX * wE[2] - pivotY * wE[6]; + + switch (renderer.filledMode) { + case SpriteFilledMode.Horizontal: + this._filledLinear(renderer, modelMatrix, true); + break; + case SpriteFilledMode.Vertical: + this._filledLinear(renderer, modelMatrix, false); + break; + case SpriteFilledMode.Radial90: + this._filledRadial90( + renderer, + modelMatrix, + renderer.filledOrigin, + renderer.filledAmount, + renderer.filledClockWise + ); + break; + case SpriteFilledMode.Radial180: + this._filledRadial180( + renderer, + modelMatrix, + renderer.filledOrigin, + renderer.filledAmount, + renderer.filledClockWise + ); + break; + case SpriteFilledMode.Radial360: + this._filledRadial360( + renderer, + modelMatrix, + renderer.filledOrigin, + renderer.filledAmount, + renderer.filledClockWise + ); + break; + default: + break; + } + + // @ts-ignore + BoundingBox.transform(renderer.sprite._getBounds(), modelMatrix, renderer._bounds); + } + + static updateUVs(renderer: ISpriteRenderer): void { + // UVs are computed in updatePositions. + } + + static updateColor(renderer: ISpriteRenderer, alpha: number): void { + const subChunk = renderer._subChunk; + const { r, g, b, a } = renderer.color; + const finalAlpha = a * alpha; + const vertices = subChunk.chunk.vertices; + const vertexArea = subChunk.vertexArea; + for (let i = 0, o = vertexArea.start + 5, n = vertexArea.size / 9; i < n; ++i, o += 9) { + vertices[o] = r; + vertices[o + 1] = g; + vertices[o + 2] = b; + vertices[o + 3] = finalAlpha; + } + } + + private static _filledLinear(renderer: ISpriteRenderer, matrix: Matrix, isHorizontal: boolean): void { + const amount = renderer.filledAmount; + if (amount <= this._fillAmountEpsilon) { + renderer._subChunk.indices.length = 0; + return; + } + + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[3]; + + const subChunk = renderer._subChunk; + const vertices = subChunk.chunk.vertices; + + let x0: number, y0: number, u0: number, v0: number; + let x1: number, y1: number, u1: number, v1: number; + let x2: number, y2: number, u2: number, v2: number; + let x3: number, y3: number, u3: number, v3: number; + + if (isHorizontal) { + const originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Left; + const startX = originIsStart ? lPosLB.x : lPosRB.x - (lPosRB.x - lPosLB.x) * amount; + const endX = originIsStart ? lPosLB.x + (lPosRB.x - lPosLB.x) * amount : lPosRB.x; + const startU = originIsStart ? left : right - (right - left) * amount; + const endU = originIsStart ? left + (right - left) * amount : right; + (x0 = startX), (y0 = lPosLB.y), (u0 = startU), (v0 = bottom); + (x1 = endX), (y1 = lPosRB.y), (u1 = endU), (v1 = bottom); + (x2 = startX), (y2 = lPosLT.y), (u2 = startU), (v2 = top); + (x3 = endX), (y3 = lPosRT.y), (u3 = endU), (v3 = top); + } else { + const originIsStart = renderer.filledOrigin === SpriteFilledOrigin.Bottom; + const startY = originIsStart ? lPosLB.y : lPosLT.y - (lPosLT.y - lPosLB.y) * amount; + const endY = originIsStart ? lPosLB.y + (lPosLT.y - lPosLB.y) * amount : lPosLT.y; + const startV = originIsStart ? bottom : top - (top - bottom) * amount; + const endV = originIsStart ? bottom + (top - bottom) * amount : top; + (x0 = lPosLB.x), (y0 = startY), (u0 = left), (v0 = startV); + (x1 = lPosRB.x), (y1 = startY), (u1 = right), (v1 = startV); + (x2 = lPosLT.x), (y2 = endY), (u2 = left), (v2 = endV); + (x3 = lPosRT.x), (y3 = endY), (u3 = right), (v3 = endV); + } + + const { elements: wE } = matrix; + const start = subChunk.vertexArea.start; + // LB + vertices[start] = wE[0] * x0 + wE[4] * y0 + wE[12]; + vertices[start + 1] = wE[1] * x0 + wE[5] * y0 + wE[13]; + vertices[start + 2] = wE[2] * x0 + wE[6] * y0 + wE[14]; + vertices[start + 3] = u0; + vertices[start + 4] = v0; + // RB + vertices[start + 9] = wE[0] * x1 + wE[4] * y1 + wE[12]; + vertices[start + 10] = wE[1] * x1 + wE[5] * y1 + wE[13]; + vertices[start + 11] = wE[2] * x1 + wE[6] * y1 + wE[14]; + vertices[start + 12] = u1; + vertices[start + 13] = v1; + // LT + vertices[start + 18] = wE[0] * x2 + wE[4] * y2 + wE[12]; + vertices[start + 19] = wE[1] * x2 + wE[5] * y2 + wE[13]; + vertices[start + 20] = wE[2] * x2 + wE[6] * y2 + wE[14]; + vertices[start + 21] = u2; + vertices[start + 22] = v2; + // RT + vertices[start + 27] = wE[0] * x3 + wE[4] * y3 + wE[12]; + vertices[start + 28] = wE[1] * x3 + wE[5] * y3 + wE[13]; + vertices[start + 29] = wE[2] * x3 + wE[6] * y3 + wE[14]; + vertices[start + 30] = u3; + vertices[start + 31] = v3; + + const indices = subChunk.indices; + indices[0] = 0; + indices[1] = 1; + indices[2] = 2; + indices[3] = 2; + indices[4] = 1; + indices[5] = 3; + indices.length = 6; + } + + private static _filledRadial90( + renderer: ISpriteRenderer, + matrix: Matrix, + origin: SpriteFilledOrigin, + amount: number, + cw: boolean + ): void { + if (amount <= this._fillAmountEpsilon) { + renderer._subChunk.indices.length = 0; + return; + } + + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[3]; + + // Transform 4 corners to world space + const [wLB, wRB, wLT, wRT] = this._worldPositions; + const [uvLB, uvRB, uvLT, uvRT] = this._uvs; + wLB.set(lPosLB.x, lPosLB.y, 0).transformToVec3(matrix), uvLB.set(left, bottom); + wRB.set(lPosRB.x, lPosRB.y, 0).transformToVec3(matrix), uvRB.set(right, bottom); + wLT.set(lPosLT.x, lPosLT.y, 0).transformToVec3(matrix), uvLT.set(left, top); + wRT.set(lPosRT.x, lPosRT.y, 0).transformToVec3(matrix), uvRT.set(right, top); + + // Map vertices based on origin corner: + // [center, CW-adjacent, CCW-adjacent, opposite] + const { _inPositions: inPositions, _inUVs: inUVs, _outPositions: outPositions, _outUVs: outUVs } = this; + switch (origin) { + case SpriteFilledOrigin.BottomRight: + (inPositions[0] = wRB), (inUVs[0] = uvRB); + (inPositions[1] = wRT), (inUVs[1] = uvRT); + (inPositions[2] = wLB), (inUVs[2] = uvLB); + (inPositions[3] = wLT), (inUVs[3] = uvLT); + break; + case SpriteFilledOrigin.TopRight: + (inPositions[0] = wRT), (inUVs[0] = uvRT); + (inPositions[1] = wLT), (inUVs[1] = uvLT); + (inPositions[2] = wRB), (inUVs[2] = uvRB); + (inPositions[3] = wLB), (inUVs[3] = uvLB); + break; + case SpriteFilledOrigin.TopLeft: + (inPositions[0] = wLT), (inUVs[0] = uvLT); + (inPositions[1] = wLB), (inUVs[1] = uvLB); + (inPositions[2] = wRT), (inUVs[2] = uvRT); + (inPositions[3] = wRB), (inUVs[3] = uvRB); + break; + // BottomLeft is the default; any unsupported origin falls back to it instead of rendering stale data. + case SpriteFilledOrigin.BottomLeft: + default: + (inPositions[0] = wLB), (inUVs[0] = uvLB); + (inPositions[1] = wRB), (inUVs[1] = uvRB); + (inPositions[2] = wLT), (inUVs[2] = uvLT); + (inPositions[3] = wRT), (inUVs[3] = uvRT); + break; + } + + const startAngle = cw ? 90 - amount * 90 : 0; + const endAngle = cw ? 90 : amount * 90; + + this._vertexOffset = this._indicesOffset = 0; + this._radialCut(renderer._subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + renderer._subChunk.indices.length = this._indicesOffset; + } + + private static _filledRadial180( + renderer: ISpriteRenderer, + matrix: Matrix, + origin: SpriteFilledOrigin, + amount: number, + cw: boolean + ): void { + if (amount <= this._fillAmountEpsilon) { + renderer._subChunk.indices.length = 0; + return; + } + + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[3]; + + // Transform corners and compute edge midpoints + const [wLB, wMB, wRB, wLM, , wRM, wLT, wMT, wRT] = this._worldPositions; + const [uvLB, uvMB, uvRB, uvLM, , uvRM, uvLT, uvMT, uvRT] = this._uvs; + wLB.set(lPosLB.x, lPosLB.y, 0).transformToVec3(matrix), uvLB.set(left, bottom); + wRB.set(lPosRB.x, lPosRB.y, 0).transformToVec3(matrix), uvRB.set(right, bottom); + wLT.set(lPosLT.x, lPosLT.y, 0).transformToVec3(matrix), uvLT.set(left, top); + wRT.set(lPosRT.x, lPosRT.y, 0).transformToVec3(matrix), uvRT.set(right, top); + Vector3.lerp(wLB, wRB, 0.5, wMB), Vector2.lerp(uvLB, uvRB, 0.5, uvMB); + Vector3.lerp(wLB, wLT, 0.5, wLM), Vector2.lerp(uvLB, uvLT, 0.5, uvLM); + Vector3.lerp(wLT, wRT, 0.5, wMT), Vector2.lerp(uvLT, uvRT, 0.5, uvMT); + Vector3.lerp(wRB, wRT, 0.5, wRM), Vector2.lerp(uvRB, uvRT, 0.5, uvRM); + + const startAngle = cw ? 180 - amount * 180 : 0; + const endAngle = cw ? 180 : amount * 180; + + this._vertexOffset = this._indicesOffset = 0; + const { _inPositions: inPositions, _inUVs: inUVs, _outPositions: outPositions, _outUVs: outUVs } = this; + const { _subChunk: subChunk } = renderer; + + // Center is at the origin edge midpoint; two quadrants cover the full sprite. + // Quadrant A (0°-90°), Quadrant B (90°-180°) + switch (origin) { + case SpriteFilledOrigin.Top: + // Center=MT, A: [MT,LT,MB,LB], B: [MT,MB,RT,RB] + (inPositions[0] = wMT), (inUVs[0] = uvMT); + (inPositions[1] = wLT), (inUVs[1] = uvLT); + (inPositions[2] = wMB), (inUVs[2] = uvMB); + (inPositions[3] = wLB), (inUVs[3] = uvLB); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wMB), (inUVs[1] = uvMB); + (inPositions[2] = wRT), (inUVs[2] = uvRT); + (inPositions[3] = wRB), (inUVs[3] = uvRB); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + case SpriteFilledOrigin.Left: + // Center=LM, A: [LM,LB,RM,RB], B: [LM,RM,LT,RT] + (inPositions[0] = wLM), (inUVs[0] = uvLM); + (inPositions[1] = wLB), (inUVs[1] = uvLB); + (inPositions[2] = wRM), (inUVs[2] = uvRM); + (inPositions[3] = wRB), (inUVs[3] = uvRB); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wRM), (inUVs[1] = uvRM); + (inPositions[2] = wLT), (inUVs[2] = uvLT); + (inPositions[3] = wRT), (inUVs[3] = uvRT); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + case SpriteFilledOrigin.Right: + // Center=RM, A: [RM,RT,LM,LT], B: [RM,LM,RB,LB] + (inPositions[0] = wRM), (inUVs[0] = uvRM); + (inPositions[1] = wRT), (inUVs[1] = uvRT); + (inPositions[2] = wLM), (inUVs[2] = uvLM); + (inPositions[3] = wLT), (inUVs[3] = uvLT); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wLM), (inUVs[1] = uvLM); + (inPositions[2] = wRB), (inUVs[2] = uvRB); + (inPositions[3] = wLB), (inUVs[3] = uvLB); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + // Bottom is the default; any unsupported origin falls back to it instead of rendering stale data. + case SpriteFilledOrigin.Bottom: + default: + // Center=MB, A: [MB,RB,MT,RT], B: [MB,MT,LB,LT] + (inPositions[0] = wMB), (inUVs[0] = uvMB); + (inPositions[1] = wRB), (inUVs[1] = uvRB); + (inPositions[2] = wMT), (inUVs[2] = uvMT); + (inPositions[3] = wRT), (inUVs[3] = uvRT); + this._radialCut(subChunk, inPositions, inUVs, startAngle, endAngle, outPositions, outUVs); + (inPositions[1] = wMT), (inUVs[1] = uvMT); + (inPositions[2] = wLB), (inUVs[2] = uvLB); + (inPositions[3] = wLT), (inUVs[3] = uvLT); + this._radialCut(subChunk, inPositions, inUVs, startAngle - 90, endAngle - 90, outPositions, outUVs); + break; + } + + subChunk.indices.length = this._indicesOffset; + } + + private static _filledRadial360( + renderer: ISpriteRenderer, + matrix: Matrix, + origin: SpriteFilledOrigin, + amount: number, + cw: boolean + ): void { + if (amount <= this._fillAmountEpsilon) { + renderer._subChunk.indices.length = 0; + return; + } + + let startAngle = 0; + switch (origin) { + case SpriteFilledOrigin.Top: + startAngle = cw ? 450 - amount * 360 : 90; + break; + case SpriteFilledOrigin.Left: + startAngle = cw ? 540 - amount * 360 : 180; + break; + case SpriteFilledOrigin.Bottom: + startAngle = cw ? 630 - amount * 360 : 270; + break; + // Right is handled here, and this branch also catches any unsupported origin as a fallback. + case SpriteFilledOrigin.Right: + default: + startAngle = cw ? 360 - amount * 360 : 0; + break; + } + const endAngle = startAngle + amount * 360; + + this._processRadialGrid(renderer, matrix, startAngle, endAngle); + } + + /** + * Prepare the 3x3 grid and process 4 quadrants for radial fill. + */ + private static _processRadialGrid( + renderer: ISpriteRenderer, + matrix: Matrix, + startAngle: number, + endAngle: number + ): void { + const sprite = renderer.sprite; + const [lPosLB, lPosRB, lPosLT, lPosRT] = sprite._getPositions(); + const spriteUVs = sprite._getUVs(); + const { x: left, y: bottom } = spriteUVs[0]; + const { x: right, y: top } = spriteUVs[3]; + + // --------------- + // LT - MT - RT + // | | | + // LM - C - RM + // | | | + // LB - MB - RB + // --------------- + const [wPosLB, wPosMB, wPosRB, wPosLM, wPosC, wPosRM, wPosLT, wPosMT, wPosRT] = this._worldPositions; + const [uvLB, uvMB, uvRB, uvLM, uvC, uvRM, uvLT, uvMT, uvRT] = this._uvs; + + wPosLB.set(lPosLB.x, lPosLB.y, 0).transformToVec3(matrix), uvLB.set(left, bottom); + wPosRB.set(lPosRB.x, lPosRB.y, 0).transformToVec3(matrix), uvRB.set(right, bottom); + wPosLT.set(lPosLT.x, lPosLT.y, 0).transformToVec3(matrix), uvLT.set(left, top); + wPosRT.set(lPosRT.x, lPosRT.y, 0).transformToVec3(matrix), uvRT.set(right, top); + Vector3.lerp(wPosLB, wPosRB, 0.5, wPosMB), Vector2.lerp(uvLB, uvRB, 0.5, uvMB); + Vector3.lerp(wPosLB, wPosLT, 0.5, wPosLM), Vector2.lerp(uvLB, uvLT, 0.5, uvLM); + Vector3.lerp(wPosLT, wPosRT, 0.5, wPosMT), Vector2.lerp(uvLT, uvRT, 0.5, uvMT); + Vector3.lerp(wPosRB, wPosRT, 0.5, wPosRM), Vector2.lerp(uvRB, uvRT, 0.5, uvRM); + Vector3.lerp(wPosLB, wPosRT, 0.5, wPosC), Vector2.lerp(uvLB, uvRT, 0.5, uvC); + + this._vertexOffset = this._indicesOffset = 0; + const { _inPositions: inPositions, _inUVs: inUVs, _outPositions: outPositions, _outUVs: outUVs } = this; + const { _subChunk: subChunk } = renderer; + let quadrantStart = 0; + let quadrantEnd = 0; + (inPositions[0] = wPosC), (inUVs[0] = uvC); + + { + // First quadrant (0°-90°) + if (startAngle >= 90) { + quadrantStart = startAngle - 360; + quadrantEnd = endAngle - 360; + } else { + quadrantStart = startAngle; + quadrantEnd = endAngle; + } + (inPositions[1] = wPosRM), (inUVs[1] = uvRM); + (inPositions[2] = wPosMT), (inUVs[2] = uvMT); + (inPositions[3] = wPosRT), (inUVs[3] = uvRT); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + { + // Second quadrant (90°-180°) + if (startAngle >= 180) { + quadrantStart = startAngle - 360 - 90; + quadrantEnd = endAngle - 360 - 90; + } else { + quadrantStart = startAngle - 90; + quadrantEnd = endAngle - 90; + } + (inPositions[1] = wPosMT), (inUVs[1] = uvMT); + (inPositions[2] = wPosLM), (inUVs[2] = uvLM); + (inPositions[3] = wPosLT), (inUVs[3] = uvLT); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + { + // Third quadrant (180°-270°) + if (startAngle >= 270) { + quadrantStart = startAngle - 360 - 180; + quadrantEnd = endAngle - 360 - 180; + } else { + quadrantStart = startAngle - 180; + quadrantEnd = endAngle - 180; + } + (inPositions[1] = wPosLM), (inUVs[1] = uvLM); + (inPositions[2] = wPosMB), (inUVs[2] = uvMB); + (inPositions[3] = wPosLB), (inUVs[3] = uvLB); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + { + // Fourth quadrant (270°-360°) + if (startAngle >= 360) { + quadrantStart = startAngle - 360 - 270; + quadrantEnd = endAngle - 360 - 270; + } else { + quadrantStart = startAngle - 270; + quadrantEnd = endAngle - 270; + } + (inPositions[1] = wPosMB), (inUVs[1] = uvMB); + (inPositions[2] = wPosRM), (inUVs[2] = uvRM); + (inPositions[3] = wPosRB), (inUVs[3] = uvRB); + this._radialCut(subChunk, inPositions, inUVs, quadrantStart, quadrantEnd, outPositions, outUVs); + } + + subChunk.indices.length = this._indicesOffset; + } + + private static _radialCut( + subChunk: SubPrimitiveChunk, + positions: Vector3[], + uvs: Vector2[], + start: number, + end: number, + outPositions: Vector3[], + outUVs: Vector2[] + ): void { + if (start >= 90 || end <= 0) return; + outPositions[0].copyFrom(positions[0]); + outUVs[0].copyFrom(uvs[0]); + + if (start <= 0) { + outPositions[1].copyFrom(positions[1]); + outUVs[1].copyFrom(uvs[1]); + } else { + const startTan = Math.tan((start * Math.PI) / 180); + if (startTan < 1) { + Vector3.lerp(positions[1], positions[3], startTan, outPositions[1]); + Vector2.lerp(uvs[1], uvs[3], startTan, outUVs[1]); + } else { + Vector3.lerp(positions[2], positions[3], 1 / startTan, outPositions[1]); + Vector2.lerp(uvs[2], uvs[3], 1 / startTan, outUVs[1]); + } + } + + if (end >= 90) { + outPositions[2].copyFrom(positions[2]); + outUVs[2].copyFrom(uvs[2]); + } else { + const endTan = Math.tan((end * Math.PI) / 180); + if (endTan < 1) { + Vector3.lerp(positions[1], positions[3], endTan, outPositions[2]); + Vector2.lerp(uvs[1], uvs[3], endTan, outUVs[2]); + } else { + Vector3.lerp(positions[2], positions[3], 1 / endTan, outPositions[2]); + Vector2.lerp(uvs[2], uvs[3], 1 / endTan, outUVs[2]); + } + } + + // 45° is the quadrant diagonal: a sector spanning it reaches the far corner and needs a quad; + // otherwise the cut produces a triangle. + if (start < 45 && end > 45) { + outPositions[3].copyFrom(positions[3]); + outUVs[3].copyFrom(uvs[3]); + this._addQuad(subChunk, outPositions, outUVs); + } else { + this._addTriangle(subChunk, outPositions, outUVs); + } + } + + private static _addTriangle(subChunk: SubPrimitiveChunk, positions: Vector3[], uvs: Vector2[]): void { + const vertices = subChunk.chunk.vertices; + const indices = subChunk.indices; + const vertexOffset = this._vertexOffset; + const vertexCount = vertexOffset / 9; + const start = subChunk.vertexArea.start + vertexOffset; + for (let i = 0, o = start; i < 3; ++i, o += 9) { + const position = positions[i]; + const uv = uvs[i]; + vertices[o] = position.x; + vertices[o + 1] = position.y; + vertices[o + 2] = position.z; + vertices[o + 3] = uv.x; + vertices[o + 4] = uv.y; + } + const indicesOffset = this._indicesOffset; + indices[indicesOffset] = vertexCount; + indices[indicesOffset + 1] = vertexCount + 1; + indices[indicesOffset + 2] = vertexCount + 2; + this._vertexOffset += 3 * 9; + this._indicesOffset += 3; + } + + private static _addQuad(subChunk: SubPrimitiveChunk, positions: Vector3[], uvs: Vector2[]): void { + const vertices = subChunk.chunk.vertices; + const indices = subChunk.indices; + const vertexOffset = this._vertexOffset; + const vertexCount = vertexOffset / 9; + const start = subChunk.vertexArea.start + vertexOffset; + for (let i = 0, o = start; i < 4; ++i, o += 9) { + const position = positions[i]; + const uv = uvs[i]; + vertices[o] = position.x; + vertices[o + 1] = position.y; + vertices[o + 2] = position.z; + vertices[o + 3] = uv.x; + vertices[o + 4] = uv.y; + } + const indicesOffset = this._indicesOffset; + indices[indicesOffset] = vertexCount; + indices[indicesOffset + 1] = vertexCount + 1; + indices[indicesOffset + 2] = vertexCount + 2; + indices[indicesOffset + 3] = vertexCount + 2; + indices[indicesOffset + 4] = vertexCount + 1; + indices[indicesOffset + 5] = vertexCount + 3; + this._vertexOffset += 4 * 9; + this._indicesOffset += 6; + } +} diff --git a/packages/core/src/2d/assembler/ISpriteRenderer.ts b/packages/core/src/2d/assembler/ISpriteRenderer.ts index a72f4e9436..04b50b3fc9 100644 --- a/packages/core/src/2d/assembler/ISpriteRenderer.ts +++ b/packages/core/src/2d/assembler/ISpriteRenderer.ts @@ -1,6 +1,8 @@ import { Color } from "@galacean/engine-math"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { SpriteFilledMode } from "../enums/SpriteFilledMode"; +import { SpriteFilledOrigin } from "../enums/SpriteFilledOrigin"; import { SpriteTileMode } from "../enums/SpriteTileMode"; import { Sprite } from "../sprite"; @@ -12,6 +14,10 @@ export interface ISpriteRenderer { color?: Color; tileMode?: SpriteTileMode; tiledAdaptiveThreshold?: number; + filledMode?: SpriteFilledMode; + filledAmount?: number; + filledOrigin?: SpriteFilledOrigin; + filledClockWise?: boolean; _subChunk: SubPrimitiveChunk; _getChunkManager(): PrimitiveChunkManager; } diff --git a/packages/core/src/2d/atlas/FontAtlas.ts b/packages/core/src/2d/atlas/FontAtlas.ts index e84bfae4a9..b4e860fc16 100644 --- a/packages/core/src/2d/atlas/FontAtlas.ts +++ b/packages/core/src/2d/atlas/FontAtlas.ts @@ -12,10 +12,10 @@ export class FontAtlas extends ReferResource { texture: Texture2D; _charInfoMap: Record = {}; - private _space: number = 1; - private _curX: number = 1; - private _curY: number = 1; - private _nextY: number = 1; + private _space: number = 4; + private _curX: number = 4; + private _curY: number = 4; + private _nextY: number = 4; constructor(engine: Engine) { super(engine); diff --git a/packages/core/src/2d/enums/SpriteDrawMode.ts b/packages/core/src/2d/enums/SpriteDrawMode.ts index 46bcfd3783..6f35d8c1ef 100644 --- a/packages/core/src/2d/enums/SpriteDrawMode.ts +++ b/packages/core/src/2d/enums/SpriteDrawMode.ts @@ -7,5 +7,7 @@ export enum SpriteDrawMode { /** When modifying the size of the renderer, it scales to fill the range according to the sprite border settings. */ Sliced, /** When modifying the size of the renderer, it will tile to fill the range according to the sprite border settings. */ - Tiled + Tiled, + /** Fill the sprite partially, controlled by fill amount, mode and origin. */ + Filled } diff --git a/packages/core/src/2d/enums/SpriteFilledMode.ts b/packages/core/src/2d/enums/SpriteFilledMode.ts new file mode 100644 index 0000000000..a01cda9e53 --- /dev/null +++ b/packages/core/src/2d/enums/SpriteFilledMode.ts @@ -0,0 +1,15 @@ +/** + * Sprite's filled mode enumeration. + */ +export enum SpriteFilledMode { + /** Fill horizontally. */ + Horizontal, + /** Fill vertically. */ + Vertical, + /** Fill radially over 90 degrees. */ + Radial90, + /** Fill radially over 180 degrees. */ + Radial180, + /** Fill radially over 360 degrees. */ + Radial360 +} diff --git a/packages/core/src/2d/enums/SpriteFilledOrigin.ts b/packages/core/src/2d/enums/SpriteFilledOrigin.ts new file mode 100644 index 0000000000..e9b3193970 --- /dev/null +++ b/packages/core/src/2d/enums/SpriteFilledOrigin.ts @@ -0,0 +1,21 @@ +/** + * Sprite's filled origin enumeration. + */ +export enum SpriteFilledOrigin { + /** Origin at the right. */ + Right, + /** Origin at the top-right. */ + TopRight, + /** Origin at the top. */ + Top, + /** Origin at the top-left. */ + TopLeft, + /** Origin at the left. */ + Left, + /** Origin at the bottom-left. */ + BottomLeft, + /** Origin at the bottom. */ + Bottom, + /** Origin at the bottom-right. */ + BottomRight +} diff --git a/packages/core/src/2d/enums/TextOverflow.ts b/packages/core/src/2d/enums/TextOverflow.ts index adef863f1f..bf8fdf8aa4 100644 --- a/packages/core/src/2d/enums/TextOverflow.ts +++ b/packages/core/src/2d/enums/TextOverflow.ts @@ -1,9 +1,11 @@ /** - * The way to handle the situation where wrapped text is too tall to fit in the height. + * The way to handle the situation where the text is too large to fit in the bounds. */ export enum OverflowMode { /** Overflow when the text is too tall */ Overflow = 0, /** Truncate with height when the text is too tall */ - Truncate = 1 + Truncate = 1, + /** Shrink the font size until the text fits within the bounds (both width and height) */ + Shrink = 2 } diff --git a/packages/core/src/2d/index.ts b/packages/core/src/2d/index.ts index 47be64ccfc..5481f42b28 100644 --- a/packages/core/src/2d/index.ts +++ b/packages/core/src/2d/index.ts @@ -1,11 +1,14 @@ export type { ISpriteAssembler } from "./assembler/ISpriteAssembler"; export type { ISpriteRenderer } from "./assembler/ISpriteRenderer"; +export { FilledSpriteAssembler } from "./assembler/FilledSpriteAssembler"; export { SimpleSpriteAssembler } from "./assembler/SimpleSpriteAssembler"; export { SlicedSpriteAssembler } from "./assembler/SlicedSpriteAssembler"; export { TiledSpriteAssembler } from "./assembler/TiledSpriteAssembler"; export { SpriteAtlas } from "./atlas/SpriteAtlas"; export { FontStyle } from "./enums/FontStyle"; export { SpriteDrawMode } from "./enums/SpriteDrawMode"; +export { SpriteFilledMode } from "./enums/SpriteFilledMode"; +export { SpriteFilledOrigin } from "./enums/SpriteFilledOrigin"; export { SpriteMaskInteraction } from "./enums/SpriteMaskInteraction"; export { SpriteModifyFlags } from "./enums/SpriteModifyFlags"; export { SpriteTileMode } from "./enums/SpriteTileMode"; diff --git a/packages/core/src/2d/sprite/MaskRenderable.ts b/packages/core/src/2d/sprite/MaskRenderable.ts new file mode 100644 index 0000000000..441492ba85 --- /dev/null +++ b/packages/core/src/2d/sprite/MaskRenderable.ts @@ -0,0 +1,398 @@ +import { BoundingBox, Vector2, Vector3 } from "@galacean/engine-math"; +import { RenderElement } from "../../RenderPipeline/RenderElement"; +import { VertexMergeBatcher } from "../../RenderPipeline/VertexMergeBatcher"; +import { Renderer, RendererUpdateFlags } from "../../Renderer"; +import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; +import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; +import { ShaderProperty } from "../../shader/ShaderProperty"; +import type { ISpriteRenderer } from "../assembler/ISpriteRenderer"; +import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; +import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; +import { Sprite } from "./Sprite"; +import { SpriteMaskUtils } from "./SpriteMaskUtils"; + +/** + * Public contract of the MaskRenderable mixin, used for declaration file generation. + */ +export interface IMaskRenderable { + influenceLayers: SpriteMaskLayer; + flipX: boolean; + flipY: boolean; + sprite: Sprite; + alphaCutoff: number; + _renderElement: RenderElement; + _maskIndex: number; + _containsWorldPoint(worldPoint: Vector3): boolean; + _initMask(): void; + _cloneMaskData(target: IMaskRenderable): void; + _destroyMaskResources(): void; + _updateMaskBounds(worldBounds: BoundingBox): void; + _renderMask(distanceForSort: number): void; + _onSpriteChange(type: SpriteModifyFlags): void; + _onSpriteChangeExtra(type: SpriteModifyFlags): void; + _getSpriteWidth(): number; + _getSpriteHeight(): number; + _getSpritePivot(): Vector2; +} + +type RendererConstructor = abstract new (...args: any[]) => Renderer; + +/** + * Mixin that provides shared mask rendering logic for both 2D SpriteMask and UI Mask. + */ +export function MaskRenderable( + Base: T +): (abstract new (...args: any[]) => IMaskRenderable) & T { + abstract class MaskRenderableBase extends Base implements IMaskRenderable { + private static _maskTextureProperty = ShaderProperty.getByName("renderer_MaskTexture"); + private static _alphaCutoffProperty = ShaderProperty.getByName("renderer_MaskAlphaCutoff"); + + @assignmentClone + private _influenceLayers: SpriteMaskLayer = SpriteMaskLayer.Everything; + /** @internal */ + @ignoreClone + _renderElement: RenderElement; + /** @internal */ + @ignoreClone + _maskIndex: number = -1; + @ignoreClone + private _sprite: Sprite = null; + @assignmentClone + private _flipX: boolean = false; + @assignmentClone + private _flipY: boolean = false; + @assignmentClone + private _alphaCutoff: number = 0.5; + + /** + * The mask layers the sprite mask influence to. + */ + get influenceLayers(): SpriteMaskLayer { + return this._influenceLayers; + } + + set influenceLayers(value: SpriteMaskLayer) { + if (this._influenceLayers !== value) { + this._influenceLayers = value; + // @ts-ignore + if (this._phasedActiveInScene) { + // @ts-ignore + this.scene._maskManager.onMaskInfluenceLayersChange(); + } + } + } + + /** + * Flips the sprite on the X axis. + */ + get flipX(): boolean { + return this._flipX; + } + + set flipX(value: boolean) { + if (this._flipX !== value) { + this._flipX = value; + // @ts-ignore + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } + } + + /** + * Flips the sprite on the Y axis. + */ + get flipY(): boolean { + return this._flipY; + } + + set flipY(value: boolean) { + if (this._flipY !== value) { + this._flipY = value; + // @ts-ignore + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } + } + + /** + * The Sprite to render. + */ + get sprite(): Sprite { + return this._sprite; + } + + set sprite(value: Sprite | null) { + const lastSprite = this._sprite; + if (lastSprite !== value) { + if (lastSprite) { + // @ts-ignore + this._addResourceReferCount(lastSprite, -1); + lastSprite._updateFlagManager.removeListener(this._onSpriteChange); + } + // @ts-ignore + this._dirtyUpdateFlag |= MaskDirtyFlags.All; + if (value) { + // @ts-ignore + this._addResourceReferCount(value, 1); + value._updateFlagManager.addListener(this._onSpriteChange); + // @ts-ignore + this.shaderData.setTexture(MaskRenderableBase._maskTextureProperty, value.texture); + } else { + // @ts-ignore + this.shaderData.setTexture(MaskRenderableBase._maskTextureProperty, null); + } + this._sprite = value; + } + } + + /** + * The minimum alpha value used by the mask to select the area of influence defined over the mask's sprite. Value between 0 and 1. + */ + get alphaCutoff(): number { + return this._alphaCutoff; + } + + set alphaCutoff(value: number) { + if (this._alphaCutoff !== value) { + this._alphaCutoff = value; + // @ts-ignore + this.shaderData.setFloat(MaskRenderableBase._alphaCutoffProperty, value); + } + } + + /** + * @internal + */ + // @ts-ignore + override _canBatch(preElement: RenderElement, curElement: RenderElement): boolean { + return VertexMergeBatcher.canBatchSpriteMask(preElement, curElement); + } + + /** + * @internal + */ + // @ts-ignore + override _batch(preElement: RenderElement | null, curElement: RenderElement): void { + VertexMergeBatcher.batch(preElement, curElement); + } + + /** + * @internal + */ + // @ts-ignore + override _onEnableInScene(): void { + // @ts-ignore + super._onEnableInScene(); + // @ts-ignore + this.scene._maskManager.addSpriteMask(this); + } + + /** + * @internal + */ + // @ts-ignore + override _onDisableInScene(): void { + // @ts-ignore + super._onDisableInScene(); + // @ts-ignore + this.scene._maskManager.removeSpriteMask(this); + } + + /** + * @internal + */ + _containsWorldPoint(worldPoint: Vector3): boolean { + return SpriteMaskUtils.containsWorldPoint( + worldPoint, + this._sprite, + // @ts-ignore + this._transformEntity.transform.worldMatrix, + this._getSpriteWidth(), + this._getSpriteHeight(), + this._getSpritePivot(), + this._flipX, + this._flipY, + this._alphaCutoff + ); + } + + /** + * @internal + * Initialize shared mask resources. Must be called from subclass constructor. + */ + _initMask(): void { + SimpleSpriteAssembler.resetData(this as unknown as ISpriteRenderer); + // @ts-ignore + this.setMaterial(this._engine._basicResources.spriteMaskDefaultMaterial); + // @ts-ignore + this.shaderData.setFloat(MaskRenderableBase._alphaCutoffProperty, this._alphaCutoff); + this._renderElement = new RenderElement(); + this._onSpriteChange = this._onSpriteChange.bind(this); + } + + /** + * @internal + * Clone mask data to target. Called from subclass _cloneTo. + */ + _cloneMaskData(target: MaskRenderableBase): void { + target.sprite = this._sprite; + } + + /** + * @internal + * Release mask sprite resources. Called from subclass _onDestroy. + */ + _destroyMaskResources(): void { + const sprite = this._sprite; + if (sprite) { + // @ts-ignore + this._addResourceReferCount(sprite, -1); + sprite._updateFlagManager.removeListener(this._onSpriteChange); + } + this._sprite = null; + this._renderElement = null; + } + + /** + * @internal + * Update bounds using SimpleSpriteAssembler directly. + */ + _updateMaskBounds(worldBounds: BoundingBox): void { + const sprite = this._sprite; + if (sprite) { + SimpleSpriteAssembler.updatePositions( + this as unknown as ISpriteRenderer, + // @ts-ignore + this._transformEntity.transform.worldMatrix, + this._getSpriteWidth(), + this._getSpriteHeight(), + this._getSpritePivot(), + this._flipX, + this._flipY + ); + } else { + // @ts-ignore + const { worldPosition } = this._transformEntity.transform; + worldBounds.min.copyFrom(worldPosition); + worldBounds.max.copyFrom(worldPosition); + } + } + + /** + * @internal + * Shared render logic for mask geometry. + */ + _renderMask(distanceForSort: number): void { + const { _sprite: sprite } = this; + const width = this._getSpriteWidth(); + const height = this._getSpriteHeight(); + if (!sprite?.texture || !width || !height) { + return; + } + + // @ts-ignore + let material = this.getMaterial(); + if (!material) { + return; + } + if (material.destroyed) { + // @ts-ignore + material = this._engine._basicResources.spriteMaskDefaultMaterial; + } + + // Update position + // @ts-ignore + if (this._dirtyUpdateFlag & RendererUpdateFlags.WorldVolume) { + SimpleSpriteAssembler.updatePositions( + this as unknown as ISpriteRenderer, + // @ts-ignore + this._transformEntity.transform.worldMatrix, + width, + height, + this._getSpritePivot(), + this._flipX, + this._flipY + ); + // @ts-ignore + this._dirtyUpdateFlag &= ~RendererUpdateFlags.WorldVolume; + } + + // Update uv + // @ts-ignore + if (this._dirtyUpdateFlag & MaskDirtyFlags.UV) { + SimpleSpriteAssembler.updateUVs(this as unknown as ISpriteRenderer); + // @ts-ignore + this._dirtyUpdateFlag &= ~MaskDirtyFlags.UV; + } + + const renderElement = this._renderElement; + const subChunk = (this as any)._subChunk; + // @ts-ignore + renderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, sprite.texture, subChunk); + // @ts-ignore + renderElement.priority = this.priority; + renderElement.distanceForSort = distanceForSort; + renderElement.subShader = material.shader.subShaders[0]; + } + + /** @internal */ + @ignoreClone + _onSpriteChange(type: SpriteModifyFlags): void { + switch (type) { + case SpriteModifyFlags.texture: + // @ts-ignore + this.shaderData.setTexture(MaskRenderableBase._maskTextureProperty, this.sprite.texture); + break; + case SpriteModifyFlags.region: + case SpriteModifyFlags.atlasRegionOffset: + // @ts-ignore + this._dirtyUpdateFlag |= MaskDirtyFlags.WorldVolumeAndUV; + break; + case SpriteModifyFlags.atlasRegion: + // @ts-ignore + this._dirtyUpdateFlag |= MaskDirtyFlags.UV; + break; + case SpriteModifyFlags.destroy: + this.sprite = null; + break; + default: + this._onSpriteChangeExtra(type); + break; + } + } + + /** + * @internal + * Hook for subclass-specific sprite change handling. + * SpriteMask overrides this to handle size/pivot changes. + */ + _onSpriteChangeExtra(type: SpriteModifyFlags): void {} + + /** @internal */ + _getSpriteWidth(): number { + return 0; + } + /** @internal */ + _getSpriteHeight(): number { + return 0; + } + /** @internal */ + _getSpritePivot(): Vector2 { + return null; + } + } + + return MaskRenderableBase as unknown as (abstract new (...args: any[]) => IMaskRenderable) & T; +} + +/** + * @remarks Extends `RendererUpdateFlags`. + */ +export enum MaskDirtyFlags { + /** UV. */ + UV = 0x2, + /** Automatic Size. */ + AutomaticSize = 0x8, + /** WorldVolume and UV. */ + WorldVolumeAndUV = 0x3, + /** All. */ + All = 0xb +} diff --git a/packages/core/src/2d/sprite/SpriteMask.ts b/packages/core/src/2d/sprite/SpriteMask.ts index 1f740081f9..771faead34 100644 --- a/packages/core/src/2d/sprite/SpriteMask.ts +++ b/packages/core/src/2d/sprite/SpriteMask.ts @@ -1,44 +1,25 @@ import { BoundingBox } from "@galacean/engine-math"; import { Entity } from "../../Entity"; -import { VertexMergeBatcher } from "../../RenderPipeline/VertexMergeBatcher"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { RenderContext } from "../../RenderPipeline/RenderContext"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; -import { RenderElement } from "../../RenderPipeline/RenderElement"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; -import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; import { ShaderProperty } from "../../shader/ShaderProperty"; -import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; -import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; -import { Sprite } from "./Sprite"; +import { MaskDirtyFlags, MaskRenderable } from "./MaskRenderable"; /** * A component for masking Sprites. */ -export class SpriteMask extends Renderer implements ISpriteRenderer { - /** @internal */ - static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskTexture"); +export class SpriteMask extends MaskRenderable(Renderer) { /** @internal */ static _alphaCutoffProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskAlphaCutoff"); - - /** The mask layers the sprite mask influence to. */ - @assignmentClone - influenceLayers: SpriteMaskLayer = SpriteMaskLayer.Everything; /** @internal */ - @ignoreClone - _renderElement: RenderElement; - + static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskTexture"); /** @internal */ @ignoreClone _subChunk: SubPrimitiveChunk; - /** @internal */ - @ignoreClone - _maskIndex: number = -1; - - @ignoreClone - private _sprite: Sprite = null; @ignoreClone private _automaticWidth: number = 0; @@ -48,13 +29,6 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { private _customWidth: number = undefined; @assignmentClone private _customHeight: number = undefined; - @assignmentClone - private _flipX: boolean = false; - @assignmentClone - private _flipY: boolean = false; - - @assignmentClone - private _alphaCutoff: number = 0.5; /** * Render width (in world coordinates). @@ -67,7 +41,7 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { if (this._customWidth !== undefined) { return this._customWidth; } else { - this._dirtyUpdateFlag & SpriteMaskUpdateFlags.AutomaticSize && this._calDefaultSize(); + this._dirtyUpdateFlag & MaskDirtyFlags.AutomaticSize && this._calDefaultSize(); return this._automaticWidth; } } @@ -90,7 +64,7 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { if (this._customHeight !== undefined) { return this._customHeight; } else { - this._dirtyUpdateFlag & SpriteMaskUpdateFlags.AutomaticSize && this._calDefaultSize(); + this._dirtyUpdateFlag & MaskDirtyFlags.AutomaticSize && this._calDefaultSize(); return this._automaticHeight; } } @@ -102,84 +76,12 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { } } - /** - * Flips the sprite on the X axis. - */ - get flipX(): boolean { - return this._flipX; - } - - set flipX(value: boolean) { - if (this._flipX !== value) { - this._flipX = value; - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - } - } - - /** - * Flips the sprite on the Y axis. - */ - get flipY(): boolean { - return this._flipY; - } - - set flipY(value: boolean) { - if (this._flipY !== value) { - this._flipY = value; - this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; - } - } - - /** - * The Sprite to render. - */ - get sprite(): Sprite { - return this._sprite; - } - - set sprite(value: Sprite | null) { - const lastSprite = this._sprite; - if (lastSprite !== value) { - if (lastSprite) { - this._addResourceReferCount(lastSprite, -1); - lastSprite._updateFlagManager.removeListener(this._onSpriteChange); - } - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.All; - if (value) { - this._addResourceReferCount(value, 1); - value._updateFlagManager.addListener(this._onSpriteChange); - this.shaderData.setTexture(SpriteMask._textureProperty, value.texture); - } else { - this.shaderData.setTexture(SpriteMask._textureProperty, null); - } - this._sprite = value; - } - } - - /** - * The minimum alpha value used by the mask to select the area of influence defined over the mask's sprite. Value between 0 and 1. - */ - get alphaCutoff(): number { - return this._alphaCutoff; - } - - set alphaCutoff(value: number) { - if (this._alphaCutoff !== value) { - this._alphaCutoff = value; - this.shaderData.setFloat(SpriteMask._alphaCutoffProperty, value); - } - } - /** * @internal */ constructor(entity: Entity) { super(entity); - SimpleSpriteAssembler.resetData(this); - this.setMaterial(this._engine._basicResources.spriteMaskDefaultMaterial); - this.shaderData.setFloat(SpriteMask._alphaCutoffProperty, this._alphaCutoff); - this._renderElement = new RenderElement(); - this._onSpriteChange = this._onSpriteChange.bind(this); + this._initMask(); } /** @@ -195,37 +97,7 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { */ override _cloneTo(target: SpriteMask): void { super._cloneTo(target); - target.sprite = this._sprite; - } - - /** - * @internal - */ - override _canBatch(preElement: RenderElement, curElement: RenderElement): boolean { - return VertexMergeBatcher.canBatchSpriteMask(preElement, curElement); - } - - /** - * @internal - */ - override _batch(preElement: RenderElement | null, curElement: RenderElement): void { - VertexMergeBatcher.batch(preElement, curElement); - } - - /** - * @internal - */ - override _onEnableInScene(): void { - super._onEnableInScene(); - this.scene._maskManager.addSpriteMask(this); - } - - /** - * @internal - */ - override _onDisableInScene(): void { - super._onDisableInScene(); - this.scene._maskManager.removeSpriteMask(this); + this._cloneMaskData(target); } /** @@ -236,144 +108,64 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { } protected override _updateBounds(worldBounds: BoundingBox): void { - const sprite = this._sprite; - if (sprite) { - SimpleSpriteAssembler.updatePositions( - this, - this._transformEntity.transform.worldMatrix, - this.width, - this.height, - sprite.pivot, - this._flipX, - this._flipY - ); - } else { - const { worldPosition } = this._transformEntity.transform; - worldBounds.min.copyFrom(worldPosition); - worldBounds.max.copyFrom(worldPosition); - } + this._updateMaskBounds(worldBounds); } /** * @inheritdoc */ protected override _render(context: RenderContext): void { - const { _sprite: sprite } = this; - if (!sprite?.texture || !this.width || !this.height) { - return; - } - - let material = this.getMaterial(); - if (!material) { - return; - } - const { _engine: engine } = this; - // @todo: This question needs to be raised rather than hidden. - if (material.destroyed) { - material = engine._basicResources.spriteMaskDefaultMaterial; - } - - // Update position - if (this._dirtyUpdateFlag & RendererUpdateFlags.WorldVolume) { - SimpleSpriteAssembler.updatePositions( - this, - this._transformEntity.transform.worldMatrix, - this.width, - this.height, - sprite.pivot, - this._flipX, - this._flipY - ); - this._dirtyUpdateFlag &= ~RendererUpdateFlags.WorldVolume; - } - - // Update uv - if (this._dirtyUpdateFlag & SpriteMaskUpdateFlags.UV) { - SimpleSpriteAssembler.updateUVs(this); - this._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.UV; - } - - const renderElement = this._renderElement; - const subChunk = this._subChunk; - renderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, this.sprite.texture, subChunk); - renderElement.priority = this.priority; - renderElement.distanceForSort = this._distanceForSort; - renderElement.subShader = material.shader.subShaders[0]; + this._renderMask(this._distanceForSort); } /** * @inheritdoc */ protected override _onDestroy(): void { - const sprite = this._sprite; - if (sprite) { - this._addResourceReferCount(sprite, -1); - sprite._updateFlagManager.removeListener(this._onSpriteChange); - } + this._destroyMaskResources(); super._onDestroy(); - this._sprite = null; if (this._subChunk) { this._getChunkManager().freeSubChunk(this._subChunk); this._subChunk = null; } + } - this._renderElement = null; + override _getSpriteWidth(): number { + return this.width; } - private _calDefaultSize(): void { - const sprite = this._sprite; - if (sprite) { - this._automaticWidth = sprite.width; - this._automaticHeight = sprite.height; - } else { - this._automaticWidth = this._automaticHeight = 0; - } - this._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.AutomaticSize; + override _getSpriteHeight(): number { + return this.height; } - @ignoreClone - private _onSpriteChange(type: SpriteModifyFlags): void { + override _getSpritePivot() { + return this.sprite?.pivot; + } + + override _onSpriteChangeExtra(type: SpriteModifyFlags): void { switch (type) { - case SpriteModifyFlags.texture: - this.shaderData.setTexture(SpriteMask._textureProperty, this.sprite.texture); - break; case SpriteModifyFlags.size: - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.AutomaticSize; + this._dirtyUpdateFlag |= MaskDirtyFlags.AutomaticSize; if (this._customWidth === undefined || this._customHeight === undefined) { this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; } break; - case SpriteModifyFlags.region: - case SpriteModifyFlags.atlasRegionOffset: - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.WorldVolumeAndUV; - break; - case SpriteModifyFlags.atlasRegion: - this._dirtyUpdateFlag |= SpriteMaskUpdateFlags.UV; - break; case SpriteModifyFlags.pivot: this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; break; - case SpriteModifyFlags.destroy: - this.sprite = null; - break; - default: - break; } } -} -/** - * @remarks Extends `RendererUpdateFlags`. - */ -enum SpriteMaskUpdateFlags { - /** UV. */ - UV = 0x2, - /** Automatic Size. */ - AutomaticSize = 0x4, - /** WorldVolume and UV. */ - WorldVolumeAndUV = 0x3, - /** All. */ - All = 0x7 + private _calDefaultSize(): void { + const sprite = this.sprite; + if (sprite) { + this._automaticWidth = sprite.width; + this._automaticHeight = sprite.height; + } else { + this._automaticWidth = this._automaticHeight = 0; + } + this._dirtyUpdateFlag &= ~MaskDirtyFlags.AutomaticSize; + } } diff --git a/packages/core/src/2d/sprite/SpriteMaskUtils.ts b/packages/core/src/2d/sprite/SpriteMaskUtils.ts new file mode 100644 index 0000000000..b7033ee800 --- /dev/null +++ b/packages/core/src/2d/sprite/SpriteMaskUtils.ts @@ -0,0 +1,136 @@ +import { Matrix, Vector2, Vector3 } from "@galacean/engine-math"; +import { Texture2D, TextureFormat } from "../../texture"; +import { Sprite } from "./Sprite"; + +/** + * Internal helpers for sprite mask hit testing. + * @internal + */ +export class SpriteMaskUtils { + private static _tempMat: Matrix = new Matrix(); + private static _tempVec3: Vector3 = new Vector3(); + private static _u8Buffer1 = new Uint8Array(1); + private static _u8Buffer2 = new Uint8Array(2); + private static _u8Buffer4 = new Uint8Array(4); + private static _u16Buffer1 = new Uint16Array(1); + private static _u16Buffer4 = new Uint16Array(4); + private static _f32Buffer4 = new Float32Array(4); + private static _u32Buffer4 = new Uint32Array(4); + + static containsWorldPoint( + worldPoint: Vector3, + sprite: Sprite | null, + worldMatrix: Matrix, + width: number, + height: number, + pivot: Vector2, + flipX: boolean, + flipY: boolean, + alphaCutoff: number = 0 + ): boolean { + if (!sprite || !width || !height) { + return false; + } + + const worldMatrixInv = SpriteMaskUtils._tempMat; + Matrix.invert(worldMatrix, worldMatrixInv); + const localPosition = SpriteMaskUtils._tempVec3; + Vector3.transformCoordinate(worldPoint, worldMatrixInv, localPosition); + + const sx = flipX ? -width : width; + const sy = flipY ? -height : height; + if (!sx || !sy) { + return false; + } + + const spriteX = localPosition.x / sx + pivot.x; + const spriteY = localPosition.y / sy + pivot.y; + const spritePositions = sprite._getPositions(); + const { x: left, y: bottom } = spritePositions[0]; + const { x: right, y: top } = spritePositions[3]; + if (!(spriteX >= left && spriteX <= right && spriteY >= bottom && spriteY <= top)) { + return false; + } + + if (alphaCutoff <= 0) { + return true; + } + + const texture = sprite.texture; + if (!texture) { + return false; + } + + const spriteUVs = sprite._getUVs(); + const leftU = spriteUVs[0].x; + const bottomV = spriteUVs[0].y; + const rightU = spriteUVs[3].x; + const topV = spriteUVs[3].y; + const positionWidth = right - left; + const positionHeight = top - bottom; + if (!positionWidth || !positionHeight) { + return false; + } + + const tx = (spriteX - left) / positionWidth; + const ty = (spriteY - bottom) / positionHeight; + const u = leftU + (rightU - leftU) * tx; + const v = bottomV + (topV - bottomV) * ty; + const x = Math.min(Math.max(Math.floor(u * texture.width), 0), texture.width - 1); + const y = Math.min(Math.max(Math.floor(v * texture.height), 0), texture.height - 1); + return SpriteMaskUtils._sampleTextureAlpha(texture, x, y) >= alphaCutoff; + } + + private static _sampleTextureAlpha(texture: Texture2D, x: number, y: number): number { + try { + switch (texture.format) { + case TextureFormat.R8G8B8A8: { + const buffer = SpriteMaskUtils._u8Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3] / 255; + } + case TextureFormat.R4G4B4A4: { + const buffer = SpriteMaskUtils._u16Buffer1; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return (buffer[0] & 0xf) / 15; + } + case TextureFormat.R5G5B5A1: { + const buffer = SpriteMaskUtils._u16Buffer1; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[0] & 0x1; + } + case TextureFormat.Alpha8: + case TextureFormat.R8: { + const buffer = SpriteMaskUtils._u8Buffer1; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[0] / 255; + } + case TextureFormat.LuminanceAlpha: + case TextureFormat.R8G8: { + const buffer = SpriteMaskUtils._u8Buffer2; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[1] / 255; + } + case TextureFormat.R16G16B16A16: { + const buffer = SpriteMaskUtils._u16Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3] / 65535; + } + case TextureFormat.R32G32B32A32: { + const buffer = SpriteMaskUtils._f32Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3]; + } + case TextureFormat.R32G32B32A32_UInt: { + const buffer = SpriteMaskUtils._u32Buffer4; + texture.getPixelBuffer(x, y, 1, 1, buffer); + return buffer[3] / 4294967295; + } + default: + return 1; + } + } catch { + return 1; + } + } +} diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts index 2a6986af6a..3e2bff4409 100644 --- a/packages/core/src/2d/sprite/SpriteRenderer.ts +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -10,10 +10,13 @@ import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManage import { ShaderProperty } from "../../shader/ShaderProperty"; import { ISpriteAssembler } from "../assembler/ISpriteAssembler"; import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; +import { FilledSpriteAssembler } from "../assembler/FilledSpriteAssembler"; import { SimpleSpriteAssembler } from "../assembler/SimpleSpriteAssembler"; import { SlicedSpriteAssembler } from "../assembler/SlicedSpriteAssembler"; import { TiledSpriteAssembler } from "../assembler/TiledSpriteAssembler"; import { SpriteDrawMode } from "../enums/SpriteDrawMode"; +import { SpriteFilledMode } from "../enums/SpriteFilledMode"; +import { SpriteFilledOrigin } from "../enums/SpriteFilledOrigin"; import { SpriteMaskInteraction } from "../enums/SpriteMaskInteraction"; import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; import { SpriteTileMode } from "../enums/SpriteTileMode"; @@ -39,6 +42,14 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; @assignmentClone private _tiledAdaptiveThreshold: number = 0.5; + @assignmentClone + private _filledMode: SpriteFilledMode = SpriteFilledMode.Radial360; + @assignmentClone + private _filledAmount: number = 1; + @assignmentClone + private _filledOrigin: SpriteFilledOrigin = SpriteFilledOrigin.Bottom; + @assignmentClone + private _filledClockWise: boolean = true; @deepClone private _color: Color = new Color(1, 1, 1, 1); @@ -78,6 +89,9 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._assembler = TiledSpriteAssembler; break; + case SpriteDrawMode.Filled: + this._assembler = FilledSpriteAssembler; + break; default: break; } @@ -119,6 +133,76 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { } } + /** + * The fill amount of the sprite renderer, range from 0 to 1. (Only works in filled mode.) + */ + get filledAmount(): number { + return this._filledAmount; + } + + set filledAmount(value: number) { + value = MathUtil.clamp(value, 0, 1); + if (this._filledAmount !== value) { + this._filledAmount = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill mode of the sprite renderer. (Only works in filled mode.) + */ + get filledMode(): SpriteFilledMode { + return this._filledMode; + } + + set filledMode(value: SpriteFilledMode) { + if (this._filledMode !== value) { + this._filledMode = value; + // Keep the current origin if it is still valid for the new mode, otherwise snap to the mode's + // default, since each mode only accepts a subset of origins. + this._filledOrigin = SpriteRenderer._correctOrigin(value, this._filledOrigin); + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill origin of the sprite renderer. (Only works in filled mode.) + */ + get filledOrigin(): SpriteFilledOrigin { + return this._filledOrigin; + } + + set filledOrigin(value: SpriteFilledOrigin) { + // Snap to a value valid for the current mode so state stays self-consistent (getter === rendered). + value = SpriteRenderer._correctOrigin(this._filledMode, value); + if (this._filledOrigin !== value) { + this._filledOrigin = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * Whether the fill is clockwise. (Only works in filled radial mode.) + */ + get filledClockWise(): boolean { + return this._filledClockWise; + } + + set filledClockWise(value: boolean) { + if (this._filledClockWise !== value) { + this._filledClockWise = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; + } + } + } + /** * The Sprite to render. */ @@ -437,6 +521,9 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; break; + case SpriteDrawMode.Filled: + this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeUVAndColor; + break; } break; case SpriteModifyFlags.border: @@ -456,7 +543,11 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.WorldVolumeAndUV; break; case SpriteModifyFlags.atlasRegion: - this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.UV; + // Filled mode bakes UVs inside `updatePositions`, so a UV-only refresh would be a no-op. + this._dirtyUpdateFlag |= + this._drawMode === SpriteDrawMode.Filled + ? SpriteRendererUpdateFlags.WorldVolumeAndUV + : SpriteRendererUpdateFlags.UV; break; case SpriteModifyFlags.pivot: this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; @@ -471,6 +562,39 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { private _onColorChanged(): void { this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.Color; } + + /** + * Returns an origin valid for the given fill mode: the passed origin when it belongs to the mode's + * accepted subset, otherwise the mode's default. Keeps `filledMode`/`filledOrigin` always consistent. + */ + private static _correctOrigin(mode: SpriteFilledMode, origin: SpriteFilledOrigin): SpriteFilledOrigin { + switch (mode) { + case SpriteFilledMode.Horizontal: + return origin === SpriteFilledOrigin.Left || origin === SpriteFilledOrigin.Right + ? origin + : SpriteFilledOrigin.Left; + case SpriteFilledMode.Vertical: + return origin === SpriteFilledOrigin.Top || origin === SpriteFilledOrigin.Bottom + ? origin + : SpriteFilledOrigin.Bottom; + case SpriteFilledMode.Radial90: + // Radial90 accepts the four corner origins. + return origin === SpriteFilledOrigin.TopLeft || + origin === SpriteFilledOrigin.TopRight || + origin === SpriteFilledOrigin.BottomLeft || + origin === SpriteFilledOrigin.BottomRight + ? origin + : SpriteFilledOrigin.BottomLeft; + default: + // Radial180 / Radial360 accept the four edge origins. + return origin === SpriteFilledOrigin.Top || + origin === SpriteFilledOrigin.Bottom || + origin === SpriteFilledOrigin.Left || + origin === SpriteFilledOrigin.Right + ? origin + : SpriteFilledOrigin.Bottom; + } + } } /** diff --git a/packages/core/src/2d/sprite/index.ts b/packages/core/src/2d/sprite/index.ts index 162d016472..c48fb9261b 100644 --- a/packages/core/src/2d/sprite/index.ts +++ b/packages/core/src/2d/sprite/index.ts @@ -1,3 +1,6 @@ +export type { IMaskRenderable } from "./MaskRenderable"; +export { MaskDirtyFlags, MaskRenderable } from "./MaskRenderable"; export { Sprite } from "./Sprite"; export { SpriteMask } from "./SpriteMask"; +export { SpriteMaskUtils } from "./SpriteMaskUtils"; export { SpriteRenderer } from "./SpriteRenderer"; diff --git a/packages/core/src/2d/text/TextRenderer.ts b/packages/core/src/2d/text/TextRenderer.ts index 3649180c79..7d2c26b2e0 100644 --- a/packages/core/src/2d/text/TextRenderer.ts +++ b/packages/core/src/2d/text/TextRenderer.ts @@ -1,14 +1,15 @@ -import { BoundingBox, Color, Vector3 } from "@galacean/engine-math"; +import { BoundingBox, Color, Vector2, Vector3 } from "@galacean/engine-math"; import { Engine } from "../../Engine"; import { Entity } from "../../Entity"; -import { VertexMergeBatcher } from "../../RenderPipeline/VertexMergeBatcher"; import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManager"; import { RenderContext } from "../../RenderPipeline/RenderContext"; -import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; import { RenderElement } from "../../RenderPipeline/RenderElement"; +import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; +import { VertexMergeBatcher } from "../../RenderPipeline/VertexMergeBatcher"; import { Renderer } from "../../Renderer"; import { TransformModifyFlags } from "../../Transform"; import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; +import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; import { ShaderData, ShaderProperty } from "../../shader"; import { ShaderDataGroup } from "../../shader/enums/ShaderDataGroup"; import { Texture2D } from "../../texture"; @@ -21,15 +22,18 @@ import { Font } from "./Font"; import { ITextRenderer } from "./ITextRenderer"; import { SubFont } from "./SubFont"; import { TextUtils } from "./TextUtils"; -import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; /** * Renders a text for 2D graphics. */ export class TextRenderer extends Renderer implements ITextRenderer { - private static _textureProperty = ShaderProperty.getByName("renderElement_TextTexture"); + private static _tempVec20 = new Vector2(); private static _tempVec30 = new Vector3(); private static _tempVec31 = new Vector3(); + private static _textureProperty = ShaderProperty.getByName("renderElement_TextTexture"); + private static _textTextureSizeProperty = ShaderProperty.getByName("renderElement_TextTextureSize"); + private static _outlineColorProperty = ShaderProperty.getByName("renderer_OutlineColor"); + private static _outlineWidthProperty = ShaderProperty.getByName("renderer_OutlineWidth"); private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; private static _charRenderInfos: CharRenderInfo[] = []; @@ -69,6 +73,10 @@ export class TextRenderer extends Renderer implements ITextRenderer { private _enableWrapping = false; @assignmentClone private _overflowMode = OverflowMode.Overflow; + @deepClone + private _outlineColor = new Color(0, 0, 0, 1); + @ignoreClone + private _outlineWidth = 0; /** * Rendering color for the Text. @@ -255,6 +263,35 @@ export class TextRenderer extends Renderer implements ITextRenderer { } } + /** + * The outline width in pixels. 0 means outline is disabled. Clamped to [0, 8]. + */ + get outlineWidth(): number { + return this._outlineWidth; + } + + set outlineWidth(value: number) { + value = Math.max(0, Math.min(value, 3)); + if (this._outlineWidth !== value) { + this._outlineWidth = value; + this.shaderData.setFloat(TextRenderer._outlineWidthProperty, value); + this._setDirtyFlagTrue(DirtyFlag.Position); + } + } + + /** + * The outline color. Only effective when outlineWidth > 0. + */ + get outlineColor(): Color { + return this._outlineColor; + } + + set outlineColor(value: Color) { + if (this._outlineColor !== value) { + this._outlineColor.copyFrom(value); + } + } + /** * Interacts with the masks. */ @@ -309,8 +346,13 @@ export class TextRenderer extends Renderer implements ITextRenderer { this._font = engine._textDefaultFont; this._addResourceReferCount(this._font, 1); this.setMaterial(engine._basicResources.textDefaultMaterial); + const shaderData = this.shaderData; + shaderData.setFloat(TextRenderer._outlineWidthProperty, this._outlineWidth); + shaderData.setColor(TextRenderer._outlineColorProperty, this._outlineColor); //@ts-ignore this._color._onValueChanged = this._onColorChanged.bind(this); + // @ts-ignore + this._outlineColor._onValueChanged = this._onOutlineColorChanged.bind(this); } /** @@ -337,6 +379,7 @@ export class TextRenderer extends Renderer implements ITextRenderer { super._cloneTo(target); target.font = this._font; target._subFont = this._subFont; + target.outlineWidth = this._outlineWidth; } /** @@ -382,7 +425,7 @@ export class TextRenderer extends Renderer implements ITextRenderer { * @internal */ override _canBatch(preElement: RenderElement, curElement: RenderElement): boolean { - return VertexMergeBatcher.canBatchSprite(preElement, curElement); + return VertexMergeBatcher.canBatchText(preElement, curElement); } /** @@ -436,12 +479,17 @@ export class TextRenderer extends Renderer implements ITextRenderer { const distanceForSort = this._distanceForSort; const renderPipeline = camera._renderPipeline; const textChunks = this._textChunks; + const textTextureSize = TextRenderer._tempVec20; for (let i = 0, n = textChunks.length; i < n; ++i) { const { subChunk, texture } = textChunks[i]; const renderElement = textRenderElementPool.get(); renderElement.set(this, material, subChunk.chunk.primitive, subChunk.subMesh, texture, subChunk); renderElement.shaderData ||= new ShaderData(ShaderDataGroup.RenderElement); renderElement.shaderData.setTexture(TextRenderer._textureProperty, texture); + renderElement.shaderData.setVector2( + TextRenderer._textTextureSizeProperty, + textTextureSize.set(texture.width, texture.height) + ); renderElement.priority = priority; renderElement.distanceForSort = distanceForSort; renderPipeline.pushRenderElement(context, renderElement); @@ -454,6 +502,16 @@ export class TextRenderer extends Renderer implements ITextRenderer { this._subFont.nativeFontString = TextUtils.getNativeFontString(font.name, this.fontSize, this.fontStyle); } + /** + * Switch the sub font to a specific font size, used by the SHRINK overflow measurement. + */ + private _applyFontSizeForShrink(fontSize: number): void { + const font = this._font; + const subFont = font._getSubFont(fontSize, this._fontStyle); + subFont.nativeFontString = TextUtils.getNativeFontString(font.name, fontSize, this._fontStyle); + this._subFont = subFont; + } + private _updatePosition(): void { const { transform } = this.entity; const e = transform.worldMatrix.elements; @@ -527,22 +585,37 @@ export class TextRenderer extends Renderer implements ITextRenderer { const { _pixelsPerUnit } = Engine; const { min, max } = this._localBounds; const charRenderInfos = TextRenderer._charRenderInfos; + const rendererWidth = this.width * _pixelsPerUnit; + const rendererHeight = this.height * _pixelsPerUnit; + let fontSize = this._fontSize; + let textMetrics: ReturnType; + if (this._overflowMode === OverflowMode.Shrink) { + const result = TextUtils.measureTextWithShrink( + this, + rendererWidth, + rendererHeight, + this._fontSize, + this._lineSpacing, + this._characterSpacing, + this.enableWrapping, + (size) => this._applyFontSizeForShrink(size) + ); + fontSize = result.fontSize; + textMetrics = result.metrics; + } else { + const characterSpacing = this._characterSpacing * fontSize; + textMetrics = this.enableWrapping + ? TextUtils.measureTextWithWrap( + this, + rendererWidth, + rendererHeight, + this._lineSpacing * fontSize, + characterSpacing + ) + : TextUtils.measureTextWithoutWrap(this, rendererHeight, this._lineSpacing * fontSize, characterSpacing); + } const charFont = this._getSubFont(); - const characterSpacing = this._characterSpacing * this._fontSize; - const textMetrics = this.enableWrapping - ? TextUtils.measureTextWithWrap( - this, - this.width * _pixelsPerUnit, - this.height * _pixelsPerUnit, - this._lineSpacing * this._fontSize, - characterSpacing - ) - : TextUtils.measureTextWithoutWrap( - this, - this.height * _pixelsPerUnit, - this._lineSpacing * this._fontSize, - characterSpacing - ); + const characterSpacing = this._characterSpacing * fontSize; const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics; const charRenderInfoPool = this.engine._charRenderInfoPool; const linesLen = lines.length; @@ -551,9 +624,7 @@ export class TextRenderer extends Renderer implements ITextRenderer { if (linesLen > 0) { const { horizontalAlignment } = this; const pixelsPerUnitReciprocal = 1.0 / _pixelsPerUnit; - const rendererWidth = this._width * _pixelsPerUnit; const halfRendererWidth = rendererWidth * 0.5; - const rendererHeight = this._height * _pixelsPerUnit; const halfLineHeight = lineHeight * 0.5; let startY = 0; @@ -564,7 +635,10 @@ export class TextRenderer extends Renderer implements ITextRenderer { startY = rendererHeight * 0.5 - halfLineHeight + topDiff; break; case TextVerticalAlignment.Center: - startY = height * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; + // Center the text block (lineHeight * lineCount) within the renderer, independent of + // `height` — which equals the renderer height for Truncate/Shrink and would otherwise + // push the text upward by (rendererHeight - blockHeight) / 2 when the box is taller. + startY = lineHeight * linesLen * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; break; case TextVerticalAlignment.Bottom: startY = height - rendererHeight * 0.5 - halfLineHeight - bottomDiff; @@ -606,10 +680,11 @@ export class TextRenderer extends Renderer implements ITextRenderer { charRenderInfo.texture = charFont._getTextureByIndex(charInfo.index); charRenderInfo.uvs = charInfo.uvs; const { w, ascent, descent } = charInfo; - const left = startX * pixelsPerUnitReciprocal; - const right = (startX + w) * pixelsPerUnitReciprocal; - const top = (startY + ascent) * pixelsPerUnitReciprocal; - const bottom = (startY - descent) * pixelsPerUnitReciprocal; + const ow = this._outlineWidth * pixelsPerUnitReciprocal; + const left = startX * pixelsPerUnitReciprocal - ow; + const right = (startX + w) * pixelsPerUnitReciprocal + ow; + const top = (startY + ascent) * pixelsPerUnitReciprocal + ow; + const bottom = (startY - descent) * pixelsPerUnitReciprocal - ow; localPositions.set(left, top, right, bottom); i === firstLine && (maxY = Math.max(maxY, top)); minY = Math.min(minY, bottom); @@ -688,7 +763,7 @@ export class TextRenderer extends Renderer implements ITextRenderer { this._text === "" || this._fontSize === 0 || (this.enableWrapping && this.width <= 0) || - (this.overflowMode === OverflowMode.Truncate && this.height <= 0) + ((this.overflowMode === OverflowMode.Truncate || this.overflowMode === OverflowMode.Shrink) && this.height <= 0) ); } @@ -700,6 +775,10 @@ export class TextRenderer extends Renderer implements ITextRenderer { const vertices = subChunk.chunk.vertices; const indices = (subChunk.indices = []); const charRenderInfos = textChunk.charRenderInfos; + const ow = this._outlineWidth; + const texture = textChunk.texture; + const owU = ow > 0 ? ow / texture.width : 0; + const owV = ow > 0 ? ow / texture.height : 0; for (let i = 0, ii = 0, io = 0, vo = subChunk.vertexArea.start + 3; i < count; ++i, io += 4) { const charRenderInfo = charRenderInfos[i]; charRenderInfo.indexInChunk = i; @@ -709,10 +788,13 @@ export class TextRenderer extends Renderer implements ITextRenderer { indices[ii++] = tempIndices[j] + io; } - // Set uv and color for vertices + // Set uv and color for vertices, expand uv outward by outline width for (let j = 0; j < 4; ++j, vo += 9) { const uv = charRenderInfo.uvs[j]; - uv.copyToArray(vertices, vo); + const su = j === 1 || j === 2 ? 1 : -1; + const sv = j >= 2 ? 1 : -1; + vertices[vo] = uv.x + owU * su; + vertices[vo + 1] = uv.y + owV * sv; vertices[vo + 2] = r; vertices[vo + 3] = g; vertices[vo + 4] = b; @@ -745,6 +827,11 @@ export class TextRenderer extends Renderer implements ITextRenderer { private _onColorChanged(): void { this._setDirtyFlagTrue(DirtyFlag.Color); } + + @ignoreClone + private _onOutlineColorChanged(): void { + this.shaderData.setColor(TextRenderer._outlineColorProperty, this._outlineColor); + } } class TextChunk { diff --git a/packages/core/src/2d/text/TextUtils.ts b/packages/core/src/2d/text/TextUtils.ts index ce2440573f..5305049fa9 100644 --- a/packages/core/src/2d/text/TextUtils.ts +++ b/packages/core/src/2d/text/TextUtils.ts @@ -100,7 +100,8 @@ export class TextUtils { rendererWidth: number, rendererHeight: number, lineSpacing: number, - characterSpacing: number + characterSpacing: number, + uploadCharTexture: boolean = true ): TextMetrics { const subFont = renderer._getSubFont(); const fontString = subFont.nativeFontString; @@ -137,7 +138,7 @@ export class TextUtils { for (let j = 0, m = subText.length; j < m; ++j) { const char = subText[j]; - const charInfo = TextUtils._getCharInfo(char, fontString, subFont); + const charInfo = TextUtils._getCharInfo(char, fontString, subFont, uploadCharTexture); const charCode = char.charCodeAt(0); const isSpace = charCode === 32; @@ -284,7 +285,8 @@ export class TextUtils { renderer: ITextRenderer, rendererHeight: number, lineSpacing: number, - characterSpacing: number + characterSpacing: number, + uploadCharTexture: boolean = true ): TextMetrics { const subFont = renderer._getSubFont(); const fontString = subFont.nativeFontString; @@ -306,7 +308,7 @@ export class TextUtils { let maxDescent = 0; for (let j = 0; j < lineLength; ++j) { - const charInfo = TextUtils._getCharInfo(line[j], fontString, subFont); + const charInfo = TextUtils._getCharInfo(line[j], fontString, subFont, uploadCharTexture); curWidth += charInfo.xAdvance; const { offsetY } = charInfo; const halfH = charInfo.h * 0.5; @@ -337,6 +339,85 @@ export class TextUtils { }; } + /** + * Measure text in SHRINK overflow mode: keep shrinking the font size until the text fits + * within the bounds (both width and height). Mirrors Cocos Creator's `Overflow.SHRINK`. + * + * @param renderer - The text renderer + * @param rendererWidth - The width of the bounds in pixels + * @param rendererHeight - The height of the bounds in pixels + * @param originalFontSize - The font size set on the renderer (the upper bound, never enlarged) + * @param lineSpacing - The line spacing ratio (relative to font size) + * @param characterSpacing - The character spacing ratio (relative to font size) + * @param enableWrapping - Whether wrapping is enabled + * @param applyFontSize - Callback that switches the renderer's sub font to the given font size + * @returns The fitted text metrics and the actual font size used for layout + */ + static measureTextWithShrink( + renderer: ITextRenderer, + rendererWidth: number, + rendererHeight: number, + originalFontSize: number, + lineSpacing: number, + characterSpacing: number, + enableWrapping: boolean, + applyFontSize: (fontSize: number) => void + ): { metrics: TextMetrics; fontSize: number } { + // During the binary search we only need the text dimensions, so pass `uploadCharTexture=false` + // to avoid building GPU font atlases for the intermediate font sizes that won't be used. + // The fitted size is then re-measured once with upload=true to populate its atlas. This trades + // one extra full measure pass (CPU) for skipping ~log2(size) throwaway atlas textures (GPU); + // we can't reuse the search's last measure because its char bitmaps were never cached. + const measureAt = (fontSize: number, uploadCharTexture: boolean): TextMetrics => { + applyFontSize(fontSize); + return enableWrapping + ? TextUtils.measureTextWithWrap( + renderer, + rendererWidth, + rendererHeight, + lineSpacing * fontSize, + characterSpacing * fontSize, + uploadCharTexture + ) + : TextUtils.measureTextWithoutWrap( + renderer, + rendererHeight, + lineSpacing * fontSize, + characterSpacing * fontSize, + uploadCharTexture + ); + }; + // The content height is `lineHeight * lineCount`; `metrics.height` is clamped to the bounds + // unless overflowMode is Overflow, so it can't be used to detect vertical overflow here. + const isFit = (metrics: TextMetrics): boolean => + metrics.width <= rendererWidth && metrics.lineHeight * metrics.lines.length <= rendererHeight; + + // If the text already fits at the original size, keep it (SHRINK only shrinks, never enlarges). + let metrics = measureAt(originalFontSize, false); + if (isFit(metrics)) { + metrics = measureAt(originalFontSize, true); + return { metrics, fontSize: originalFontSize }; + } + + // Binary search for the largest integer font size that fits (measure only, no atlas upload). + let low = 1; + let high = Math.floor(originalFontSize); + let fitFontSize = low; + while (low <= high) { + const mid = (low + high) >> 1; + metrics = measureAt(mid, false); + if (isFit(metrics)) { + fitFontSize = mid; + low = mid + 1; + } else { + high = mid - 1; + } + } + // Re-measure the fitted size with upload=true so the sub font / atlas correspond to it. + metrics = measureAt(fitFontSize, true); + return { metrics, fontSize: fitFontSize }; + } + /** * Get native font hash. * @param fontName - The font name @@ -462,12 +543,16 @@ export class TextUtils { /** * @internal */ - static _getCharInfo(char: string, fontString: string, font: SubFont): CharInfo { + static _getCharInfo(char: string, fontString: string, font: SubFont, uploadCharTexture: boolean = true): CharInfo { let charInfo = font._getCharInfo(char); if (!charInfo) { charInfo = TextUtils.measureChar(char, fontString); - font._uploadCharTexture(charInfo); - font._addCharInfo(char, charInfo); + // SHRINK 的二分阶段只需字符尺寸(measureChar 已给出),传 uploadCharTexture=false 跳过 GPU + // 字形图集上传与缓存,避免给用不到的中间字号建字体 atlas 纹理。 + if (uploadCharTexture) { + font._uploadCharTexture(charInfo); + font._addCharInfo(char, charInfo); + } } return charInfo; diff --git a/packages/core/src/Engine.ts b/packages/core/src/Engine.ts index dc26be9235..0bc043be47 100644 --- a/packages/core/src/Engine.ts +++ b/packages/core/src/Engine.ts @@ -711,6 +711,7 @@ export class Engine extends EventDispatcher { this._renderElementPool.garbageCollection(); this._textRenderElementPool.garbageCollection(); this._renderContext.garbageCollection(); + this.inputManager._gc(); const scenes = this._sceneManager._scenes.getLoopArray(); for (let i = 0, n = scenes.length; i < n; i++) { scenes[i]?.physics?._gc(); diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index b072fa4261..13eeb54c75 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -9,7 +9,7 @@ import { Script } from "./Script"; import { Transform } from "./Transform"; import { UpdateFlagManager } from "./UpdateFlagManager"; import { ReferResource } from "./asset/ReferResource"; -import { EngineObject } from "./base"; +import { EngineObject, Logger } from "./base"; import { CloneUtils } from "./clone/CloneUtils"; import { ComponentCloner } from "./clone/ComponentCloner"; import { ActiveChangeFlag } from "./enums/ActiveChangeFlag"; @@ -214,16 +214,14 @@ export class Entity extends EngineObject { } set siblingIndex(value: number) { - if (this._siblingIndex === -1) { - throw `The entity ${this.name} is not in the hierarchy`; - } - if (this._isRoot) { this._setSiblingIndex(this._scene._rootEntities, value); - } else { + } else if (this._parent) { const parent = this._parent; this._setSiblingIndex(parent._children, value); parent._dispatchModify(EntityModifyFlags.Child, parent); + } else { + Logger.warn(`The entity ${this.name} is not in the hierarchy`); } } @@ -405,6 +403,7 @@ export class Entity extends EngineObject { for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; child._parent = null; + child._siblingIndex = -1; let activeChangeFlag = ActiveChangeFlag.None; child._isActiveInHierarchy && (activeChangeFlag |= ActiveChangeFlag.Hierarchy); @@ -412,8 +411,13 @@ export class Entity extends EngineObject { activeChangeFlag && child._processInActive(activeChangeFlag); Entity._traverseSetOwnerScene(child, null); // Must after child._processInActive(). + + child._setParentChange(); } children.length = 0; + // Dispatch a single `Child` modify event for the whole clear so subscribers + // (e.g. UICanvas) can invalidate their cached hierarchy state once. + this._dispatchModify(EntityModifyFlags.Child, this); } /** diff --git a/packages/core/src/RenderPipeline/MaskManager.ts b/packages/core/src/RenderPipeline/MaskManager.ts index b77c37d4d5..69a637b305 100644 --- a/packages/core/src/RenderPipeline/MaskManager.ts +++ b/packages/core/src/RenderPipeline/MaskManager.ts @@ -1,4 +1,6 @@ -import { SpriteMask } from "../2d"; +import { Vector3 } from "@galacean/engine-math"; +import { SpriteMaskInteraction } from "../2d/enums/SpriteMaskInteraction"; +import { IMaskRenderable } from "../2d/sprite/MaskRenderable"; import { CameraClearFlags } from "../enums/CameraClearFlags"; import { SpriteMaskLayer } from "../enums/SpriteMaskLayer"; import { Material } from "../material"; @@ -40,17 +42,49 @@ export class MaskManager { hasStencilWritten = false; private _preMaskLayer = SpriteMaskLayer.Nothing; - private _allSpriteMasks = new DisorderedArray(); + private _allSpriteMasks = new DisorderedArray(); + private _filteredMasksByLayer = new Map(); + private _isFilteredMasksDirty = true; - addSpriteMask(mask: SpriteMask): void { + addSpriteMask(mask: IMaskRenderable): void { mask._maskIndex = this._allSpriteMasks.length; this._allSpriteMasks.add(mask); + this._setFilteredMasksDirty(); } - removeSpriteMask(mask: SpriteMask): void { + removeSpriteMask(mask: IMaskRenderable): void { const replaced = this._allSpriteMasks.deleteByIndex(mask._maskIndex); replaced && (replaced._maskIndex = mask._maskIndex); mask._maskIndex = -1; + this._setFilteredMasksDirty(); + } + + onMaskInfluenceLayersChange(): void { + this._setFilteredMasksDirty(); + } + + isVisibleByMask(maskInteraction: SpriteMaskInteraction, maskLayer: SpriteMaskLayer, worldPoint: Vector3): boolean { + if (maskInteraction === SpriteMaskInteraction.None) { + return true; + } + + const masks = this._getMasksByLayer(maskLayer); + let insideMask = false; + for (let i = 0, n = masks.length; i < n; i++) { + if (masks[i]._containsWorldPoint(worldPoint)) { + insideMask = true; + break; + } + } + + switch (maskInteraction) { + case SpriteMaskInteraction.VisibleInsideMask: + return insideMask; + case SpriteMaskInteraction.VisibleOutsideMask: + return !insideMask; + default: + return true; + } } drawMask(context: RenderContext, pipelineStageTagValue: string, maskLayer: SpriteMaskLayer): void { @@ -134,6 +168,38 @@ export class MaskManager { const allSpriteMasks = this._allSpriteMasks; allSpriteMasks.length = 0; allSpriteMasks.garbageCollection(); + this._filteredMasksByLayer.clear(); + this._isFilteredMasksDirty = true; + } + + private _setFilteredMasksDirty(): void { + this._isFilteredMasksDirty = true; + } + + private _getMasksByLayer(maskLayer: SpriteMaskLayer): IMaskRenderable[] { + if (maskLayer === SpriteMaskLayer.Nothing) { + return []; + } + + if (this._isFilteredMasksDirty) { + this._filteredMasksByLayer.clear(); + this._isFilteredMasksDirty = false; + } + + let filteredMasks = this._filteredMasksByLayer.get(maskLayer); + if (!filteredMasks) { + filteredMasks = []; + const allMasks = this._allSpriteMasks; + const maskElements = allMasks._elements; + for (let i = 0, n = allMasks.length; i < n; i++) { + const mask = maskElements[i]; + if (mask.influenceLayers & maskLayer) { + filteredMasks.push(mask); + } + } + this._filteredMasksByLayer.set(maskLayer, filteredMasks); + } + return filteredMasks; } private _buildMaskRenderElement( diff --git a/packages/core/src/RenderPipeline/VertexMergeBatcher.ts b/packages/core/src/RenderPipeline/VertexMergeBatcher.ts index 9bf7777b62..369f66ea73 100644 --- a/packages/core/src/RenderPipeline/VertexMergeBatcher.ts +++ b/packages/core/src/RenderPipeline/VertexMergeBatcher.ts @@ -13,6 +13,36 @@ export class VertexMergeBatcher { const renderer = curElement.component; const maskInteraction = preRenderer.maskInteraction; + const preRendererAny = preRenderer as any; + const curRendererAny = renderer as any; + const rectMaskEnabledA = preRendererAny._rectMaskEnabled; + if (rectMaskEnabledA !== curRendererAny._rectMaskEnabled) { + return false; + } + if (rectMaskEnabledA) { + const rectMaskRectA = preRendererAny._rectMaskRect; + const rectMaskRectB = curRendererAny._rectMaskRect; + const rectMaskSoftnessA = preRendererAny._rectMaskSoftness; + const rectMaskSoftnessB = curRendererAny._rectMaskSoftness; + if ( + !rectMaskRectA || + !rectMaskRectB || + !rectMaskSoftnessA || + !rectMaskSoftnessB || + rectMaskRectA.x !== rectMaskRectB.x || + rectMaskRectA.y !== rectMaskRectB.y || + rectMaskRectA.z !== rectMaskRectB.z || + rectMaskRectA.w !== rectMaskRectB.w || + rectMaskSoftnessA.x !== rectMaskSoftnessB.x || + rectMaskSoftnessA.y !== rectMaskSoftnessB.y || + rectMaskSoftnessA.z !== rectMaskSoftnessB.z || + rectMaskSoftnessA.w !== rectMaskSoftnessB.w || + preRendererAny._rectMaskHardClip !== curRendererAny._rectMaskHardClip + ) { + return false; + } + } + // Order: cheap reference checks → mask state → tag lookup (rare opt-out) return ( preElement.subChunk.chunk === curElement.subChunk.chunk && @@ -24,6 +54,30 @@ export class VertexMergeBatcher { ); } + /** + * Text-specific batch check: extends sprite check with outline parity. + * Different outlineWidth or outlineColor must split into separate draw calls, + * because outline uniforms are shared per draw call. + */ + static canBatchText(preElement: RenderElement, curElement: RenderElement): boolean { + if (!VertexMergeBatcher.canBatchSprite(preElement, curElement)) { + return false; + } + const preRendererAny = preElement.component as any; + const curRendererAny = curElement.component as any; + if (preRendererAny._outlineWidth !== curRendererAny._outlineWidth) { + return false; + } + if (preRendererAny._outlineWidth > 0) { + const a = preRendererAny._outlineColor; + const b = curRendererAny._outlineColor; + if (a.r !== b.r || a.g !== b.g || a.b !== b.b || a.a !== b.a) { + return false; + } + } + return true; + } + static canBatchSpriteMask(preElement: RenderElement, curElement: RenderElement): boolean { if (preElement.subChunk.chunk !== curElement.subChunk.chunk) { return false; diff --git a/packages/core/src/SceneManager.ts b/packages/core/src/SceneManager.ts index 92de60d66b..ab27c57061 100644 --- a/packages/core/src/SceneManager.ts +++ b/packages/core/src/SceneManager.ts @@ -94,9 +94,12 @@ export class SceneManager { const scenePromise = this.engine.resourceManager.load({ url, type: AssetType.Scene }); scenePromise.then((scene: Scene) => { if (destroyOldScene) { - const scenes = this._scenes.getArray(); + // Use loop array because `destroy` removes the scene from `_scenes` during iteration, + // and skip `scene` itself because concurrent loads of the same url resolve to the same instance + const scenes = this._scenes.getLoopArray(); for (let i = 0, n = scenes.length; i < n; i++) { - scenes[i].destroy(); + const oldScene = scenes[i]; + oldScene !== scene && oldScene.destroy(); } } this.addScene(scene); diff --git a/packages/core/src/Transform.ts b/packages/core/src/Transform.ts index 5f1235caf0..748bb68915 100644 --- a/packages/core/src/Transform.ts +++ b/packages/core/src/Transform.ts @@ -48,6 +48,8 @@ export class Transform extends Component { private _worldRight: Vector3 = null; @ignoreClone private _worldUp: Vector3 = null; + @ignoreClone + private _frontFaceInvert: boolean = false; @ignoreClone protected _isParentDirty: boolean = true; @@ -571,11 +573,16 @@ export class Transform extends Component { * @internal */ _isFrontFaceInvert(): boolean { - const scale = this.lossyWorldScale; - let isInvert = scale.x < 0; - scale.y < 0 && (isInvert = !isInvert); - scale.z < 0 && (isInvert = !isInvert); - return isInvert; + if (this._isContainDirtyFlag(TransformModifyFlags.WorldFrontFaceInvert)) { + const s = this._scale; + let invert = s.x < 0; + s.y < 0 && (invert = !invert); + s.z < 0 && (invert = !invert); + const parent = this._getParentTransform(); + this._frontFaceInvert = parent ? invert !== parent._isFrontFaceInvert() : invert; + this._setDirtyFlagFalse(TransformModifyFlags.WorldFrontFaceInvert); + } + return this._frontFaceInvert; } /** @@ -910,24 +917,27 @@ export enum TransformModifyFlags { */ IsWorldUniformScaling = 0x100, + // Note: 0x200 (UITransformModifyFlags.Size) and 0x400 (Pivot) are reserved by UITransform — do not reuse. + WorldFrontFaceInvert = 0x800, + /** WorldMatrix | WorldPosition */ WmWp = 0x84, /** WorldMatrix | WorldEuler | WorldQuat */ WmWeWq = 0x98, - /** WorldMatrix | WorldEuler | WorldQuat | WorldScale*/ - WmWeWqWs = 0xb8, + /** WorldMatrix | WorldEuler | WorldQuat | WorldScale | WorldFrontFaceInvert */ + WmWeWqWs = 0x8b8, /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat */ WmWpWeWq = 0x9c, - /** WorldMatrix | WorldScale */ - WmWs = 0xa0, - /** WorldMatrix | WorldScale | WorldUniformScaling */ - WmWsWus = 0x1a0, - /** WorldMatrix | WorldPosition | WorldScale */ - WmWpWs = 0xa4, - /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale */ - WmWpWeWqWs = 0xbc, - /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldUniformScaling */ - WmWpWeWqWsWus = 0x1bc, - /** LocalQuat | LocalMatrix | WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldUniformScaling */ - LqLmWmWpWeWqWsWus = 0x1fe + /** WorldMatrix | WorldScale | WorldFrontFaceInvert */ + WmWs = 0x8a0, + /** WorldMatrix | WorldScale | WorldUniformScaling | WorldFrontFaceInvert */ + WmWsWus = 0x9a0, + /** WorldMatrix | WorldPosition | WorldScale | WorldFrontFaceInvert */ + WmWpWs = 0x8a4, + /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldFrontFaceInvert */ + WmWpWeWqWs = 0x8bc, + /** WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldUniformScaling | WorldFrontFaceInvert */ + WmWpWeWqWsWus = 0x9bc, + /** LocalQuat | LocalMatrix | WorldMatrix | WorldPosition | WorldEuler | WorldQuat | WorldScale | WorldUniformScaling | WorldFrontFaceInvert */ + LqLmWmWpWeWqWsWus = 0x9fe } diff --git a/packages/core/src/asset/AssetPromise.ts b/packages/core/src/asset/AssetPromise.ts index 99f00e86f2..1720a64c9f 100644 --- a/packages/core/src/asset/AssetPromise.ts +++ b/packages/core/src/asset/AssetPromise.ts @@ -164,17 +164,17 @@ export class AssetPromise implements PromiseLike { * @returns AssetPromise */ onProgress( - onTaskComplete: (loaded: number, total: number) => void, + onTaskComplete?: (loaded: number, total: number) => void, onTaskDetail?: (identifier: string, loaded: number, total: number) => void ): AssetPromise { const completeProgress = this._taskCompleteProgress; const detailProgress = this._taskDetailProgress; - if (completeProgress) { + if (completeProgress && onTaskComplete) { onTaskComplete(completeProgress.loaded, completeProgress.total); } - if (detailProgress) { - for (let url in detailProgress) { + if (detailProgress && onTaskDetail) { + for (const url in detailProgress) { const { loaded, total } = detailProgress[url]; onTaskDetail(url, loaded, total); } diff --git a/packages/core/src/asset/ResourceManager.ts b/packages/core/src/asset/ResourceManager.ts index 24a7b11ac1..0c61fb9cb4 100644 --- a/packages/core/src/asset/ResourceManager.ts +++ b/packages/core/src/asset/ResourceManager.ts @@ -44,7 +44,7 @@ export class ResourceManager { /** Asset path pool, key is the `instanceID` of resource, value is asset path. */ private _assetPool: Record = Object.create(null); /** Asset url pool, key is the asset path and the value is the asset. */ - private _assetUrlPool: Record = Object.create(null); + private _assetUrlPool: Record = Object.create(null); /** Referable resource pool, key is the `instanceID` of resource. */ private _referResourcePool: Record = Object.create(null); @@ -88,14 +88,14 @@ export class ResourceManager { */ load(path: string): AssetPromise; - load(assetInfo: string | LoadItem | (LoadItem | string)[]): AssetPromise { + load(assetInfo: string | LoadItem | (LoadItem | string)[]): AssetPromise { // single item if (!Array.isArray(assetInfo)) { return this._loadSingleItem(assetInfo); } // multi items const promises = assetInfo.map((item) => this._loadSingleItem(item)); - return AssetPromise.all(promises); + return AssetPromise.all(promises) as AssetPromise; } /** diff --git a/packages/core/src/clone/CloneManager.ts b/packages/core/src/clone/CloneManager.ts index be46462982..e74b6cea57 100644 --- a/packages/core/src/clone/CloneManager.ts +++ b/packages/core/src/clone/CloneManager.ts @@ -1,4 +1,5 @@ import { Entity } from "../Entity"; +import { ReferResource } from "../asset/ReferResource"; import { TypedArray } from "../base/Constant"; import { ICustomClone } from "./ComponentCloner"; import { CloneMode } from "./enums/CloneMode"; @@ -6,7 +7,7 @@ import { CloneMode } from "./enums/CloneMode"; /** * Property decorator, ignore the property when cloning. */ -export function ignoreClone(target: Object, propertyKey: string): void { +export function ignoreClone(target: object, propertyKey: string): void { CloneManager.registerCloneMode(target, propertyKey, CloneMode.Ignore); } @@ -17,7 +18,7 @@ export function ignoreClone(target: Object, propertyKey: string): void { * If it's a primitive type, the value will be copied. * If it's a class type, the reference will be copied. */ -export function assignmentClone(target: Object, propertyKey: string): void { +export function assignmentClone(target: object, propertyKey: string): void { CloneManager.registerCloneMode(target, propertyKey, CloneMode.Assignment); } @@ -29,7 +30,7 @@ export function assignmentClone(target: Object, propertyKey: string): void { * @remarks * Applicable to Object, Array, TypedArray and Class types. */ -export function shallowClone(target: Object, propertyKey: string): void { +export function shallowClone(target: object, propertyKey: string): void { CloneManager.registerCloneMode(target, propertyKey, CloneMode.Shallow); } @@ -42,7 +43,7 @@ export function shallowClone(target: Object, propertyKey: string): void { * If Class is encountered during the deep cloning process, the custom cloning function of the object will be called first. * Custom cloning requires the object to implement the IClone interface. */ -export function deepClone(target: Object, propertyKey: string): void { +export function deepClone(target: object, propertyKey: string): void { CloneManager.registerCloneMode(target, propertyKey, CloneMode.Deep); } @@ -52,9 +53,9 @@ export function deepClone(target: Object, propertyKey: string): void { */ export class CloneManager { /** @internal */ - static _subCloneModeMap = new Map(); + static _subCloneModeMap = new Map(); /** @internal */ - static _cloneModeMap = new Map(); + static _cloneModeMap = new Map(); private static _objectType = Object.getPrototypeOf(Object); @@ -64,7 +65,7 @@ export class CloneManager { * @param propertyKey - Clone property name * @param mode - Clone mode */ - static registerCloneMode(target: Object, propertyKey: string, mode: CloneMode): void { + static registerCloneMode(target: object, propertyKey: string, mode: CloneMode): void { let targetMap = CloneManager._subCloneModeMap.get(target.constructor); if (!targetMap) { targetMap = Object.create(null); @@ -76,7 +77,7 @@ export class CloneManager { /** * Get the clone mode according to the prototype chain. */ - static getCloneMode(type: Function): Object { + static getCloneMode(type: Function): object { let cloneModes = CloneManager._cloneModeMap.get(type); if (!cloneModes) { cloneModes = Object.create(null); @@ -95,31 +96,56 @@ export class CloneManager { } static cloneProperty( - source: Object, - target: Object, + source: object, + target: object, k: string | number, cloneMode: CloneMode, srcRoot: Entity, targetRoot: Entity, - deepInstanceMap: Map + deepInstanceMap: Map ): void { const sourceProperty = source[k]; - // Remappable references (Entity/Component) are always remapped, regardless of clone decorator + // 1. Remappable references (Entity/Component) are always remapped, highest priority if (sourceProperty instanceof Object && (sourceProperty)._remap) { target[k] = (sourceProperty)._remap(srcRoot, targetRoot); return; } + // 2. Explicit ignore if (cloneMode === CloneMode.Ignore) return; - // Primitives, undecorated, or @assignmentClone: direct assign - if (!(sourceProperty instanceof Object) || cloneMode === undefined || cloneMode === CloneMode.Assignment) { + // 3. Primitives / null / undefined - direct assign + if (!(sourceProperty instanceof Object)) { target[k] = sourceProperty; return; } - // @shallowClone / @deepClone: deep copy complex objects + // 4. Determine effective clone mode + let effectiveCloneMode: CloneMode = cloneMode; + if (effectiveCloneMode === undefined) { + // Undecorated: infer from runtime type + effectiveCloneMode = CloneManager._inferCloneMode(sourceProperty, target[k]); + } else if (effectiveCloneMode !== CloneMode.Assignment) { + // Decorated Shallow/Deep: upgrade to Deep if target already has independent same-type instance. + // Assignment is never upgraded — it means the user explicitly wants a reference copy. + const targetProperty = target[k]; + if ( + targetProperty && + targetProperty !== sourceProperty && + targetProperty.constructor === sourceProperty.constructor + ) { + effectiveCloneMode = CloneMode.Deep; + } + } + + // 5. Assignment - direct reference copy + if (effectiveCloneMode === CloneMode.Assignment) { + target[k] = sourceProperty; + return; + } + + // 6. Shallow/Deep clone for complex types const type = sourceProperty.constructor; switch (type) { case Uint8Array: @@ -130,13 +156,44 @@ export class CloneManager { case Int32Array: case Float32Array: case Float64Array: - let targetPropertyT = target[k]; + const targetPropertyT = target[k]; if (targetPropertyT == null || targetPropertyT.length !== (sourceProperty).length) { target[k] = (sourceProperty).slice(); } else { targetPropertyT.set(sourceProperty); } break; + case Map: + let targetPropertyM = >target[k]; + if (targetPropertyM == null) { + target[k] = targetPropertyM = new Map(); + } else { + targetPropertyM.clear(); + } + (>sourceProperty).forEach((value, key) => { + if (key instanceof Object && (key)._remap) { + key = (key)._remap(srcRoot, targetRoot); + } + if (value instanceof Object && (value)._remap) { + value = (value)._remap(srcRoot, targetRoot); + } + targetPropertyM.set(key, value); + }); + break; + case Set: + let targetPropertyS = >target[k]; + if (targetPropertyS == null) { + target[k] = targetPropertyS = new Set(); + } else { + targetPropertyS.clear(); + } + (>sourceProperty).forEach((value) => { + if (value instanceof Object && (value)._remap) { + value = (value)._remap(srcRoot, targetRoot); + } + targetPropertyS.add(value); + }); + break; case Array: let targetPropertyA = >target[k]; const length = (>sourceProperty).length; @@ -150,7 +207,7 @@ export class CloneManager { >sourceProperty, targetPropertyA, i, - cloneMode, + cloneMode, // Pass original mode: decorated → children inherit, undecorated → children infer independently srcRoot, targetRoot, deepInstanceMap @@ -158,25 +215,27 @@ export class CloneManager { } break; default: - let targetProperty = target[k]; - // If the target property is undefined, create new instance and keep reference sharing like the source - if (!targetProperty) { - targetProperty = deepInstanceMap.get(sourceProperty); - if (!targetProperty) { - targetProperty = new sourceProperty.constructor(); - deepInstanceMap.set(sourceProperty, targetProperty); - } - target[k] = targetProperty; + // Check if we've already visited this source object (cycle detection) + if (deepInstanceMap.has(sourceProperty)) { + target[k] = deepInstanceMap.get(sourceProperty); + return; + } + + let targetPropertyD = target[k]; + if (!targetPropertyD) { + targetPropertyD = new sourceProperty.constructor(); + target[k] = targetPropertyD; } + deepInstanceMap.set(sourceProperty, targetPropertyD); if ((sourceProperty).copyFrom) { - (targetProperty).copyFrom(sourceProperty); + (targetPropertyD).copyFrom(sourceProperty); } else { const cloneModes = CloneManager.getCloneMode(sourceProperty.constructor); - for (let k in sourceProperty) { + for (const k in sourceProperty) { CloneManager.cloneProperty( - sourceProperty, - targetProperty, + sourceProperty, + targetPropertyD, k, cloneModes[k], srcRoot, @@ -184,14 +243,46 @@ export class CloneManager { deepInstanceMap ); } - (sourceProperty)._cloneTo?.(targetProperty, srcRoot, targetRoot); + (sourceProperty)._cloneTo?.(targetPropertyD, srcRoot, targetRoot); } break; } } - static deepCloneObject(source: Object, target: Object, deepInstanceMap: Map): void { - for (let k in source) { + /** + * Infer the appropriate clone mode for an undecorated property based on its runtime type. + * This enables user custom scripts to get correct clone behavior without decorators. + */ + private static _inferCloneMode(sourceProperty: object, targetProperty: any): CloneMode { + // If target already has an independent instance of the same type, + // deep clone to preserve isolation (e.g., constructor-created objects) + if ( + targetProperty && + targetProperty !== sourceProperty && + targetProperty.constructor === sourceProperty.constructor + ) { + return CloneMode.Deep; + } + + // Arrays need recursive processing (may contain Entity/Component refs) + if (Array.isArray(sourceProperty)) return CloneMode.Deep; + + // TypedArrays - copy data + if (ArrayBuffer.isView(sourceProperty)) return CloneMode.Deep; + + // Maps and Sets - create independent copies + if (sourceProperty instanceof Map || sourceProperty instanceof Set) return CloneMode.Deep; + + // Value types with copyFrom (math types like Vector3, Color, etc.) + if ((sourceProperty).copyFrom) return CloneMode.Deep; + + // Engine resources are reference-counted and shared. All other user/value + // classes are cloned so prefab instances do not share mutable state. + return sourceProperty instanceof ReferResource ? CloneMode.Assignment : CloneMode.Deep; + } + + static deepCloneObject(source: object, target: object, deepInstanceMap: Map): void { + for (const k in source) { CloneManager.cloneProperty(source, target, k, CloneMode.Deep, null, null, deepInstanceMap); } } diff --git a/packages/core/src/input/InputManager.ts b/packages/core/src/input/InputManager.ts index 617fc4c366..e82d056fb7 100644 --- a/packages/core/src/input/InputManager.ts +++ b/packages/core/src/input/InputManager.ts @@ -181,6 +181,13 @@ export class InputManager { this._initialized && this._pointerManager._firePointerScript(scenes); } + /** + * @internal + */ + _gc(): void { + this._initialized && this._pointerManager._gc(); + } + /** * @internal */ diff --git a/packages/core/src/input/pointer/Pointer.ts b/packages/core/src/input/pointer/Pointer.ts index a7412bc2ed..298db706a3 100644 --- a/packages/core/src/input/pointer/Pointer.ts +++ b/packages/core/src/input/pointer/Pointer.ts @@ -1,9 +1,7 @@ import { Vector2 } from "@galacean/engine-math"; -import { ClearableObjectPool } from "../../utils/ClearableObjectPool"; import { DisorderedArray } from "../../utils/DisorderedArray"; import { PointerButton } from "../enums/PointerButton"; import { PointerPhase } from "../enums/PointerPhase"; -import { PointerEventData } from "./PointerEventData"; import { PointerEventEmitter } from "./emitter/PointerEventEmitter"; /** @@ -23,6 +21,8 @@ export class Pointer { pressedButtons: PointerButton; /** The position of the pointer in screen space pixel coordinates. */ position: Vector2 = new Vector2(); + /** The position of the pointer when it was last pressed down (in screen space pixel coordinates). */ + pressedPosition: Vector2 = new Vector2(); /** The change of the pointer. */ deltaPosition: Vector2 = new Vector2(); /** @internal */ @@ -49,16 +49,6 @@ export class Pointer { this.id = id; } - /** - * @internal - */ - _addEmitters) => PointerEventEmitter>( - type: T, - pool: ClearableObjectPool - ) { - this._emitters.push(new type(pool)); - } - /** * @internal */ diff --git a/packages/core/src/input/pointer/PointerEventData.ts b/packages/core/src/input/pointer/PointerEventData.ts index 89f9ae09b6..33dc41b001 100644 --- a/packages/core/src/input/pointer/PointerEventData.ts +++ b/packages/core/src/input/pointer/PointerEventData.ts @@ -1,4 +1,5 @@ import { Vector3 } from "@galacean/engine-math"; +import { Entity } from "../../Entity"; import { IPoolElement } from "../../utils/ObjectPool"; import { Pointer } from "./Pointer"; @@ -10,11 +11,17 @@ export class PointerEventData implements IPoolElement { pointer: Pointer; /** The position of the event trigger (in world space). */ worldPosition: Vector3 = new Vector3(); + /** The hit-tested target entity (deepest entity on the bubble path). */ + target: Entity = null; + /** The entity currently handling the event (same as target on non-bubbling fire, varies on bubble). */ + currentTarget: Entity = null; /** * @internal */ dispose() { this.pointer = null; + this.target = null; + this.currentTarget = null; } } diff --git a/packages/core/src/input/pointer/PointerManager.ts b/packages/core/src/input/pointer/PointerManager.ts index fc6d8a2010..71ad2178cf 100644 --- a/packages/core/src/input/pointer/PointerManager.ts +++ b/packages/core/src/input/pointer/PointerManager.ts @@ -116,10 +116,7 @@ export class PointerManager implements IInput { pointer = pointerPool[j]; if (!pointer) { pointer = pointerPool[j] = new Pointer(j); - engine._physicsInitialized && pointer._addEmitters(PhysicsPointerEventEmitter, eventPool); - PointerManager._pointerEventEmitters.forEach((emitter) => { - pointer._addEmitters(emitter, eventPool); - }); + this._addEmitters(pointer); } pointer._uniqueID = pointerId; pointer._events.push(evt); @@ -193,6 +190,13 @@ export class PointerManager implements IInput { } } + /** + * @internal + */ + _gc(): void { + this._eventPool.garbageCollection(); + } + /** * @internal */ @@ -201,6 +205,16 @@ export class PointerManager implements IInput { this._pointerPool.length = 0; } + private _addEmitters(pointer: Pointer): void { + const { _eventPool: eventPool, _engine: engine } = this; + const emitters = pointer._emitters; + engine._physicsInitialized && emitters.push(new PhysicsPointerEventEmitter(eventPool)); + const emitterTypes = PointerManager._pointerEventEmitters; + for (let i = 0, n = emitterTypes.length; i < n; i++) { + emitters.push(new emitterTypes[i](eventPool)); + } + } + private _onPointerEvent(evt: PointerEvent) { this._nativeEvents.push(evt); } @@ -245,6 +259,10 @@ export class PointerManager implements IInput { pointer._downMap[button] = frameCount; pointer._frameEvents |= PointerEventType.Down; pointer.phase = PointerPhase.Down; + pointer.pressedPosition.set( + (event.clientX - left) * widthPixelRatio, + (event.clientY - top) * heightPixelRatio + ); break; } case "pointerup": { diff --git a/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts b/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts index 5778f924a9..0a4ef5493e 100644 --- a/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts +++ b/packages/core/src/input/pointer/emitter/PhysicsPointerEventEmitter.ts @@ -24,7 +24,7 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { continue; } const cameras = scene._componentsManager._activeCameras; - let scenePhysics = scene.physics; + const scenePhysics = scene.physics; for (let j = cameras.length - 1; j >= 0; j--) { const camera = cameras.get(j); if (camera.renderTarget) continue; @@ -53,13 +53,13 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { override processDrag(pointer: Pointer): void { const entity = this._draggedEntity; - entity && this._fireDrag(entity, this._createEventData(pointer)); + entity && this._fireDrag(entity, this._createEventData(pointer, entity)); } override processDown(pointer: Pointer): void { const entity = (this._pressedEntity = this._draggedEntity = this._enteredEntity); if (entity) { - const eventData = this._createEventData(pointer); + const eventData = this._createEventData(pointer, entity); this._fireDown(entity, eventData); this._fireBeginDrag(entity, eventData); } @@ -69,14 +69,14 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { const { _enteredEntity: enteredEntity, _draggedEntity: draggedEntity } = this; if (enteredEntity) { const sameTarget = this._pressedEntity === enteredEntity; - const eventData = this._createEventData(pointer); + const eventData = this._createEventData(pointer, enteredEntity); this._fireUp(enteredEntity, eventData); sameTarget && this._fireClick(enteredEntity, eventData); this._fireDrop(enteredEntity, eventData); } this._pressedEntity = null; if (draggedEntity) { - this._fireEndDrag(draggedEntity, this._createEventData(pointer)); + this._fireEndDrag(draggedEntity, this._createEventData(pointer, draggedEntity)); this._draggedEntity = null; } } @@ -84,13 +84,13 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { override processLeave(pointer: Pointer): void { const enteredEntity = this._enteredEntity; if (enteredEntity) { - this._fireExit(enteredEntity, this._createEventData(pointer)); + this._fireExit(enteredEntity, this._createEventData(pointer, enteredEntity)); this._enteredEntity = null; } const draggedEntity = this._draggedEntity; if (draggedEntity) { - this._fireEndDrag(draggedEntity, this._createEventData(pointer)); + this._fireEndDrag(draggedEntity, this._createEventData(pointer, draggedEntity)); this._draggedEntity = null; } this._pressedEntity = null; @@ -108,10 +108,10 @@ export class PhysicsPointerEventEmitter extends PointerEventEmitter { const enteredEntity = this._enteredEntity; if (entity !== enteredEntity) { if (enteredEntity) { - this._fireExit(enteredEntity, this._createEventData(pointer)); + this._fireExit(enteredEntity, this._createEventData(pointer, enteredEntity)); } if (entity) { - this._fireEnter(entity, this._createEventData(pointer)); + this._fireEnter(entity, this._createEventData(pointer, entity)); } this._enteredEntity = entity; } diff --git a/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts b/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts index 6f6c62cdfb..98188891e6 100644 --- a/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts +++ b/packages/core/src/input/pointer/emitter/PointerEventEmitter.ts @@ -32,10 +32,12 @@ export abstract class PointerEventEmitter { protected abstract _init(): void; - protected _createEventData(pointer: Pointer): PointerEventData { + protected _createEventData(pointer: Pointer, target: Entity, currentTarget: Entity = target): PointerEventData { const data = this._pool.get(); data.pointer = pointer; data.worldPosition.copyFrom(this._hitResult.point); + data.target = target; + data.currentTarget = currentTarget; return data; } diff --git a/packages/core/src/mesh/ModelMesh.ts b/packages/core/src/mesh/ModelMesh.ts index 27b92ced49..8ee92c4825 100644 --- a/packages/core/src/mesh/ModelMesh.ts +++ b/packages/core/src/mesh/ModelMesh.ts @@ -925,7 +925,7 @@ export class ModelMesh extends Mesh { return null; } if (!buffer.readable) { - throw "Not allowed to access data while vertex buffer readable is false."; + throw new Error("Not allowed to access data while vertex buffer readable is false."); } const vertexCount = this.vertexCount; diff --git a/packages/core/src/shader/ShaderData.ts b/packages/core/src/shader/ShaderData.ts index a3bb7992b8..038406fb80 100644 --- a/packages/core/src/shader/ShaderData.ts +++ b/packages/core/src/shader/ShaderData.ts @@ -561,7 +561,7 @@ export class ShaderData implements IReferable, IClone { if (out) { const macroMap = this._macroMap; out.length = 0; - for (var key in macroMap) { + for (const key in macroMap) { out.push(macroMap[key]); } } else { @@ -592,7 +592,7 @@ export class ShaderData implements IReferable, IClone { const propertyValueMap = this._propertyValueMap; const propertyIdMap = ShaderProperty._propertyIdMap; - for (let key in propertyValueMap) { + for (const key in propertyValueMap) { properties.push(propertyIdMap[key]); } @@ -608,8 +608,12 @@ export class ShaderData implements IReferable, IClone { } cloneTo(target: ShaderData): void { - CloneManager.deepCloneObject(this._macroCollection, target._macroCollection, new Map()); - Object.assign(target._macroMap, this._macroMap); + CloneManager.deepCloneObject(this._macroCollection, target._macroCollection, new Map()); + const targetMacroMap = target._macroMap; + for (const key in targetMacroMap) { + delete targetMacroMap[key]; + } + Object.assign(targetMacroMap, this._macroMap); const referCount = target._getReferCount(); const propertyValueMap = this._propertyValueMap; const targetPropertyValueMap = target._propertyValueMap; diff --git a/packages/core/src/shader/ShaderFactory.ts b/packages/core/src/shader/ShaderFactory.ts index d8380e0846..5f9d8efe87 100644 --- a/packages/core/src/shader/ShaderFactory.ts +++ b/packages/core/src/shader/ShaderFactory.ts @@ -210,11 +210,17 @@ mat3 _normalMatFromModel(mat3 m) { static injectInstanceUBO( engine: Engine, vertexSource: string, - fragmentSource: string + fragmentSource: string, + activeMacros?: ReadonlySet ): { vertexSource: string; fragmentSource: string; instanceLayout: InstanceBufferLayout | null } { const fieldMap: Record = Object.create(null); - vertexSource = ShaderFactory._scanInstanceUniforms(vertexSource, fieldMap); - fragmentSource = ShaderFactory._scanInstanceUniforms(fragmentSource, fieldMap); + if (activeMacros) { + vertexSource = ShaderFactory._scanInstanceUniformsWithMacros(vertexSource, fieldMap, activeMacros); + fragmentSource = ShaderFactory._scanInstanceUniformsWithMacros(fragmentSource, fieldMap, activeMacros); + } else { + vertexSource = ShaderFactory._scanInstanceUniforms(vertexSource, fieldMap); + fragmentSource = ShaderFactory._scanInstanceUniforms(fragmentSource, fieldMap); + } // Even when fieldMap is empty, derived built-ins (e.g. `renderer_MVPMat`) may have // had their declarations stripped by scan and still need a `#define` to compile @@ -265,6 +271,62 @@ mat3 _normalMatFromModel(mat3 m) { }); } + private static readonly _ifdefRegex = /^[ \t]*#ifdef\s+(\w+)/; + private static readonly _ifndefRegex = /^[ \t]*#ifndef\s+(\w+)/; + private static readonly _elseRegex = /^[ \t]*#else\b/; + private static readonly _endifRegex = /^[ \t]*#endif\b/; + + /** + * Scan raw GLSL while respecting simple conditional-compilation branches so renderer uniforms + * in inactive branches are not moved into the instance UBO. + */ + private static _scanInstanceUniformsWithMacros( + source: string, + fieldMap: Record, + activeMacros: ReadonlySet + ): string { + const branchStack: boolean[] = [true]; + const lines = source.split("\n"); + + for (let i = 0, n = lines.length; i < n; i++) { + const line = lines[i]; + let match = line.match(ShaderFactory._ifdefRegex); + if (match) { + const parentActive = branchStack[branchStack.length - 1]; + branchStack.push(parentActive && activeMacros.has(match[1])); + continue; + } + + match = line.match(ShaderFactory._ifndefRegex); + if (match) { + const parentActive = branchStack[branchStack.length - 1]; + branchStack.push(parentActive && !activeMacros.has(match[1])); + continue; + } + + if (ShaderFactory._elseRegex.test(line)) { + if (branchStack.length > 1) { + const parentActive = branchStack[branchStack.length - 2]; + branchStack[branchStack.length - 1] = parentActive && !branchStack[branchStack.length - 1]; + } + continue; + } + + if (ShaderFactory._endifRegex.test(line)) { + if (branchStack.length > 1) { + branchStack.pop(); + } + continue; + } + + if (branchStack[branchStack.length - 1]) { + lines[i] = ShaderFactory._scanInstanceUniforms(line, fieldMap); + } + } + + return lines.join("\n"); + } + private static _buildLayout(engine: Engine, fieldMap: Record): InstanceBufferLayout { const maxUBOSize = engine._hardwareRenderer.maxUniformBlockSize; const std140Map = ShaderFactory._std140TypeInfoMap; diff --git a/packages/core/src/shader/ShaderPass.ts b/packages/core/src/shader/ShaderPass.ts index d0e9c1aa0b..c020bdaebf 100644 --- a/packages/core/src/shader/ShaderPass.ts +++ b/packages/core/src/shader/ShaderPass.ts @@ -55,6 +55,7 @@ export class ShaderPass extends ShaderPart { private static _shaderMacroList: ShaderMacro[] = []; private static _macroMap: Map = new Map(); + private static _activeMacroSet: Set = new Set(); /** * Create a shader pass from precompiled instructions. @@ -147,17 +148,20 @@ export class ShaderPass extends ShaderPart { } const macroMap = ShaderPass._macroMap; + const activeMacroSet = ShaderPass._activeMacroSet; macroMap.clear(); + activeMacroSet.clear(); for (let i = 0, n = shaderMacroList.length; i < n; i++) { const macro = shaderMacroList[i]; macroMap.set(macro.name, macro.value ?? ""); + activeMacroSet.add(macro.name); } let vertexSource = ShaderMacroProcessor.evaluate(this._vertexShaderInstructions, macroMap); let fragmentSource = ShaderMacroProcessor.evaluate(this._fragmentShaderInstructions, macroMap); let instanceLayout: InstanceBufferLayout | null = null; if (isGPUInstance) { - const injected = ShaderFactory.injectInstanceUBO(engine, vertexSource, fragmentSource); + const injected = ShaderFactory.injectInstanceUBO(engine, vertexSource, fragmentSource, activeMacroSet); vertexSource = injected.vertexSource; fragmentSource = injected.fragmentSource; instanceLayout = injected.instanceLayout; diff --git a/packages/core/src/ui/UIUtils.ts b/packages/core/src/ui/UIUtils.ts index a61a7d54d3..e62a75b464 100644 --- a/packages/core/src/ui/UIUtils.ts +++ b/packages/core/src/ui/UIUtils.ts @@ -1,14 +1,20 @@ -import { Matrix, Vector4 } from "@galacean/engine-math"; +import { Color, Matrix, Vector4 } from "@galacean/engine-math"; import { Camera } from "../Camera"; import { Engine } from "../Engine"; import { Layer } from "../Layer"; +import { Blitter } from "../RenderPipeline/Blitter"; import { RenderQueue } from "../RenderPipeline"; import { ContextRendererUpdateFlag } from "../RenderPipeline/RenderContext"; import { Scene } from "../Scene"; import { VirtualCamera } from "../VirtualCamera"; import { EngineObject } from "../base"; -import { RenderQueueType, ShaderData, ShaderDataGroup, ShaderMacro } from "../shader"; +import { CameraClearFlags } from "../enums/CameraClearFlags"; +import { Material } from "../material"; +import { RenderQueueType, Shader, ShaderData, ShaderDataGroup, ShaderMacro } from "../shader"; import { ShaderMacroCollection } from "../shader/ShaderMacroCollection"; +import { RenderTarget } from "../texture/RenderTarget"; +import { Texture2D } from "../texture/Texture2D"; +import { TextureFormat } from "../texture/enums/TextureFormat"; import { DisorderedArray } from "../utils/DisorderedArray"; import { IUICanvas } from "./IUICanvas"; @@ -19,6 +25,11 @@ export class UIUtils { private static _virtualCamera: VirtualCamera; private static _viewport: Vector4; private static _overlayCamera: OverlayCamera; + private static _overlayRT: RenderTarget; + private static _overlayBlitMaterial: Material; + private static _clearColor = new Color(0, 0, 0, 0); + /** Flip V so that Y-up RT content maps correctly onto the default framebuffer. */ + private static _flipYScaleOffset = new Vector4(1, -1, 0, 1); static renderOverlay(engine: Engine, scene: Scene, uiCanvases: DisorderedArray): void { engine._macroCollection.enable(UIUtils._shouldSRGBCorrect); @@ -31,10 +42,19 @@ export class UIUtils { camera.engine = engine; camera.scene = scene; renderContext.camera = camera as unknown as Camera; + + const { width, height } = canvas; const { elements: projectE } = virtualCamera.projectionMatrix; const { elements: viewE } = virtualCamera.viewMatrix; - (projectE[0] = 2 / canvas.width), (projectE[5] = 2 / canvas.height), (projectE[10] = 0); - renderContext.setRenderTarget(null, viewport, 0); + (projectE[0] = 2 / width), (projectE[5] = 2 / height), (projectE[10] = 0); + + // Render to an intermediate RT with Depth24Stencil8 so that stencil-based UI Mask works. + // The default canvas framebuffer is created without a stencil buffer + // (see WebGLGraphicDevice._webGLOptions.stencil = false). + const overlayRT = UIUtils._getOverlayRT(engine, width, height); + renderContext.setRenderTarget(overlayRT, viewport, 0); + rhi.clearRenderTarget(engine, CameraClearFlags.All, UIUtils._clearColor); + for (let i = 0, n = uiCanvases.length; i < n; i++) { const uiCanvas = uiCanvases.get(i); if (uiCanvas) { @@ -55,9 +75,51 @@ export class UIUtils { engine._renderCount++; } } + + // Blit overlay RT to default framebuffer with premultiplied alpha blending. + // Blitter.blitTexture picks the non-flipping `blitMesh` when destination is null, + // but the RT contents are written in standard Y-up NDC, so we flip V here via + // sourceScaleOffset to match the default framebuffer orientation. + Blitter.blitTexture( + engine, + overlayRT.getColorTexture(0) as Texture2D, + null, + 0, + viewport, + UIUtils._getOverlayBlitMaterial(engine), + 0, + UIUtils._flipYScaleOffset + ); + renderContext.camera = null; engine._macroCollection.disable(UIUtils._shouldSRGBCorrect); } + + private static _getOverlayRT(engine: Engine, width: number, height: number): RenderTarget { + let rt = UIUtils._overlayRT; + if (!rt || rt.width !== width || rt.height !== height) { + if (rt) { + rt.getColorTexture(0).destroy(); + rt.destroy(); + } + const colorTexture = new Texture2D(engine, width, height, TextureFormat.R8G8B8A8, false); + colorTexture.isGCIgnored = true; + rt = new RenderTarget(engine, width, height, colorTexture, TextureFormat.Depth24Stencil8); + rt.isGCIgnored = true; + UIUtils._overlayRT = rt; + } + return rt; + } + + private static _getOverlayBlitMaterial(engine: Engine): Material { + let material = UIUtils._overlayBlitMaterial; + if (!material) { + material = new Material(engine, Shader.find("2D/UIOverlayBlit")); + material.isGCIgnored = true; + UIUtils._overlayBlitMaterial = material; + } + return material; + } } class OverlayCamera { diff --git a/packages/galacean/src/ShaderPool.ts b/packages/galacean/src/ShaderPool.ts index 52878d4d3f..8567f9ac03 100644 --- a/packages/galacean/src/ShaderPool.ts +++ b/packages/galacean/src/ShaderPool.ts @@ -8,7 +8,9 @@ import { SpriteMaskSource, TextSource, TrailSource, + UIOverlayBlitSource, UIDefaultSource, + SpineSource, SkyboxSource, BackgroundTextureSource, SkyProceduralSource, @@ -65,7 +67,9 @@ export class ShaderPool { SpriteMaskSource, TextSource, TrailSource, + UIOverlayBlitSource, UIDefaultSource, + SpineSource, // Particle shaders ParticleSource, ParticleFeedbackSource, diff --git a/packages/loader/src/SceneLoader.ts b/packages/loader/src/SceneLoader.ts index 7c94a82109..5f1975cec6 100644 --- a/packages/loader/src/SceneLoader.ts +++ b/packages/loader/src/SceneLoader.ts @@ -143,6 +143,15 @@ export function applySceneData( if (fog.fogColor) scene.fogColor.set(fog.fogColor[0], fog.fogColor[1], fog.fogColor[2], fog.fogColor[3]); } + // Parse physics only when the engine has a native physics backend. Render-only + // engines should remain able to load scene files that carry physics settings. + const physics = sceneData.physics; + if (physics && (scene.engine as any)._physicsInitialized) { + const gravity = physics.gravity; + if (gravity) scene.physics.gravity.set(gravity[0], gravity[1], gravity[2]); + if (physics.fixedTimeStep != undefined) scene.physics.fixedTimeStep = physics.fixedTimeStep; + } + // Post Process if (sceneData.postProcess) { Logger.warn("Post Process is not supported in scene yet, please add PostProcess component in entity instead."); @@ -167,7 +176,7 @@ export function applySceneData( return Promise.all(promises).then(() => {}); } -@resourceLoader(AssetType.Scene, ["scene"], true) +@resourceLoader(AssetType.Scene, ["scene"], false) class SceneLoader extends Loader { load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { const { engine } = resourceManager; diff --git a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts index 2e038238ce..47591cf90d 100644 --- a/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts +++ b/packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts @@ -192,7 +192,12 @@ export class ReflectionParser { const buffer = ReflectionParser._componentBuffer; buffer.length = 0; entity.getComponents(type, buffer); - const result = buffer[comp.index] ?? null; + let result = buffer[comp.index] ?? null; + if (!result) { + buffer.length = 0; + entity.getComponentsIncludeChildren(type, buffer); + result = buffer[comp.index] ?? null; + } buffer.length = 0; return result; } diff --git a/packages/loader/src/schema/SceneSchema.ts b/packages/loader/src/schema/SceneSchema.ts index 659f4b6186..c63fbb57fb 100644 --- a/packages/loader/src/schema/SceneSchema.ts +++ b/packages/loader/src/schema/SceneSchema.ts @@ -52,6 +52,10 @@ export interface SceneFile extends HierarchyFile { fogDensity?: number; fogColor?: Vec4Tuple; }; + physics?: { + gravity?: Vec3Tuple; + fixedTimeStep?: number; + }; ambientOcclusion?: { enabledAmbientOcclusion?: boolean; quality?: number; diff --git a/packages/shader-compiler/src/ShaderCompiler.ts b/packages/shader-compiler/src/ShaderCompiler.ts index 7cd2e7c051..f9358cd8a9 100644 --- a/packages/shader-compiler/src/ShaderCompiler.ts +++ b/packages/shader-compiler/src/ShaderCompiler.ts @@ -23,6 +23,15 @@ export class ShaderCompiler { private _includeMap: IncludeMap = {}; private readonly _chunkOutputCache: ChunkOutputCache = new Map(); + /** + * Release token and AST pools retained at their compilation-time peak. + * Call this after shader warm-up when no further compilation is expected soon. + * Later compilation remains supported and will allocate pool entries on demand. + */ + static releaseCompilationCache(): void { + ShaderCompilerUtils.releaseAllShaderCompilerObjectPool(); + } + /** Replace the `#include` lookup table and clear the derived chunk cache. */ _setIncludeMap(includeMap: IncludeMap): void { this._includeMap = includeMap; diff --git a/packages/shader-compiler/src/ShaderCompilerUtils.ts b/packages/shader-compiler/src/ShaderCompilerUtils.ts index 6937d09096..2ba9872449 100644 --- a/packages/shader-compiler/src/ShaderCompilerUtils.ts +++ b/packages/shader-compiler/src/ShaderCompilerUtils.ts @@ -21,6 +21,13 @@ export class ShaderCompilerUtils { } } + /** Release all objects retained by the compiler pools. */ + static releaseAllShaderCompilerObjectPool(): void { + for (let i = 0, n = ShaderCompilerUtils._shaderCompilerObjectPoolSet.length; i < n; i++) { + ShaderCompilerUtils._shaderCompilerObjectPoolSet[i].garbageCollection(); + } + } + static createGSError( message: string, errorName: GSErrorName, diff --git a/packages/shader-compiler/src/lalr/LALR1.ts b/packages/shader-compiler/src/lalr/LALR1.ts index 7a19e86d34..387d2d390e 100644 --- a/packages/shader-compiler/src/lalr/LALR1.ts +++ b/packages/shader-compiler/src/lalr/LALR1.ts @@ -30,6 +30,9 @@ export class LALR1 { generate() { this.computeFirstSet(); this.buildStateTable(); + // Runtime parsing only needs the numeric action/goto tables. The state closure graph is + // construction-only data and otherwise keeps thousands of StateItem/Set objects alive. + State.clearPool(); } private buildStateTable() { @@ -63,12 +66,12 @@ export class LALR1 { const productionList = this.grammar.getProductionList(item.curSymbol); if (item.nextSymbol) { - let newLookaheadSet = new Set(); + const newLookaheadSet = new Set(); let lastFirstSet: Set | undefined; let terminalExist = false; // when A :=> a.BC, a; ==》 B :=> .xy, First(Ca) // newLookAhead = First(Ca) - for (let i = 1, nextSymbol = item.symbolByOffset(1); !!nextSymbol; nextSymbol = item.symbolByOffset(++i)) { + for (let i = 1, nextSymbol = item.symbolByOffset(1); nextSymbol; nextSymbol = item.symbolByOffset(++i)) { if (GrammarUtils.isTerminal(nextSymbol)) { newLookaheadSet.add(nextSymbol); terminalExist = true; diff --git a/packages/shader-compiler/src/lalr/State.ts b/packages/shader-compiler/src/lalr/State.ts index 6ef782249e..531241e777 100644 --- a/packages/shader-compiler/src/lalr/State.ts +++ b/packages/shader-compiler/src/lalr/State.ts @@ -7,6 +7,12 @@ export default class State { static pool: Map = new Map(); static _id = 0; + /** Release the state graph after the action/goto tables have been generated. */ + static clearPool(): void { + this.closureMap.clear(); + this.pool.clear(); + } + readonly id: number; readonly cores: Set; private _items: Set; diff --git a/packages/shader/src/Shaders/2D/Spine.shader b/packages/shader/src/Shaders/2D/Spine.shader new file mode 100644 index 0000000000..cd6c977334 --- /dev/null +++ b/packages/shader/src/Shaders/2D/Spine.shader @@ -0,0 +1,80 @@ +Shader "2D/Spine" { + SubShader "Default" { + Pass "Default" { + Tags { pipelineStage = "Forward" } + + BlendFactor sourceColorBlendFactor; + BlendFactor destinationColorBlendFactor; + BlendFactor sourceAlphaBlendFactor; + BlendFactor destinationAlphaBlendFactor; + + BlendState = { + Enabled = true; + SourceColorBlendFactor = sourceColorBlendFactor; + DestinationColorBlendFactor = destinationColorBlendFactor; + SourceAlphaBlendFactor = sourceAlphaBlendFactor; + DestinationAlphaBlendFactor = destinationAlphaBlendFactor; + } + DepthState = { + WriteEnabled = false; + } + RasterState = { + CullMode = CullMode.Off; + } + RenderQueueType = Transparent; + + VertexShader = SpineVertex; + FragmentShader = SpineFragment; + + #include "ShaderLibrary/Common/Common.glsl" + + struct a2v { + vec3 POSITION; + vec2 TEXCOORD_0; + vec4 LIGHT_COLOR; + #ifdef RENDERER_TINT_BLACK + vec3 DARK_COLOR; + #endif + }; + + struct v2f { + vec2 v_uv; + vec4 v_lightColor; + #ifdef RENDERER_TINT_BLACK + vec3 v_darkColor; + #endif + }; + + mat4 renderer_MVPMat; + sampler2D material_SpineTexture; + float renderer_PremultipliedAlpha; + + v2f SpineVertex(a2v attr) { + v2f v; + gl_Position = renderer_MVPMat * vec4(attr.POSITION, 1.0); + v.v_uv = attr.TEXCOORD_0; + v.v_lightColor = attr.LIGHT_COLOR; + #ifdef RENDERER_TINT_BLACK + v.v_darkColor = attr.DARK_COLOR; + #endif + return v; + } + + void SpineFragment(v2f v) { + vec4 texColor = texture2D(material_SpineTexture, v.v_uv); + vec4 lightColor = sRGBToLinear(v.v_lightColor); + #ifdef RENDERER_TINT_BLACK + vec4 darkColor = sRGBToLinear(vec4(v.v_darkColor, 1.0)); + vec3 dark_premult = (texColor.a - texColor.rgb) * darkColor.rgb; + vec3 dark_nonpremult = (1.0 - texColor.rgb) * darkColor.rgb; + vec3 dark = mix(dark_nonpremult, dark_premult, renderer_PremultipliedAlpha); + vec3 light = texColor.rgb * lightColor.rgb; + gl_FragColor.rgb = dark + light; + gl_FragColor.a = texColor.a * lightColor.a; + #else + gl_FragColor = texColor * lightColor; + #endif + } + } + } +} diff --git a/packages/shader/src/Shaders/2D/Text.shader b/packages/shader/src/Shaders/2D/Text.shader index 333e6ad35d..f4d1c14be5 100644 --- a/packages/shader/src/Shaders/2D/Text.shader +++ b/packages/shader/src/Shaders/2D/Text.shader @@ -30,27 +30,81 @@ Shader "2D/Text" { struct v2f { vec2 v_uv; vec4 v_color; + vec2 v_worldPosition; }; mat4 renderer_MVPMat; sampler2D renderElement_TextTexture; + vec2 renderElement_TextTextureSize; + vec4 renderer_OutlineColor; + float renderer_OutlineWidth; + vec4 renderer_UIRectClipRect; + float renderer_UIRectClipEnabled; + vec4 renderer_UIRectClipSoftness; + float renderer_UIRectClipHardClip; v2f TextVertex(a2v attr) { v2f v; gl_Position = renderer_MVPMat * vec4(attr.POSITION, 1.0); v.v_uv = attr.TEXCOORD_0; v.v_color = attr.COLOR_0; + v.v_worldPosition = attr.POSITION.xy; return v; } - void TextFragment(v2f v) { - vec4 texColor = texture2D(renderElement_TextTexture, v.v_uv); + float getUIRectClipAlpha(v2f v) { + vec4 edgeDistance = vec4( + v.v_worldPosition.x - renderer_UIRectClipRect.x, + v.v_worldPosition.y - renderer_UIRectClipRect.y, + renderer_UIRectClipRect.z - v.v_worldPosition.x, + renderer_UIRectClipRect.w - v.v_worldPosition.y + ); + vec4 hardClipFactor = step(vec4(0.0), edgeDistance); + vec4 softness = max(renderer_UIRectClipSoftness, vec4(1e-5)); + vec4 softClipFactor = clamp(edgeDistance / softness, 0.0, 1.0); + vec4 useSoftness = step(vec4(1e-5), renderer_UIRectClipSoftness); + vec4 clipFactor = mix(hardClipFactor, softClipFactor, useSoftness); + return clipFactor.x * clipFactor.y * clipFactor.z * clipFactor.w; + } + + float sampleCoverage(vec2 uv) { + vec4 texColor = texture2D(renderElement_TextTexture, uv); #ifdef GRAPHICS_API_WEBGL2 - float coverage = texColor.r; + return texColor.r; #else - float coverage = texColor.a; + return texColor.a; #endif - gl_FragColor = vec4(v.v_color.rgb, v.v_color.a * coverage); + } + + void TextFragment(v2f v) { + float coverage = sampleCoverage(v.v_uv); + vec4 finalColor; + if (renderer_OutlineWidth > 0.0) { + vec2 texelSize = 1.0 / renderElement_TextTextureSize; + vec2 outlineStep = texelSize * renderer_OutlineWidth; + float outlineCoverage = coverage; + outlineCoverage = max(outlineCoverage, sampleCoverage(v.v_uv + vec2( outlineStep.x, 0.0))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v.v_uv + vec2(-outlineStep.x, 0.0))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v.v_uv + vec2( 0.0, outlineStep.y))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v.v_uv + vec2( 0.0, -outlineStep.y))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v.v_uv + vec2( outlineStep.x * 0.7071, outlineStep.y * 0.7071))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v.v_uv + vec2(-outlineStep.x * 0.7071, outlineStep.y * 0.7071))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v.v_uv + vec2( outlineStep.x * 0.7071, -outlineStep.y * 0.7071))); + outlineCoverage = max(outlineCoverage, sampleCoverage(v.v_uv + vec2(-outlineStep.x * 0.7071, -outlineStep.y * 0.7071))); + + vec3 rgb = mix(renderer_OutlineColor.rgb, v.v_color.rgb, coverage); + float alpha = max(coverage, outlineCoverage * renderer_OutlineColor.a) * v.v_color.a; + finalColor = vec4(rgb, alpha); + } else { + finalColor = vec4(v.v_color.rgb, v.v_color.a * coverage); + } + if (renderer_UIRectClipEnabled > 0.5) { + finalColor.a *= getUIRectClipAlpha(v); + if (renderer_UIRectClipHardClip > 0.5 && finalColor.a < 0.001) { + discard; + } + } + gl_FragColor = finalColor; } } } diff --git a/packages/shader/src/Shaders/2D/UIDefault.shader b/packages/shader/src/Shaders/2D/UIDefault.shader index 9025f93605..4306eab1a1 100644 --- a/packages/shader/src/Shaders/2D/UIDefault.shader +++ b/packages/shader/src/Shaders/2D/UIDefault.shader @@ -27,6 +27,10 @@ Shader "2D/UIDefault" { mat4 renderer_MVPMat; sampler2D renderer_UITexture; + vec4 renderer_UIRectClipRect; + float renderer_UIRectClipEnabled; + vec4 renderer_UIRectClipSoftness; + float renderer_UIRectClipHardClip; struct Attributes { vec3 POSITION; @@ -37,6 +41,7 @@ Shader "2D/UIDefault" { struct Varyings { vec2 v_uv; vec4 v_color; + vec2 v_worldPosition; }; Varyings vert(Attributes attr) { @@ -45,13 +50,35 @@ Shader "2D/UIDefault" { gl_Position = renderer_MVPMat * vec4(attr.POSITION, 1.0); v.v_uv = attr.TEXCOORD_0; v.v_color = attr.COLOR_0; + v.v_worldPosition = attr.POSITION.xy; return v; } + float getUIRectClipAlpha(Varyings v) { + vec4 edgeDistance = vec4( + v.v_worldPosition.x - renderer_UIRectClipRect.x, + v.v_worldPosition.y - renderer_UIRectClipRect.y, + renderer_UIRectClipRect.z - v.v_worldPosition.x, + renderer_UIRectClipRect.w - v.v_worldPosition.y + ); + vec4 hardClipFactor = step(vec4(0.0), edgeDistance); + vec4 softness = max(renderer_UIRectClipSoftness, vec4(1e-5)); + vec4 softClipFactor = clamp(edgeDistance / softness, 0.0, 1.0); + vec4 useSoftness = step(vec4(1e-5), renderer_UIRectClipSoftness); + vec4 clipFactor = mix(hardClipFactor, softClipFactor, useSoftness); + return clipFactor.x * clipFactor.y * clipFactor.z * clipFactor.w; + } + void frag(Varyings v) { vec4 baseColor = texture2DSRGB(renderer_UITexture, v.v_uv); vec4 finalColor = baseColor * v.v_color; + if (renderer_UIRectClipEnabled > 0.5) { + finalColor.a *= getUIRectClipAlpha(v); + if (renderer_UIRectClipHardClip > 0.5 && finalColor.a < 0.001) { + discard; + } + } #ifdef ENGINE_SHOULD_SRGB_CORRECT finalColor = outputSRGBCorrection(finalColor); diff --git a/packages/shader/src/Shaders/2D/UIOverlayBlit.shader b/packages/shader/src/Shaders/2D/UIOverlayBlit.shader new file mode 100644 index 0000000000..1961ea3920 --- /dev/null +++ b/packages/shader/src/Shaders/2D/UIOverlayBlit.shader @@ -0,0 +1,64 @@ +Shader "2D/UIOverlayBlit" { + SubShader "Default" { + Pass "Forward" { + Tags { pipelineStage = "Forward" } + + BlendState = { + Enabled = true; + SourceColorBlendFactor = BlendFactor.One; + DestinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha; + SourceAlphaBlendFactor = BlendFactor.One; + DestinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; + } + DepthState = { + Enabled = false; + WriteEnabled = false; + } + + VertexShader = vert; + FragmentShader = frag; + + #include "ShaderLibrary/Common/Common.glsl" + + mediump sampler2D renderer_BlitTexture; + #ifdef HAS_TEX_LOD + float renderer_BlitMipLevel; + #endif + vec4 renderer_SourceScaleOffset; + + struct Attributes { + vec4 POSITION_UV; + }; + + struct Varyings { + vec2 v_uv; + }; + + Varyings vert(Attributes attr) { + Varyings v; + gl_Position = vec4(attr.POSITION_UV.xy, 0.0, 1.0); + v.v_uv = attr.POSITION_UV.zw; + return v; + } + + #ifdef HAS_TEX_LOD + vec4 texture2DLodSRGB(sampler2D tex, vec2 uv, float lod) { + vec4 color = texture2DLodEXT(tex, uv, lod); + #ifdef ENGINE_NO_SRGB + color = sRGBToLinear(color); + #endif + return color; + } + #endif + + void frag(Varyings v) { + vec2 uv = v.v_uv * renderer_SourceScaleOffset.xy + renderer_SourceScaleOffset.zw; + #ifdef HAS_TEX_LOD + gl_FragColor = texture2DLodSRGB(renderer_BlitTexture, uv, renderer_BlitMipLevel); + #else + gl_FragColor = texture2DSRGB(renderer_BlitTexture, uv); + #endif + } + } + } +} diff --git a/packages/shader/src/Shaders/index.ts b/packages/shader/src/Shaders/index.ts index f2ff92859d..5bbb1724a4 100644 --- a/packages/shader/src/Shaders/index.ts +++ b/packages/shader/src/Shaders/index.ts @@ -1,9 +1,11 @@ // Auto-generated by shader-compiler-precompile --emit-sources — do not edit. +import _2D_Spine from "./2D/Spine.shader"; import _2D_Sprite from "./2D/Sprite.shader"; import _2D_SpriteMask from "./2D/SpriteMask.shader"; import _2D_Text from "./2D/Text.shader"; import _2D_UIDefault from "./2D/UIDefault.shader"; +import _2D_UIOverlayBlit from "./2D/UIOverlayBlit.shader"; import BlinnPhong from "./BlinnPhong.shader"; import Blit_Blit from "./Blit/Blit.shader"; import Blit_BlitScreen from "./Blit/BlitScreen.shader"; @@ -30,10 +32,12 @@ export interface IShaderSource { // prettier-ignore export const shaders: IShaderSource[] = [ + { source: _2D_Spine, path: "Shaders/2D/Spine.shader" }, { source: _2D_Sprite, path: "Shaders/2D/Sprite.shader" }, { source: _2D_SpriteMask, path: "Shaders/2D/SpriteMask.shader" }, { source: _2D_Text, path: "Shaders/2D/Text.shader" }, { source: _2D_UIDefault, path: "Shaders/2D/UIDefault.shader" }, + { source: _2D_UIOverlayBlit, path: "Shaders/2D/UIOverlayBlit.shader" }, { source: BlinnPhong, path: "Shaders/BlinnPhong.shader" }, { source: Blit_Blit, path: "Shaders/Blit/Blit.shader" }, { source: Blit_BlitScreen, path: "Shaders/Blit/BlitScreen.shader" }, diff --git a/packages/spine-core-3.8/package.json b/packages/spine-core-3.8/package.json new file mode 100644 index 0000000000..6a8d6e6c33 --- /dev/null +++ b/packages/spine-core-3.8/package.json @@ -0,0 +1,40 @@ +{ + "name": "@galacean/engine-spine-core-3.8", + "version": "2.0.0-alpha.38", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "url": "https://github.com/galacean/engine.git" + }, + "license": "MIT", + "main": "dist/main.js", + "module": "dist/module.js", + "debug": "src/index.ts", + "browser": "dist/browser.js", + "types": "types/index.d.ts", + "scripts": { + "b:types": "tsc" + }, + "umd": { + "name": "Galacean.SpineCore38", + "globals": { + "@galacean/engine": "Galacean", + "@galacean/engine-spine": "Galacean.Spine" + } + }, + "files": [ + "dist/**/*", + "types/**/*" + ], + "dependencies": { + "@galacean/engine-spine": "workspace:*" + }, + "devDependencies": { + "@galacean/engine": "workspace:*" + }, + "peerDependencies": { + "@galacean/engine": "workspace:*" + } +} diff --git a/packages/spine-core-3.8/src/Spine38Runtime.ts b/packages/spine-core-3.8/src/Spine38Runtime.ts new file mode 100644 index 0000000000..f2d8a9c7e9 --- /dev/null +++ b/packages/spine-core-3.8/src/Spine38Runtime.ts @@ -0,0 +1,94 @@ +import { + AnimationState, + AnimationStateData, + AtlasAttachmentLoader, + Skeleton, + SkeletonBinary, + SkeletonData, + SkeletonJson, + TextureAtlas +} from "./spine-core"; +import type { Texture2D } from "@galacean/engine"; +import type { ISpineRenderTarget } from "@galacean/engine-spine"; +import { SpineGenerator } from "./SpineGenerator"; +import { SpineTexture } from "./SpineTexture"; + +/** + * Spine 3.8 runtime backend. + * + * @remarks + * Owns everything specific to the spine 3.8 runtime line: parsing (`SkeletonJson`/`SkeletonBinary`/ + * `TextureAtlas`), object-model construction, world-transform stepping (3.8 has no `Physics` argument), + * and the attachment-reading mesh generator. Registered automatically when this package is imported. + * + * Deliberately does not `implements ISpineRuntime`: that interface types its methods against the + * 4.2 npm package as the "canonical contract", but 4.2 and 3.8 have actually diverged enough + * (4.2 added `TextureAtlas.setTextures`; 4.2 dropped `Skeleton`/`SkeletonData`'s `updateCacheReset`/ + * `findBoneIndex`/`findSlotIndex`/`findPathConstraintIndex`, all present in 3.8) that TS's structural + * check fails even though every member `ISpineRuntime` actually calls lines up. Registered via a cast + * in `index.ts` instead of fighting the type checker method-by-method. + */ +export class Spine38Runtime { + private _generator = new SpineGenerator(); + + parseAtlasPageNames(atlasText: string): string[] { + // Unlike 4.2, spine 3.8's `TextureAtlas` constructor requires a synchronous texture loader and + // calls `setFilters`/`setWraps` on whatever it returns immediately during parsing, so a bare + // `null` loader would throw; hand back a no-op stub instead (its return type is `any`). + const pageNames: string[] = []; + new TextureAtlas(atlasText, (path) => { + pageNames.push(path); + return { setFilters() {}, setWraps() {} }; + }); + return pageNames; + } + + createTextureAtlas(atlasText: string, textures: Texture2D[]): TextureAtlas { + let index = 0; + return new TextureAtlas(atlasText, (path) => { + const engineTexture = textures.find((item) => item.name === path) || textures[index]; + index++; + return new SpineTexture(engineTexture); + }); + } + + createSkeletonData( + skeletonRawData: string | ArrayBuffer, + textureAtlas: TextureAtlas, + skeletonDataScale: number + ): SkeletonData { + const atlasLoader = new AtlasAttachmentLoader(textureAtlas); + if (typeof skeletonRawData === "string") { + const skeletonJson = new SkeletonJson(atlasLoader); + skeletonJson.scale = skeletonDataScale; + return skeletonJson.readSkeletonData(skeletonRawData); + } else { + const skeletonBinary = new SkeletonBinary(atlasLoader); + skeletonBinary.scale = skeletonDataScale; + return skeletonBinary.readSkeletonData(new Uint8Array(skeletonRawData as ArrayBuffer)); + } + } + + createAnimationStateData(skeletonData: SkeletonData): AnimationStateData { + return new AnimationStateData(skeletonData); + } + + createSkeleton(skeletonData: SkeletonData): Skeleton { + return new Skeleton(skeletonData); + } + + createAnimationState(stateData: AnimationStateData): AnimationState { + return new AnimationState(stateData); + } + + updateState(skeleton: Skeleton, state: AnimationState, delta: number): void { + state.update(delta); + state.apply(skeleton); + skeleton.update(delta); + skeleton.updateWorldTransform(); + } + + buildPrimitive(skeleton: Skeleton, target: ISpineRenderTarget): void { + this._generator.buildPrimitive(skeleton, target); + } +} diff --git a/packages/spine-core-3.8/src/SpineGenerator.ts b/packages/spine-core-3.8/src/SpineGenerator.ts new file mode 100644 index 0000000000..5ba048f320 --- /dev/null +++ b/packages/spine-core-3.8/src/SpineGenerator.ts @@ -0,0 +1,358 @@ +import { + ArrayLike, + BlendMode, + ClippingAttachment, + Color, + MeshAttachment, + RegionAttachment, + Skeleton, + SkeletonClipping, + TextureAtlasRegion +} from "./spine-core"; +import { BoundingBox, SubPrimitive } from "@galacean/engine"; +import { SpineTexture } from "./SpineTexture"; +import { ClearablePool } from "./util/ClearablePool"; +import { ReturnablePool } from "./util/ReturnablePool"; +import type { ISpineRenderTarget } from "@galacean/engine-spine"; +import { SpineBlendMode, SpineVertexStride } from "@galacean/engine-spine"; + +class SubRenderItem { + subPrimitive: SubPrimitive; + blendMode: BlendMode; + texture: any; +} + +/** + * @internal + */ +export class SpineGenerator { + static tempDark = new Color(); + static tempColor = new Color(); + static tempVerts = new Array(8); + static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0]; + static subPrimitivePool = new ReturnablePool(SubPrimitive); + static subRenderItemPool = new ClearablePool(SubRenderItem); + + private _subRenderItems: SubRenderItem[] = []; + private _clipper: SkeletonClipping = new SkeletonClipping(); + + buildPrimitive(skeleton: Skeleton, renderer: ISpineRenderTarget) { + const { _indices, _vertices, _localBounds, _vertexCount, _subPrimitives, zSpacing, premultipliedAlpha, tintBlack } = + renderer; + + _localBounds.min.set(Infinity, Infinity, Infinity); + _localBounds.max.set(-Infinity, -Infinity, -Infinity); + + const { _clipper, _subRenderItems } = this; + + const { tempVerts, subRenderItemPool, subPrimitivePool } = SpineGenerator; + const { withTint: vertexStrideWithTint, withoutTint: vertexStrideWithoutTint } = SpineVertexStride; + + _subRenderItems.length = 0; + subRenderItemPool.clear(); + + let triangles: Array; + let uvs: ArrayLike; + + let verticesLength = 0; + let indicesLength = 0; + let start = 0; + let count = 0; + + let blend = BlendMode.Normal; + let texture: SpineTexture = null; + let tempBlendMode: BlendMode | null = null; + let tempTexture: SpineTexture | null = null; + + let primitiveIndex = 0; + + const drawOrder = skeleton.drawOrder; + for (let slotIndex = 0, n = drawOrder.length; slotIndex < n; ++slotIndex) { + const slot = drawOrder[slotIndex]; + if (!slot.bone.active) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const attachment = slot.getAttachment(); + if (!attachment) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const z = zSpacing * slotIndex; + const isClipping = _clipper.isClipping(); + let numFloats = 0; + let attachmentColor: Color = null; + + // This vertexSize will be passed to spine-core's computeWorldVertices function. + // + // Expected format by computeWorldVertices: + // - Without tintBlack: [x, y, u, v, r, g, b, a] = 8 components + // - With tintBlack: [x, y, u, v, r, g, b, a, dr, dg, db, da] = 12 components + // + // Our actual vertex buffer format: + // - vertexStrideWithoutTint: [x, y, z, u, v, r, g, b, a] = 9 components + // - vertexStrideWithTint: [x, y, z, u, v, r, g, b, a, dr, dg, db] = 12 components + // (Note: we optimized 'da' as uniform instead of buffer attribute) + // + // Calculation: + // - Without tintBlack: 9 - 1 (remove z) = 8 ✓ + // - With tintBlack: 12 - 1 (remove z) + 1 (add back da) = 12 ✓ + const vertexSize = tintBlack ? vertexStrideWithTint : vertexStrideWithoutTint - 1; + const clippedVertexSize = isClipping ? 2 : vertexSize; + + switch (attachment.constructor) { + case RegionAttachment: + const regionAttachment = attachment; + attachmentColor = regionAttachment.color; + numFloats = clippedVertexSize << 2; + // spine 3.8's RegionAttachment.computeWorldVertices takes the Bone, not the Slot (4.2 takes Slot). + regionAttachment.computeWorldVertices(slot.bone, tempVerts, 0, clippedVertexSize); + triangles = SpineGenerator.QUAD_TRIANGLES; + uvs = regionAttachment.uvs; + // `region` is statically typed as the base TextureRegion (no `texture` field); it's + // always a TextureAtlasRegion in practice, since AtlasAttachmentLoader is the only + // source of regions. Its `.texture` is in turn always a SpineTexture, since + // createTextureAtlas is the only place that constructs one. + texture = (regionAttachment.region as TextureAtlasRegion).texture as unknown as SpineTexture; + break; + case MeshAttachment: + const 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 as TextureAtlasRegion).texture as unknown as SpineTexture; + break; + case ClippingAttachment: + const clip = attachment; + _clipper.clipStart(slot, clip); + continue; + default: + _clipper.clipEndWithSlot(slot); + continue; + } + + if (texture != null) { + let finalVertices: ArrayLike; + let finalVerticesLength: number; + let finalIndices: ArrayLike; + let finalIndicesLength: number; + + const skeleton = slot.bone.skeleton; + const skeletonColor = skeleton.color; + const slotColor = slot.color; + const finalColor = SpineGenerator.tempColor; + const finalAlpha = skeletonColor.a * slotColor.a * attachmentColor.a; + + finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r; + finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g; + finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b; + finalColor.a = finalAlpha; + + if (premultipliedAlpha) { + finalColor.r *= finalAlpha; + finalColor.g *= finalAlpha; + finalColor.b *= finalAlpha; + } + + const darkColor = SpineGenerator.tempDark; + const slotDarkColor = slot.darkColor; + if (!slotDarkColor) { + darkColor.set(0, 0, 0, 1); + } else { + if (premultipliedAlpha) { + darkColor.r = slotDarkColor.r * finalAlpha; + darkColor.g = slotDarkColor.g * finalAlpha; + darkColor.b = slotDarkColor.b * finalAlpha; + } else { + darkColor.setFromColor(slotDarkColor); + } + } + + if (isClipping) { + // spine 3.8's SkeletonClipping.clipTriangles takes an extra (unused) verticesLength param + // right after `vertices` that 4.2 dropped. + _clipper.clipTriangles( + tempVerts, + numFloats, + triangles, + triangles.length, + uvs, + finalColor, + darkColor, + tintBlack + ); + finalVertices = _clipper.clippedVertices; + finalVerticesLength = finalVertices.length; + finalIndices = _clipper.clippedTriangles; + finalIndicesLength = finalIndices.length; + } else { + const { r, g, b, a } = finalColor; + for (let v = 2, u = 0, n = numFloats; v < n; v += vertexSize, u += 2) { + tempVerts[v] = r; + tempVerts[v + 1] = g; + tempVerts[v + 2] = b; + tempVerts[v + 3] = a; + tempVerts[v + 4] = uvs[u]; + tempVerts[v + 5] = uvs[u + 1]; + if (tintBlack) { + tempVerts[v + 6] = darkColor.r; + tempVerts[v + 7] = darkColor.g; + tempVerts[v + 8] = darkColor.b; + tempVerts[v + 9] = darkColor.a; + } + } + finalVertices = tempVerts; + finalVerticesLength = numFloats; + finalIndices = triangles; + finalIndicesLength = triangles.length; + } + + if (finalVerticesLength == 0 || finalIndicesLength == 0) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const stride = tintBlack ? vertexStrideWithTint : vertexStrideWithoutTint; + const indexStart = verticesLength / stride; + let i = verticesLength; + let j = 0; + for (; j < finalVerticesLength; ) { + const x = finalVertices[j++]; + const y = finalVertices[j++]; + _vertices[i++] = x; + _vertices[i++] = y; + _vertices[i++] = z; + _vertices[i++] = finalVertices[j++]; // u + _vertices[i++] = finalVertices[j++]; // v + _vertices[i++] = finalVertices[j++]; // r + _vertices[i++] = finalVertices[j++]; // g + _vertices[i++] = finalVertices[j++]; // b + _vertices[i++] = finalVertices[j++]; // a + if (tintBlack) { + _vertices[i++] = finalVertices[j++]; // darkR + _vertices[i++] = finalVertices[j++]; // darkG + _vertices[i++] = finalVertices[j++]; // darkB + j++; // darkA + } + this._expandBounds(x, y, z, _localBounds); + } + verticesLength = i; + + for (i = indicesLength, j = 0; j < finalIndicesLength; i++, j++) { + _indices[i] = finalIndices[j] + indexStart; + } + indicesLength += finalIndicesLength; + + const textureChanged = tempTexture !== null && tempTexture !== texture; + blend = slot.data.blendMode; + const blendModeChanged = tempBlendMode !== null && tempBlendMode !== blend; + + if (blendModeChanged || textureChanged) { + // Finish accumulated count first + if (count > 0) { + primitiveIndex = this._createRenderItem( + _subPrimitives, + primitiveIndex, + start, + count, + tempTexture, + tempBlendMode + ); + start += count; + count = 0; + } + } + count += finalIndicesLength; + tempTexture = texture; + tempBlendMode = blend; + } + + _clipper.clipEndWithSlot(slot); + } // slot traverse end + + // add reset sub primitive + if (count > 0) { + primitiveIndex = this._createRenderItem(_subPrimitives, primitiveIndex, start, count, texture, blend); + count = 0; + } + + _clipper.clipEnd(); + + const lastLen = _subPrimitives.length; + const curLen = _subRenderItems.length; + for (let i = curLen; i < lastLen; i++) { + const item = _subPrimitives[i]; + subPrimitivePool.return(item); + } + + renderer._clearSubPrimitives(); + for (let i = 0, l = curLen; i < l; ++i) { + const item = _subRenderItems[i]; + const { blendMode, texture } = item; + renderer._addSubPrimitive(item.subPrimitive); + const material = renderer._getMaterial(texture.getImage(), blendMode as unknown as SpineBlendMode); + renderer.setMaterial(i, material); + } + + if (indicesLength > _vertexCount || renderer._needResizeBuffer) { + renderer._createAndBindBuffer(indicesLength); + renderer._needResizeBuffer = false; + this.buildPrimitive(skeleton, renderer); + return; + } + + if (renderer._vertexBuffer) { + renderer._vertexBuffer.setData(_vertices); + renderer._indexBuffer.setData(_indices); + } + } + + private _createRenderItem( + subPrimitives: SubPrimitive[], + primitiveIndex: number, + start: number, + count: number, + texture: SpineTexture, + blend: BlendMode + ): number { + const { subPrimitivePool, subRenderItemPool } = SpineGenerator; + const origin = subPrimitives[primitiveIndex]; + + if (origin) { + primitiveIndex++; + } + + const subPrimitive = origin || subPrimitivePool.get(); + subPrimitive.start = start; + subPrimitive.count = count; + + const renderItem = subRenderItemPool.get(); + renderItem.blendMode = blend; + renderItem.subPrimitive = subPrimitive; + renderItem.texture = texture; + + this._subRenderItems.push(renderItem); + + return primitiveIndex; + } + + private _expandBounds(x: number, y: number, z: number, localBounds: BoundingBox) { + const { min, max } = localBounds; + min.set(Math.min(min.x, x), Math.min(min.y, y), Math.min(min.z, z)); + max.set(Math.max(max.x, x), Math.max(max.y, y), Math.max(max.z, z)); + } +} diff --git a/packages/spine-core-3.8/src/SpineTexture.ts b/packages/spine-core-3.8/src/SpineTexture.ts new file mode 100644 index 0000000000..cd604451ab --- /dev/null +++ b/packages/spine-core-3.8/src/SpineTexture.ts @@ -0,0 +1,50 @@ +import { Texture, TextureFilter, TextureWrap } from "./spine-core/Texture"; +import { Texture2D, TextureFilterMode, TextureWrapMode } from "@galacean/engine"; + +/** + * @internal + */ +export class SpineTexture extends Texture { + constructor(image: Texture2D) { + super(image); + image.generateMipmaps(); + } + + // The vendored 3.8 `Texture` base class exposes the Texture2D via a `texture` getter instead of + // spine-core 4.2's `getImage()`; this alias keeps SpineGenerator's call sites identical across backends. + getImage(): Texture2D { + return this._texture; + } + + setFilters(minFilter: TextureFilter, magFilter: TextureFilter) { + if (minFilter === TextureFilter.Nearest) { + this._texture.filterMode = TextureFilterMode.Point; + } else if (minFilter === TextureFilter.Linear) { + this._texture.filterMode = TextureFilterMode.Bilinear; + } else { + // The remaining min filters are the MipMap* family (mag filters are never MipMap*); + // mipmaps are generated in the constructor. + this._texture.filterMode = TextureFilterMode.Trilinear; + } + } + + setWraps(uWrap: TextureWrap, vWrap: TextureWrap) { + this._texture.wrapModeU = this._convertWrapMode(uWrap); + this._texture.wrapModeV = this._convertWrapMode(vWrap); + } + + dispose() {} + + private _convertWrapMode(wrap: TextureWrap): TextureWrapMode { + switch (wrap) { + case TextureWrap.ClampToEdge: + return TextureWrapMode.Clamp; + case TextureWrap.Repeat: + return TextureWrapMode.Repeat; + case TextureWrap.MirroredRepeat: + return TextureWrapMode.Mirror; + default: + throw new Error("Unsupported texture wrap mode."); + } + } +} diff --git a/packages/spine-core-3.8/src/index.ts b/packages/spine-core-3.8/src/index.ts new file mode 100644 index 0000000000..c9e0710bc4 --- /dev/null +++ b/packages/spine-core-3.8/src/index.ts @@ -0,0 +1,17 @@ +import type { ISpineRuntime } from "@galacean/engine-spine"; +import { registerSpineRuntime } from "@galacean/engine-spine"; +import { Spine38Runtime } from "./Spine38Runtime"; + +// Self-register on import: `import "@galacean/engine-spine-core-3.8"` wires the 3.8 backend +// into the shared `@galacean/engine-spine` package. Cast needed because Spine38Runtime doesn't +// `implements ISpineRuntime` at the type level — see the class doc comment for why. +registerSpineRuntime(new Spine38Runtime() as unknown as ISpineRuntime); + +export { Spine38Runtime }; + +// Re-export the vendored spine 3.8 runtime API. Power users that construct spine-core objects +// directly (custom attachments, manual skeleton parsing) import them from this core package. +export * from "./spine-core"; + +export const version = `__buildVersion`; +console.log(`Galacean spine-core-3.8 version: ${version}`); diff --git a/packages/spine-core-3.8/src/spine-core/Animation.ts b/packages/spine-core-3.8/src/spine-core/Animation.ts new file mode 100644 index 0000000000..e15edbdc9d --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Animation.ts @@ -0,0 +1,1828 @@ +import { Skeleton } from "./Skeleton"; +import { Utils, MathUtils, ArrayLike } from "./Utils"; +import { Slot } from "./Slot"; +import { VertexAttachment, Attachment } from "./attachments/Attachment"; +import { IkConstraint } from "./IkConstraint"; +import { TransformConstraint } from "./TransformConstraint"; +import { PathConstraint } from "./PathConstraint"; +import { Event } from "./Event"; + +/** A simple container for a list of timelines and a name. */ +export class Animation { + /** The animation's name, which is unique across all animations in the skeleton. */ + name: string; + timelines: Array; + timelineIds: Array; + + /** The duration of the animation in seconds, which is the highest time of all keys in the timeline. */ + duration: number; + + constructor(name: string, timelines: Array, duration: number) { + if (name == null) throw new Error("name cannot be null."); + if (timelines == null) throw new Error("timelines cannot be null."); + this.name = name; + this.timelines = timelines; + this.timelineIds = []; + for (let i = 0; i < timelines.length; i++) this.timelineIds[timelines[i].getPropertyId()] = true; + this.duration = duration; + } + + hasTimeline(id: number) { + return this.timelineIds[id] == true; + } + + /** Applies all the animation's timelines to the specified skeleton. + * + * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. + * @param loop If true, the animation repeats after {@link #getDuration()}. + * @param events May be null to ignore fired events. */ + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + loop: boolean, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + if (skeleton == null) throw new Error("skeleton cannot be null."); + + if (loop && this.duration != 0) { + time %= this.duration; + if (lastTime > 0) lastTime %= this.duration; + } + + const timelines = this.timelines; + for (let i = 0, n = timelines.length; i < n; i++) + timelines[i].apply(skeleton, lastTime, time, events, alpha, blend, direction); + } + + /** @param target After the first and before the last value. + * @returns index of first value greater than the target. */ + static binarySearch(values: ArrayLike, target: number, step: number = 1) { + let low = 0; + let high = values.length / step - 2; + if (high == 0) return step; + let current = high >>> 1; + while (true) { + if (values[(current + 1) * step] <= target) low = current + 1; + else high = current; + if (low == high) return (low + 1) * step; + current = (low + high) >>> 1; + } + } + + static linearSearch(values: ArrayLike, target: number, step: number) { + for (let i = 0, last = values.length - step; i <= last; i += step) if (values[i] > target) return i; + return -1; + } +} + +/** The interface for all timelines. */ +export interface Timeline { + /** Applies this timeline to the skeleton. + * @param skeleton The skeleton the timeline is being applied to. This provides access to the bones, slots, and other + * skeleton components the timeline may change. + * @param lastTime The time this timeline was last applied. Timelines such as {@link EventTimeline}} trigger only at specific + * times rather than every frame. In that case, the timeline triggers everything between `lastTime` + * (exclusive) and `time` (inclusive). + * @param time The time within the animation. Most timelines find the key before and the key after this time so they can + * interpolate between the keys. + * @param events If any events are fired, they are added to this list. Can be null to ignore fired events or if the timeline + * does not fire events. + * @param alpha 0 applies the current or setup value (depending on `blend`). 1 applies the timeline value. + * Between 0 and 1 applies a value between the current or setup value and the timeline value. By adjusting + * `alpha` over time, an animation can be mixed in or out. `alpha` can also be useful to + * apply animations on top of each other (layering). + * @param blend Controls how mixing is applied when `alpha` < 1. + * @param direction Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions, + * such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}. */ + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ): void; + + /** Uniquely encodes both the type of this timeline and the skeleton property that it affects. */ + getPropertyId(): number; +} + +/** Controls how a timeline value is mixed with the setup pose value or current pose value when a timeline's `alpha` + * < 1. + * + * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */ +export enum MixBlend { + /** Transitions from the setup value to the timeline value (the current value is not used). Before the first key, the setup + * value is set. */ + setup, + /** Transitions from the current value to the timeline value. Before the first key, transitions from the current value to + * the setup value. Timelines which perform instant transitions, such as {@link DrawOrderTimeline} or + * {@link AttachmentTimeline}, use the setup value before the first key. + * + * `first` is intended for the first animations applied, not for animations layered on top of those. */ + first, + /** Transitions from the current value to the timeline value. No change is made before the first key (the current value is + * kept until the first key). + * + * `replace` is intended for animations layered on top of others, not for the first animations applied. */ + replace, + /** Transitions from the current value to the current value plus the timeline value. No change is made before the first key + * (the current value is kept until the first key). + * + * `add` is intended for animations layered on top of others, not for the first animations applied. Properties + * keyed by additive animations must be set manually or by another animation before applying the additive animations, else + * the property values will increase continually. */ + add +} + +/** Indicates whether a timeline's `alpha` is mixing out over time toward 0 (the setup or current pose value) or + * mixing in toward 1 (the timeline's value). + * + * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */ +export enum MixDirection { + mixIn, + mixOut +} + +export enum TimelineType { + rotate, + translate, + scale, + shear, + attachment, + color, + deform, + event, + drawOrder, + ikConstraint, + transformConstraint, + pathConstraintPosition, + pathConstraintSpacing, + pathConstraintMix, + twoColor +} + +/** The base class for timelines that use interpolation between key frame values. */ +export abstract class CurveTimeline implements Timeline { + static LINEAR = 0; + static STEPPED = 1; + static BEZIER = 2; + static BEZIER_SIZE = 10 * 2 - 1; + + private curves: ArrayLike; // type, x, y, ... + + abstract getPropertyId(): number; + + constructor(frameCount: number) { + if (frameCount <= 0) throw new Error("frameCount must be > 0: " + frameCount); + this.curves = Utils.newFloatArray((frameCount - 1) * CurveTimeline.BEZIER_SIZE); + } + + /** The number of key frames for this timeline. */ + getFrameCount() { + return this.curves.length / CurveTimeline.BEZIER_SIZE + 1; + } + + /** Sets the specified key frame to linear interpolation. */ + setLinear(frameIndex: number) { + this.curves[frameIndex * CurveTimeline.BEZIER_SIZE] = CurveTimeline.LINEAR; + } + + /** Sets the specified key frame to stepped interpolation. */ + setStepped(frameIndex: number) { + this.curves[frameIndex * CurveTimeline.BEZIER_SIZE] = CurveTimeline.STEPPED; + } + + /** Returns the interpolation type for the specified key frame. + * @returns Linear is 0, stepped is 1, Bezier is 2. */ + getCurveType(frameIndex: number): number { + const index = frameIndex * CurveTimeline.BEZIER_SIZE; + if (index == this.curves.length) return CurveTimeline.LINEAR; + const type = this.curves[index]; + if (type == CurveTimeline.LINEAR) return CurveTimeline.LINEAR; + if (type == CurveTimeline.STEPPED) return CurveTimeline.STEPPED; + return CurveTimeline.BEZIER; + } + + /** Sets the specified key frame to Bezier interpolation. `cx1` and `cx2` are from 0 to 1, + * representing the percent of time between the two key frames. `cy1` and `cy2` are the percent of the + * difference between the key frame's values. */ + setCurve(frameIndex: number, cx1: number, cy1: number, cx2: number, cy2: number) { + const tmpx = (-cx1 * 2 + cx2) * 0.03, + tmpy = (-cy1 * 2 + cy2) * 0.03; + const dddfx = ((cx1 - cx2) * 3 + 1) * 0.006, + dddfy = ((cy1 - cy2) * 3 + 1) * 0.006; + let ddfx = tmpx * 2 + dddfx, + ddfy = tmpy * 2 + dddfy; + let dfx = cx1 * 0.3 + tmpx + dddfx * 0.16666667, + dfy = cy1 * 0.3 + tmpy + dddfy * 0.16666667; + + let i = frameIndex * CurveTimeline.BEZIER_SIZE; + const curves = this.curves; + curves[i++] = CurveTimeline.BEZIER; + + let x = dfx, + y = dfy; + for (let n = i + CurveTimeline.BEZIER_SIZE - 1; i < n; i += 2) { + curves[i] = x; + curves[i + 1] = y; + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + x += dfx; + y += dfy; + } + } + + /** Returns the interpolated percentage for the specified key frame and linear percentage. */ + getCurvePercent(frameIndex: number, percent: number) { + percent = MathUtils.clamp(percent, 0, 1); + const curves = this.curves; + let i = frameIndex * CurveTimeline.BEZIER_SIZE; + const type = curves[i]; + if (type == CurveTimeline.LINEAR) return percent; + if (type == CurveTimeline.STEPPED) return 0; + i++; + let x = 0; + for (let start = i, n = i + CurveTimeline.BEZIER_SIZE - 1; i < n; i += 2) { + x = curves[i]; + if (x >= percent) { + let prevX: number, prevY: number; + if (i == start) { + prevX = 0; + prevY = 0; + } else { + prevX = curves[i - 2]; + prevY = curves[i - 1]; + } + return prevY + ((curves[i + 1] - prevY) * (percent - prevX)) / (x - prevX); + } + } + const y = curves[i - 1]; + return y + ((1 - y) * (percent - x)) / (1 - x); // Last point is 1,1. + } + + abstract apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ): void; +} + +/** Changes a bone's local {@link Bone#rotation}. */ +export class RotateTimeline extends CurveTimeline { + static ENTRIES = 2; + static PREV_TIME = -2; + static PREV_ROTATION = -1; + static ROTATION = 1; + + /** The index of the bone in {@link Skeleton#bones} that will be changed. */ + boneIndex: number; + + /** The time in seconds and rotation in degrees for each key frame. */ + frames: ArrayLike; // time, degrees, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount << 1); + } + + getPropertyId() { + return (TimelineType.rotate << 24) + this.boneIndex; + } + + /** Sets the time and angle of the specified keyframe. */ + setFrame(frameIndex: number, time: number, degrees: number) { + frameIndex <<= 1; + this.frames[frameIndex] = time; + this.frames[frameIndex + RotateTimeline.ROTATION] = degrees; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const bone = skeleton.bones[this.boneIndex]; + if (!bone.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.rotation = bone.data.rotation; + return; + case MixBlend.first: + const r = bone.data.rotation - bone.rotation; + bone.rotation += (r - (16384 - ((16384.499999999996 - r / 360) | 0)) * 360) * alpha; + } + return; + } + + if (time >= frames[frames.length - RotateTimeline.ENTRIES]) { + // Time is after last frame. + let r = frames[frames.length + RotateTimeline.PREV_ROTATION]; + switch (blend) { + case MixBlend.setup: + bone.rotation = bone.data.rotation + r * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + r += bone.data.rotation - bone.rotation; + r -= (16384 - ((16384.499999999996 - r / 360) | 0)) * 360; // Wrap within -180 and 180. + case MixBlend.add: + bone.rotation += r * alpha; + } + return; + } + + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, RotateTimeline.ENTRIES); + const prevRotation = frames[frame + RotateTimeline.PREV_ROTATION]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + (frame >> 1) - 1, + 1 - (time - frameTime) / (frames[frame + RotateTimeline.PREV_TIME] - frameTime) + ); + + let r = frames[frame + RotateTimeline.ROTATION] - prevRotation; + r = prevRotation + (r - (16384 - ((16384.499999999996 - r / 360) | 0)) * 360) * percent; + switch (blend) { + case MixBlend.setup: + bone.rotation = bone.data.rotation + (r - (16384 - ((16384.499999999996 - r / 360) | 0)) * 360) * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + r += bone.data.rotation - bone.rotation; + case MixBlend.add: + bone.rotation += (r - (16384 - ((16384.499999999996 - r / 360) | 0)) * 360) * alpha; + } + } +} + +/** Changes a bone's local {@link Bone#x} and {@link Bone#y}. */ +export class TranslateTimeline extends CurveTimeline { + static ENTRIES = 3; + static PREV_TIME = -3; + static PREV_X = -2; + static PREV_Y = -1; + static X = 1; + static Y = 2; + + /** The index of the bone in {@link Skeleton#bones} that will be changed. */ + boneIndex: number; + + /** The time in seconds, x, and y values for each key frame. */ + frames: ArrayLike; // time, x, y, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * TranslateTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.translate << 24) + this.boneIndex; + } + + /** Sets the time in seconds, x, and y values for the specified key frame. */ + setFrame(frameIndex: number, time: number, x: number, y: number) { + frameIndex *= TranslateTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + TranslateTimeline.X] = x; + this.frames[frameIndex + TranslateTimeline.Y] = y; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const bone = skeleton.bones[this.boneIndex]; + if (!bone.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.x = bone.data.x; + bone.y = bone.data.y; + return; + case MixBlend.first: + bone.x += (bone.data.x - bone.x) * alpha; + bone.y += (bone.data.y - bone.y) * alpha; + } + return; + } + + let x = 0, + y = 0; + if (time >= frames[frames.length - TranslateTimeline.ENTRIES]) { + // Time is after last frame. + x = frames[frames.length + TranslateTimeline.PREV_X]; + y = frames[frames.length + TranslateTimeline.PREV_Y]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, TranslateTimeline.ENTRIES); + x = frames[frame + TranslateTimeline.PREV_X]; + y = frames[frame + TranslateTimeline.PREV_Y]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / TranslateTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + TranslateTimeline.PREV_TIME] - frameTime) + ); + + x += (frames[frame + TranslateTimeline.X] - x) * percent; + y += (frames[frame + TranslateTimeline.Y] - y) * percent; + } + switch (blend) { + case MixBlend.setup: + bone.x = bone.data.x + x * alpha; + bone.y = bone.data.y + y * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + bone.x += (bone.data.x + x - bone.x) * alpha; + bone.y += (bone.data.y + y - bone.y) * alpha; + break; + case MixBlend.add: + bone.x += x * alpha; + bone.y += y * alpha; + } + } +} + +/** Changes a bone's local {@link Bone#scaleX)} and {@link Bone#scaleY}. */ +export class ScaleTimeline extends TranslateTimeline { + constructor(frameCount: number) { + super(frameCount); + } + + override getPropertyId() { + return (TimelineType.scale << 24) + this.boneIndex; + } + + override apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const bone = skeleton.bones[this.boneIndex]; + if (!bone.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.scaleX = bone.data.scaleX; + bone.scaleY = bone.data.scaleY; + return; + case MixBlend.first: + bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha; + bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha; + } + return; + } + + let x = 0, + y = 0; + if (time >= frames[frames.length - ScaleTimeline.ENTRIES]) { + // Time is after last frame. + x = frames[frames.length + ScaleTimeline.PREV_X] * bone.data.scaleX; + y = frames[frames.length + ScaleTimeline.PREV_Y] * bone.data.scaleY; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, ScaleTimeline.ENTRIES); + x = frames[frame + ScaleTimeline.PREV_X]; + y = frames[frame + ScaleTimeline.PREV_Y]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / ScaleTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + ScaleTimeline.PREV_TIME] - frameTime) + ); + + x = (x + (frames[frame + ScaleTimeline.X] - x) * percent) * bone.data.scaleX; + y = (y + (frames[frame + ScaleTimeline.Y] - y) * percent) * bone.data.scaleY; + } + if (alpha == 1) { + if (blend == MixBlend.add) { + bone.scaleX += x - bone.data.scaleX; + bone.scaleY += y - bone.data.scaleY; + } else { + bone.scaleX = x; + bone.scaleY = y; + } + } else { + let bx = 0, + by = 0; + if (direction == MixDirection.mixOut) { + switch (blend) { + case MixBlend.setup: + bx = bone.data.scaleX; + by = bone.data.scaleY; + bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha; + bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + bx = bone.scaleX; + by = bone.scaleY; + bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha; + bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha; + break; + case MixBlend.add: + bx = bone.scaleX; + by = bone.scaleY; + bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bone.data.scaleX) * alpha; + bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - bone.data.scaleY) * alpha; + } + } else { + switch (blend) { + case MixBlend.setup: + bx = Math.abs(bone.data.scaleX) * MathUtils.signum(x); + by = Math.abs(bone.data.scaleY) * MathUtils.signum(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + bx = Math.abs(bone.scaleX) * MathUtils.signum(x); + by = Math.abs(bone.scaleY) * MathUtils.signum(y); + bone.scaleX = bx + (x - bx) * alpha; + bone.scaleY = by + (y - by) * alpha; + break; + case MixBlend.add: + bx = MathUtils.signum(x); + by = MathUtils.signum(y); + bone.scaleX = Math.abs(bone.scaleX) * bx + (x - Math.abs(bone.data.scaleX) * bx) * alpha; + bone.scaleY = Math.abs(bone.scaleY) * by + (y - Math.abs(bone.data.scaleY) * by) * alpha; + } + } + } + } +} + +/** Changes a bone's local {@link Bone#shearX} and {@link Bone#shearY}. */ +export class ShearTimeline extends TranslateTimeline { + constructor(frameCount: number) { + super(frameCount); + } + + override getPropertyId() { + return (TimelineType.shear << 24) + this.boneIndex; + } + + override apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const bone = skeleton.bones[this.boneIndex]; + if (!bone.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.shearX = bone.data.shearX; + bone.shearY = bone.data.shearY; + return; + case MixBlend.first: + bone.shearX += (bone.data.shearX - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY - bone.shearY) * alpha; + } + return; + } + + let x = 0, + y = 0; + if (time >= frames[frames.length - ShearTimeline.ENTRIES]) { + // Time is after last frame. + x = frames[frames.length + ShearTimeline.PREV_X]; + y = frames[frames.length + ShearTimeline.PREV_Y]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, ShearTimeline.ENTRIES); + x = frames[frame + ShearTimeline.PREV_X]; + y = frames[frame + ShearTimeline.PREV_Y]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / ShearTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + ShearTimeline.PREV_TIME] - frameTime) + ); + + x = x + (frames[frame + ShearTimeline.X] - x) * percent; + y = y + (frames[frame + ShearTimeline.Y] - y) * percent; + } + switch (blend) { + case MixBlend.setup: + bone.shearX = bone.data.shearX + x * alpha; + bone.shearY = bone.data.shearY + y * alpha; + break; + case MixBlend.first: + case MixBlend.replace: + bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; + bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; + break; + case MixBlend.add: + bone.shearX += x * alpha; + bone.shearY += y * alpha; + } + } +} + +/** Changes a slot's {@link Slot#color}. */ +export class ColorTimeline extends CurveTimeline { + static ENTRIES = 5; + static PREV_TIME = -5; + static PREV_R = -4; + static PREV_G = -3; + static PREV_B = -2; + static PREV_A = -1; + static R = 1; + static G = 2; + static B = 3; + static A = 4; + + /** The index of the slot in {@link Skeleton#slots} that will be changed. */ + slotIndex: number; + + /** The time in seconds, red, green, blue, and alpha values for each key frame. */ + frames: ArrayLike; // time, r, g, b, a, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * ColorTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.color << 24) + this.slotIndex; + } + + /** Sets the time in seconds, red, green, blue, and alpha for the specified key frame. */ + setFrame(frameIndex: number, time: number, r: number, g: number, b: number, a: number) { + frameIndex *= ColorTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + ColorTimeline.R] = r; + this.frames[frameIndex + ColorTimeline.G] = g; + this.frames[frameIndex + ColorTimeline.B] = b; + this.frames[frameIndex + ColorTimeline.A] = a; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const slot = skeleton.slots[this.slotIndex]; + if (!slot.bone.active) return; + const frames = this.frames; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + slot.color.setFromColor(slot.data.color); + return; + case MixBlend.first: + const color = slot.color, + setup = slot.data.color; + color.add( + (setup.r - color.r) * alpha, + (setup.g - color.g) * alpha, + (setup.b - color.b) * alpha, + (setup.a - color.a) * alpha + ); + } + return; + } + + let r = 0, + g = 0, + b = 0, + a = 0; + if (time >= frames[frames.length - ColorTimeline.ENTRIES]) { + // Time is after last frame. + const i = frames.length; + r = frames[i + ColorTimeline.PREV_R]; + g = frames[i + ColorTimeline.PREV_G]; + b = frames[i + ColorTimeline.PREV_B]; + a = frames[i + ColorTimeline.PREV_A]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, ColorTimeline.ENTRIES); + r = frames[frame + ColorTimeline.PREV_R]; + g = frames[frame + ColorTimeline.PREV_G]; + b = frames[frame + ColorTimeline.PREV_B]; + a = frames[frame + ColorTimeline.PREV_A]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / ColorTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + ColorTimeline.PREV_TIME] - frameTime) + ); + + r += (frames[frame + ColorTimeline.R] - r) * percent; + g += (frames[frame + ColorTimeline.G] - g) * percent; + b += (frames[frame + ColorTimeline.B] - b) * percent; + a += (frames[frame + ColorTimeline.A] - a) * percent; + } + if (alpha == 1) slot.color.set(r, g, b, a); + else { + const color = slot.color; + if (blend == MixBlend.setup) color.setFromColor(slot.data.color); + color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha); + } + } +} + +/** Changes a slot's {@link Slot#color} and {@link Slot#darkColor} for two color tinting. */ +export class TwoColorTimeline extends CurveTimeline { + static ENTRIES = 8; + static PREV_TIME = -8; + static PREV_R = -7; + static PREV_G = -6; + static PREV_B = -5; + static PREV_A = -4; + static PREV_R2 = -3; + static PREV_G2 = -2; + static PREV_B2 = -1; + static R = 1; + static G = 2; + static B = 3; + static A = 4; + static R2 = 5; + static G2 = 6; + static B2 = 7; + + /** The index of the slot in {@link Skeleton#slots()} that will be changed. The {@link Slot#darkColor()} must not be + * null. */ + slotIndex: number; + + /** The time in seconds, red, green, blue, and alpha values of the color, red, green, blue of the dark color, for each key frame. */ + frames: ArrayLike; // time, r, g, b, a, r2, g2, b2, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * TwoColorTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.twoColor << 24) + this.slotIndex; + } + + /** Sets the time in seconds, light, and dark colors for the specified key frame. */ + setFrame( + frameIndex: number, + time: number, + r: number, + g: number, + b: number, + a: number, + r2: number, + g2: number, + b2: number + ) { + frameIndex *= TwoColorTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + TwoColorTimeline.R] = r; + this.frames[frameIndex + TwoColorTimeline.G] = g; + this.frames[frameIndex + TwoColorTimeline.B] = b; + this.frames[frameIndex + TwoColorTimeline.A] = a; + this.frames[frameIndex + TwoColorTimeline.R2] = r2; + this.frames[frameIndex + TwoColorTimeline.G2] = g2; + this.frames[frameIndex + TwoColorTimeline.B2] = b2; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const slot = skeleton.slots[this.slotIndex]; + if (!slot.bone.active) return; + const frames = this.frames; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + slot.color.setFromColor(slot.data.color); + slot.darkColor.setFromColor(slot.data.darkColor); + return; + case MixBlend.first: + const light = slot.color, + dark = slot.darkColor, + setupLight = slot.data.color, + setupDark = slot.data.darkColor; + light.add( + (setupLight.r - light.r) * alpha, + (setupLight.g - light.g) * alpha, + (setupLight.b - light.b) * alpha, + (setupLight.a - light.a) * alpha + ); + dark.add((setupDark.r - dark.r) * alpha, (setupDark.g - dark.g) * alpha, (setupDark.b - dark.b) * alpha, 0); + } + return; + } + + let r = 0, + g = 0, + b = 0, + a = 0, + r2 = 0, + g2 = 0, + b2 = 0; + if (time >= frames[frames.length - TwoColorTimeline.ENTRIES]) { + // Time is after last frame. + const i = frames.length; + r = frames[i + TwoColorTimeline.PREV_R]; + g = frames[i + TwoColorTimeline.PREV_G]; + b = frames[i + TwoColorTimeline.PREV_B]; + a = frames[i + TwoColorTimeline.PREV_A]; + r2 = frames[i + TwoColorTimeline.PREV_R2]; + g2 = frames[i + TwoColorTimeline.PREV_G2]; + b2 = frames[i + TwoColorTimeline.PREV_B2]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, TwoColorTimeline.ENTRIES); + r = frames[frame + TwoColorTimeline.PREV_R]; + g = frames[frame + TwoColorTimeline.PREV_G]; + b = frames[frame + TwoColorTimeline.PREV_B]; + a = frames[frame + TwoColorTimeline.PREV_A]; + r2 = frames[frame + TwoColorTimeline.PREV_R2]; + g2 = frames[frame + TwoColorTimeline.PREV_G2]; + b2 = frames[frame + TwoColorTimeline.PREV_B2]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / TwoColorTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + TwoColorTimeline.PREV_TIME] - frameTime) + ); + + r += (frames[frame + TwoColorTimeline.R] - r) * percent; + g += (frames[frame + TwoColorTimeline.G] - g) * percent; + b += (frames[frame + TwoColorTimeline.B] - b) * percent; + a += (frames[frame + TwoColorTimeline.A] - a) * percent; + r2 += (frames[frame + TwoColorTimeline.R2] - r2) * percent; + g2 += (frames[frame + TwoColorTimeline.G2] - g2) * percent; + b2 += (frames[frame + TwoColorTimeline.B2] - b2) * percent; + } + if (alpha == 1) { + slot.color.set(r, g, b, a); + slot.darkColor.set(r2, g2, b2, 1); + } else { + const light = slot.color, + dark = slot.darkColor; + if (blend == MixBlend.setup) { + light.setFromColor(slot.data.color); + dark.setFromColor(slot.data.darkColor); + } + light.add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha); + dark.add((r2 - dark.r) * alpha, (g2 - dark.g) * alpha, (b2 - dark.b) * alpha, 0); + } + } +} + +/** Changes a slot's {@link Slot#attachment}. */ +export class AttachmentTimeline implements Timeline { + /** The index of the slot in {@link Skeleton#slots} that will be changed. */ + slotIndex: number; + + /** The time in seconds for each key frame. */ + frames: ArrayLike; // time, ... + + /** The attachment name for each key frame. May contain null values to clear the attachment. */ + attachmentNames: Array; + + constructor(frameCount: number) { + this.frames = Utils.newFloatArray(frameCount); + this.attachmentNames = new Array(frameCount); + } + + getPropertyId() { + return (TimelineType.attachment << 24) + this.slotIndex; + } + + /** The number of key frames for this timeline. */ + getFrameCount() { + return this.frames.length; + } + + /** Sets the time in seconds and the attachment name for the specified key frame. */ + setFrame(frameIndex: number, time: number, attachmentName: string) { + this.frames[frameIndex] = time; + this.attachmentNames[frameIndex] = attachmentName; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + events: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const slot = skeleton.slots[this.slotIndex]; + if (!slot.bone.active) return; + if (direction == MixDirection.mixOut) { + if (blend == MixBlend.setup) this.setAttachment(skeleton, slot, slot.data.attachmentName); + return; + } + + const frames = this.frames; + if (time < frames[0]) { + if (blend == MixBlend.setup || blend == MixBlend.first) + this.setAttachment(skeleton, slot, slot.data.attachmentName); + return; + } + + let frameIndex = 0; + if (time >= frames[frames.length - 1]) + // Time is after last frame. + frameIndex = frames.length - 1; + else frameIndex = Animation.binarySearch(frames, time, 1) - 1; + + const attachmentName = this.attachmentNames[frameIndex]; + skeleton.slots[this.slotIndex].setAttachment( + attachmentName == null ? null : skeleton.getAttachment(this.slotIndex, attachmentName) + ); + } + + setAttachment(skeleton: Skeleton, slot: Slot, attachmentName: string) { + slot.attachment = attachmentName == null ? null : skeleton.getAttachment(this.slotIndex, attachmentName); + } +} + +let zeros: ArrayLike = null; + +/** Changes a slot's {@link Slot#deform} to deform a {@link VertexAttachment}. */ +export class DeformTimeline extends CurveTimeline { + /** The index of the slot in {@link Skeleton#getSlots()} that will be changed. */ + slotIndex: number; + + /** The attachment that will be deformed. */ + attachment: VertexAttachment; + + /** The time in seconds for each key frame. */ + frames: ArrayLike; // time, ... + + /** The vertices for each key frame. */ + frameVertices: Array>; + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount); + this.frameVertices = new Array>(frameCount); + if (zeros == null) zeros = Utils.newFloatArray(64); + } + + getPropertyId() { + return (TimelineType.deform << 27) + +this.attachment.id + this.slotIndex; + } + + /** Sets the time in seconds and the vertices for the specified key frame. + * @param vertices Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. */ + setFrame(frameIndex: number, time: number, vertices: ArrayLike) { + this.frames[frameIndex] = time; + this.frameVertices[frameIndex] = vertices; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const slot: Slot = skeleton.slots[this.slotIndex]; + if (!slot.bone.active) return; + const slotAttachment: Attachment = slot.getAttachment(); + if ( + !(slotAttachment instanceof VertexAttachment) || + !((slotAttachment).deformAttachment == this.attachment) + ) + return; + + const deformArray: Array = slot.deform; + if (deformArray.length == 0) blend = MixBlend.setup; + + const frameVertices = this.frameVertices; + const vertexCount = frameVertices[0].length; + + const frames = this.frames; + if (time < frames[0]) { + const vertexAttachment = slotAttachment; + switch (blend) { + case MixBlend.setup: + deformArray.length = 0; + return; + case MixBlend.first: + if (alpha == 1) { + deformArray.length = 0; + break; + } + const deform: Array = Utils.setArraySize(deformArray, vertexCount); + if (vertexAttachment.bones == null) { + // Unweighted vertex positions. + const setupVertices = vertexAttachment.vertices; + for (var i = 0; i < vertexCount; i++) deform[i] += (setupVertices[i] - deform[i]) * alpha; + } else { + // Weighted deform offsets. + alpha = 1 - alpha; + for (var i = 0; i < vertexCount; i++) deform[i] *= alpha; + } + } + return; + } + + const deform: Array = Utils.setArraySize(deformArray, vertexCount); + if (time >= frames[frames.length - 1]) { + // Time is after last frame. + const lastVertices = frameVertices[frames.length - 1]; + if (alpha == 1) { + if (blend == MixBlend.add) { + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + deform[i] += lastVertices[i] - setupVertices[i]; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) deform[i] += lastVertices[i]; + } + } else { + Utils.arrayCopy(lastVertices, 0, deform, 0, vertexCount); + } + } else { + switch (blend) { + case MixBlend.setup: { + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const setup = setupVertices[i]; + deform[i] = setup + (lastVertices[i] - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) deform[i] = lastVertices[i] * alpha; + } + break; + } + case MixBlend.first: + case MixBlend.replace: + for (let i = 0; i < vertexCount; i++) deform[i] += (lastVertices[i] - deform[i]) * alpha; + break; + case MixBlend.add: + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + deform[i] += (lastVertices[i] - setupVertices[i]) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) deform[i] += lastVertices[i] * alpha; + } + } + } + return; + } + + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time); + const prevVertices = frameVertices[frame - 1]; + const nextVertices = frameVertices[frame]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent(frame - 1, 1 - (time - frameTime) / (frames[frame - 1] - frameTime)); + + if (alpha == 1) { + if (blend == MixBlend.add) { + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i]; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += prev + (nextVertices[i] - prev) * percent; + } + } + } else { + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] = prev + (nextVertices[i] - prev) * percent; + } + } + } else { + switch (blend) { + case MixBlend.setup: { + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i], + setup = setupVertices[i]; + deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + break; + } + case MixBlend.first: + case MixBlend.replace: + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha; + } + break; + case MixBlend.add: + const vertexAttachment = slotAttachment as VertexAttachment; + if (vertexAttachment.bones == null) { + // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha; + } + } else { + // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + } + } + } +} + +/** Fires an {@link Event} when specific animation times are reached. */ +export class EventTimeline implements Timeline { + /** The time in seconds for each key frame. */ + frames: ArrayLike; // time, ... + + /** The event for each key frame. */ + events: Array; + + constructor(frameCount: number) { + this.frames = Utils.newFloatArray(frameCount); + this.events = new Array(frameCount); + } + + getPropertyId() { + return TimelineType.event << 24; + } + + /** The number of key frames for this timeline. */ + getFrameCount() { + return this.frames.length; + } + + /** Sets the time in seconds and the event for the specified key frame. */ + setFrame(frameIndex: number, event: Event) { + this.frames[frameIndex] = event.time; + this.events[frameIndex] = event; + } + + /** Fires events for frames > `lastTime` and <= `time`. */ + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + if (firedEvents == null) return; + const frames = this.frames; + const frameCount = this.frames.length; + + if (lastTime > time) { + // Fire events after last time for looped animations. + this.apply(skeleton, lastTime, Number.MAX_VALUE, firedEvents, alpha, blend, direction); + lastTime = -1; + } else if (lastTime >= frames[frameCount - 1]) + // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. + + let frame = 0; + if (lastTime < frames[0]) frame = 0; + else { + frame = Animation.binarySearch(frames, lastTime); + const frameTime = frames[frame]; + while (frame > 0) { + // Fire multiple events with the same frame. + if (frames[frame - 1] != frameTime) break; + frame--; + } + } + for (; frame < frameCount && time >= frames[frame]; frame++) firedEvents.push(this.events[frame]); + } +} + +/** Changes a skeleton's {@link Skeleton#drawOrder}. */ +export class DrawOrderTimeline implements Timeline { + /** The time in seconds for each key frame. */ + frames: ArrayLike; // time, ... + + /** The draw order for each key frame. See {@link #setFrame(int, float, int[])}. */ + drawOrders: Array>; + + constructor(frameCount: number) { + this.frames = Utils.newFloatArray(frameCount); + this.drawOrders = new Array>(frameCount); + } + + getPropertyId() { + return TimelineType.drawOrder << 24; + } + + /** The number of key frames for this timeline. */ + getFrameCount() { + return this.frames.length; + } + + /** Sets the time in seconds and the draw order for the specified key frame. + * @param drawOrder For each slot in {@link Skeleton#slots}, the index of the new draw order. May be null to use setup pose + * draw order. */ + setFrame(frameIndex: number, time: number, drawOrder: Array) { + this.frames[frameIndex] = time; + this.drawOrders[frameIndex] = drawOrder; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const drawOrder: Array = skeleton.drawOrder; + const slots: Array = skeleton.slots; + if (direction == MixDirection.mixOut) { + if (blend == MixBlend.setup) Utils.arrayCopy(skeleton.slots, 0, skeleton.drawOrder, 0, skeleton.slots.length); + return; + } + + const frames = this.frames; + if (time < frames[0]) { + if (blend == MixBlend.setup || blend == MixBlend.first) + Utils.arrayCopy(skeleton.slots, 0, skeleton.drawOrder, 0, skeleton.slots.length); + return; + } + + let frame = 0; + if (time >= frames[frames.length - 1]) + // Time is after last frame. + frame = frames.length - 1; + else frame = Animation.binarySearch(frames, time) - 1; + + const drawOrderToSetupIndex = this.drawOrders[frame]; + if (drawOrderToSetupIndex == null) Utils.arrayCopy(slots, 0, drawOrder, 0, slots.length); + else { + for (let i = 0, n = drawOrderToSetupIndex.length; i < n; i++) drawOrder[i] = slots[drawOrderToSetupIndex[i]]; + } + } +} + +/** Changes an IK constraint's {@link IkConstraint#mix}, {@link IkConstraint#softness}, + * {@link IkConstraint#bendDirection}, {@link IkConstraint#stretch}, and {@link IkConstraint#compress}. */ +export class IkConstraintTimeline extends CurveTimeline { + static ENTRIES = 6; + static PREV_TIME = -6; + static PREV_MIX = -5; + static PREV_SOFTNESS = -4; + static PREV_BEND_DIRECTION = -3; + static PREV_COMPRESS = -2; + static PREV_STRETCH = -1; + static MIX = 1; + static SOFTNESS = 2; + static BEND_DIRECTION = 3; + static COMPRESS = 4; + static STRETCH = 5; + + /** The index of the IK constraint slot in {@link Skeleton#ikConstraints} that will be changed. */ + ikConstraintIndex: number; + + /** The time in seconds, mix, softness, bend direction, compress, and stretch for each key frame. */ + frames: ArrayLike; // time, mix, softness, bendDirection, compress, stretch, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * IkConstraintTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.ikConstraint << 24) + this.ikConstraintIndex; + } + + /** Sets the time in seconds, mix, softness, bend direction, compress, and stretch for the specified key frame. */ + setFrame( + frameIndex: number, + time: number, + mix: number, + softness: number, + bendDirection: number, + compress: boolean, + stretch: boolean + ) { + frameIndex *= IkConstraintTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + IkConstraintTimeline.MIX] = mix; + this.frames[frameIndex + IkConstraintTimeline.SOFTNESS] = softness; + this.frames[frameIndex + IkConstraintTimeline.BEND_DIRECTION] = bendDirection; + this.frames[frameIndex + IkConstraintTimeline.COMPRESS] = compress ? 1 : 0; + this.frames[frameIndex + IkConstraintTimeline.STRETCH] = stretch ? 1 : 0; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + const constraint: IkConstraint = skeleton.ikConstraints[this.ikConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + constraint.mix = constraint.data.mix; + constraint.softness = constraint.data.softness; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + return; + case MixBlend.first: + constraint.mix += (constraint.data.mix - constraint.mix) * alpha; + constraint.softness += (constraint.data.softness - constraint.softness) * alpha; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } + return; + } + + if (time >= frames[frames.length - IkConstraintTimeline.ENTRIES]) { + // Time is after last frame. + if (blend == MixBlend.setup) { + constraint.mix = + constraint.data.mix + (frames[frames.length + IkConstraintTimeline.PREV_MIX] - constraint.data.mix) * alpha; + constraint.softness = + constraint.data.softness + + (frames[frames.length + IkConstraintTimeline.PREV_SOFTNESS] - constraint.data.softness) * alpha; + if (direction == MixDirection.mixOut) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = frames[frames.length + IkConstraintTimeline.PREV_BEND_DIRECTION]; + constraint.compress = frames[frames.length + IkConstraintTimeline.PREV_COMPRESS] != 0; + constraint.stretch = frames[frames.length + IkConstraintTimeline.PREV_STRETCH] != 0; + } + } else { + constraint.mix += (frames[frames.length + IkConstraintTimeline.PREV_MIX] - constraint.mix) * alpha; + constraint.softness += + (frames[frames.length + IkConstraintTimeline.PREV_SOFTNESS] - constraint.softness) * alpha; + if (direction == MixDirection.mixIn) { + constraint.bendDirection = frames[frames.length + IkConstraintTimeline.PREV_BEND_DIRECTION]; + constraint.compress = frames[frames.length + IkConstraintTimeline.PREV_COMPRESS] != 0; + constraint.stretch = frames[frames.length + IkConstraintTimeline.PREV_STRETCH] != 0; + } + } + return; + } + + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, IkConstraintTimeline.ENTRIES); + const mix = frames[frame + IkConstraintTimeline.PREV_MIX]; + const softness = frames[frame + IkConstraintTimeline.PREV_SOFTNESS]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / IkConstraintTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + IkConstraintTimeline.PREV_TIME] - frameTime) + ); + + if (blend == MixBlend.setup) { + constraint.mix = + constraint.data.mix + + (mix + (frames[frame + IkConstraintTimeline.MIX] - mix) * percent - constraint.data.mix) * alpha; + constraint.softness = + constraint.data.softness + + (softness + (frames[frame + IkConstraintTimeline.SOFTNESS] - softness) * percent - constraint.data.softness) * + alpha; + if (direction == MixDirection.mixOut) { + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } else { + constraint.bendDirection = frames[frame + IkConstraintTimeline.PREV_BEND_DIRECTION]; + constraint.compress = frames[frame + IkConstraintTimeline.PREV_COMPRESS] != 0; + constraint.stretch = frames[frame + IkConstraintTimeline.PREV_STRETCH] != 0; + } + } else { + constraint.mix += (mix + (frames[frame + IkConstraintTimeline.MIX] - mix) * percent - constraint.mix) * alpha; + constraint.softness += + (softness + (frames[frame + IkConstraintTimeline.SOFTNESS] - softness) * percent - constraint.softness) * alpha; + if (direction == MixDirection.mixIn) { + constraint.bendDirection = frames[frame + IkConstraintTimeline.PREV_BEND_DIRECTION]; + constraint.compress = frames[frame + IkConstraintTimeline.PREV_COMPRESS] != 0; + constraint.stretch = frames[frame + IkConstraintTimeline.PREV_STRETCH] != 0; + } + } + } +} + +/** Changes a transform constraint's {@link TransformConstraint#rotateMix}, {@link TransformConstraint#translateMix}, + * {@link TransformConstraint#scaleMix}, and {@link TransformConstraint#shearMix}. */ +export class TransformConstraintTimeline extends CurveTimeline { + static ENTRIES = 5; + static PREV_TIME = -5; + static PREV_ROTATE = -4; + static PREV_TRANSLATE = -3; + static PREV_SCALE = -2; + static PREV_SHEAR = -1; + static ROTATE = 1; + static TRANSLATE = 2; + static SCALE = 3; + static SHEAR = 4; + + /** The index of the transform constraint slot in {@link Skeleton#transformConstraints} that will be changed. */ + transformConstraintIndex: number; + + /** The time in seconds, rotate mix, translate mix, scale mix, and shear mix for each key frame. */ + frames: ArrayLike; // time, rotate mix, translate mix, scale mix, shear mix, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * TransformConstraintTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.transformConstraint << 24) + this.transformConstraintIndex; + } + + /** The time in seconds, rotate mix, translate mix, scale mix, and shear mix for the specified key frame. */ + setFrame( + frameIndex: number, + time: number, + rotateMix: number, + translateMix: number, + scaleMix: number, + shearMix: number + ) { + frameIndex *= TransformConstraintTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + TransformConstraintTimeline.ROTATE] = rotateMix; + this.frames[frameIndex + TransformConstraintTimeline.TRANSLATE] = translateMix; + this.frames[frameIndex + TransformConstraintTimeline.SCALE] = scaleMix; + this.frames[frameIndex + TransformConstraintTimeline.SHEAR] = shearMix; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + + const constraint: TransformConstraint = skeleton.transformConstraints[this.transformConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + const data = constraint.data; + switch (blend) { + case MixBlend.setup: + constraint.rotateMix = data.rotateMix; + constraint.translateMix = data.translateMix; + constraint.scaleMix = data.scaleMix; + constraint.shearMix = data.shearMix; + return; + case MixBlend.first: + constraint.rotateMix += (data.rotateMix - constraint.rotateMix) * alpha; + constraint.translateMix += (data.translateMix - constraint.translateMix) * alpha; + constraint.scaleMix += (data.scaleMix - constraint.scaleMix) * alpha; + constraint.shearMix += (data.shearMix - constraint.shearMix) * alpha; + } + return; + } + + let rotate = 0, + translate = 0, + scale = 0, + shear = 0; + if (time >= frames[frames.length - TransformConstraintTimeline.ENTRIES]) { + // Time is after last frame. + const i = frames.length; + rotate = frames[i + TransformConstraintTimeline.PREV_ROTATE]; + translate = frames[i + TransformConstraintTimeline.PREV_TRANSLATE]; + scale = frames[i + TransformConstraintTimeline.PREV_SCALE]; + shear = frames[i + TransformConstraintTimeline.PREV_SHEAR]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, TransformConstraintTimeline.ENTRIES); + rotate = frames[frame + TransformConstraintTimeline.PREV_ROTATE]; + translate = frames[frame + TransformConstraintTimeline.PREV_TRANSLATE]; + scale = frames[frame + TransformConstraintTimeline.PREV_SCALE]; + shear = frames[frame + TransformConstraintTimeline.PREV_SHEAR]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / TransformConstraintTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + TransformConstraintTimeline.PREV_TIME] - frameTime) + ); + + rotate += (frames[frame + TransformConstraintTimeline.ROTATE] - rotate) * percent; + translate += (frames[frame + TransformConstraintTimeline.TRANSLATE] - translate) * percent; + scale += (frames[frame + TransformConstraintTimeline.SCALE] - scale) * percent; + shear += (frames[frame + TransformConstraintTimeline.SHEAR] - shear) * percent; + } + if (blend == MixBlend.setup) { + const data = constraint.data; + constraint.rotateMix = data.rotateMix + (rotate - data.rotateMix) * alpha; + constraint.translateMix = data.translateMix + (translate - data.translateMix) * alpha; + constraint.scaleMix = data.scaleMix + (scale - data.scaleMix) * alpha; + constraint.shearMix = data.shearMix + (shear - data.shearMix) * alpha; + } else { + constraint.rotateMix += (rotate - constraint.rotateMix) * alpha; + constraint.translateMix += (translate - constraint.translateMix) * alpha; + constraint.scaleMix += (scale - constraint.scaleMix) * alpha; + constraint.shearMix += (shear - constraint.shearMix) * alpha; + } + } +} + +/** Changes a path constraint's {@link PathConstraint#position}. */ +export class PathConstraintPositionTimeline extends CurveTimeline { + static ENTRIES = 2; + static PREV_TIME = -2; + static PREV_VALUE = -1; + static VALUE = 1; + + /** The index of the path constraint slot in {@link Skeleton#pathConstraints} that will be changed. */ + pathConstraintIndex: number; + + /** The time in seconds and path constraint position for each key frame. */ + frames: ArrayLike; // time, position, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * PathConstraintPositionTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.pathConstraintPosition << 24) + this.pathConstraintIndex; + } + + /** Sets the time in seconds and path constraint position for the specified key frame. */ + setFrame(frameIndex: number, time: number, value: number) { + frameIndex *= PathConstraintPositionTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + PathConstraintPositionTimeline.VALUE] = value; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + const constraint: PathConstraint = skeleton.pathConstraints[this.pathConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + constraint.position = constraint.data.position; + return; + case MixBlend.first: + constraint.position += (constraint.data.position - constraint.position) * alpha; + } + return; + } + + let position = 0; + if (time >= frames[frames.length - PathConstraintPositionTimeline.ENTRIES]) + // Time is after last frame. + position = frames[frames.length + PathConstraintPositionTimeline.PREV_VALUE]; + else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, PathConstraintPositionTimeline.ENTRIES); + position = frames[frame + PathConstraintPositionTimeline.PREV_VALUE]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / PathConstraintPositionTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PathConstraintPositionTimeline.PREV_TIME] - frameTime) + ); + + position += (frames[frame + PathConstraintPositionTimeline.VALUE] - position) * percent; + } + if (blend == MixBlend.setup) + constraint.position = constraint.data.position + (position - constraint.data.position) * alpha; + else constraint.position += (position - constraint.position) * alpha; + } +} + +/** Changes a path constraint's {@link PathConstraint#spacing}. */ +export class PathConstraintSpacingTimeline extends PathConstraintPositionTimeline { + constructor(frameCount: number) { + super(frameCount); + } + + override getPropertyId() { + return (TimelineType.pathConstraintSpacing << 24) + this.pathConstraintIndex; + } + + override apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + const constraint: PathConstraint = skeleton.pathConstraints[this.pathConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + constraint.spacing = constraint.data.spacing; + return; + case MixBlend.first: + constraint.spacing += (constraint.data.spacing - constraint.spacing) * alpha; + } + return; + } + + let spacing = 0; + if (time >= frames[frames.length - PathConstraintSpacingTimeline.ENTRIES]) + // Time is after last frame. + spacing = frames[frames.length + PathConstraintSpacingTimeline.PREV_VALUE]; + else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, PathConstraintSpacingTimeline.ENTRIES); + spacing = frames[frame + PathConstraintSpacingTimeline.PREV_VALUE]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / PathConstraintSpacingTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PathConstraintSpacingTimeline.PREV_TIME] - frameTime) + ); + + spacing += (frames[frame + PathConstraintSpacingTimeline.VALUE] - spacing) * percent; + } + + if (blend == MixBlend.setup) + constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha; + else constraint.spacing += (spacing - constraint.spacing) * alpha; + } +} + +/** Changes a transform constraint's {@link PathConstraint#rotateMix} and + * {@link TransformConstraint#translateMix}. */ +export class PathConstraintMixTimeline extends CurveTimeline { + static ENTRIES = 3; + static PREV_TIME = -3; + static PREV_ROTATE = -2; + static PREV_TRANSLATE = -1; + static ROTATE = 1; + static TRANSLATE = 2; + + /** The index of the path constraint slot in {@link Skeleton#getPathConstraints()} that will be changed. */ + pathConstraintIndex: number; + + /** The time in seconds, rotate mix, and translate mix for each key frame. */ + frames: ArrayLike; // time, rotate mix, translate mix, ... + + constructor(frameCount: number) { + super(frameCount); + this.frames = Utils.newFloatArray(frameCount * PathConstraintMixTimeline.ENTRIES); + } + + getPropertyId() { + return (TimelineType.pathConstraintMix << 24) + this.pathConstraintIndex; + } + + /** The time in seconds, rotate mix, and translate mix for the specified key frame. */ + setFrame(frameIndex: number, time: number, rotateMix: number, translateMix: number) { + frameIndex *= PathConstraintMixTimeline.ENTRIES; + this.frames[frameIndex] = time; + this.frames[frameIndex + PathConstraintMixTimeline.ROTATE] = rotateMix; + this.frames[frameIndex + PathConstraintMixTimeline.TRANSLATE] = translateMix; + } + + apply( + skeleton: Skeleton, + lastTime: number, + time: number, + firedEvents: Array, + alpha: number, + blend: MixBlend, + direction: MixDirection + ) { + const frames = this.frames; + const constraint: PathConstraint = skeleton.pathConstraints[this.pathConstraintIndex]; + if (!constraint.active) return; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + constraint.rotateMix = constraint.data.rotateMix; + constraint.translateMix = constraint.data.translateMix; + return; + case MixBlend.first: + constraint.rotateMix += (constraint.data.rotateMix - constraint.rotateMix) * alpha; + constraint.translateMix += (constraint.data.translateMix - constraint.translateMix) * alpha; + } + return; + } + + let rotate = 0, + translate = 0; + if (time >= frames[frames.length - PathConstraintMixTimeline.ENTRIES]) { + // Time is after last frame. + rotate = frames[frames.length + PathConstraintMixTimeline.PREV_ROTATE]; + translate = frames[frames.length + PathConstraintMixTimeline.PREV_TRANSLATE]; + } else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, PathConstraintMixTimeline.ENTRIES); + rotate = frames[frame + PathConstraintMixTimeline.PREV_ROTATE]; + translate = frames[frame + PathConstraintMixTimeline.PREV_TRANSLATE]; + const frameTime = frames[frame]; + const percent = this.getCurvePercent( + frame / PathConstraintMixTimeline.ENTRIES - 1, + 1 - (time - frameTime) / (frames[frame + PathConstraintMixTimeline.PREV_TIME] - frameTime) + ); + + rotate += (frames[frame + PathConstraintMixTimeline.ROTATE] - rotate) * percent; + translate += (frames[frame + PathConstraintMixTimeline.TRANSLATE] - translate) * percent; + } + + if (blend == MixBlend.setup) { + constraint.rotateMix = constraint.data.rotateMix + (rotate - constraint.data.rotateMix) * alpha; + constraint.translateMix = constraint.data.translateMix + (translate - constraint.data.translateMix) * alpha; + } else { + constraint.rotateMix += (rotate - constraint.rotateMix) * alpha; + constraint.translateMix += (translate - constraint.translateMix) * alpha; + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/AnimationState.ts b/packages/spine-core-3.8/src/spine-core/AnimationState.ts new file mode 100644 index 0000000000..7bb6b64177 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/AnimationState.ts @@ -0,0 +1,1193 @@ +import { AnimationStateData } from "./AnimationStateData"; +import { IntSet, Pool, Utils, MathUtils } from "./Utils"; +import { Skeleton } from "./Skeleton"; +import { + MixBlend, + AttachmentTimeline, + MixDirection, + RotateTimeline, + DrawOrderTimeline, + Timeline, + EventTimeline +} from "./Animation"; +import { Slot } from "./Slot"; +import { Animation } from "./Animation"; +import { Event } from "./Event"; + +/** Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies + * multiple animations on top of each other (layering). + * + * See [Applying Animations](http://esotericsoftware.com/spine-applying-animations/) in the Spine Runtimes Guide. */ +export class AnimationState { + static emptyAnimation = new Animation("", [], 0); + + /** 1. A previously applied timeline has set this property. + * + * Result: Mix from the current pose to the timeline pose. */ + static SUBSEQUENT = 0; + /** 1. This is the first timeline to set this property. + * 2. The next track entry applied after this one does not have a timeline to set this property. + * + * Result: Mix from the setup pose to the timeline pose. */ + static FIRST = 1; + /** 1) A previously applied timeline has set this property.
    + * 2) The next track entry to be applied does have a timeline to set this property.
    + * 3) The next track entry after that one does not have a timeline to set this property.
    + * Result: Mix from the current pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading + * animations that key the same property. A subsequent timeline will set this property using a mix. */ + static HOLD_SUBSEQUENT = 2; + /** 1) This is the first timeline to set this property.
    + * 2) The next track entry to be applied does have a timeline to set this property.
    + * 3) The next track entry after that one does not have a timeline to set this property.
    + * Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations + * that key the same property. A subsequent timeline will set this property using a mix. */ + static HOLD_FIRST = 3; + /** 1. This is the first timeline to set this property. + * 2. The next track entry to be applied does have a timeline to set this property. + * 3. The next track entry after that one does have a timeline to set this property. + * 4. timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property. + * + * Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than + * 2 track entries in a row have a timeline that sets the same property. + * + * Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid + * "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A + * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into + * place. */ + static HOLD_MIX = 4; + + static SETUP = 1; + static CURRENT = 2; + + /** The AnimationStateData to look up mix durations. */ + data: AnimationStateData; + + /** The list of tracks that currently have animations, which may contain null entries. */ + tracks = new Array(); + + /** Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower + * or faster. Defaults to 1. + * + * See TrackEntry {@link TrackEntry#timeScale} for affecting a single animation. */ + timeScale = 1; + unkeyedState = 0; + + events = new Array(); + listeners = new Array(); + queue = new EventQueue(this); + propertyIDs = new IntSet(); + animationsChanged = false; + + trackEntryPool = new Pool(() => new TrackEntry()); + + constructor(data: AnimationStateData) { + this.data = data; + } + + /** Increments each track entry {@link TrackEntry#trackTime()}, setting queued animations as current if needed. */ + update(delta: number) { + delta *= this.timeScale; + const tracks = this.tracks; + for (let i = 0, n = tracks.length; i < n; i++) { + const current = tracks[i]; + if (current == null) continue; + + current.animationLast = current.nextAnimationLast; + current.trackLast = current.nextTrackLast; + + let currentDelta = delta * current.timeScale; + + if (current.delay > 0) { + current.delay -= currentDelta; + if (current.delay > 0) continue; + currentDelta = -current.delay; + current.delay = 0; + } + + let next = current.next; + if (next != null) { + // When the next entry's delay is passed, change to the next entry, preserving leftover time. + const nextTime = current.trackLast - next.delay; + if (nextTime >= 0) { + next.delay = 0; + next.trackTime += current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale; + current.trackTime += currentDelta; + this.setCurrent(i, next, true); + while (next.mixingFrom != null) { + next.mixTime += delta; + next = next.mixingFrom; + } + continue; + } + } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) { + tracks[i] = null; + this.queue.end(current); + this.disposeNext(current); + continue; + } + if (current.mixingFrom != null && this.updateMixingFrom(current, delta)) { + // End mixing from entries once all have completed. + let from = current.mixingFrom; + current.mixingFrom = null; + if (from != null) from.mixingTo = null; + while (from != null) { + this.queue.end(from); + from = from.mixingFrom; + } + } + + current.trackTime += currentDelta; + } + + this.queue.drain(); + } + + /** Returns true when all mixing from entries are complete. */ + updateMixingFrom(to: TrackEntry, delta: number): boolean { + const from = to.mixingFrom; + if (from == null) return true; + + const finished = this.updateMixingFrom(from, delta); + + from.animationLast = from.nextAnimationLast; + from.trackLast = from.nextTrackLast; + + // Require mixTime > 0 to ensure the mixing from entry was applied at least once. + if (to.mixTime > 0 && to.mixTime >= to.mixDuration) { + // Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame). + if (from.totalAlpha == 0 || to.mixDuration == 0) { + to.mixingFrom = from.mixingFrom; + if (from.mixingFrom != null) from.mixingFrom.mixingTo = to; + to.interruptAlpha = from.interruptAlpha; + this.queue.end(from); + } + return finished; + } + + from.trackTime += delta * from.timeScale; + to.mixTime += delta; + return false; + } + + /** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the + * animation state can be applied to multiple skeletons to pose them identically. + * @returns True if any animations were applied. */ + apply(skeleton: Skeleton): boolean { + if (skeleton == null) throw new Error("skeleton cannot be null."); + if (this.animationsChanged) this._animationsChanged(); + + const events = this.events; + const tracks = this.tracks; + let applied = false; + + for (let i = 0, n = tracks.length; i < n; i++) { + const current = tracks[i]; + if (current == null || current.delay > 0) continue; + applied = true; + const blend: MixBlend = i == 0 ? MixBlend.first : current.mixBlend; + + // Apply mixing from entries first. + let mix = current.alpha; + if (current.mixingFrom != null) mix *= this.applyMixingFrom(current, skeleton, blend); + else if (current.trackTime >= current.trackEnd && current.next == null) mix = 0; + + // Apply current entry. + const animationLast = current.animationLast, + animationTime = current.getAnimationTime(); + const timelineCount = current.animation.timelines.length; + const timelines = current.animation.timelines; + if ((i == 0 && mix == 1) || blend == MixBlend.add) { + for (let ii = 0; ii < timelineCount; ii++) { + // Fixes issue #302 on IOS9 where mix, blend sometimes became undefined and caused assets + // to sometimes stop rendering when using color correction, as their RGBA values become NaN. + // (https://github.com/pixijs/pixi-spine/issues/302) + Utils.webkit602BugfixHelper(mix, blend); + const timeline = timelines[ii]; + if (timeline instanceof AttachmentTimeline) + this.applyAttachmentTimeline(timeline, skeleton, animationTime, blend, true); + else timeline.apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.mixIn); + } + } else { + const timelineMode = current.timelineMode; + + const firstFrame = current.timelinesRotation.length == 0; + if (firstFrame) Utils.setArraySize(current.timelinesRotation, timelineCount << 1, null); + const timelinesRotation = current.timelinesRotation; + + for (let ii = 0; ii < timelineCount; ii++) { + const timeline = timelines[ii]; + const timelineBlend = timelineMode[ii] == AnimationState.SUBSEQUENT ? blend : MixBlend.setup; + if (timeline instanceof RotateTimeline) { + this.applyRotateTimeline( + timeline, + skeleton, + animationTime, + mix, + timelineBlend, + timelinesRotation, + ii << 1, + firstFrame + ); + } else if (timeline instanceof AttachmentTimeline) { + this.applyAttachmentTimeline(timeline, skeleton, animationTime, blend, true); + } else { + // This fixes the WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109 + Utils.webkit602BugfixHelper(mix, blend); + timeline.apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.mixIn); + } + } + } + this.queueEvents(current, animationTime); + events.length = 0; + current.nextAnimationLast = animationTime; + current.nextTrackLast = current.trackTime; + } + + // Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so + // subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or + // the time is before the first key). + const setupState = this.unkeyedState + AnimationState.SETUP; + const slots = skeleton.slots; + for (let i = 0, n = skeleton.slots.length; i < n; i++) { + const slot = slots[i]; + if (slot.attachmentState == setupState) { + const attachmentName = slot.data.attachmentName; + slot.attachment = attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName); + } + } + this.unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot. + + this.queue.drain(); + return applied; + } + + applyMixingFrom(to: TrackEntry, skeleton: Skeleton, blend: MixBlend) { + const from = to.mixingFrom; + if (from.mixingFrom != null) this.applyMixingFrom(from, skeleton, blend); + + let mix = 0; + if (to.mixDuration == 0) { + // Single frame mix to undo mixingFrom changes. + mix = 1; + if (blend == MixBlend.first) blend = MixBlend.setup; + } else { + mix = to.mixTime / to.mixDuration; + if (mix > 1) mix = 1; + if (blend != MixBlend.first) blend = from.mixBlend; + } + + const events = mix < from.eventThreshold ? this.events : null; + const attachments = mix < from.attachmentThreshold, + drawOrder = mix < from.drawOrderThreshold; + const animationLast = from.animationLast, + animationTime = from.getAnimationTime(); + const timelineCount = from.animation.timelines.length; + const timelines = from.animation.timelines; + const alphaHold = from.alpha * to.interruptAlpha, + alphaMix = alphaHold * (1 - mix); + if (blend == MixBlend.add) { + for (let i = 0; i < timelineCount; i++) + timelines[i].apply(skeleton, animationLast, animationTime, events, alphaMix, blend, MixDirection.mixOut); + } else { + const timelineMode = from.timelineMode; + const timelineHoldMix = from.timelineHoldMix; + + const firstFrame = from.timelinesRotation.length == 0; + if (firstFrame) Utils.setArraySize(from.timelinesRotation, timelineCount << 1, null); + const timelinesRotation = from.timelinesRotation; + + from.totalAlpha = 0; + for (let i = 0; i < timelineCount; i++) { + const timeline = timelines[i]; + let direction = MixDirection.mixOut; + let timelineBlend: MixBlend; + let alpha = 0; + switch (timelineMode[i]) { + case AnimationState.SUBSEQUENT: + if (!drawOrder && timeline instanceof DrawOrderTimeline) continue; + timelineBlend = blend; + alpha = alphaMix; + break; + case AnimationState.FIRST: + timelineBlend = MixBlend.setup; + alpha = alphaMix; + break; + case AnimationState.HOLD_SUBSEQUENT: + timelineBlend = blend; + alpha = alphaHold; + break; + case AnimationState.HOLD_FIRST: + timelineBlend = MixBlend.setup; + alpha = alphaHold; + break; + default: + timelineBlend = MixBlend.setup; + const holdMix = timelineHoldMix[i]; + alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration); + break; + } + from.totalAlpha += alpha; + + if (timeline instanceof RotateTimeline) + this.applyRotateTimeline( + timeline, + skeleton, + animationTime, + alpha, + timelineBlend, + timelinesRotation, + i << 1, + firstFrame + ); + else if (timeline instanceof AttachmentTimeline) + this.applyAttachmentTimeline(timeline, skeleton, animationTime, timelineBlend, attachments); + else { + // This fixes the WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109 + Utils.webkit602BugfixHelper(alpha, blend); + if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup) + direction = MixDirection.mixIn; + timeline.apply(skeleton, animationLast, animationTime, events, alpha, timelineBlend, direction); + } + } + } + + if (to.mixDuration > 0) this.queueEvents(from, animationTime); + this.events.length = 0; + from.nextAnimationLast = animationTime; + from.nextTrackLast = from.trackTime; + + return mix; + } + + applyAttachmentTimeline( + timeline: AttachmentTimeline, + skeleton: Skeleton, + time: number, + blend: MixBlend, + attachments: boolean + ) { + const slot = skeleton.slots[timeline.slotIndex]; + if (!slot.bone.active) return; + + const frames = timeline.frames; + if (time < frames[0]) { + // Time is before first frame. + if (blend == MixBlend.setup || blend == MixBlend.first) + this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments); + } else { + let frameIndex; + if (time >= frames[frames.length - 1]) + // Time is after last frame. + frameIndex = frames.length - 1; + else frameIndex = Animation.binarySearch(frames, time) - 1; + this.setAttachment(skeleton, slot, timeline.attachmentNames[frameIndex], attachments); + } + + // If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later. + if (slot.attachmentState <= this.unkeyedState) slot.attachmentState = this.unkeyedState + AnimationState.SETUP; + } + + setAttachment(skeleton: Skeleton, slot: Slot, attachmentName: string, attachments: boolean) { + slot.attachment = attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName); + if (attachments) slot.attachmentState = this.unkeyedState + AnimationState.CURRENT; + } + + applyRotateTimeline( + timeline: Timeline, + skeleton: Skeleton, + time: number, + alpha: number, + blend: MixBlend, + timelinesRotation: Array, + i: number, + firstFrame: boolean + ) { + if (firstFrame) timelinesRotation[i] = 0; + + if (alpha == 1) { + timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.mixIn); + return; + } + + const rotateTimeline = timeline as RotateTimeline; + const frames = rotateTimeline.frames; + const bone = skeleton.bones[rotateTimeline.boneIndex]; + if (!bone.active) return; + let r1 = 0, + r2 = 0; + if (time < frames[0]) { + switch (blend) { + case MixBlend.setup: + bone.rotation = bone.data.rotation; + default: + return; + case MixBlend.first: + r1 = bone.rotation; + r2 = bone.data.rotation; + } + } else { + r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation; + if (time >= frames[frames.length - RotateTimeline.ENTRIES]) + // Time is after last frame. + r2 = bone.data.rotation + frames[frames.length + RotateTimeline.PREV_ROTATION]; + else { + // Interpolate between the previous frame and the current frame. + const frame = Animation.binarySearch(frames, time, RotateTimeline.ENTRIES); + const prevRotation = frames[frame + RotateTimeline.PREV_ROTATION]; + const frameTime = frames[frame]; + const percent = rotateTimeline.getCurvePercent( + (frame >> 1) - 1, + 1 - (time - frameTime) / (frames[frame + RotateTimeline.PREV_TIME] - frameTime) + ); + + r2 = frames[frame + RotateTimeline.ROTATION] - prevRotation; + r2 -= (16384 - ((16384.499999999996 - r2 / 360) | 0)) * 360; + r2 = prevRotation + r2 * percent + bone.data.rotation; + r2 -= (16384 - ((16384.499999999996 - r2 / 360) | 0)) * 360; + } + } + + // Mix between rotations using the direction of the shortest route on the first frame while detecting crosses. + let total = 0, + diff = r2 - r1; + diff -= (16384 - ((16384.499999999996 - diff / 360) | 0)) * 360; + if (diff == 0) { + total = timelinesRotation[i]; + } else { + let lastTotal = 0, + lastDiff = 0; + if (firstFrame) { + lastTotal = 0; + lastDiff = diff; + } else { + lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops. + lastDiff = timelinesRotation[i + 1]; // Difference between bones. + } + let current = diff > 0, + dir = lastTotal >= 0; + // Detect cross at 0 (not 180). + if (MathUtils.signum(lastDiff) != MathUtils.signum(diff) && Math.abs(lastDiff) <= 90) { + // A cross after a 360 rotation is a loop. + if (Math.abs(lastTotal) > 180) lastTotal += 360 * MathUtils.signum(lastTotal); + dir = current; + } + total = diff + lastTotal - (lastTotal % 360); // Store loops as part of lastTotal. + if (dir != current) total += 360 * MathUtils.signum(lastTotal); + timelinesRotation[i] = total; + } + timelinesRotation[i + 1] = diff; + r1 += total * alpha; + bone.rotation = r1 - (16384 - ((16384.499999999996 - r1 / 360) | 0)) * 360; + } + + queueEvents(entry: TrackEntry, animationTime: number) { + const animationStart = entry.animationStart, + animationEnd = entry.animationEnd; + const duration = animationEnd - animationStart; + const trackLastWrapped = entry.trackLast % duration; + + // Queue events before complete. + const events = this.events; + let i = 0, + n = events.length; + for (; i < n; i++) { + const event = events[i]; + if (event.time < trackLastWrapped) break; + if (event.time > animationEnd) continue; // Discard events outside animation start/end. + this.queue.event(entry, event); + } + + // Queue complete if completed a loop iteration or the animation. + let complete = false; + if (entry.loop) complete = duration == 0 || trackLastWrapped > entry.trackTime % duration; + else complete = animationTime >= animationEnd && entry.animationLast < animationEnd; + if (complete) this.queue.complete(entry); + + // Queue events after complete. + for (; i < n; i++) { + const event = events[i]; + if (event.time < animationStart) continue; // Discard events outside animation start/end. + this.queue.event(entry, events[i]); + } + } + + /** Removes all animations from all tracks, leaving skeletons in their current pose. + * + * It may be desired to use {@link AnimationState#setEmptyAnimation()} to mix the skeletons back to the setup pose, + * rather than leaving them in their current pose. */ + clearTracks() { + const oldDrainDisabled = this.queue.drainDisabled; + this.queue.drainDisabled = true; + for (let i = 0, n = this.tracks.length; i < n; i++) this.clearTrack(i); + this.tracks.length = 0; + this.queue.drainDisabled = oldDrainDisabled; + this.queue.drain(); + } + + /** Removes all animations from the track, leaving skeletons in their current pose. + * + * It may be desired to use {@link AnimationState#setEmptyAnimation()} to mix the skeletons back to the setup pose, + * rather than leaving them in their current pose. */ + clearTrack(trackIndex: number) { + if (trackIndex >= this.tracks.length) return; + const current = this.tracks[trackIndex]; + if (current == null) return; + + this.queue.end(current); + + this.disposeNext(current); + + let entry = current; + while (true) { + const from = entry.mixingFrom; + if (from == null) break; + this.queue.end(from); + entry.mixingFrom = null; + entry.mixingTo = null; + entry = from; + } + + this.tracks[current.trackIndex] = null; + + this.queue.drain(); + } + + setCurrent(index: number, current: TrackEntry, interrupt: boolean) { + const from = this.expandToIndex(index); + this.tracks[index] = current; + + if (from != null) { + if (interrupt) this.queue.interrupt(from); + current.mixingFrom = from; + from.mixingTo = current; + current.mixTime = 0; + + // Store the interrupted mix percentage. + if (from.mixingFrom != null && from.mixDuration > 0) + current.interruptAlpha *= Math.min(1, from.mixTime / from.mixDuration); + + from.timelinesRotation.length = 0; // Reset rotation for mixing out, in case entry was mixed in. + } + + this.queue.start(current); + } + + /** Sets an animation by name. + * + * {@link #setAnimationWith(}. */ + setAnimation(trackIndex: number, animationName: string, loop: boolean) { + const animation = this.data.skeletonData.findAnimation(animationName); + if (animation == null) throw new Error("Animation not found: " + animationName); + return this.setAnimationWith(trackIndex, animation, loop); + } + + /** Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never + * applied to a skeleton, it is replaced (not mixed from). + * @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + * duration. In either case {@link TrackEntry#trackEnd} determines when the track is cleared. + * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept + * after the {@link AnimationStateListener#dispose()} event occurs. */ + setAnimationWith(trackIndex: number, animation: Animation, loop: boolean) { + if (animation == null) throw new Error("animation cannot be null."); + let interrupt = true; + let current = this.expandToIndex(trackIndex); + if (current != null) { + if (current.nextTrackLast == -1) { + // Don't mix from an entry that was never applied. + this.tracks[trackIndex] = current.mixingFrom; + this.queue.interrupt(current); + this.queue.end(current); + this.disposeNext(current); + current = current.mixingFrom; + interrupt = false; + } else this.disposeNext(current); + } + const entry = this.trackEntry(trackIndex, animation, loop, current); + this.setCurrent(trackIndex, entry, interrupt); + this.queue.drain(); + return entry; + } + + /** Queues an animation by name. + * + * See {@link #addAnimationWith()}. */ + addAnimation(trackIndex: number, animationName: string, loop: boolean, delay: number) { + const animation = this.data.skeletonData.findAnimation(animationName); + if (animation == null) throw new Error("Animation not found: " + animationName); + return this.addAnimationWith(trackIndex, animation, loop, delay); + } + + /** Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is + * equivalent to calling {@link #setAnimationWith()}. + * @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry + * minus any mix duration (from the {@link AnimationStateData}) plus the specified `delay` (ie the mix + * ends at (`delay` = 0) or before (`delay` < 0) the previous track entry duration). If the + * previous entry is looping, its next loop completion is used instead of its duration. + * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept + * after the {@link AnimationStateListener#dispose()} event occurs. */ + addAnimationWith(trackIndex: number, animation: Animation, loop: boolean, delay: number) { + if (animation == null) throw new Error("animation cannot be null."); + + let last = this.expandToIndex(trackIndex); + if (last != null) { + while (last.next != null) last = last.next; + } + + const entry = this.trackEntry(trackIndex, animation, loop, last); + + if (last == null) { + this.setCurrent(trackIndex, entry, true); + this.queue.drain(); + } else { + last.next = entry; + if (delay <= 0) { + const duration = last.animationEnd - last.animationStart; + if (duration != 0) { + if (last.loop) delay += duration * (1 + ((last.trackTime / duration) | 0)); + else delay += Math.max(duration, last.trackTime); + delay -= this.data.getMix(last.animation, animation); + } else delay = last.trackTime; + } + } + + entry.delay = delay; + return entry; + } + + /** Sets an empty animation for a track, discarding any queued animations, and sets the track entry's + * {@link TrackEntry#mixduration}. An empty animation has no timelines and serves as a placeholder for mixing in or out. + * + * Mixing out is done by setting an empty animation with a mix duration using either {@link #setEmptyAnimation()}, + * {@link #setEmptyAnimations()}, or {@link #addEmptyAnimation()}. Mixing to an empty animation causes + * the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation + * transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of + * 0 still mixes out over one frame. + * + * Mixing in is done by first setting an empty animation, then adding an animation using + * {@link #addAnimation()} and on the returned track entry, set the + * {@link TrackEntry#setMixDuration()}. Mixing from an empty animation causes the new animation to be applied more and + * more over the mix duration. Properties keyed in the new animation transition from the value from lower tracks or from the + * setup pose value if no lower tracks key the property to the value keyed in the new animation. */ + setEmptyAnimation(trackIndex: number, mixDuration: number) { + const entry = this.setAnimationWith(trackIndex, AnimationState.emptyAnimation, false); + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /** Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's + * {@link TrackEntry#mixDuration}. If the track is empty, it is equivalent to calling + * {@link #setEmptyAnimation()}. + * + * See {@link #setEmptyAnimation()}. + * @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry + * minus any mix duration plus the specified `delay` (ie the mix ends at (`delay` = 0) or + * before (`delay` < 0) the previous track entry duration). If the previous entry is looping, its next + * loop completion is used instead of its duration. + * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept + * after the {@link AnimationStateListener#dispose()} event occurs. */ + addEmptyAnimation(trackIndex: number, mixDuration: number, delay: number) { + if (delay <= 0) delay -= mixDuration; + const entry = this.addAnimationWith(trackIndex, AnimationState.emptyAnimation, false, delay); + entry.mixDuration = mixDuration; + entry.trackEnd = mixDuration; + return entry; + } + + /** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix + * duration. */ + setEmptyAnimations(mixDuration: number) { + const oldDrainDisabled = this.queue.drainDisabled; + this.queue.drainDisabled = true; + for (let i = 0, n = this.tracks.length; i < n; i++) { + const current = this.tracks[i]; + if (current != null) this.setEmptyAnimation(current.trackIndex, mixDuration); + } + this.queue.drainDisabled = oldDrainDisabled; + this.queue.drain(); + } + + expandToIndex(index: number) { + if (index < this.tracks.length) return this.tracks[index]; + Utils.ensureArrayCapacity(this.tracks, index + 1, null); + this.tracks.length = index + 1; + return null; + } + + /** @param last May be null. */ + trackEntry(trackIndex: number, animation: Animation, loop: boolean, last: TrackEntry) { + const entry = this.trackEntryPool.obtain(); + entry.trackIndex = trackIndex; + entry.animation = animation; + entry.loop = loop; + entry.holdPrevious = false; + + entry.eventThreshold = 0; + entry.attachmentThreshold = 0; + entry.drawOrderThreshold = 0; + + entry.animationStart = 0; + entry.animationEnd = animation.duration; + entry.animationLast = -1; + entry.nextAnimationLast = -1; + + entry.delay = 0; + entry.trackTime = 0; + entry.trackLast = -1; + entry.nextTrackLast = -1; + entry.trackEnd = Number.MAX_VALUE; + entry.timeScale = 1; + + entry.alpha = 1; + entry.interruptAlpha = 1; + entry.mixTime = 0; + entry.mixDuration = last == null ? 0 : this.data.getMix(last.animation, animation); + entry.mixBlend = MixBlend.replace; + return entry; + } + + disposeNext(entry: TrackEntry) { + let next = entry.next; + while (next != null) { + this.queue.dispose(next); + next = next.next; + } + entry.next = null; + } + + _animationsChanged() { + this.animationsChanged = false; + + this.propertyIDs.clear(); + + for (let i = 0, n = this.tracks.length; i < n; i++) { + let entry = this.tracks[i]; + if (entry == null) continue; + while (entry.mixingFrom != null) entry = entry.mixingFrom; + + do { + if (entry.mixingFrom == null || entry.mixBlend != MixBlend.add) this.computeHold(entry); + entry = entry.mixingTo; + } while (entry != null); + } + } + + computeHold(entry: TrackEntry) { + const to = entry.mixingTo; + const timelines = entry.animation.timelines; + const timelinesCount = entry.animation.timelines.length; + const timelineMode = Utils.setArraySize(entry.timelineMode, timelinesCount); + entry.timelineHoldMix.length = 0; + const timelineDipMix = Utils.setArraySize(entry.timelineHoldMix, timelinesCount); + const propertyIDs = this.propertyIDs; + + if (to != null && to.holdPrevious) { + for (let i = 0; i < timelinesCount; i++) { + timelineMode[i] = propertyIDs.add(timelines[i].getPropertyId()) + ? AnimationState.HOLD_FIRST + : AnimationState.HOLD_SUBSEQUENT; + } + return; + } + + outer: for (let i = 0; i < timelinesCount; i++) { + const timeline = timelines[i]; + const id = timeline.getPropertyId(); + if (!propertyIDs.add(id)) timelineMode[i] = AnimationState.SUBSEQUENT; + else if ( + to == null || + timeline instanceof AttachmentTimeline || + timeline instanceof DrawOrderTimeline || + timeline instanceof EventTimeline || + !to.animation.hasTimeline(id) + ) { + timelineMode[i] = AnimationState.FIRST; + } else { + for (let next = to.mixingTo; next != null; next = next.mixingTo) { + if (next.animation.hasTimeline(id)) continue; + if (entry.mixDuration > 0) { + timelineMode[i] = AnimationState.HOLD_MIX; + timelineDipMix[i] = next; + continue outer; + } + break; + } + timelineMode[i] = AnimationState.HOLD_FIRST; + } + } + } + + /** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */ + getCurrent(trackIndex: number) { + if (trackIndex >= this.tracks.length) return null; + return this.tracks[trackIndex]; + } + + /** Adds a listener to receive events for all track entries. */ + addListener(listener: AnimationStateListener) { + if (listener == null) throw new Error("listener cannot be null."); + this.listeners.push(listener); + } + + /** Removes the listener added with {@link #addListener()}. */ + removeListener(listener: AnimationStateListener) { + const index = this.listeners.indexOf(listener); + if (index >= 0) this.listeners.splice(index, 1); + } + + /** Removes all listeners added with {@link #addListener()}. */ + clearListeners() { + this.listeners.length = 0; + } + + /** Discards all listener notifications that have not yet been delivered. This can be useful to call from an + * {@link AnimationStateListener} when it is known that further notifications that may have been already queued for delivery + * are not wanted because new animations are being set. */ + clearListenerNotifications() { + this.queue.clear(); + } +} + +/** Stores settings and other state for the playback of an animation on an {@link AnimationState} track. + * + * References to a track entry must not be kept after the {@link AnimationStateListener#dispose()} event occurs. */ +export class TrackEntry { + /** The animation to apply for this track entry. */ + animation: Animation; + + /** The animation queued to start after this animation, or null. `next` makes up a linked list. */ + next: TrackEntry; + + /** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no + * mixing is currently occuring. When mixing from multiple animations, `mixingFrom` makes up a linked list. */ + mixingFrom: TrackEntry; + + /** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is + * currently occuring. When mixing to multiple animations, `mixingTo` makes up a linked list. */ + mixingTo: TrackEntry; + + /** The listener for events generated by this track entry, or null. + * + * A track entry returned from {@link AnimationState#setAnimation()} is already the current animation + * for the track, so the track entry listener {@link AnimationStateListener#start()} will not be called. */ + listener: AnimationStateListener; + + /** The index of the track where this track entry is either current or queued. + * + * See {@link AnimationState#getCurrent()}. */ + trackIndex: number; + + /** If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + * duration. */ + loop: boolean; + + /** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead + * of being mixed out. + * + * When mixing between animations that key the same property, if a lower track also keys that property then the value will + * briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0% + * while the second animation mixes from 0% to 100%. Setting `holdPrevious` to true applies the first animation + * at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which + * keys the property, only when a higher track also keys the property. + * + * Snapping will occur if `holdPrevious` is true and this animation does not key all the same properties as the + * previous animation. */ + holdPrevious: boolean; + + /** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) is less than the + * `eventThreshold`, event timelines are applied while this animation is being mixed out. Defaults to 0, so event + * timelines are not applied while this animation is being mixed out. */ + eventThreshold: number; + + /** When the mix percentage ({@link #mixtime} / {@link #mixDuration}) is less than the + * `attachmentThreshold`, attachment timelines are applied while this animation is being mixed out. Defaults to + * 0, so attachment timelines are not applied while this animation is being mixed out. */ + attachmentThreshold: number; + + /** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) is less than the + * `drawOrderThreshold`, draw order timelines are applied while this animation is being mixed out. Defaults to 0, + * so draw order timelines are not applied while this animation is being mixed out. */ + drawOrderThreshold: number; + + /** Seconds when this animation starts, both initially and after looping. Defaults to 0. + * + * When changing the `animationStart` time, it often makes sense to set {@link #animationLast} to the same + * value to prevent timeline keys before the start time from triggering. */ + animationStart: number; + + /** Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will + * loop back to {@link #animationStart} at this time. Defaults to the animation {@link Animation#duration}. */ + animationEnd: number; + + /** The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this + * animation is applied, event timelines will fire all events between the `animationLast` time (exclusive) and + * `animationTime` (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation + * is applied. */ + animationLast: number; + + nextAnimationLast: number; + + /** Seconds to postpone playing the animation. When this track entry is the current track entry, `delay` + * postpones incrementing the {@link #trackTime}. When this track entry is queued, `delay` is the time from + * the start of the previous animation to when this track entry will become the current track entry (ie when the previous + * track entry {@link TrackEntry#trackTime} >= this track entry's `delay`). + * + * {@link #timeScale} affects the delay. */ + delay: number; + + /** Current time in seconds this track entry has been the current track entry. The track time determines + * {@link #animationTime}. The track time can be set to start the animation at a time other than 0, without affecting + * looping. */ + trackTime: number; + + trackLast: number; + nextTrackLast: number; + + /** The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float + * value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time + * is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the + * properties keyed by the animation are set to the setup pose and the track is cleared. + * + * It may be desired to use {@link AnimationState#addEmptyAnimation()} rather than have the animation + * abruptly cease being applied. */ + trackEnd: number; + + /** Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or + * faster. Defaults to 1. + * + * {@link #mixTime} is not affected by track entry time scale, so {@link #mixDuration} may need to be adjusted to + * match the animation speed. + * + * When using {@link AnimationState#addAnimation()} with a `delay` <= 0, note the + * {@link #delay} is set using the mix duration from the {@link AnimationStateData}, assuming time scale to be 1. If + * the time scale is not 1, the delay may need to be adjusted. + * + * See AnimationState {@link AnimationState#timeScale} for affecting all animations. */ + timeScale: number; + + /** Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults + * to 1, which overwrites the skeleton's current pose with this animation. + * + * Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to + * use alpha on track 0 if the skeleton pose is from the last frame render. */ + alpha: number; + + /** Seconds from 0 to the {@link #getMixDuration()} when mixing from the previous animation to this animation. May be + * slightly more than `mixDuration` when the mix is complete. */ + mixTime: number; + + /** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData + * {@link AnimationStateData#getMix()} based on the animation before this animation (if any). + * + * A mix duration of 0 still mixes out over one frame to provide the track entry being mixed out a chance to revert the + * properties it was animating. + * + * The `mixDuration` can be set manually rather than use the value from + * {@link AnimationStateData#getMix()}. In that case, the `mixDuration` can be set for a new + * track entry only before {@link AnimationState#update(float)} is first called. + * + * When using {@link AnimationState#addAnimation()} with a `delay` <= 0, note the + * {@link #delay} is set using the mix duration from the {@link AnimationStateData}, not a mix duration set + * afterward. */ + mixDuration: number; + interruptAlpha: number; + totalAlpha: number; + + /** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}, which + * replaces the values from the lower tracks with the animation values. {@link MixBlend#add} adds the animation values to + * the values from the lower tracks. + * + * The `mixBlend` can be set for a new track entry only before {@link AnimationState#apply()} is first + * called. */ + mixBlend = MixBlend.replace; + timelineMode = new Array(); + timelineHoldMix = new Array(); + timelinesRotation = new Array(); + + reset() { + this.next = null; + this.mixingFrom = null; + this.mixingTo = null; + this.animation = null; + this.listener = null; + this.timelineMode.length = 0; + this.timelineHoldMix.length = 0; + this.timelinesRotation.length = 0; + } + + /** Uses {@link #trackTime} to compute the `animationTime`, which is between {@link #animationStart} + * and {@link #animationEnd}. When the `trackTime` is 0, the `animationTime` is equal to the + * `animationStart` time. */ + getAnimationTime() { + if (this.loop) { + const duration = this.animationEnd - this.animationStart; + if (duration == 0) return this.animationStart; + return (this.trackTime % duration) + this.animationStart; + } + return Math.min(this.trackTime + this.animationStart, this.animationEnd); + } + + setAnimationLast(animationLast: number) { + this.animationLast = animationLast; + this.nextAnimationLast = animationLast; + } + + /** Returns true if at least one loop has been completed. + * + * See {@link AnimationStateListener#complete()}. */ + isComplete() { + return this.trackTime >= this.animationEnd - this.animationStart; + } + + /** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the + * long way around when using {@link #alpha} and starting animations on other tracks. + * + * Mixing with {@link MixBlend#replace} involves finding a rotation between two others, which has two possible solutions: + * the short way or the long way around. The two rotations likely change over time, so which direction is the short or long + * way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the + * long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. */ + resetRotationDirections() { + this.timelinesRotation.length = 0; + } +} + +export class EventQueue { + objects: Array = []; + drainDisabled = false; + animState: AnimationState; + + constructor(animState: AnimationState) { + this.animState = animState; + } + + start(entry: TrackEntry) { + this.objects.push(EventType.start); + this.objects.push(entry); + this.animState.animationsChanged = true; + } + + interrupt(entry: TrackEntry) { + this.objects.push(EventType.interrupt); + this.objects.push(entry); + } + + end(entry: TrackEntry) { + this.objects.push(EventType.end); + this.objects.push(entry); + this.animState.animationsChanged = true; + } + + dispose(entry: TrackEntry) { + this.objects.push(EventType.dispose); + this.objects.push(entry); + } + + complete(entry: TrackEntry) { + this.objects.push(EventType.complete); + this.objects.push(entry); + } + + event(entry: TrackEntry, event: Event) { + this.objects.push(EventType.event); + this.objects.push(entry); + this.objects.push(event); + } + + drain() { + if (this.drainDisabled) return; + this.drainDisabled = true; + + const objects = this.objects; + const listeners = this.animState.listeners; + + for (let i = 0; i < objects.length; i += 2) { + const type = objects[i] as EventType; + const entry = objects[i + 1] as TrackEntry; + switch (type) { + case EventType.start: + if (entry.listener != null && entry.listener.start) entry.listener.start(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].start) listeners[ii].start(entry); + break; + case EventType.interrupt: + if (entry.listener != null && entry.listener.interrupt) entry.listener.interrupt(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].interrupt) listeners[ii].interrupt(entry); + break; + case EventType.end: + if (entry.listener != null && entry.listener.end) entry.listener.end(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].end) listeners[ii].end(entry); + // Fall through. + case EventType.dispose: + if (entry.listener != null && entry.listener.dispose) entry.listener.dispose(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].dispose) listeners[ii].dispose(entry); + this.animState.trackEntryPool.free(entry); + break; + case EventType.complete: + if (entry.listener != null && entry.listener.complete) entry.listener.complete(entry); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].complete) listeners[ii].complete(entry); + break; + case EventType.event: + const event = objects[i++ + 2] as Event; + if (entry.listener != null && entry.listener.event) entry.listener.event(entry, event); + for (let ii = 0; ii < listeners.length; ii++) if (listeners[ii].event) listeners[ii].event(entry, event); + break; + } + } + this.clear(); + + this.drainDisabled = false; + } + + clear() { + this.objects.length = 0; + } +} + +export enum EventType { + start, + interrupt, + end, + dispose, + complete, + event +} + +/** The interface to implement for receiving TrackEntry events. It is always safe to call AnimationState methods when receiving + * events. + * + * See TrackEntry {@link TrackEntry#listener} and AnimationState + * {@link AnimationState#addListener()}. */ +export interface AnimationStateListener { + /** Invoked when this entry has been set as the current entry. */ + start(entry: TrackEntry): void; + + /** Invoked when another entry has replaced this entry as the current entry. This entry may continue being applied for + * mixing. */ + interrupt(entry: TrackEntry): void; + + /** Invoked when this entry is no longer the current entry and will never be applied again. */ + end(entry: TrackEntry): void; + + /** Invoked when this entry will be disposed. This may occur without the entry ever being set as the current entry. + * References to the entry should not be kept after dispose is called, as it may be destroyed or reused. */ + dispose(entry: TrackEntry): void; + + /** Invoked every time this entry's animation completes a loop. */ + complete(entry: TrackEntry): void; + + /** Invoked when this entry's animation triggers an event. */ + event(entry: TrackEntry, event: Event): void; +} + +export abstract class AnimationStateAdapter implements AnimationStateListener { + start(entry: TrackEntry) {} + + interrupt(entry: TrackEntry) {} + + end(entry: TrackEntry) {} + + dispose(entry: TrackEntry) {} + + complete(entry: TrackEntry) {} + + event(entry: TrackEntry, event: Event) {} +} diff --git a/packages/spine-core-3.8/src/spine-core/AnimationStateData.ts b/packages/spine-core-3.8/src/spine-core/AnimationStateData.ts new file mode 100644 index 0000000000..009fcfad95 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/AnimationStateData.ts @@ -0,0 +1,48 @@ +import { SkeletonData } from "./SkeletonData"; +import { Animation } from "./Animation"; +import { Map } from "./Utils"; + +/** Stores mix (crossfade) durations to be applied when {@link AnimationState} animations are changed. */ +export class AnimationStateData { + /** The SkeletonData to look up animations when they are specified by name. */ + skeletonData: SkeletonData; + + animationToMixTime: Map = {}; + + /** The mix duration to use when no mix duration has been defined between two animations. */ + defaultMix = 0; + + constructor(skeletonData: SkeletonData) { + if (skeletonData == null) throw new Error("skeletonData cannot be null."); + this.skeletonData = skeletonData; + } + + /** Sets a mix duration by animation name. + * + * See {@link #setMixWith()}. */ + setMix(fromName: string, toName: string, duration: number) { + const from = this.skeletonData.findAnimation(fromName); + if (from == null) throw new Error("Animation not found: " + fromName); + const to = this.skeletonData.findAnimation(toName); + if (to == null) throw new Error("Animation not found: " + toName); + this.setMixWith(from, to, duration); + } + + /** Sets the mix duration when changing from the specified animation to the other. + * + * See {@link TrackEntry#mixDuration}. */ + setMixWith(from: Animation, to: Animation, duration: number) { + if (from == null) throw new Error("from cannot be null."); + if (to == null) throw new Error("to cannot be null."); + const key = from.name + "." + to.name; + this.animationToMixTime[key] = duration; + } + + /** Returns the mix duration to use when changing from the specified animation to the other, or the {@link #defaultMix} if + * no mix duration has been set. */ + getMix(from: Animation, to: Animation) { + const key = from.name + "." + to.name; + const value = this.animationToMixTime[key]; + return value === undefined ? this.defaultMix : value; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/AtlasAttachmentLoader.ts b/packages/spine-core-3.8/src/spine-core/AtlasAttachmentLoader.ts new file mode 100644 index 0000000000..eca31cbf2e --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/AtlasAttachmentLoader.ts @@ -0,0 +1,55 @@ +import { AttachmentLoader } from "./attachments/AttachmentLoader"; +import { TextureAtlas } from "./TextureAtlas"; +import { Skin } from "./Skin"; +import { RegionAttachment } from "./attachments/RegionAttachment"; +import { MeshAttachment } from "./attachments/MeshAttachment"; +import { BoundingBoxAttachment } from "./attachments/BoundingBoxAttachment"; +import { PathAttachment } from "./attachments/PathAttachment"; +import { PointAttachment } from "./attachments/PointAttachment"; +import { ClippingAttachment } from "./attachments/ClippingAttachment"; + +/** An {@link AttachmentLoader} that configures attachments using texture regions from an {@link TextureAtlas}. + * + * See [Loading skeleton data](http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data) in the + * Spine Runtimes Guide. */ +export class AtlasAttachmentLoader implements AttachmentLoader { + atlas: TextureAtlas; + + constructor(atlas: TextureAtlas) { + this.atlas = atlas; + } + + newRegionAttachment(skin: Skin, name: string, path: string): RegionAttachment { + const region = this.atlas.findRegion(path); + if (region == null) throw new Error("Region not found in atlas: " + path + " (region attachment: " + name + ")"); + region.renderObject = region; + const attachment = new RegionAttachment(name); + attachment.setRegion(region); + return attachment; + } + + newMeshAttachment(skin: Skin, name: string, path: string): MeshAttachment { + const region = this.atlas.findRegion(path); + if (region == null) throw new Error("Region not found in atlas: " + path + " (mesh attachment: " + name + ")"); + region.renderObject = region; + const attachment = new MeshAttachment(name); + attachment.region = region; + return attachment; + } + + newBoundingBoxAttachment(skin: Skin, name: string): BoundingBoxAttachment { + return new BoundingBoxAttachment(name); + } + + newPathAttachment(skin: Skin, name: string): PathAttachment { + return new PathAttachment(name); + } + + newPointAttachment(skin: Skin, name: string): PointAttachment { + return new PointAttachment(name); + } + + newClippingAttachment(skin: Skin, name: string): ClippingAttachment { + return new ClippingAttachment(name); + } +} diff --git a/packages/spine-core-3.8/src/spine-core/BlendMode.ts b/packages/spine-core-3.8/src/spine-core/BlendMode.ts new file mode 100644 index 0000000000..2f004954b4 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/BlendMode.ts @@ -0,0 +1,7 @@ +/** Determines how images are blended with existing pixels when drawn. */ +export enum BlendMode { + Normal, + Additive, + Multiply, + Screen +} diff --git a/packages/spine-core-3.8/src/spine-core/Bone.ts b/packages/spine-core-3.8/src/spine-core/Bone.ts new file mode 100644 index 0000000000..1d4c9b7ee3 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Bone.ts @@ -0,0 +1,391 @@ +import { Updatable } from "./Updatable"; +import { BoneData, TransformMode } from "./BoneData"; +import { Skeleton } from "./Skeleton"; +import { MathUtils, Vector2 } from "./Utils"; + +/** Stores a bone's current pose. + * + * A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a + * local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a + * constraint or application code modifies the world transform after it was computed from the local transform. */ +export class Bone implements Updatable { + /** The bone's setup pose data. */ + data: BoneData; + + /** The skeleton this bone belongs to. */ + skeleton: Skeleton; + + /** The parent bone, or null if this is the root bone. */ + parent: Bone; + + /** The immediate children of this bone. */ + children = new Array(); + + /** The local x translation. */ + x = 0; + + /** The local y translation. */ + y = 0; + + /** The local rotation in degrees, counter clockwise. */ + rotation = 0; + + /** The local scaleX. */ + scaleX = 0; + + /** The local scaleY. */ + scaleY = 0; + + /** The local shearX. */ + shearX = 0; + + /** The local shearY. */ + shearY = 0; + + /** The applied local x translation. */ + ax = 0; + + /** The applied local y translation. */ + ay = 0; + + /** The applied local rotation in degrees, counter clockwise. */ + arotation = 0; + + /** The applied local scaleX. */ + ascaleX = 0; + + /** The applied local scaleY. */ + ascaleY = 0; + + /** The applied local shearX. */ + ashearX = 0; + + /** The applied local shearY. */ + ashearY = 0; + + /** If true, the applied transform matches the world transform. If false, the world transform has been modified since it was + * computed and {@link #updateAppliedTransform()} must be called before accessing the applied transform. */ + appliedValid = false; + + /** Part of the world transform matrix for the X axis. If changed, {@link #appliedValid} should be set to false. */ + a = 0; + + /** Part of the world transform matrix for the Y axis. If changed, {@link #appliedValid} should be set to false. */ + b = 0; + + /** Part of the world transform matrix for the X axis. If changed, {@link #appliedValid} should be set to false. */ + c = 0; + + /** Part of the world transform matrix for the Y axis. If changed, {@link #appliedValid} should be set to false. */ + d = 0; + + /** The world X position. If changed, {@link #appliedValid} should be set to false. */ + worldY = 0; + + /** The world Y position. If changed, {@link #appliedValid} should be set to false. */ + worldX = 0; + + sorted = false; + active = false; + + /** @param parent May be null. */ + constructor(data: BoneData, skeleton: Skeleton, parent: Bone) { + if (data == null) throw new Error("data cannot be null."); + if (skeleton == null) throw new Error("skeleton cannot be null."); + this.data = data; + this.skeleton = skeleton; + this.parent = parent; + this.setToSetupPose(); + } + + /** Returns false when the bone has not been computed because {@link BoneData#skinRequired} is true and the + * {@link Skeleton#skin active skin} does not {@link Skin#bones contain} this bone. */ + isActive() { + return this.active; + } + + /** Same as {@link #updateWorldTransform()}. This method exists for Bone to implement {@link Updatable}. */ + update() { + this.updateWorldTransformWith(this.x, this.y, this.rotation, this.scaleX, this.scaleY, this.shearX, this.shearY); + } + + /** Computes the world transform using the parent bone and this bone's local transform. + * + * See {@link #updateWorldTransformWith()}. */ + updateWorldTransform() { + this.updateWorldTransformWith(this.x, this.y, this.rotation, this.scaleX, this.scaleY, this.shearX, this.shearY); + } + + /** Computes the world transform using the parent bone and the specified local transform. Child bones are not updated. + * + * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine + * Runtimes Guide. */ + updateWorldTransformWith( + x: number, + y: number, + rotation: number, + scaleX: number, + scaleY: number, + shearX: number, + shearY: number + ) { + this.ax = x; + this.ay = y; + this.arotation = rotation; + this.ascaleX = scaleX; + this.ascaleY = scaleY; + this.ashearX = shearX; + this.ashearY = shearY; + this.appliedValid = true; + + const parent = this.parent; + if (parent == null) { + // Root bone. + const skeleton = this.skeleton; + const rotationY = rotation + 90 + shearY; + const sx = skeleton.scaleX; + const sy = skeleton.scaleY; + this.a = MathUtils.cosDeg(rotation + shearX) * scaleX * sx; + this.b = MathUtils.cosDeg(rotationY) * scaleY * sx; + this.c = MathUtils.sinDeg(rotation + shearX) * scaleX * sy; + this.d = MathUtils.sinDeg(rotationY) * scaleY * sy; + this.worldX = x * sx + skeleton.x; + this.worldY = y * sy + skeleton.y; + return; + } + + let pa = parent.a, + pb = parent.b, + pc = parent.c, + pd = parent.d; + this.worldX = pa * x + pb * y + parent.worldX; + this.worldY = pc * x + pd * y + parent.worldY; + + switch (this.data.transformMode) { + case TransformMode.Normal: { + const rotationY = rotation + 90 + shearY; + const la = MathUtils.cosDeg(rotation + shearX) * scaleX; + const lb = MathUtils.cosDeg(rotationY) * scaleY; + const lc = MathUtils.sinDeg(rotation + shearX) * scaleX; + const ld = MathUtils.sinDeg(rotationY) * scaleY; + this.a = pa * la + pb * lc; + this.b = pa * lb + pb * ld; + this.c = pc * la + pd * lc; + this.d = pc * lb + pd * ld; + return; + } + case TransformMode.OnlyTranslation: { + const rotationY = rotation + 90 + shearY; + this.a = MathUtils.cosDeg(rotation + shearX) * scaleX; + this.b = MathUtils.cosDeg(rotationY) * scaleY; + this.c = MathUtils.sinDeg(rotation + shearX) * scaleX; + this.d = MathUtils.sinDeg(rotationY) * scaleY; + break; + } + case TransformMode.NoRotationOrReflection: { + let s = pa * pa + pc * pc; + let prx = 0; + if (s > 0.0001) { + s = Math.abs(pa * pd - pb * pc) / s; + pa /= this.skeleton.scaleX; + pc /= this.skeleton.scaleY; + pb = pc * s; + pd = pa * s; + prx = Math.atan2(pc, pa) * MathUtils.radDeg; + } else { + pa = 0; + pc = 0; + prx = 90 - Math.atan2(pd, pb) * MathUtils.radDeg; + } + const rx = rotation + shearX - prx; + const ry = rotation + shearY - prx + 90; + const la = MathUtils.cosDeg(rx) * scaleX; + const lb = MathUtils.cosDeg(ry) * scaleY; + const lc = MathUtils.sinDeg(rx) * scaleX; + const ld = MathUtils.sinDeg(ry) * scaleY; + this.a = pa * la - pb * lc; + this.b = pa * lb - pb * ld; + this.c = pc * la + pd * lc; + this.d = pc * lb + pd * ld; + break; + } + case TransformMode.NoScale: + case TransformMode.NoScaleOrReflection: { + const cos = MathUtils.cosDeg(rotation); + const sin = MathUtils.sinDeg(rotation); + let za = (pa * cos + pb * sin) / this.skeleton.scaleX; + let zc = (pc * cos + pd * sin) / this.skeleton.scaleY; + let s = Math.sqrt(za * za + zc * zc); + if (s > 0.00001) s = 1 / s; + za *= s; + zc *= s; + s = Math.sqrt(za * za + zc * zc); + if ( + this.data.transformMode == TransformMode.NoScale && + pa * pd - pb * pc < 0 != (this.skeleton.scaleX < 0 != this.skeleton.scaleY < 0) + ) + s = -s; + const r = Math.PI / 2 + Math.atan2(zc, za); + const zb = Math.cos(r) * s; + const zd = Math.sin(r) * s; + const la = MathUtils.cosDeg(shearX) * scaleX; + const lb = MathUtils.cosDeg(90 + shearY) * scaleY; + const lc = MathUtils.sinDeg(shearX) * scaleX; + const ld = MathUtils.sinDeg(90 + shearY) * scaleY; + this.a = za * la + zb * lc; + this.b = za * lb + zb * ld; + this.c = zc * la + zd * lc; + this.d = zc * lb + zd * ld; + break; + } + } + this.a *= this.skeleton.scaleX; + this.b *= this.skeleton.scaleX; + this.c *= this.skeleton.scaleY; + this.d *= this.skeleton.scaleY; + } + + /** Sets this bone's local transform to the setup pose. */ + setToSetupPose() { + const data = this.data; + this.x = data.x; + this.y = data.y; + this.rotation = data.rotation; + this.scaleX = data.scaleX; + this.scaleY = data.scaleY; + this.shearX = data.shearX; + this.shearY = data.shearY; + } + + /** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */ + getWorldRotationX() { + return Math.atan2(this.c, this.a) * MathUtils.radDeg; + } + + /** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */ + getWorldRotationY() { + return Math.atan2(this.d, this.b) * MathUtils.radDeg; + } + + /** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */ + getWorldScaleX() { + return Math.sqrt(this.a * this.a + this.c * this.c); + } + + /** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */ + getWorldScaleY() { + return Math.sqrt(this.b * this.b + this.d * this.d); + } + + /** Computes the applied transform values from the world transform. This allows the applied transform to be accessed after the + * world transform has been modified (by a constraint, {@link #rotateWorld()}, etc). + * + * If {@link #updateWorldTransform()} has been called for a bone and {@link #appliedValid} is false, then + * {@link #updateAppliedTransform()} must be called before accessing the applied transform. + * + * Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after + * calling this method is equivalent to the local tranform used to compute the world transform, but may not be identical. */ + updateAppliedTransform() { + this.appliedValid = true; + const parent = this.parent; + if (parent == null) { + this.ax = this.worldX; + this.ay = this.worldY; + this.arotation = Math.atan2(this.c, this.a) * MathUtils.radDeg; + this.ascaleX = Math.sqrt(this.a * this.a + this.c * this.c); + this.ascaleY = Math.sqrt(this.b * this.b + this.d * this.d); + this.ashearX = 0; + this.ashearY = + Math.atan2(this.a * this.b + this.c * this.d, this.a * this.d - this.b * this.c) * MathUtils.radDeg; + return; + } + const pa = parent.a, + pb = parent.b, + pc = parent.c, + pd = parent.d; + const pid = 1 / (pa * pd - pb * pc); + const dx = this.worldX - parent.worldX, + dy = this.worldY - parent.worldY; + this.ax = dx * pd * pid - dy * pb * pid; + this.ay = dy * pa * pid - dx * pc * pid; + const ia = pid * pd; + const id = pid * pa; + const ib = pid * pb; + const ic = pid * pc; + const ra = ia * this.a - ib * this.c; + const rb = ia * this.b - ib * this.d; + const rc = id * this.c - ic * this.a; + const rd = id * this.d - ic * this.b; + this.ashearX = 0; + this.ascaleX = Math.sqrt(ra * ra + rc * rc); + if (this.ascaleX > 0.0001) { + const det = ra * rd - rb * rc; + this.ascaleY = det / this.ascaleX; + this.ashearY = Math.atan2(ra * rb + rc * rd, det) * MathUtils.radDeg; + this.arotation = Math.atan2(rc, ra) * MathUtils.radDeg; + } else { + this.ascaleX = 0; + this.ascaleY = Math.sqrt(rb * rb + rd * rd); + this.ashearY = 0; + this.arotation = 90 - Math.atan2(rd, rb) * MathUtils.radDeg; + } + } + + /** Transforms a point from world coordinates to the bone's local coordinates. */ + worldToLocal(world: Vector2) { + const a = this.a, + b = this.b, + c = this.c, + d = this.d; + const invDet = 1 / (a * d - b * c); + const x = world.x - this.worldX, + y = world.y - this.worldY; + world.x = x * d * invDet - y * b * invDet; + world.y = y * a * invDet - x * c * invDet; + return world; + } + + /** Transforms a point from the bone's local coordinates to world coordinates. */ + localToWorld(local: Vector2) { + const x = local.x, + y = local.y; + local.x = x * this.a + y * this.b + this.worldX; + local.y = x * this.c + y * this.d + this.worldY; + return local; + } + + /** Transforms a world rotation to a local rotation. */ + worldToLocalRotation(worldRotation: number) { + const sin = MathUtils.sinDeg(worldRotation), + cos = MathUtils.cosDeg(worldRotation); + return ( + Math.atan2(this.a * sin - this.c * cos, this.d * cos - this.b * sin) * MathUtils.radDeg + + this.rotation - + this.shearX + ); + } + + /** Transforms a local rotation to a world rotation. */ + localToWorldRotation(localRotation: number) { + localRotation -= this.rotation - this.shearX; + const sin = MathUtils.sinDeg(localRotation), + cos = MathUtils.cosDeg(localRotation); + return Math.atan2(cos * this.c + sin * this.d, cos * this.a + sin * this.b) * MathUtils.radDeg; + } + + /** Rotates the world transform the specified amount and sets {@link #appliedValid} to false. + * {@link #updateWorldTransform()} will need to be called on any child bones, recursively, and any constraints reapplied. */ + rotateWorld(degrees: number) { + const a = this.a, + b = this.b, + c = this.c, + d = this.d; + const cos = MathUtils.cosDeg(degrees), + sin = MathUtils.sinDeg(degrees); + this.a = cos * a - sin * c; + this.b = cos * b - sin * d; + this.c = sin * a + cos * c; + this.d = sin * b + cos * d; + this.appliedValid = false; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/BoneData.ts b/packages/spine-core-3.8/src/spine-core/BoneData.ts new file mode 100644 index 0000000000..a51d1b16cf --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/BoneData.ts @@ -0,0 +1,66 @@ +import { Color } from "./Utils"; + +/** Stores the setup pose for a {@link Bone}. */ +export class BoneData { + /** The index of the bone in {@link Skeleton#getBones()}. */ + index: number; + + /** The name of the bone, which is unique across all bones in the skeleton. */ + name: string; + + /** @returns May be null. */ + parent: BoneData; + + /** The bone's length. */ + length: number; + + /** The local x translation. */ + x = 0; + + /** The local y translation. */ + y = 0; + + /** The local rotation. */ + rotation = 0; + + /** The local scaleX. */ + scaleX = 1; + + /** The local scaleY. */ + scaleY = 1; + + /** The local shearX. */ + shearX = 0; + + /** The local shearX. */ + shearY = 0; + + /** The transform mode for how parent world transforms affect this bone. */ + transformMode = TransformMode.Normal; + + /** When true, {@link Skeleton#updateWorldTransform()} only updates this bone if the {@link Skeleton#skin} contains this + * bone. + * @see Skin#bones */ + skinRequired = false; + + /** The color of the bone as it was in Spine. Available only when nonessential data was exported. Bones are not usually + * rendered at runtime. */ + color = new Color(); + + constructor(index: number, name: string, parent: BoneData) { + if (index < 0) throw new Error("index must be >= 0."); + if (name == null) throw new Error("name cannot be null."); + this.index = index; + this.name = name; + this.parent = parent; + } +} + +/** Determines how a bone inherits world transforms from parent bones. */ +export enum TransformMode { + Normal, + OnlyTranslation, + NoRotationOrReflection, + NoScale, + NoScaleOrReflection +} diff --git a/packages/spine-core-3.8/src/spine-core/ConstraintData.ts b/packages/spine-core-3.8/src/spine-core/ConstraintData.ts new file mode 100644 index 0000000000..5d928d2169 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/ConstraintData.ts @@ -0,0 +1,8 @@ +/** The base class for all constraint datas. */ +export abstract class ConstraintData { + constructor( + public name: string, + public order: number, + public skinRequired: boolean + ) {} +} diff --git a/packages/spine-core-3.8/src/spine-core/Event.ts b/packages/spine-core-3.8/src/spine-core/Event.ts new file mode 100644 index 0000000000..a6318c90a9 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Event.ts @@ -0,0 +1,22 @@ +import { EventData } from "./EventData"; + +/** Stores the current pose values for an {@link Event}. + * + * See Timeline {@link Timeline#apply()}, + * AnimationStateListener {@link AnimationStateListener#event()}, and + * [Events](http://esotericsoftware.com/spine-events) in the Spine User Guide. */ +export class Event { + data: EventData; + intValue: number; + floatValue: number; + stringValue: string; + time: number; + volume: number; + balance: number; + + constructor(time: number, data: EventData) { + if (data == null) throw new Error("data cannot be null."); + this.time = time; + this.data = data; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/EventData.ts b/packages/spine-core-3.8/src/spine-core/EventData.ts new file mode 100644 index 0000000000..46aa71702e --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/EventData.ts @@ -0,0 +1,16 @@ +/** Stores the setup pose values for an {@link Event}. + * + * See [Events](http://esotericsoftware.com/spine-events) in the Spine User Guide. */ +export class EventData { + name: string; + intValue: number; + floatValue: number; + stringValue: string; + audioPath: string; + volume: number; + balance: number; + + constructor(name: string) { + this.name = name; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/IkConstraint.ts b/packages/spine-core-3.8/src/spine-core/IkConstraint.ts new file mode 100644 index 0000000000..7f05ce8e04 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/IkConstraint.ts @@ -0,0 +1,346 @@ +import { Updatable } from "./Updatable"; +import { IkConstraintData } from "./IkConstraintData"; +import { Bone } from "./Bone"; +import { Skeleton } from "./Skeleton"; +import { TransformMode } from "./BoneData"; +import { MathUtils } from "./Utils"; + +/** Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of + * the last bone is as close to the target bone as possible. + * + * See [IK constraints](http://esotericsoftware.com/spine-ik-constraints) in the Spine User Guide. */ +export class IkConstraint implements Updatable { + /** The IK constraint's setup pose data. */ + data: IkConstraintData; + + /** The bones that will be modified by this IK constraint. */ + bones: Array; + + /** The bone that is the IK target. */ + target: Bone; + + /** Controls the bend direction of the IK bones, either 1 or -1. */ + bendDirection = 0; + + /** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */ + compress = false; + + /** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained + * and the parent bone has local nonuniform scale, stretch is not applied. */ + stretch = false; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + mix = 1; + + /** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */ + softness = 0; + active = false; + + constructor(data: IkConstraintData, skeleton: Skeleton) { + if (data == null) throw new Error("data cannot be null."); + if (skeleton == null) throw new Error("skeleton cannot be null."); + this.data = data; + this.mix = data.mix; + this.softness = data.softness; + this.bendDirection = data.bendDirection; + this.compress = data.compress; + this.stretch = data.stretch; + + this.bones = new Array(); + for (let i = 0; i < data.bones.length; i++) this.bones.push(skeleton.findBone(data.bones[i].name)); + this.target = skeleton.findBone(data.target.name); + } + + isActive() { + return this.active; + } + + /** Applies the constraint to the constrained bones. */ + apply() { + this.update(); + } + + update() { + const target = this.target; + const bones = this.bones; + switch (bones.length) { + case 1: + this.apply1(bones[0], target.worldX, target.worldY, this.compress, this.stretch, this.data.uniform, this.mix); + break; + case 2: + this.apply2( + bones[0], + bones[1], + target.worldX, + target.worldY, + this.bendDirection, + this.stretch, + this.softness, + this.mix + ); + break; + } + } + + /** Applies 1 bone IK. The target is specified in the world coordinate system. */ + apply1( + bone: Bone, + targetX: number, + targetY: number, + compress: boolean, + stretch: boolean, + uniform: boolean, + alpha: number + ) { + if (!bone.appliedValid) bone.updateAppliedTransform(); + const p = bone.parent; + + let pa = p.a, + pb = p.b, + pc = p.c, + pd = p.d; + let rotationIK = -bone.ashearX - bone.arotation, + tx = 0, + ty = 0; + + switch (bone.data.transformMode) { + case TransformMode.OnlyTranslation: + tx = targetX - bone.worldX; + ty = targetY - bone.worldY; + break; + case TransformMode.NoRotationOrReflection: + const s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc); + const sa = pa / bone.skeleton.scaleX; + const sc = pc / bone.skeleton.scaleY; + pb = -sc * s * bone.skeleton.scaleX; + pd = sa * s * bone.skeleton.scaleY; + rotationIK += Math.atan2(sc, sa) * MathUtils.radDeg; + // Fall through + default: + const x = targetX - p.worldX, + y = targetY - p.worldY; + const d = pa * pd - pb * pc; + tx = (x * pd - y * pb) / d - bone.ax; + ty = (y * pa - x * pc) / d - bone.ay; + } + rotationIK += Math.atan2(ty, tx) * MathUtils.radDeg; + if (bone.ascaleX < 0) rotationIK += 180; + if (rotationIK > 180) rotationIK -= 360; + else if (rotationIK < -180) rotationIK += 360; + let sx = bone.ascaleX, + sy = bone.ascaleY; + if (compress || stretch) { + switch (bone.data.transformMode) { + case TransformMode.NoScale: + case TransformMode.NoScaleOrReflection: + tx = targetX - bone.worldX; + ty = targetY - bone.worldY; + } + const b = bone.data.length * sx, + dd = Math.sqrt(tx * tx + ty * ty); + if ((compress && dd < b) || (stretch && dd > b && b > 0.0001)) { + const s = (dd / b - 1) * alpha + 1; + sx *= s; + if (uniform) sy *= s; + } + } + bone.updateWorldTransformWith( + bone.ax, + bone.ay, + bone.arotation + rotationIK * alpha, + sx, + sy, + bone.ashearX, + bone.ashearY + ); + } + + /** Applies 2 bone IK. The target is specified in the world coordinate system. + * @param child A direct descendant of the parent bone. */ + apply2( + parent: Bone, + child: Bone, + targetX: number, + targetY: number, + bendDir: number, + stretch: boolean, + softness: number, + alpha: number + ) { + if (alpha == 0) { + child.updateWorldTransform(); + return; + } + if (!parent.appliedValid) parent.updateAppliedTransform(); + if (!child.appliedValid) child.updateAppliedTransform(); + let px = parent.ax, + py = parent.ay, + psx = parent.ascaleX, + sx = psx, + psy = parent.ascaleY, + csx = child.ascaleX; + let os1 = 0, + os2 = 0, + s2 = 0; + if (psx < 0) { + psx = -psx; + os1 = 180; + s2 = -1; + } else { + os1 = 0; + s2 = 1; + } + if (psy < 0) { + psy = -psy; + s2 = -s2; + } + if (csx < 0) { + csx = -csx; + os2 = 180; + } else os2 = 0; + let cx = child.ax, + cy = 0, + cwx = 0, + cwy = 0, + a = parent.a, + b = parent.b, + c = parent.c, + d = parent.d; + const u = Math.abs(psx - psy) <= 0.0001; + if (!u) { + cy = 0; + cwx = a * cx + parent.worldX; + cwy = c * cx + parent.worldY; + } else { + cy = child.ay; + cwx = a * cx + b * cy + parent.worldX; + cwy = c * cx + d * cy + parent.worldY; + } + const pp = parent.parent; + a = pp.a; + b = pp.b; + c = pp.c; + d = pp.d; + let id = 1 / (a * d - b * c), + x = cwx - pp.worldX, + y = cwy - pp.worldY; + const dx = (x * d - y * b) * id - px, + dy = (y * a - x * c) * id - py; + let l1 = Math.sqrt(dx * dx + dy * dy), + l2 = child.data.length * csx, + a1, + a2; + if (l1 < 0.0001) { + this.apply1(parent, targetX, targetY, false, stretch, false, alpha); + child.updateWorldTransformWith(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY); + return; + } + x = targetX - pp.worldX; + y = targetY - pp.worldY; + let tx = (x * d - y * b) * id - px, + ty = (y * a - x * c) * id - py; + let dd = tx * tx + ty * ty; + if (softness != 0) { + softness *= (psx * (csx + 1)) / 2; + const td = Math.sqrt(dd), + sd = td - l1 - l2 * psx + softness; + if (sd > 0) { + let p = Math.min(1, sd / (softness * 2)) - 1; + p = (sd - softness * (1 - p * p)) / td; + tx -= p * tx; + ty -= p * ty; + dd = tx * tx + ty * ty; + } + } + outer: if (u) { + l2 *= psx; + let cos = (dd - l1 * l1 - l2 * l2) / (2 * l1 * l2); + if (cos < -1) cos = -1; + else if (cos > 1) { + cos = 1; + if (stretch) sx *= (Math.sqrt(dd) / (l1 + l2) - 1) * alpha + 1; + } + a2 = Math.acos(cos) * bendDir; + a = l1 + l2 * cos; + b = l2 * Math.sin(a2); + a1 = Math.atan2(ty * a - tx * b, tx * a + ty * b); + } else { + a = psx * l2; + b = psy * l2; + const aa = a * a, + bb = b * b, + ta = Math.atan2(ty, tx); + c = bb * l1 * l1 + aa * dd - aa * bb; + const c1 = -2 * bb * l1, + c2 = bb - aa; + d = c1 * c1 - 4 * c2 * c; + if (d >= 0) { + let q = Math.sqrt(d); + if (c1 < 0) q = -q; + q = -(c1 + q) / 2; + const r0 = q / c2, + r1 = c / q; + const r = Math.abs(r0) < Math.abs(r1) ? r0 : r1; + if (r * r <= dd) { + y = Math.sqrt(dd - r * r) * bendDir; + a1 = ta - Math.atan2(y, r); + a2 = Math.atan2(y / psy, (r - l1) / psx); + break outer; + } + } + let minAngle = MathUtils.PI, + minX = l1 - a, + minDist = minX * minX, + minY = 0; + let maxAngle = 0, + maxX = l1 + a, + maxDist = maxX * maxX, + maxY = 0; + c = (-a * l1) / (aa - bb); + if (c >= -1 && c <= 1) { + c = Math.acos(c); + x = a * Math.cos(c) + l1; + y = b * Math.sin(c); + d = x * x + y * y; + if (d < minDist) { + minAngle = c; + minDist = d; + minX = x; + minY = y; + } + if (d > maxDist) { + maxAngle = c; + maxDist = d; + maxX = x; + maxY = y; + } + } + if (dd <= (minDist + maxDist) / 2) { + a1 = ta - Math.atan2(minY * bendDir, minX); + a2 = minAngle * bendDir; + } else { + a1 = ta - Math.atan2(maxY * bendDir, maxX); + a2 = maxAngle * bendDir; + } + } + const os = Math.atan2(cy, cx) * s2; + let rotation = parent.arotation; + a1 = (a1 - os) * MathUtils.radDeg + os1 - rotation; + if (a1 > 180) a1 -= 360; + else if (a1 < -180) a1 += 360; + parent.updateWorldTransformWith(px, py, rotation + a1 * alpha, sx, parent.ascaleY, 0, 0); + rotation = child.arotation; + a2 = ((a2 + os) * MathUtils.radDeg - child.ashearX) * s2 + os2 - rotation; + if (a2 > 180) a2 -= 360; + else if (a2 < -180) a2 += 360; + child.updateWorldTransformWith( + cx, + cy, + rotation + a2 * alpha, + child.ascaleX, + child.ascaleY, + child.ashearX, + child.ashearY + ); + } +} diff --git a/packages/spine-core-3.8/src/spine-core/IkConstraintData.ts b/packages/spine-core-3.8/src/spine-core/IkConstraintData.ts new file mode 100644 index 0000000000..200085b990 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/IkConstraintData.ts @@ -0,0 +1,37 @@ +import { ConstraintData } from "./ConstraintData"; +import { BoneData } from "./BoneData"; + +/** Stores the setup pose for an {@link IkConstraint}. + *

    + * See [IK constraints](http://esotericsoftware.com/spine-ik-constraints) in the Spine User Guide. */ +export class IkConstraintData extends ConstraintData { + /** The bones that are constrained by this IK constraint. */ + bones = new Array(); + + /** The bone that is the IK target. */ + target: BoneData; + + /** Controls the bend direction of the IK bones, either 1 or -1. */ + bendDirection = 1; + + /** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */ + compress = false; + + /** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained + * and the parent bone has local nonuniform scale, stretch is not applied. */ + stretch = false; + + /** When true, only a single bone is being constrained, and {@link #getCompress()} or {@link #getStretch()} is used, the bone + * is scaled on both the X and Y axes. */ + uniform = false; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + mix = 1; + + /** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */ + softness = 0; + + constructor(name: string) { + super(name, 0, false); + } +} diff --git a/packages/spine-core-3.8/src/spine-core/PathConstraint.ts b/packages/spine-core-3.8/src/spine-core/PathConstraint.ts new file mode 100644 index 0000000000..057ebd1f20 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/PathConstraint.ts @@ -0,0 +1,500 @@ +import { Updatable } from "./Updatable"; +import { PathConstraintData, SpacingMode, RotateMode, PositionMode } from "./PathConstraintData"; +import { Bone } from "./Bone"; +import { Slot } from "./Slot"; +import { Skeleton } from "./Skeleton"; +import { PathAttachment } from "./attachments/PathAttachment"; +import { Utils, MathUtils } from "./Utils"; + +/** Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the + * constrained bones so they follow a {@link PathAttachment}. + * + * See [Path constraints](http://esotericsoftware.com/spine-path-constraints) in the Spine User Guide. */ +export class PathConstraint implements Updatable { + static NONE = -1; + static BEFORE = -2; + static AFTER = -3; + static epsilon = 0.00001; + + /** The path constraint's setup pose data. */ + data: PathConstraintData; + + /** The bones that will be modified by this path constraint. */ + bones: Array; + + /** The slot whose path attachment will be used to constrained the bones. */ + target: Slot; + + /** The position along the path. */ + position = 0; + + /** The spacing between bones. */ + spacing = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + rotateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained translations. */ + translateMix = 0; + + spaces = new Array(); + positions = new Array(); + world = new Array(); + curves = new Array(); + lengths = new Array(); + segments = new Array(); + + active = false; + + constructor(data: PathConstraintData, skeleton: Skeleton) { + if (data == null) throw new Error("data cannot be null."); + if (skeleton == null) throw new Error("skeleton cannot be null."); + this.data = data; + this.bones = new Array(); + for (let i = 0, n = data.bones.length; i < n; i++) this.bones.push(skeleton.findBone(data.bones[i].name)); + this.target = skeleton.findSlot(data.target.name); + this.position = data.position; + this.spacing = data.spacing; + this.rotateMix = data.rotateMix; + this.translateMix = data.translateMix; + } + + isActive() { + return this.active; + } + + /** Applies the constraint to the constrained bones. */ + apply() { + this.update(); + } + + update() { + const attachment = this.target.getAttachment(); + if (!(attachment instanceof PathAttachment)) return; + + const rotateMix = this.rotateMix, + translateMix = this.translateMix; + const translate = translateMix > 0, + rotate = rotateMix > 0; + if (!translate && !rotate) return; + + const data = this.data; + const percentSpacing = data.spacingMode == SpacingMode.Percent; + const rotateMode = data.rotateMode; + const tangents = rotateMode == RotateMode.Tangent, + scale = rotateMode == RotateMode.ChainScale; + const boneCount = this.bones.length, + spacesCount = tangents ? boneCount : boneCount + 1; + const bones = this.bones; + let spaces = Utils.setArraySize(this.spaces, spacesCount), + lengths: Array = null; + const spacing = this.spacing; + if (scale || !percentSpacing) { + if (scale) lengths = Utils.setArraySize(this.lengths, boneCount); + const lengthSpacing = data.spacingMode == SpacingMode.Length; + for (let i = 0, n = spacesCount - 1; i < n; ) { + const bone = bones[i]; + const setupLength = bone.data.length; + if (setupLength < PathConstraint.epsilon) { + if (scale) lengths[i] = 0; + spaces[++i] = 0; + } else if (percentSpacing) { + if (scale) { + const x = setupLength * bone.a, + y = setupLength * bone.c; + const length = Math.sqrt(x * x + y * y); + lengths[i] = length; + } + spaces[++i] = spacing; + } else { + const x = setupLength * bone.a, + y = setupLength * bone.c; + const length = Math.sqrt(x * x + y * y); + if (scale) lengths[i] = length; + spaces[++i] = ((lengthSpacing ? setupLength + spacing : spacing) * length) / setupLength; + } + } + } else { + for (let i = 1; i < spacesCount; i++) spaces[i] = spacing; + } + + const positions = this.computeWorldPositions( + attachment, + spacesCount, + tangents, + data.positionMode == PositionMode.Percent, + percentSpacing + ); + let boneX = positions[0], + boneY = positions[1], + offsetRotation = data.offsetRotation; + let tip = false; + if (offsetRotation == 0) tip = rotateMode == RotateMode.Chain; + else { + tip = false; + const p = this.target.bone; + offsetRotation *= p.a * p.d - p.b * p.c > 0 ? MathUtils.degRad : -MathUtils.degRad; + } + for (let i = 0, p = 3; i < boneCount; i++, p += 3) { + const bone = bones[i]; + bone.worldX += (boneX - bone.worldX) * translateMix; + bone.worldY += (boneY - bone.worldY) * translateMix; + const x = positions[p], + y = positions[p + 1], + dx = x - boneX, + dy = y - boneY; + if (scale) { + const length = lengths[i]; + if (length != 0) { + const s = (Math.sqrt(dx * dx + dy * dy) / length - 1) * rotateMix + 1; + bone.a *= s; + bone.c *= s; + } + } + boneX = x; + boneY = y; + if (rotate) { + let a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d, + r = 0, + cos = 0, + sin = 0; + if (tangents) r = positions[p - 1]; + else if (spaces[i + 1] == 0) r = positions[p + 2]; + else r = Math.atan2(dy, dx); + r -= Math.atan2(c, a); + if (tip) { + cos = Math.cos(r); + sin = Math.sin(r); + const length = bone.data.length; + boneX += (length * (cos * a - sin * c) - dx) * rotateMix; + boneY += (length * (sin * a + cos * c) - dy) * rotateMix; + } else { + r += offsetRotation; + } + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) + // + r += MathUtils.PI2; + r *= rotateMix; + cos = Math.cos(r); + sin = Math.sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + bone.appliedValid = false; + } + } + + computeWorldPositions( + path: PathAttachment, + spacesCount: number, + tangents: boolean, + percentPosition: boolean, + percentSpacing: boolean + ) { + const target = this.target; + let position = this.position; + let spaces = this.spaces, + out = Utils.setArraySize(this.positions, spacesCount * 3 + 2), + world: Array = null; + const closed = path.closed; + let verticesLength = path.worldVerticesLength, + curveCount = verticesLength / 6, + prevCurve = PathConstraint.NONE; + + if (!path.constantSpeed) { + const lengths = path.lengths; + curveCount -= closed ? 1 : 2; + const pathLength = lengths[curveCount]; + if (percentPosition) position *= pathLength; + if (percentSpacing) { + for (let i = 1; i < spacesCount; i++) spaces[i] *= pathLength; + } + world = Utils.setArraySize(this.world, 8); + for (let i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) { + const space = spaces[i]; + position += space; + let p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + if (prevCurve != PathConstraint.BEFORE) { + prevCurve = PathConstraint.BEFORE; + path.computeWorldVertices(target, 2, 4, world, 0, 2); + } + this.addBeforePosition(p, world, 0, out, o); + continue; + } else if (p > pathLength) { + if (prevCurve != PathConstraint.AFTER) { + prevCurve = PathConstraint.AFTER; + path.computeWorldVertices(target, verticesLength - 6, 4, world, 0, 2); + } + this.addAfterPosition(p - pathLength, world, 0, out, o); + continue; + } + + // Determine curve containing position. + for (; ; curve++) { + const length = lengths[curve]; + if (p > length) continue; + if (curve == 0) p /= length; + else { + const prev = lengths[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + if (curve != prevCurve) { + prevCurve = curve; + if (closed && curve == curveCount) { + path.computeWorldVertices(target, verticesLength - 4, 4, world, 0, 2); + path.computeWorldVertices(target, 0, 4, world, 4, 2); + } else path.computeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2); + } + this.addCurvePosition( + p, + world[0], + world[1], + world[2], + world[3], + world[4], + world[5], + world[6], + world[7], + out, + o, + tangents || (i > 0 && space == 0) + ); + } + return out; + } + + // World vertices. + if (closed) { + verticesLength += 2; + world = Utils.setArraySize(this.world, verticesLength); + path.computeWorldVertices(target, 2, verticesLength - 4, world, 0, 2); + path.computeWorldVertices(target, 0, 2, world, verticesLength - 4, 2); + world[verticesLength - 2] = world[0]; + world[verticesLength - 1] = world[1]; + } else { + curveCount--; + verticesLength -= 4; + world = Utils.setArraySize(this.world, verticesLength); + path.computeWorldVertices(target, 2, verticesLength, world, 0, 2); + } + + // Curve lengths. + const curves = Utils.setArraySize(this.curves, curveCount); + let pathLength = 0; + let x1 = world[0], + y1 = world[1], + cx1 = 0, + cy1 = 0, + cx2 = 0, + cy2 = 0, + x2 = 0, + y2 = 0; + let tmpx = 0, + tmpy = 0, + dddfx = 0, + dddfy = 0, + ddfx = 0, + ddfy = 0, + dfx = 0, + dfy = 0; + for (let i = 0, w = 2; i < curveCount; i++, w += 6) { + cx1 = world[w]; + cy1 = world[w + 1]; + cx2 = world[w + 2]; + cy2 = world[w + 3]; + x2 = world[w + 4]; + y2 = world[w + 5]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.1875; + tmpy = (y1 - cy1 * 2 + cy2) * 0.1875; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.09375; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.09375; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.75 + tmpx + dddfx * 0.16666667; + dfy = (cy1 - y1) * 0.75 + tmpy + dddfy * 0.16666667; + pathLength += Math.sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + pathLength += Math.sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx; + dfy += ddfy; + pathLength += Math.sqrt(dfx * dfx + dfy * dfy); + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + pathLength += Math.sqrt(dfx * dfx + dfy * dfy); + curves[i] = pathLength; + x1 = x2; + y1 = y2; + } + if (percentPosition) position *= pathLength; + else position *= pathLength / path.lengths[curveCount - 1]; + if (percentSpacing) { + for (let i = 1; i < spacesCount; i++) spaces[i] *= pathLength; + } + + const segments = this.segments; + let curveLength = 0; + for (let i = 0, o = 0, curve = 0, segment = 0; i < spacesCount; i++, o += 3) { + const space = spaces[i]; + position += space; + let p = position; + + if (closed) { + p %= pathLength; + if (p < 0) p += pathLength; + curve = 0; + } else if (p < 0) { + this.addBeforePosition(p, world, 0, out, o); + continue; + } else if (p > pathLength) { + this.addAfterPosition(p - pathLength, world, verticesLength - 4, out, o); + continue; + } + + // Determine curve containing position. + for (; ; curve++) { + const length = curves[curve]; + if (p > length) continue; + if (curve == 0) p /= length; + else { + const prev = curves[curve - 1]; + p = (p - prev) / (length - prev); + } + break; + } + + // Curve segment lengths. + if (curve != prevCurve) { + prevCurve = curve; + let ii = curve * 6; + x1 = world[ii]; + y1 = world[ii + 1]; + cx1 = world[ii + 2]; + cy1 = world[ii + 3]; + cx2 = world[ii + 4]; + cy2 = world[ii + 5]; + x2 = world[ii + 6]; + y2 = world[ii + 7]; + tmpx = (x1 - cx1 * 2 + cx2) * 0.03; + tmpy = (y1 - cy1 * 2 + cy2) * 0.03; + dddfx = ((cx1 - cx2) * 3 - x1 + x2) * 0.006; + dddfy = ((cy1 - cy2) * 3 - y1 + y2) * 0.006; + ddfx = tmpx * 2 + dddfx; + ddfy = tmpy * 2 + dddfy; + dfx = (cx1 - x1) * 0.3 + tmpx + dddfx * 0.16666667; + dfy = (cy1 - y1) * 0.3 + tmpy + dddfy * 0.16666667; + curveLength = Math.sqrt(dfx * dfx + dfy * dfy); + segments[0] = curveLength; + for (ii = 1; ii < 8; ii++) { + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + curveLength += Math.sqrt(dfx * dfx + dfy * dfy); + segments[ii] = curveLength; + } + dfx += ddfx; + dfy += ddfy; + curveLength += Math.sqrt(dfx * dfx + dfy * dfy); + segments[8] = curveLength; + dfx += ddfx + dddfx; + dfy += ddfy + dddfy; + curveLength += Math.sqrt(dfx * dfx + dfy * dfy); + segments[9] = curveLength; + segment = 0; + } + + // Weight by segment length. + p *= curveLength; + for (; ; segment++) { + const length = segments[segment]; + if (p > length) continue; + if (segment == 0) p /= length; + else { + const prev = segments[segment - 1]; + p = segment + (p - prev) / (length - prev); + } + break; + } + this.addCurvePosition(p * 0.1, x1, y1, cx1, cy1, cx2, cy2, x2, y2, out, o, tangents || (i > 0 && space == 0)); + } + return out; + } + + addBeforePosition(p: number, temp: Array, i: number, out: Array, o: number) { + const x1 = temp[i], + y1 = temp[i + 1], + dx = temp[i + 2] - x1, + dy = temp[i + 3] - y1, + r = Math.atan2(dy, dx); + out[o] = x1 + p * Math.cos(r); + out[o + 1] = y1 + p * Math.sin(r); + out[o + 2] = r; + } + + addAfterPosition(p: number, temp: Array, i: number, out: Array, o: number) { + const x1 = temp[i + 2], + y1 = temp[i + 3], + dx = x1 - temp[i], + dy = y1 - temp[i + 1], + r = Math.atan2(dy, dx); + out[o] = x1 + p * Math.cos(r); + out[o + 1] = y1 + p * Math.sin(r); + out[o + 2] = r; + } + + addCurvePosition( + p: number, + x1: number, + y1: number, + cx1: number, + cy1: number, + cx2: number, + cy2: number, + x2: number, + y2: number, + out: Array, + o: number, + tangents: boolean + ) { + if (p == 0 || isNaN(p)) { + out[o] = x1; + out[o + 1] = y1; + out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1); + return; + } + const tt = p * p, + ttt = tt * p, + u = 1 - p, + uu = u * u, + uuu = uu * u; + const ut = u * p, + ut3 = ut * 3, + uut3 = u * ut3, + utt3 = ut3 * p; + const x = x1 * uuu + cx1 * uut3 + cx2 * utt3 + x2 * ttt, + y = y1 * uuu + cy1 * uut3 + cy2 * utt3 + y2 * ttt; + out[o] = x; + out[o + 1] = y; + if (tangents) { + if (p < 0.001) out[o + 2] = Math.atan2(cy1 - y1, cx1 - x1); + else out[o + 2] = Math.atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt)); + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/PathConstraintData.ts b/packages/spine-core-3.8/src/spine-core/PathConstraintData.ts new file mode 100644 index 0000000000..32f5bafe50 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/PathConstraintData.ts @@ -0,0 +1,68 @@ +import { ConstraintData } from "./ConstraintData"; +import { BoneData } from "./BoneData"; +import { SlotData } from "./SlotData"; + +/** Stores the setup pose for a {@link PathConstraint}. + * + * See [Path constraints](http://esotericsoftware.com/spine-path-constraints) in the Spine User Guide. */ +export class PathConstraintData extends ConstraintData { + /** The bones that will be modified by this path constraint. */ + bones = new Array(); + + /** The slot whose path attachment will be used to constrained the bones. */ + target: SlotData; + + /** The mode for positioning the first bone on the path. */ + positionMode: PositionMode; + + /** The mode for positioning the bones after the first bone on the path. */ + spacingMode: SpacingMode; + + /** The mode for adjusting the rotation of the bones. */ + rotateMode: RotateMode; + + /** An offset added to the constrained bone rotation. */ + offsetRotation: number; + + /** The position along the path. */ + position: number; + + /** The spacing between bones. */ + spacing: number; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + rotateMix: number; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained translations. */ + translateMix: number; + + constructor(name: string) { + super(name, 0, false); + } +} + +/** Controls how the first bone is positioned along the path. + * + * See [Position mode](http://esotericsoftware.com/spine-path-constraints#Position-mode) in the Spine User Guide. */ +export enum PositionMode { + Fixed, + Percent +} + +/** Controls how bones after the first bone are positioned along the path. + * + * [Spacing mode](http://esotericsoftware.com/spine-path-constraints#Spacing-mode) in the Spine User Guide. */ +export enum SpacingMode { + Length, + Fixed, + Percent +} + +/** Controls how bones are rotated, translated, and scaled to match the path. + * + * [Rotate mode](http://esotericsoftware.com/spine-path-constraints#Rotate-mod) in the Spine User Guide. */ +export enum RotateMode { + Tangent, + Chain, + ChainScale +} diff --git a/packages/spine-core-3.8/src/spine-core/Skeleton.ts b/packages/spine-core-3.8/src/spine-core/Skeleton.ts new file mode 100644 index 0000000000..2573e25dd9 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Skeleton.ts @@ -0,0 +1,592 @@ +import { SkeletonData } from "./SkeletonData"; +import { Bone } from "./Bone"; +import { Slot } from "./Slot"; +import { IkConstraint } from "./IkConstraint"; +import { TransformConstraint } from "./TransformConstraint"; +import { PathConstraint } from "./PathConstraint"; +import { Updatable } from "./Updatable"; +import { Skin } from "./Skin"; +import { Color, Utils, Vector2 } from "./Utils"; +import { PathAttachment } from "./attachments/PathAttachment"; +import { Attachment } from "./attachments/Attachment"; +import { RegionAttachment } from "./attachments/RegionAttachment"; +import { MeshAttachment } from "./attachments/MeshAttachment"; + +/** Stores the current pose for a skeleton. + * + * See [Instance objects](http://esotericsoftware.com/spine-runtime-architecture#Instance-objects) in the Spine Runtimes Guide. */ +export class Skeleton { + /** The skeleton's setup pose data. */ + data: SkeletonData; + + /** The skeleton's bones, sorted parent first. The root bone is always the first bone. */ + bones: Array; + + /** The skeleton's slots. */ + slots: Array; + + /** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */ + drawOrder: Array; + + /** The skeleton's IK constraints. */ + ikConstraints: Array; + + /** The skeleton's transform constraints. */ + transformConstraints: Array; + + /** The skeleton's path constraints. */ + pathConstraints: Array; + + /** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link #updateCache()}. */ + _updateCache = new Array(); + updateCacheReset = new Array(); + + /** The skeleton's current skin. May be null. */ + skin: Skin; + + /** The color to tint all the skeleton's attachments. */ + color: Color; + + /** Returns the skeleton's time. This can be used for tracking, such as with Slot {@link Slot#attachmentTime}. + *

    + * See {@link #update()}. */ + time = 0; + + /** Scales the entire skeleton on the X axis. This affects all bones, even if the bone's transform mode disallows scale + * inheritance. */ + scaleX = 1; + + /** Scales the entire skeleton on the Y axis. This affects all bones, even if the bone's transform mode disallows scale + * inheritance. */ + scaleY = 1; + + /** Sets the skeleton X position, which is added to the root bone worldX position. */ + x = 0; + + /** Sets the skeleton Y position, which is added to the root bone worldY position. */ + y = 0; + + constructor(data: SkeletonData) { + if (data == null) throw new Error("data cannot be null."); + this.data = data; + + this.bones = new Array(); + for (let i = 0; i < data.bones.length; i++) { + const boneData = data.bones[i]; + let bone: Bone; + if (boneData.parent == null) bone = new Bone(boneData, this, null); + else { + const parent = this.bones[boneData.parent.index]; + bone = new Bone(boneData, this, parent); + parent.children.push(bone); + } + this.bones.push(bone); + } + + this.slots = new Array(); + this.drawOrder = new Array(); + for (let i = 0; i < data.slots.length; i++) { + const slotData = data.slots[i]; + const bone = this.bones[slotData.boneData.index]; + const slot = new Slot(slotData, bone); + this.slots.push(slot); + this.drawOrder.push(slot); + } + + this.ikConstraints = new Array(); + for (let i = 0; i < data.ikConstraints.length; i++) { + const ikConstraintData = data.ikConstraints[i]; + this.ikConstraints.push(new IkConstraint(ikConstraintData, this)); + } + + this.transformConstraints = new Array(); + for (let i = 0; i < data.transformConstraints.length; i++) { + const transformConstraintData = data.transformConstraints[i]; + this.transformConstraints.push(new TransformConstraint(transformConstraintData, this)); + } + + this.pathConstraints = new Array(); + for (let i = 0; i < data.pathConstraints.length; i++) { + const pathConstraintData = data.pathConstraints[i]; + this.pathConstraints.push(new PathConstraint(pathConstraintData, this)); + } + + this.color = new Color(1, 1, 1, 1); + this.updateCache(); + } + + /** Caches information about bones and constraints. Must be called if the {@link #getSkin()} is modified or if bones, + * constraints, or weighted path attachments are added or removed. */ + updateCache() { + const updateCache = this._updateCache; + updateCache.length = 0; + this.updateCacheReset.length = 0; + + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + bone.sorted = bone.data.skinRequired; + bone.active = !bone.sorted; + } + + if (this.skin != null) { + const skinBones = this.skin.bones; + for (let i = 0, n = this.skin.bones.length; i < n; i++) { + let bone = this.bones[skinBones[i].index]; + do { + bone.sorted = false; + bone.active = true; + bone = bone.parent; + } while (bone != null); + } + } + + // IK first, lowest hierarchy depth first. + const ikConstraints = this.ikConstraints; + const transformConstraints = this.transformConstraints; + const pathConstraints = this.pathConstraints; + const ikCount = ikConstraints.length, + transformCount = transformConstraints.length, + pathCount = pathConstraints.length; + const constraintCount = ikCount + transformCount + pathCount; + + outer: for (let i = 0; i < constraintCount; i++) { + for (let ii = 0; ii < ikCount; ii++) { + const constraint = ikConstraints[ii]; + if (constraint.data.order == i) { + this.sortIkConstraint(constraint); + continue outer; + } + } + for (let ii = 0; ii < transformCount; ii++) { + const constraint = transformConstraints[ii]; + if (constraint.data.order == i) { + this.sortTransformConstraint(constraint); + continue outer; + } + } + for (let ii = 0; ii < pathCount; ii++) { + const constraint = pathConstraints[ii]; + if (constraint.data.order == i) { + this.sortPathConstraint(constraint); + continue outer; + } + } + } + + for (let i = 0, n = bones.length; i < n; i++) this.sortBone(bones[i]); + } + + sortIkConstraint(constraint: IkConstraint) { + constraint.active = + constraint.target.isActive() && + (!constraint.data.skinRequired || + (this.skin != null && Utils.contains(this.skin.constraints, constraint.data, true))); + if (!constraint.active) return; + + const target = constraint.target; + this.sortBone(target); + + const constrained = constraint.bones; + const parent = constrained[0]; + this.sortBone(parent); + + if (constrained.length > 1) { + const child = constrained[constrained.length - 1]; + if (!(this._updateCache.indexOf(child) > -1)) this.updateCacheReset.push(child); + } + + this._updateCache.push(constraint); + + this.sortReset(parent.children); + constrained[constrained.length - 1].sorted = true; + } + + sortPathConstraint(constraint: PathConstraint) { + constraint.active = + constraint.target.bone.isActive() && + (!constraint.data.skinRequired || + (this.skin != null && Utils.contains(this.skin.constraints, constraint.data, true))); + if (!constraint.active) return; + + const slot = constraint.target; + const slotIndex = slot.data.index; + const slotBone = slot.bone; + if (this.skin != null) this.sortPathConstraintAttachment(this.skin, slotIndex, slotBone); + if (this.data.defaultSkin != null && this.data.defaultSkin != this.skin) + this.sortPathConstraintAttachment(this.data.defaultSkin, slotIndex, slotBone); + for (let i = 0, n = this.data.skins.length; i < n; i++) + this.sortPathConstraintAttachment(this.data.skins[i], slotIndex, slotBone); + + const attachment = slot.getAttachment(); + if (attachment instanceof PathAttachment) this.sortPathConstraintAttachmentWith(attachment, slotBone); + + const constrained = constraint.bones; + const boneCount = constrained.length; + for (let i = 0; i < boneCount; i++) this.sortBone(constrained[i]); + + this._updateCache.push(constraint); + + for (let i = 0; i < boneCount; i++) this.sortReset(constrained[i].children); + for (let i = 0; i < boneCount; i++) constrained[i].sorted = true; + } + + sortTransformConstraint(constraint: TransformConstraint) { + constraint.active = + constraint.target.isActive() && + (!constraint.data.skinRequired || + (this.skin != null && Utils.contains(this.skin.constraints, constraint.data, true))); + if (!constraint.active) return; + + this.sortBone(constraint.target); + + const constrained = constraint.bones; + const boneCount = constrained.length; + if (constraint.data.local) { + for (let i = 0; i < boneCount; i++) { + const child = constrained[i]; + this.sortBone(child.parent); + if (!(this._updateCache.indexOf(child) > -1)) this.updateCacheReset.push(child); + } + } else { + for (let i = 0; i < boneCount; i++) { + this.sortBone(constrained[i]); + } + } + + this._updateCache.push(constraint); + + for (let ii = 0; ii < boneCount; ii++) this.sortReset(constrained[ii].children); + for (let ii = 0; ii < boneCount; ii++) constrained[ii].sorted = true; + } + + sortPathConstraintAttachment(skin: Skin, slotIndex: number, slotBone: Bone) { + const attachments = skin.attachments[slotIndex]; + if (!attachments) return; + for (const key in attachments) { + this.sortPathConstraintAttachmentWith(attachments[key], slotBone); + } + } + + sortPathConstraintAttachmentWith(attachment: Attachment, slotBone: Bone) { + if (!(attachment instanceof PathAttachment)) return; + const pathBones = (attachment).bones; + if (pathBones == null) this.sortBone(slotBone); + else { + const bones = this.bones; + let i = 0; + while (i < pathBones.length) { + const boneCount = pathBones[i++]; + for (let n = i + boneCount; i < n; i++) { + const boneIndex = pathBones[i]; + this.sortBone(bones[boneIndex]); + } + } + } + } + + sortBone(bone: Bone) { + if (bone.sorted) return; + const parent = bone.parent; + if (parent != null) this.sortBone(parent); + bone.sorted = true; + this._updateCache.push(bone); + } + + sortReset(bones: Array) { + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (!bone.active) continue; + if (bone.sorted) this.sortReset(bone.children); + bone.sorted = false; + } + } + + /** Updates the world transform for each bone and applies all constraints. + * + * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine + * Runtimes Guide. */ + updateWorldTransform() { + const updateCacheReset = this.updateCacheReset; + for (let i = 0, n = updateCacheReset.length; i < n; i++) { + const bone = updateCacheReset[i] as Bone; + bone.ax = bone.x; + bone.ay = bone.y; + bone.arotation = bone.rotation; + bone.ascaleX = bone.scaleX; + bone.ascaleY = bone.scaleY; + bone.ashearX = bone.shearX; + bone.ashearY = bone.shearY; + bone.appliedValid = true; + } + const updateCache = this._updateCache; + for (let i = 0, n = updateCache.length; i < n; i++) updateCache[i].update(); + } + + /** Sets the bones, constraints, and slots to their setup pose values. */ + setToSetupPose() { + this.setBonesToSetupPose(); + this.setSlotsToSetupPose(); + } + + /** Sets the bones and constraints to their setup pose values. */ + setBonesToSetupPose() { + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) bones[i].setToSetupPose(); + + const ikConstraints = this.ikConstraints; + for (let i = 0, n = ikConstraints.length; i < n; i++) { + const constraint = ikConstraints[i]; + constraint.mix = constraint.data.mix; + constraint.softness = constraint.data.softness; + constraint.bendDirection = constraint.data.bendDirection; + constraint.compress = constraint.data.compress; + constraint.stretch = constraint.data.stretch; + } + + const transformConstraints = this.transformConstraints; + for (let i = 0, n = transformConstraints.length; i < n; i++) { + const constraint = transformConstraints[i]; + const data = constraint.data; + constraint.rotateMix = data.rotateMix; + constraint.translateMix = data.translateMix; + constraint.scaleMix = data.scaleMix; + constraint.shearMix = data.shearMix; + } + + const pathConstraints = this.pathConstraints; + for (let i = 0, n = pathConstraints.length; i < n; i++) { + const constraint = pathConstraints[i]; + const data = constraint.data; + constraint.position = data.position; + constraint.spacing = data.spacing; + constraint.rotateMix = data.rotateMix; + constraint.translateMix = data.translateMix; + } + } + + /** Sets the slots and draw order to their setup pose values. */ + setSlotsToSetupPose() { + const slots = this.slots; + Utils.arrayCopy(slots, 0, this.drawOrder, 0, slots.length); + for (let i = 0, n = slots.length; i < n; i++) slots[i].setToSetupPose(); + } + + /** @returns May return null. */ + getRootBone() { + if (this.bones.length == 0) return null; + return this.bones[0]; + } + + /** @returns May be null. */ + findBone(boneName: string) { + if (boneName == null) throw new Error("boneName cannot be null."); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (bone.data.name == boneName) return bone; + } + return null; + } + + /** @returns -1 if the bone was not found. */ + findBoneIndex(boneName: string) { + if (boneName == null) throw new Error("boneName cannot be null."); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) if (bones[i].data.name == boneName) return i; + return -1; + } + + /** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it + * repeatedly. + * @returns May be null. */ + findSlot(slotName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) { + const slot = slots[i]; + if (slot.data.name == slotName) return slot; + } + return null; + } + + /** @returns -1 if the bone was not found. */ + findSlotIndex(slotName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) if (slots[i].data.name == slotName) return i; + return -1; + } + + /** Sets a skin by name. + * + * See {@link #setSkin()}. */ + setSkinByName(skinName: string) { + const skin = this.data.findSkin(skinName); + if (skin == null) throw new Error("Skin not found: " + skinName); + this.setSkin(skin); + } + + /** Sets the skin used to look up attachments before looking in the {@link SkeletonData#defaultSkin default skin}. If the + * skin is changed, {@link #updateCache()} is called. + * + * Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. If there was no + * old skin, each slot's setup mode attachment is attached from the new skin. + * + * After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling + * {@link #setSlotsToSetupPose()}. Also, often {@link AnimationState#apply()} is called before the next time the + * skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin. + * @param newSkin May be null. */ + setSkin(newSkin: Skin) { + if (newSkin == this.skin) return; + if (newSkin != null) { + if (this.skin != null) newSkin.attachAll(this, this.skin); + else { + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) { + const slot = slots[i]; + const name = slot.data.attachmentName; + if (name != null) { + const attachment: Attachment = newSkin.getAttachment(i, name); + if (attachment != null) slot.setAttachment(attachment); + } + } + } + } + this.skin = newSkin; + this.updateCache(); + } + + /** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment + * name. + * + * See {@link #getAttachment()}. + * @returns May be null. */ + getAttachmentByName(slotName: string, attachmentName: string): Attachment { + return this.getAttachment(this.data.findSlotIndex(slotName), attachmentName); + } + + /** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot index and + * attachment name. First the skin is checked and if the attachment was not found, the default skin is checked. + * + * See [Runtime skins](http://esotericsoftware.com/spine-runtime-skins) in the Spine Runtimes Guide. + * @returns May be null. */ + getAttachment(slotIndex: number, attachmentName: string): Attachment { + if (attachmentName == null) throw new Error("attachmentName cannot be null."); + if (this.skin != null) { + const attachment: Attachment = this.skin.getAttachment(slotIndex, attachmentName); + if (attachment != null) return attachment; + } + if (this.data.defaultSkin != null) return this.data.defaultSkin.getAttachment(slotIndex, attachmentName); + return null; + } + + /** A convenience method to set an attachment by finding the slot with {@link #findSlot()}, finding the attachment with + * {@link #getAttachment()}, then setting the slot's {@link Slot#attachment}. + * @param attachmentName May be null to clear the slot's attachment. */ + setAttachment(slotName: string, attachmentName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) { + const slot = slots[i]; + if (slot.data.name == slotName) { + let attachment: Attachment = null; + if (attachmentName != null) { + attachment = this.getAttachment(i, attachmentName); + if (attachment == null) + throw new Error("Attachment not found: " + attachmentName + ", for slot: " + slotName); + } + slot.setAttachment(attachment); + return; + } + } + throw new Error("Slot not found: " + slotName); + } + + /** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method + * than to call it repeatedly. + * @return May be null. */ + findIkConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const ikConstraints = this.ikConstraints; + for (let i = 0, n = ikConstraints.length; i < n; i++) { + const ikConstraint = ikConstraints[i]; + if (ikConstraint.data.name == constraintName) return ikConstraint; + } + return null; + } + + /** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of + * this method than to call it repeatedly. + * @return May be null. */ + findTransformConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const transformConstraints = this.transformConstraints; + for (let i = 0, n = transformConstraints.length; i < n; i++) { + const constraint = transformConstraints[i]; + if (constraint.data.name == constraintName) return constraint; + } + return null; + } + + /** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method + * than to call it repeatedly. + * @return May be null. */ + findPathConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const pathConstraints = this.pathConstraints; + for (let i = 0, n = pathConstraints.length; i < n; i++) { + const constraint = pathConstraints[i]; + if (constraint.data.name == constraintName) return constraint; + } + return null; + } + + /** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose. + * @param offset An output value, the distance from the skeleton origin to the bottom left corner of the AABB. + * @param size An output value, the width and height of the AABB. + * @param temp Working memory to temporarily store attachments' computed world vertices. */ + getBounds(offset: Vector2, size: Vector2, temp: Array = new Array(2)) { + if (offset == null) throw new Error("offset cannot be null."); + if (size == null) throw new Error("size cannot be null."); + const drawOrder = this.drawOrder; + let minX = Number.POSITIVE_INFINITY, + minY = Number.POSITIVE_INFINITY, + maxX = Number.NEGATIVE_INFINITY, + maxY = Number.NEGATIVE_INFINITY; + for (let i = 0, n = drawOrder.length; i < n; i++) { + const slot = drawOrder[i]; + if (!slot.bone.active) continue; + let verticesLength = 0; + let vertices: ArrayLike = null; + const attachment = slot.getAttachment(); + if (attachment instanceof RegionAttachment) { + verticesLength = 8; + vertices = Utils.setArraySize(temp, verticesLength, 0); + (attachment).computeWorldVertices(slot.bone, vertices, 0, 2); + } else if (attachment instanceof MeshAttachment) { + const mesh = attachment; + verticesLength = mesh.worldVerticesLength; + vertices = Utils.setArraySize(temp, verticesLength, 0); + mesh.computeWorldVertices(slot, 0, verticesLength, vertices, 0, 2); + } + if (vertices != null) { + for (let ii = 0, nn = vertices.length; ii < nn; ii += 2) { + const x = vertices[ii], + y = vertices[ii + 1]; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + } + } + offset.set(minX, minY); + size.set(maxX - minX, maxY - minY); + } + + /** Increments the skeleton's {@link #time}. */ + update(delta: number) { + this.time += delta; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonBinary.ts b/packages/spine-core-3.8/src/spine-core/SkeletonBinary.ts new file mode 100644 index 0000000000..86ec2f722c --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonBinary.ts @@ -0,0 +1,942 @@ +import { TransformMode, BoneData } from "./BoneData"; +import { PositionMode, SpacingMode, RotateMode, PathConstraintData } from "./PathConstraintData"; +import { BlendMode } from "./BlendMode"; +import { AttachmentLoader } from "./attachments/AttachmentLoader"; +import { SkeletonData } from "./SkeletonData"; +import { Color, Utils } from "./Utils"; +import { SlotData } from "./SlotData"; +import { IkConstraintData } from "./IkConstraintData"; +import { TransformConstraintData } from "./TransformConstraintData"; +import { VertexAttachment, Attachment } from "./attachments/Attachment"; +import { MeshAttachment } from "./attachments/MeshAttachment"; +import { EventData } from "./EventData"; +import { Skin } from "./Skin"; +import { AttachmentType } from "./attachments/AttachmentType"; +import { + Timeline, + AttachmentTimeline, + ColorTimeline, + TwoColorTimeline, + RotateTimeline, + ScaleTimeline, + ShearTimeline, + TranslateTimeline, + IkConstraintTimeline, + TransformConstraintTimeline, + PathConstraintSpacingTimeline, + PathConstraintPositionTimeline, + PathConstraintMixTimeline, + DeformTimeline, + DrawOrderTimeline, + EventTimeline, + CurveTimeline +} from "./Animation"; +import { Animation } from "./Animation"; +import { Event } from "./Event"; + +/** Loads skeleton data in the Spine binary format. + * + * See [Spine binary format](http://esotericsoftware.com/spine-binary-format) and + * [JSON and binary data](http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data) in the Spine + * Runtimes Guide. */ +export class SkeletonBinary { + static AttachmentTypeValues = [ + 0 /*AttachmentType.Region*/, 1 /*AttachmentType.BoundingBox*/, 2 /*AttachmentType.Mesh*/, + 3 /*AttachmentType.LinkedMesh*/, 4 /*AttachmentType.Path*/, 5 /*AttachmentType.Point*/, + 6 /*AttachmentType.Clipping*/ + ]; + static TransformModeValues = [ + TransformMode.Normal, + TransformMode.OnlyTranslation, + TransformMode.NoRotationOrReflection, + TransformMode.NoScale, + TransformMode.NoScaleOrReflection + ]; + static PositionModeValues = [PositionMode.Fixed, PositionMode.Percent]; + static SpacingModeValues = [SpacingMode.Length, SpacingMode.Fixed, SpacingMode.Percent]; + static RotateModeValues = [RotateMode.Tangent, RotateMode.Chain, RotateMode.ChainScale]; + static BlendModeValues = [BlendMode.Normal, BlendMode.Additive, BlendMode.Multiply, BlendMode.Screen]; + + static BONE_ROTATE = 0; + static BONE_TRANSLATE = 1; + static BONE_SCALE = 2; + static BONE_SHEAR = 3; + + static SLOT_ATTACHMENT = 0; + static SLOT_COLOR = 1; + static SLOT_TWO_COLOR = 2; + + static PATH_POSITION = 0; + static PATH_SPACING = 1; + static PATH_MIX = 2; + + static CURVE_LINEAR = 0; + static CURVE_STEPPED = 1; + static CURVE_BEZIER = 2; + + /** Scales bone positions, image sizes, and translations as they are loaded. This allows different size images to be used at + * runtime than were used in Spine. + * + * See [Scaling](http://esotericsoftware.com/spine-loading-skeleton-data#Scaling) in the Spine Runtimes Guide. */ + scale = 1; + + attachmentLoader: AttachmentLoader; + private linkedMeshes = new Array(); + + constructor(attachmentLoader: AttachmentLoader) { + this.attachmentLoader = attachmentLoader; + } + + readSkeletonData(binary: Uint8Array): SkeletonData { + const scale = this.scale; + + const skeletonData = new SkeletonData(); + skeletonData.name = ""; // BOZO + + const input = new BinaryInput(binary); + + skeletonData.hash = input.readString(); + skeletonData.version = input.readString(); + if ("3.8.75" == skeletonData.version) + throw new Error("Unsupported skeleton data, please export with a newer version of Spine."); + skeletonData.x = input.readFloat(); + skeletonData.y = input.readFloat(); + skeletonData.width = input.readFloat(); + skeletonData.height = input.readFloat(); + + const nonessential = input.readBoolean(); + if (nonessential) { + skeletonData.fps = input.readFloat(); + + skeletonData.imagesPath = input.readString(); + skeletonData.audioPath = input.readString(); + } + + let n = 0; + // Strings. + n = input.readInt(true); + for (let i = 0; i < n; i++) input.strings.push(input.readString()); + + // Bones. + n = input.readInt(true); + for (let i = 0; i < n; i++) { + const name = input.readString(); + const parent = i == 0 ? null : skeletonData.bones[input.readInt(true)]; + const data = new BoneData(i, name, parent); + data.rotation = input.readFloat(); + data.x = input.readFloat() * scale; + data.y = input.readFloat() * scale; + data.scaleX = input.readFloat(); + data.scaleY = input.readFloat(); + data.shearX = input.readFloat(); + data.shearY = input.readFloat(); + data.length = input.readFloat() * scale; + data.transformMode = SkeletonBinary.TransformModeValues[input.readInt(true)]; + data.skinRequired = input.readBoolean(); + if (nonessential) Color.rgba8888ToColor(data.color, input.readInt32()); + skeletonData.bones.push(data); + } + + // Slots. + n = input.readInt(true); + for (let i = 0; i < n; i++) { + const slotName = input.readString(); + const boneData = skeletonData.bones[input.readInt(true)]; + const data = new SlotData(i, slotName, boneData); + Color.rgba8888ToColor(data.color, input.readInt32()); + + const darkColor = input.readInt32(); + if (darkColor != -1) Color.rgb888ToColor((data.darkColor = new Color()), darkColor); + + data.attachmentName = input.readStringRef(); + data.blendMode = SkeletonBinary.BlendModeValues[input.readInt(true)]; + skeletonData.slots.push(data); + } + + // IK constraints. + n = input.readInt(true); + for (let i = 0, nn; i < n; i++) { + const data = new IkConstraintData(input.readString()); + data.order = input.readInt(true); + data.skinRequired = input.readBoolean(); + nn = input.readInt(true); + for (let ii = 0; ii < nn; ii++) data.bones.push(skeletonData.bones[input.readInt(true)]); + data.target = skeletonData.bones[input.readInt(true)]; + data.mix = input.readFloat(); + data.softness = input.readFloat() * scale; + data.bendDirection = input.readByte(); + data.compress = input.readBoolean(); + data.stretch = input.readBoolean(); + data.uniform = input.readBoolean(); + skeletonData.ikConstraints.push(data); + } + + // Transform constraints. + n = input.readInt(true); + for (let i = 0, nn; i < n; i++) { + const data = new TransformConstraintData(input.readString()); + data.order = input.readInt(true); + data.skinRequired = input.readBoolean(); + nn = input.readInt(true); + for (let ii = 0; ii < nn; ii++) data.bones.push(skeletonData.bones[input.readInt(true)]); + data.target = skeletonData.bones[input.readInt(true)]; + data.local = input.readBoolean(); + data.relative = input.readBoolean(); + data.offsetRotation = input.readFloat(); + data.offsetX = input.readFloat() * scale; + data.offsetY = input.readFloat() * scale; + data.offsetScaleX = input.readFloat(); + data.offsetScaleY = input.readFloat(); + data.offsetShearY = input.readFloat(); + data.rotateMix = input.readFloat(); + data.translateMix = input.readFloat(); + data.scaleMix = input.readFloat(); + data.shearMix = input.readFloat(); + skeletonData.transformConstraints.push(data); + } + + // Path constraints. + n = input.readInt(true); + for (let i = 0, nn; i < n; i++) { + const data = new PathConstraintData(input.readString()); + data.order = input.readInt(true); + data.skinRequired = input.readBoolean(); + nn = input.readInt(true); + for (let ii = 0; ii < nn; ii++) data.bones.push(skeletonData.bones[input.readInt(true)]); + data.target = skeletonData.slots[input.readInt(true)]; + data.positionMode = SkeletonBinary.PositionModeValues[input.readInt(true)]; + data.spacingMode = SkeletonBinary.SpacingModeValues[input.readInt(true)]; + data.rotateMode = SkeletonBinary.RotateModeValues[input.readInt(true)]; + data.offsetRotation = input.readFloat(); + data.position = input.readFloat(); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = input.readFloat(); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.rotateMix = input.readFloat(); + data.translateMix = input.readFloat(); + skeletonData.pathConstraints.push(data); + } + + // Default skin. + const defaultSkin = this.readSkin(input, skeletonData, true, nonessential); + if (defaultSkin != null) { + skeletonData.defaultSkin = defaultSkin; + skeletonData.skins.push(defaultSkin); + } + + // Skins. + { + let i = skeletonData.skins.length; + Utils.setArraySize(skeletonData.skins, (n = i + input.readInt(true))); + for (; i < n; i++) skeletonData.skins[i] = this.readSkin(input, skeletonData, false, nonessential); + } + + // Linked meshes. + n = this.linkedMeshes.length; + for (let i = 0; i < n; i++) { + const linkedMesh = this.linkedMeshes[i]; + const skin = linkedMesh.skin == null ? skeletonData.defaultSkin : skeletonData.findSkin(linkedMesh.skin); + if (skin == null) throw new Error("Skin not found: " + linkedMesh.skin); + const parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Error("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.deformAttachment = linkedMesh.inheritDeform ? (parent as VertexAttachment) : linkedMesh.mesh; + linkedMesh.mesh.setParentMesh(parent as MeshAttachment); + linkedMesh.mesh.updateUVs(); + } + this.linkedMeshes.length = 0; + + // Events. + n = input.readInt(true); + for (let i = 0; i < n; i++) { + const data = new EventData(input.readStringRef()); + data.intValue = input.readInt(false); + data.floatValue = input.readFloat(); + data.stringValue = input.readString(); + data.audioPath = input.readString(); + if (data.audioPath != null) { + data.volume = input.readFloat(); + data.balance = input.readFloat(); + } + skeletonData.events.push(data); + } + + // Animations. + n = input.readInt(true); + for (let i = 0; i < n; i++) + skeletonData.animations.push(this.readAnimation(input, input.readString(), skeletonData)); + return skeletonData; + } + + private readSkin(input: BinaryInput, skeletonData: SkeletonData, defaultSkin: boolean, nonessential: boolean): Skin { + let skin = null; + let slotCount = 0; + + if (defaultSkin) { + slotCount = input.readInt(true); + if (slotCount == 0) return null; + skin = new Skin("default"); + } else { + skin = new Skin(input.readStringRef()); + skin.bones.length = input.readInt(true); + for (let i = 0, n = skin.bones.length; i < n; i++) skin.bones[i] = skeletonData.bones[input.readInt(true)]; + + for (let i = 0, n = input.readInt(true); i < n; i++) + skin.constraints.push(skeletonData.ikConstraints[input.readInt(true)]); + for (let i = 0, n = input.readInt(true); i < n; i++) + skin.constraints.push(skeletonData.transformConstraints[input.readInt(true)]); + for (let i = 0, n = input.readInt(true); i < n; i++) + skin.constraints.push(skeletonData.pathConstraints[input.readInt(true)]); + + slotCount = input.readInt(true); + } + + for (let i = 0; i < slotCount; i++) { + const slotIndex = input.readInt(true); + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const name = input.readStringRef(); + const attachment = this.readAttachment(input, skeletonData, skin, slotIndex, name, nonessential); + if (attachment != null) skin.setAttachment(slotIndex, name, attachment); + } + } + return skin; + } + + private readAttachment( + input: BinaryInput, + skeletonData: SkeletonData, + skin: Skin, + slotIndex: number, + attachmentName: string, + nonessential: boolean + ): Attachment { + const scale = this.scale; + + let name = input.readStringRef(); + if (name == null) name = attachmentName; + + const typeIndex = input.readByte(); + const type = SkeletonBinary.AttachmentTypeValues[typeIndex]; + switch (type) { + case AttachmentType.Region: { + let path = input.readStringRef(); + const rotation = input.readFloat(); + const x = input.readFloat(); + const y = input.readFloat(); + const scaleX = input.readFloat(); + const scaleY = input.readFloat(); + const width = input.readFloat(); + const height = input.readFloat(); + const color = input.readInt32(); + + if (path == null) path = name; + const region = this.attachmentLoader.newRegionAttachment(skin, name, path); + if (region == null) return null; + region.path = path; + region.x = x * scale; + region.y = y * scale; + region.scaleX = scaleX; + region.scaleY = scaleY; + region.rotation = rotation; + region.width = width * scale; + region.height = height * scale; + Color.rgba8888ToColor(region.color, color); + region.updateOffset(); + return region; + } + case AttachmentType.BoundingBox: { + const vertexCount = input.readInt(true); + const vertices = this.readVertices(input, vertexCount); + const color = nonessential ? input.readInt32() : 0; + + const box = this.attachmentLoader.newBoundingBoxAttachment(skin, name); + if (box == null) return null; + box.worldVerticesLength = vertexCount << 1; + box.vertices = vertices.vertices; + box.bones = vertices.bones; + if (nonessential) Color.rgba8888ToColor(box.color, color); + return box; + } + case AttachmentType.Mesh: { + let path = input.readStringRef(); + const color = input.readInt32(); + const vertexCount = input.readInt(true); + const uvs = this.readFloatArray(input, vertexCount << 1, 1); + const triangles = this.readShortArray(input); + const vertices = this.readVertices(input, vertexCount); + const hullLength = input.readInt(true); + let edges = null; + let width = 0, + height = 0; + if (nonessential) { + edges = this.readShortArray(input); + width = input.readFloat(); + height = input.readFloat(); + } + + if (path == null) path = name; + const mesh = this.attachmentLoader.newMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.path = path; + Color.rgba8888ToColor(mesh.color, color); + mesh.bones = vertices.bones; + mesh.vertices = vertices.vertices; + mesh.worldVerticesLength = vertexCount << 1; + mesh.triangles = triangles; + mesh.regionUVs = uvs; + mesh.updateUVs(); + mesh.hullLength = hullLength << 1; + if (nonessential) { + mesh.edges = edges; + mesh.width = width * scale; + mesh.height = height * scale; + } + return mesh; + } + case AttachmentType.LinkedMesh: { + let path = input.readStringRef(); + const color = input.readInt32(); + const skinName = input.readStringRef(); + const parent = input.readStringRef(); + const inheritDeform = input.readBoolean(); + let width = 0, + height = 0; + if (nonessential) { + width = input.readFloat(); + height = input.readFloat(); + } + + if (path == null) path = name; + const mesh = this.attachmentLoader.newMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.path = path; + Color.rgba8888ToColor(mesh.color, color); + if (nonessential) { + mesh.width = width * scale; + mesh.height = height * scale; + } + this.linkedMeshes.push(new LinkedMesh(mesh, skinName, slotIndex, parent, inheritDeform)); + return mesh; + } + case AttachmentType.Path: { + const closed = input.readBoolean(); + const constantSpeed = input.readBoolean(); + const vertexCount = input.readInt(true); + const vertices = this.readVertices(input, vertexCount); + const lengths = Utils.newArray(vertexCount / 3, 0); + for (let i = 0, n = lengths.length; i < n; i++) lengths[i] = input.readFloat() * scale; + const color = nonessential ? input.readInt32() : 0; + + const path = this.attachmentLoader.newPathAttachment(skin, name); + if (path == null) return null; + path.closed = closed; + path.constantSpeed = constantSpeed; + path.worldVerticesLength = vertexCount << 1; + path.vertices = vertices.vertices; + path.bones = vertices.bones; + path.lengths = lengths; + if (nonessential) Color.rgba8888ToColor(path.color, color); + return path; + } + case AttachmentType.Point: { + const rotation = input.readFloat(); + const x = input.readFloat(); + const y = input.readFloat(); + const color = nonessential ? input.readInt32() : 0; + + const point = this.attachmentLoader.newPointAttachment(skin, name); + if (point == null) return null; + point.x = x * scale; + point.y = y * scale; + point.rotation = rotation; + if (nonessential) Color.rgba8888ToColor(point.color, color); + return point; + } + case AttachmentType.Clipping: { + const endSlotIndex = input.readInt(true); + const vertexCount = input.readInt(true); + const vertices = this.readVertices(input, vertexCount); + const color = nonessential ? input.readInt32() : 0; + + const clip = this.attachmentLoader.newClippingAttachment(skin, name); + if (clip == null) return null; + clip.endSlot = skeletonData.slots[endSlotIndex]; + clip.worldVerticesLength = vertexCount << 1; + clip.vertices = vertices.vertices; + clip.bones = vertices.bones; + if (nonessential) Color.rgba8888ToColor(clip.color, color); + return clip; + } + } + return null; + } + + private readVertices(input: BinaryInput, vertexCount: number): Vertices { + const verticesLength = vertexCount << 1; + const vertices = new Vertices(); + const scale = this.scale; + if (!input.readBoolean()) { + vertices.vertices = this.readFloatArray(input, verticesLength, scale); + return vertices; + } + const weights = new Array(); + const bonesArray = new Array(); + for (let i = 0; i < vertexCount; i++) { + const boneCount = input.readInt(true); + bonesArray.push(boneCount); + for (let ii = 0; ii < boneCount; ii++) { + bonesArray.push(input.readInt(true)); + weights.push(input.readFloat() * scale); + weights.push(input.readFloat() * scale); + weights.push(input.readFloat()); + } + } + vertices.vertices = Utils.toFloatArray(weights); + vertices.bones = bonesArray; + return vertices; + } + + private readFloatArray(input: BinaryInput, n: number, scale: number): number[] { + const array = new Array(n); + if (scale == 1) { + for (let i = 0; i < n; i++) array[i] = input.readFloat(); + } else { + for (let i = 0; i < n; i++) array[i] = input.readFloat() * scale; + } + return array; + } + + private readShortArray(input: BinaryInput): number[] { + const n = input.readInt(true); + const array = new Array(n); + for (let i = 0; i < n; i++) array[i] = input.readShort(); + return array; + } + + private readAnimation(input: BinaryInput, name: string, skeletonData: SkeletonData): Animation { + const timelines = new Array(); + const scale = this.scale; + let duration = 0; + const tempColor1 = new Color(); + const tempColor2 = new Color(); + + // Slot timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const slotIndex = input.readInt(true); + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const timelineType = input.readByte(); + const frameCount = input.readInt(true); + switch (timelineType) { + case SkeletonBinary.SLOT_ATTACHMENT: { + const timeline = new AttachmentTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) + timeline.setFrame(frameIndex, input.readFloat(), input.readStringRef()); + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[frameCount - 1]); + break; + } + case SkeletonBinary.SLOT_COLOR: { + const timeline = new ColorTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + const time = input.readFloat(); + Color.rgba8888ToColor(tempColor1, input.readInt32()); + timeline.setFrame(frameIndex, time, tempColor1.r, tempColor1.g, tempColor1.b, tempColor1.a); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * ColorTimeline.ENTRIES]); + break; + } + case SkeletonBinary.SLOT_TWO_COLOR: { + const timeline = new TwoColorTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + const time = input.readFloat(); + Color.rgba8888ToColor(tempColor1, input.readInt32()); + Color.rgb888ToColor(tempColor2, input.readInt32()); + timeline.setFrame( + frameIndex, + time, + tempColor1.r, + tempColor1.g, + tempColor1.b, + tempColor1.a, + tempColor2.r, + tempColor2.g, + tempColor2.b + ); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * TwoColorTimeline.ENTRIES]); + break; + } + } + } + } + + // Bone timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const boneIndex = input.readInt(true); + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const timelineType = input.readByte(); + const frameCount = input.readInt(true); + switch (timelineType) { + case SkeletonBinary.BONE_ROTATE: { + const timeline = new RotateTimeline(frameCount); + timeline.boneIndex = boneIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame(frameIndex, input.readFloat(), input.readFloat()); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * RotateTimeline.ENTRIES]); + break; + } + case SkeletonBinary.BONE_TRANSLATE: + case SkeletonBinary.BONE_SCALE: + case SkeletonBinary.BONE_SHEAR: { + let timeline; + let timelineScale = 1; + if (timelineType == SkeletonBinary.BONE_SCALE) timeline = new ScaleTimeline(frameCount); + else if (timelineType == SkeletonBinary.BONE_SHEAR) timeline = new ShearTimeline(frameCount); + else { + timeline = new TranslateTimeline(frameCount); + timelineScale = scale; + } + timeline.boneIndex = boneIndex; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame( + frameIndex, + input.readFloat(), + input.readFloat() * timelineScale, + input.readFloat() * timelineScale + ); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * TranslateTimeline.ENTRIES]); + break; + } + } + } + } + + // IK constraint timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const index = input.readInt(true); + const frameCount = input.readInt(true); + const timeline = new IkConstraintTimeline(frameCount); + timeline.ikConstraintIndex = index; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame( + frameIndex, + input.readFloat(), + input.readFloat(), + input.readFloat() * scale, + input.readByte(), + input.readBoolean(), + input.readBoolean() + ); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * IkConstraintTimeline.ENTRIES]); + } + + // Transform constraint timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const index = input.readInt(true); + const frameCount = input.readInt(true); + const timeline = new TransformConstraintTimeline(frameCount); + timeline.transformConstraintIndex = index; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame( + frameIndex, + input.readFloat(), + input.readFloat(), + input.readFloat(), + input.readFloat(), + input.readFloat() + ); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * TransformConstraintTimeline.ENTRIES]); + } + + // Path constraint timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const index = input.readInt(true); + const data = skeletonData.pathConstraints[index]; + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const timelineType = input.readByte(); + const frameCount = input.readInt(true); + switch (timelineType) { + case SkeletonBinary.PATH_POSITION: + case SkeletonBinary.PATH_SPACING: { + let timeline; + let timelineScale = 1; + if (timelineType == SkeletonBinary.PATH_SPACING) { + timeline = new PathConstraintSpacingTimeline(frameCount); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) + timelineScale = scale; + } else { + timeline = new PathConstraintPositionTimeline(frameCount); + if (data.positionMode == PositionMode.Fixed) timelineScale = scale; + } + timeline.pathConstraintIndex = index; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame(frameIndex, input.readFloat(), input.readFloat() * timelineScale); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * PathConstraintPositionTimeline.ENTRIES]); + break; + } + case SkeletonBinary.PATH_MIX: { + const timeline = new PathConstraintMixTimeline(frameCount); + timeline.pathConstraintIndex = index; + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.setFrame(frameIndex, input.readFloat(), input.readFloat(), input.readFloat()); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(frameCount - 1) * PathConstraintMixTimeline.ENTRIES]); + break; + } + } + } + } + + // Deform timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const skin = skeletonData.skins[input.readInt(true)]; + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const slotIndex = input.readInt(true); + for (let iii = 0, nnn = input.readInt(true); iii < nnn; iii++) { + const attachment = skin.getAttachment(slotIndex, input.readStringRef()) as VertexAttachment; + const weighted = attachment.bones != null; + const vertices = attachment.vertices; + const deformLength = weighted ? (vertices.length / 3) * 2 : vertices.length; + + const frameCount = input.readInt(true); + const timeline = new DeformTimeline(frameCount); + timeline.slotIndex = slotIndex; + timeline.attachment = attachment; + + for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) { + const time = input.readFloat(); + let deform; + let end = input.readInt(true); + if (end == 0) deform = weighted ? Utils.newFloatArray(deformLength) : vertices; + else { + deform = Utils.newFloatArray(deformLength); + const start = input.readInt(true); + end += start; + if (scale == 1) { + for (let v = start; v < end; v++) deform[v] = input.readFloat(); + } else { + for (let v = start; v < end; v++) deform[v] = input.readFloat() * scale; + } + if (!weighted) { + for (let v = 0, vn = deform.length; v < vn; v++) deform[v] += vertices[v]; + } + } + + timeline.setFrame(frameIndex, time, deform); + if (frameIndex < frameCount - 1) this.readCurve(input, frameIndex, timeline); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[frameCount - 1]); + } + } + } + + // Draw order timeline. + const drawOrderCount = input.readInt(true); + if (drawOrderCount > 0) { + const timeline = new DrawOrderTimeline(drawOrderCount); + const slotCount = skeletonData.slots.length; + for (let i = 0; i < drawOrderCount; i++) { + const time = input.readFloat(); + const offsetCount = input.readInt(true); + const drawOrder = Utils.newArray(slotCount, 0); + for (let ii = slotCount - 1; ii >= 0; ii--) drawOrder[ii] = -1; + const unchanged = Utils.newArray(slotCount - offsetCount, 0); + let originalIndex = 0, + unchangedIndex = 0; + for (let ii = 0; ii < offsetCount; ii++) { + const slotIndex = input.readInt(true); + // Collect unchanged items. + while (originalIndex != slotIndex) unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + drawOrder[originalIndex + input.readInt(true)] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (let ii = slotCount - 1; ii >= 0; ii--) + if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex]; + timeline.setFrame(i, time, drawOrder); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[drawOrderCount - 1]); + } + + // Event timeline. + const eventCount = input.readInt(true); + if (eventCount > 0) { + const timeline = new EventTimeline(eventCount); + for (let i = 0; i < eventCount; i++) { + const time = input.readFloat(); + const eventData = skeletonData.events[input.readInt(true)]; + const event = new Event(time, eventData); + event.intValue = input.readInt(false); + event.floatValue = input.readFloat(); + event.stringValue = input.readBoolean() ? input.readString() : eventData.stringValue; + if (event.data.audioPath != null) { + event.volume = input.readFloat(); + event.balance = input.readFloat(); + } + timeline.setFrame(i, event); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[eventCount - 1]); + } + + return new Animation(name, timelines, duration); + } + + private readCurve(input: BinaryInput, frameIndex: number, timeline: CurveTimeline) { + switch (input.readByte()) { + case SkeletonBinary.CURVE_STEPPED: + timeline.setStepped(frameIndex); + break; + case SkeletonBinary.CURVE_BEZIER: + this.setCurve(timeline, frameIndex, input.readFloat(), input.readFloat(), input.readFloat(), input.readFloat()); + break; + } + } + + setCurve(timeline: CurveTimeline, frameIndex: number, cx1: number, cy1: number, cx2: number, cy2: number) { + timeline.setCurve(frameIndex, cx1, cy1, cx2, cy2); + } +} + +class BinaryInput { + constructor( + data: Uint8Array, + public strings = new Array(), + private index: number = 0, + private buffer = new DataView(data.buffer) + ) {} + + readByte(): number { + return this.buffer.getInt8(this.index++); + } + + readShort(): number { + const value = this.buffer.getInt16(this.index); + this.index += 2; + return value; + } + + readInt32(): number { + const value = this.buffer.getInt32(this.index); + this.index += 4; + return value; + } + + readInt(optimizePositive: boolean) { + let b = this.readByte(); + let result = b & 0x7f; + if ((b & 0x80) != 0) { + b = this.readByte(); + result |= (b & 0x7f) << 7; + if ((b & 0x80) != 0) { + b = this.readByte(); + result |= (b & 0x7f) << 14; + if ((b & 0x80) != 0) { + b = this.readByte(); + result |= (b & 0x7f) << 21; + if ((b & 0x80) != 0) { + b = this.readByte(); + result |= (b & 0x7f) << 28; + } + } + } + } + return optimizePositive ? result : (result >>> 1) ^ -(result & 1); + } + + readStringRef(): string { + const index = this.readInt(true); + return index == 0 ? null : this.strings[index - 1]; + } + + readString(): string { + let byteCount = this.readInt(true); + switch (byteCount) { + case 0: + return null; + case 1: + return ""; + } + byteCount--; + let chars = ""; + const charCount = 0; + for (let i = 0; i < byteCount; ) { + const b = this.readByte(); + switch (b >> 4) { + case 12: + case 13: + chars += String.fromCharCode(((b & 0x1f) << 6) | (this.readByte() & 0x3f)); + i += 2; + break; + case 14: + chars += String.fromCharCode(((b & 0x0f) << 12) | ((this.readByte() & 0x3f) << 6) | (this.readByte() & 0x3f)); + i += 3; + break; + default: + chars += String.fromCharCode(b); + i++; + } + } + return chars; + } + + readFloat(): number { + const value = this.buffer.getFloat32(this.index); + this.index += 4; + return value; + } + + readBoolean(): boolean { + return this.readByte() != 0; + } +} + +class LinkedMesh { + parent: string; + skin: string; + slotIndex: number; + mesh: MeshAttachment; + inheritDeform: boolean; + + constructor(mesh: MeshAttachment, skin: string, slotIndex: number, parent: string, inheritDeform: boolean) { + this.mesh = mesh; + this.skin = skin; + this.slotIndex = slotIndex; + this.parent = parent; + this.inheritDeform = inheritDeform; + } +} + +class Vertices { + constructor( + public bones: Array = null, + public vertices: Array | Float32Array = null + ) {} +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonBounds.ts b/packages/spine-core-3.8/src/spine-core/SkeletonBounds.ts new file mode 100644 index 0000000000..6c7323ba77 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonBounds.ts @@ -0,0 +1,215 @@ +import { BoundingBoxAttachment } from "./attachments/BoundingBoxAttachment"; +import { Pool, Utils } from "./Utils"; +import { Skeleton } from "./Skeleton"; + +/** Collects each visible {@link BoundingBoxAttachment} and computes the world vertices for its polygon. The polygon vertices are + * provided along with convenience methods for doing hit detection. */ +export class SkeletonBounds { + /** The left edge of the axis aligned bounding box. */ + minX = 0; + + /** The bottom edge of the axis aligned bounding box. */ + minY = 0; + + /** The right edge of the axis aligned bounding box. */ + maxX = 0; + + /** The top edge of the axis aligned bounding box. */ + maxY = 0; + + /** The visible bounding boxes. */ + boundingBoxes = new Array(); + + /** The world vertices for the bounding box polygons. */ + polygons = new Array>(); + + private polygonPool = new Pool>(() => { + return Utils.newFloatArray(16); + }); + + /** Clears any previous polygons, finds all visible bounding box attachments, and computes the world vertices for each bounding + * box's polygon. + * @param updateAabb If true, the axis aligned bounding box containing all the polygons is computed. If false, the + * SkeletonBounds AABB methods will always return true. */ + update(skeleton: Skeleton, updateAabb: boolean) { + if (skeleton == null) throw new Error("skeleton cannot be null."); + const boundingBoxes = this.boundingBoxes; + const polygons = this.polygons; + const polygonPool = this.polygonPool; + const slots = skeleton.slots; + const slotCount = slots.length; + + boundingBoxes.length = 0; + polygonPool.freeAll(polygons); + polygons.length = 0; + + for (let i = 0; i < slotCount; i++) { + const slot = slots[i]; + if (!slot.bone.active) continue; + const attachment = slot.getAttachment(); + if (attachment instanceof BoundingBoxAttachment) { + const boundingBox = attachment as BoundingBoxAttachment; + boundingBoxes.push(boundingBox); + + let polygon = polygonPool.obtain(); + if (polygon.length != boundingBox.worldVerticesLength) { + polygon = Utils.newFloatArray(boundingBox.worldVerticesLength); + } + polygons.push(polygon); + boundingBox.computeWorldVertices(slot, 0, boundingBox.worldVerticesLength, polygon, 0, 2); + } + } + + if (updateAabb) { + this.aabbCompute(); + } else { + this.minX = Number.POSITIVE_INFINITY; + this.minY = Number.POSITIVE_INFINITY; + this.maxX = Number.NEGATIVE_INFINITY; + this.maxY = Number.NEGATIVE_INFINITY; + } + } + + aabbCompute() { + let minX = Number.POSITIVE_INFINITY, + minY = Number.POSITIVE_INFINITY, + maxX = Number.NEGATIVE_INFINITY, + maxY = Number.NEGATIVE_INFINITY; + const polygons = this.polygons; + for (let i = 0, n = polygons.length; i < n; i++) { + const polygon = polygons[i]; + const vertices = polygon; + for (let ii = 0, nn = polygon.length; ii < nn; ii += 2) { + const x = vertices[ii]; + const y = vertices[ii + 1]; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + } + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + /** Returns true if the axis aligned bounding box contains the point. */ + aabbContainsPoint(x: number, y: number) { + return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY; + } + + /** Returns true if the axis aligned bounding box intersects the line segment. */ + aabbIntersectsSegment(x1: number, y1: number, x2: number, y2: number) { + const minX = this.minX; + const minY = this.minY; + const maxX = this.maxX; + const maxY = this.maxY; + if ( + (x1 <= minX && x2 <= minX) || + (y1 <= minY && y2 <= minY) || + (x1 >= maxX && x2 >= maxX) || + (y1 >= maxY && y2 >= maxY) + ) + return false; + const m = (y2 - y1) / (x2 - x1); + let y = m * (minX - x1) + y1; + if (y > minY && y < maxY) return true; + y = m * (maxX - x1) + y1; + if (y > minY && y < maxY) return true; + let x = (minY - y1) / m + x1; + if (x > minX && x < maxX) return true; + x = (maxY - y1) / m + x1; + if (x > minX && x < maxX) return true; + return false; + } + + /** Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. */ + aabbIntersectsSkeleton(bounds: SkeletonBounds) { + return this.minX < bounds.maxX && this.maxX > bounds.minX && this.minY < bounds.maxY && this.maxY > bounds.minY; + } + + /** Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more + * efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. */ + containsPoint(x: number, y: number): BoundingBoxAttachment { + const polygons = this.polygons; + for (let i = 0, n = polygons.length; i < n; i++) + if (this.containsPointPolygon(polygons[i], x, y)) return this.boundingBoxes[i]; + return null; + } + + /** Returns true if the polygon contains the point. */ + containsPointPolygon(polygon: ArrayLike, x: number, y: number) { + const vertices = polygon; + const nn = polygon.length; + + let prevIndex = nn - 2; + let inside = false; + for (let ii = 0; ii < nn; ii += 2) { + const vertexY = vertices[ii + 1]; + const prevY = vertices[prevIndex + 1]; + if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { + const vertexX = vertices[ii]; + if (vertexX + ((y - vertexY) / (prevY - vertexY)) * (vertices[prevIndex] - vertexX) < x) inside = !inside; + } + prevIndex = ii; + } + return inside; + } + + /** Returns the first bounding box attachment that contains any part of the line segment, or null. When doing many checks, it + * is usually more efficient to only call this method if {@link #aabbIntersectsSegment()} returns + * true. */ + intersectsSegment(x1: number, y1: number, x2: number, y2: number) { + const polygons = this.polygons; + for (let i = 0, n = polygons.length; i < n; i++) + if (this.intersectsSegmentPolygon(polygons[i], x1, y1, x2, y2)) return this.boundingBoxes[i]; + return null; + } + + /** Returns true if the polygon contains any part of the line segment. */ + intersectsSegmentPolygon(polygon: ArrayLike, x1: number, y1: number, x2: number, y2: number) { + const vertices = polygon; + const nn = polygon.length; + + const width12 = x1 - x2, + height12 = y1 - y2; + const det1 = x1 * y2 - y1 * x2; + let x3 = vertices[nn - 2], + y3 = vertices[nn - 1]; + for (let ii = 0; ii < nn; ii += 2) { + const x4 = vertices[ii], + y4 = vertices[ii + 1]; + const det2 = x3 * y4 - y3 * x4; + const width34 = x3 - x4, + height34 = y3 - y4; + const det3 = width12 * height34 - height12 * width34; + const x = (det1 * width34 - width12 * det2) / det3; + if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { + const y = (det1 * height34 - height12 * det2) / det3; + if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) + return true; + } + x3 = x4; + y3 = y4; + } + return false; + } + + /** Returns the polygon for the specified bounding box, or null. */ + getPolygon(boundingBox: BoundingBoxAttachment) { + if (boundingBox == null) throw new Error("boundingBox cannot be null."); + const index = this.boundingBoxes.indexOf(boundingBox); + return index == -1 ? null : this.polygons[index]; + } + + /** The width of the axis aligned bounding box. */ + getWidth() { + return this.maxX - this.minX; + } + + /** The height of the axis aligned bounding box. */ + getHeight() { + return this.maxY - this.minY; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonClipping.ts b/packages/spine-core-3.8/src/spine-core/SkeletonClipping.ts new file mode 100644 index 0000000000..563a93c1f6 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonClipping.ts @@ -0,0 +1,363 @@ +import { Triangulator } from "./Triangulator"; +import { ClippingAttachment } from "./attachments/ClippingAttachment"; +import { Slot } from "./Slot"; +import { Utils, Color, ArrayLike } from "./Utils"; + +export class SkeletonClipping { + private triangulator = new Triangulator(); + private clippingPolygon = new Array(); + private clipOutput = new Array(); + clippedVertices = new Array(); + clippedTriangles = new Array(); + private scratch = new Array(); + + private clipAttachment: ClippingAttachment; + private clippingPolygons: Array>; + + clipStart(slot: Slot, clip: ClippingAttachment): number { + if (this.clipAttachment != null) return 0; + this.clipAttachment = clip; + + const n = clip.worldVerticesLength; + const vertices = Utils.setArraySize(this.clippingPolygon, n); + clip.computeWorldVertices(slot, 0, n, vertices, 0, 2); + const clippingPolygon = this.clippingPolygon; + SkeletonClipping.makeClockwise(clippingPolygon); + const clippingPolygons = (this.clippingPolygons = this.triangulator.decompose( + clippingPolygon, + this.triangulator.triangulate(clippingPolygon) + )); + for (let i = 0, n = clippingPolygons.length; i < n; i++) { + const polygon = clippingPolygons[i]; + SkeletonClipping.makeClockwise(polygon); + polygon.push(polygon[0]); + polygon.push(polygon[1]); + } + + return clippingPolygons.length; + } + + clipEndWithSlot(slot: Slot) { + if (this.clipAttachment != null && this.clipAttachment.endSlot == slot.data) this.clipEnd(); + } + + clipEnd() { + if (this.clipAttachment == null) return; + this.clipAttachment = null; + this.clippingPolygons = null; + this.clippedVertices.length = 0; + this.clippedTriangles.length = 0; + this.clippingPolygon.length = 0; + } + + isClipping(): boolean { + return this.clipAttachment != null; + } + + clipTriangles( + vertices: ArrayLike, + verticesLength: number, + triangles: ArrayLike, + trianglesLength: number, + uvs: ArrayLike, + light: Color, + dark: Color, + twoColor: boolean + ) { + const clipOutput = this.clipOutput, + clippedVertices = this.clippedVertices; + const clippedTriangles = this.clippedTriangles; + const polygons = this.clippingPolygons; + const polygonsCount = this.clippingPolygons.length; + const vertexSize = twoColor ? 12 : 8; + + let index = 0; + clippedVertices.length = 0; + clippedTriangles.length = 0; + outer: for (let i = 0; i < trianglesLength; i += 3) { + let vertexOffset = triangles[i] << 1; + const x1 = vertices[vertexOffset], + y1 = vertices[vertexOffset + 1]; + const u1 = uvs[vertexOffset], + v1 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 1] << 1; + const x2 = vertices[vertexOffset], + y2 = vertices[vertexOffset + 1]; + const u2 = uvs[vertexOffset], + v2 = uvs[vertexOffset + 1]; + + vertexOffset = triangles[i + 2] << 1; + const x3 = vertices[vertexOffset], + y3 = vertices[vertexOffset + 1]; + const u3 = uvs[vertexOffset], + v3 = uvs[vertexOffset + 1]; + + for (let p = 0; p < polygonsCount; p++) { + let s = clippedVertices.length; + if (this.clip(x1, y1, x2, y2, x3, y3, polygons[p], clipOutput)) { + const clipOutputLength = clipOutput.length; + if (clipOutputLength == 0) continue; + const d0 = y2 - y3, + d1 = x3 - x2, + d2 = x1 - x3, + d4 = y3 - y1; + const d = 1 / (d0 * d2 + d1 * (y1 - y3)); + + let clipOutputCount = clipOutputLength >> 1; + const clipOutputItems = this.clipOutput; + const clippedVerticesItems = Utils.setArraySize(clippedVertices, s + clipOutputCount * vertexSize); + for (let ii = 0; ii < clipOutputLength; ii += 2) { + const x = clipOutputItems[ii], + y = clipOutputItems[ii + 1]; + clippedVerticesItems[s] = x; + clippedVerticesItems[s + 1] = y; + clippedVerticesItems[s + 2] = light.r; + clippedVerticesItems[s + 3] = light.g; + clippedVerticesItems[s + 4] = light.b; + clippedVerticesItems[s + 5] = light.a; + const c0 = x - x3, + c1 = y - y3; + const a = (d0 * c0 + d1 * c1) * d; + const b = (d4 * c0 + d2 * c1) * d; + const c = 1 - a - b; + clippedVerticesItems[s + 6] = u1 * a + u2 * b + u3 * c; + clippedVerticesItems[s + 7] = v1 * a + v2 * b + v3 * c; + if (twoColor) { + clippedVerticesItems[s + 8] = dark.r; + clippedVerticesItems[s + 9] = dark.g; + clippedVerticesItems[s + 10] = dark.b; + clippedVerticesItems[s + 11] = dark.a; + } + s += vertexSize; + } + + s = clippedTriangles.length; + const clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3 * (clipOutputCount - 2)); + clipOutputCount--; + for (let ii = 1; ii < clipOutputCount; ii++) { + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + ii; + clippedTrianglesItems[s + 2] = index + ii + 1; + s += 3; + } + index += clipOutputCount + 1; + } else { + const clippedVerticesItems = Utils.setArraySize(clippedVertices, s + 3 * vertexSize); + clippedVerticesItems[s] = x1; + clippedVerticesItems[s + 1] = y1; + clippedVerticesItems[s + 2] = light.r; + clippedVerticesItems[s + 3] = light.g; + clippedVerticesItems[s + 4] = light.b; + clippedVerticesItems[s + 5] = light.a; + if (!twoColor) { + clippedVerticesItems[s + 6] = u1; + clippedVerticesItems[s + 7] = v1; + + clippedVerticesItems[s + 8] = x2; + clippedVerticesItems[s + 9] = y2; + clippedVerticesItems[s + 10] = light.r; + clippedVerticesItems[s + 11] = light.g; + clippedVerticesItems[s + 12] = light.b; + clippedVerticesItems[s + 13] = light.a; + clippedVerticesItems[s + 14] = u2; + clippedVerticesItems[s + 15] = v2; + + clippedVerticesItems[s + 16] = x3; + clippedVerticesItems[s + 17] = y3; + clippedVerticesItems[s + 18] = light.r; + clippedVerticesItems[s + 19] = light.g; + clippedVerticesItems[s + 20] = light.b; + clippedVerticesItems[s + 21] = light.a; + clippedVerticesItems[s + 22] = u3; + clippedVerticesItems[s + 23] = v3; + } else { + clippedVerticesItems[s + 6] = u1; + clippedVerticesItems[s + 7] = v1; + clippedVerticesItems[s + 8] = dark.r; + clippedVerticesItems[s + 9] = dark.g; + clippedVerticesItems[s + 10] = dark.b; + clippedVerticesItems[s + 11] = dark.a; + + clippedVerticesItems[s + 12] = x2; + clippedVerticesItems[s + 13] = y2; + clippedVerticesItems[s + 14] = light.r; + clippedVerticesItems[s + 15] = light.g; + clippedVerticesItems[s + 16] = light.b; + clippedVerticesItems[s + 17] = light.a; + clippedVerticesItems[s + 18] = u2; + clippedVerticesItems[s + 19] = v2; + clippedVerticesItems[s + 20] = dark.r; + clippedVerticesItems[s + 21] = dark.g; + clippedVerticesItems[s + 22] = dark.b; + clippedVerticesItems[s + 23] = dark.a; + + clippedVerticesItems[s + 24] = x3; + clippedVerticesItems[s + 25] = y3; + clippedVerticesItems[s + 26] = light.r; + clippedVerticesItems[s + 27] = light.g; + clippedVerticesItems[s + 28] = light.b; + clippedVerticesItems[s + 29] = light.a; + clippedVerticesItems[s + 30] = u3; + clippedVerticesItems[s + 31] = v3; + clippedVerticesItems[s + 32] = dark.r; + clippedVerticesItems[s + 33] = dark.g; + clippedVerticesItems[s + 34] = dark.b; + clippedVerticesItems[s + 35] = dark.a; + } + + s = clippedTriangles.length; + const clippedTrianglesItems = Utils.setArraySize(clippedTriangles, s + 3); + clippedTrianglesItems[s] = index; + clippedTrianglesItems[s + 1] = index + 1; + clippedTrianglesItems[s + 2] = index + 2; + index += 3; + continue outer; + } + } + } + } + + /** Clips the input triangle against the convex, clockwise clipping area. If the triangle lies entirely within the clipping + * area, false is returned. The clipping area must duplicate the first vertex at the end of the vertices list. */ + clip( + x1: number, + y1: number, + x2: number, + y2: number, + x3: number, + y3: number, + clippingArea: Array, + output: Array + ) { + const originalOutput = output; + let clipped = false; + + // Avoid copy at the end. + let input: Array = null; + if (clippingArea.length % 4 >= 2) { + input = output; + output = this.scratch; + } else input = this.scratch; + + input.length = 0; + input.push(x1); + input.push(y1); + input.push(x2); + input.push(y2); + input.push(x3); + input.push(y3); + input.push(x1); + input.push(y1); + output.length = 0; + + const clippingVertices = clippingArea; + const clippingVerticesLast = clippingArea.length - 4; + for (let i = 0; ; i += 2) { + const edgeX = clippingVertices[i], + edgeY = clippingVertices[i + 1]; + const edgeX2 = clippingVertices[i + 2], + edgeY2 = clippingVertices[i + 3]; + const deltaX = edgeX - edgeX2, + deltaY = edgeY - edgeY2; + + const inputVertices = input; + const inputVerticesLength = input.length - 2, + outputStart = output.length; + for (let ii = 0; ii < inputVerticesLength; ii += 2) { + const inputX = inputVertices[ii], + inputY = inputVertices[ii + 1]; + const inputX2 = inputVertices[ii + 2], + inputY2 = inputVertices[ii + 3]; + const side2 = deltaX * (inputY2 - edgeY2) - deltaY * (inputX2 - edgeX2) > 0; + if (deltaX * (inputY - edgeY2) - deltaY * (inputX - edgeX2) > 0) { + if (side2) { + // v1 inside, v2 inside + output.push(inputX2); + output.push(inputY2); + continue; + } + // v1 inside, v2 outside + const c0 = inputY2 - inputY, + c2 = inputX2 - inputX; + const s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.abs(s) > 0.000001) { + const ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.push(edgeX + (edgeX2 - edgeX) * ua); + output.push(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.push(edgeX); + output.push(edgeY); + } + } else if (side2) { + // v1 outside, v2 inside + const c0 = inputY2 - inputY, + c2 = inputX2 - inputX; + const s = c0 * (edgeX2 - edgeX) - c2 * (edgeY2 - edgeY); + if (Math.abs(s) > 0.000001) { + const ua = (c2 * (edgeY - inputY) - c0 * (edgeX - inputX)) / s; + output.push(edgeX + (edgeX2 - edgeX) * ua); + output.push(edgeY + (edgeY2 - edgeY) * ua); + } else { + output.push(edgeX); + output.push(edgeY); + } + output.push(inputX2); + output.push(inputY2); + } + clipped = true; + } + + if (outputStart == output.length) { + // All edges outside. + originalOutput.length = 0; + return true; + } + + output.push(output[0]); + output.push(output[1]); + + if (i == clippingVerticesLast) break; + const temp = output; + output = input; + output.length = 0; + input = temp; + } + + if (originalOutput != output) { + originalOutput.length = 0; + for (let i = 0, n = output.length - 2; i < n; i++) originalOutput[i] = output[i]; + } else originalOutput.length = originalOutput.length - 2; + + return clipped; + } + + public static makeClockwise(polygon: ArrayLike) { + const vertices = polygon; + const verticeslength = polygon.length; + + let area = vertices[verticeslength - 2] * vertices[1] - vertices[0] * vertices[verticeslength - 1], + p1x = 0, + p1y = 0, + p2x = 0, + p2y = 0; + for (let i = 0, n = verticeslength - 3; i < n; i += 2) { + p1x = vertices[i]; + p1y = vertices[i + 1]; + p2x = vertices[i + 2]; + p2y = vertices[i + 3]; + area += p1x * p2y - p2x * p1y; + } + if (area < 0) return; + + for (let i = 0, lastX = verticeslength - 2, n = verticeslength >> 1; i < n; i += 2) { + const x = vertices[i], + y = vertices[i + 1]; + const other = lastX - i; + vertices[i] = vertices[other]; + vertices[i + 1] = vertices[other + 1]; + vertices[other] = x; + vertices[other + 1] = y; + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonData.ts b/packages/spine-core-3.8/src/spine-core/SkeletonData.ts new file mode 100644 index 0000000000..67c4f657bb --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonData.ts @@ -0,0 +1,198 @@ +import { BoneData } from "./BoneData"; +import { SlotData } from "./SlotData"; +import { Skin } from "./Skin"; +import { EventData } from "./EventData"; +import { IkConstraintData } from "./IkConstraintData"; +import { TransformConstraintData } from "./TransformConstraintData"; +import { PathConstraintData } from "./PathConstraintData"; +import { Animation } from "./Animation"; + +/** Stores the setup pose and all of the stateless data for a skeleton. + * + * See [Data objects](http://esotericsoftware.com/spine-runtime-architecture#Data-objects) in the Spine Runtimes + * Guide. */ +export class SkeletonData { + /** The skeleton's name, which by default is the name of the skeleton data file, if possible. May be null. */ + name: string; + + /** The skeleton's bones, sorted parent first. The root bone is always the first bone. */ + bones = new Array(); // Ordered parents first. + + /** The skeleton's slots. */ + slots = new Array(); // Setup pose draw order. + skins = new Array(); + + /** The skeleton's default skin. By default this skin contains all attachments that were not in a skin in Spine. + * + * See {@link Skeleton#getAttachmentByName()}. + * May be null. */ + defaultSkin: Skin; + + /** The skeleton's events. */ + events = new Array(); + + /** The skeleton's animations. */ + animations = new Array(); + + /** The skeleton's IK constraints. */ + ikConstraints = new Array(); + + /** The skeleton's transform constraints. */ + transformConstraints = new Array(); + + /** The skeleton's path constraints. */ + pathConstraints = new Array(); + + /** The X coordinate of the skeleton's axis aligned bounding box in the setup pose. */ + x: number; + + /** The Y coordinate of the skeleton's axis aligned bounding box in the setup pose. */ + y: number; + + /** The width of the skeleton's axis aligned bounding box in the setup pose. */ + width: number; + + /** The height of the skeleton's axis aligned bounding box in the setup pose. */ + height: number; + + /** The Spine version used to export the skeleton data, or null. */ + version: string; + + /** The skeleton data hash. This value will change if any of the skeleton data has changed. May be null. */ + hash: string; + + // Nonessential + /** The dopesheet FPS in Spine. Available only when nonessential data was exported. */ + fps = 0; + + /** The path to the images directory as defined in Spine. Available only when nonessential data was exported. May be null. */ + imagesPath: string; + + /** The path to the audio directory as defined in Spine. Available only when nonessential data was exported. May be null. */ + audioPath: string; + + /** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it + * multiple times. + * @returns May be null. */ + findBone(boneName: string) { + if (boneName == null) throw new Error("boneName cannot be null."); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (bone.name == boneName) return bone; + } + return null; + } + + findBoneIndex(boneName: string) { + if (boneName == null) throw new Error("boneName cannot be null."); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) if (bones[i].name == boneName) return i; + return -1; + } + + /** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it + * multiple times. + * @returns May be null. */ + findSlot(slotName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) { + const slot = slots[i]; + if (slot.name == slotName) return slot; + } + return null; + } + + findSlotIndex(slotName: string) { + if (slotName == null) throw new Error("slotName cannot be null."); + const slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) if (slots[i].name == slotName) return i; + return -1; + } + + /** Finds a skin by comparing each skin's name. It is more efficient to cache the results of this method than to call it + * multiple times. + * @returns May be null. */ + findSkin(skinName: string) { + if (skinName == null) throw new Error("skinName cannot be null."); + const skins = this.skins; + for (let i = 0, n = skins.length; i < n; i++) { + const skin = skins[i]; + if (skin.name == skinName) return skin; + } + return null; + } + + /** Finds an event by comparing each events's name. It is more efficient to cache the results of this method than to call it + * multiple times. + * @returns May be null. */ + findEvent(eventDataName: string) { + if (eventDataName == null) throw new Error("eventDataName cannot be null."); + const events = this.events; + for (let i = 0, n = events.length; i < n; i++) { + const event = events[i]; + if (event.name == eventDataName) return event; + } + return null; + } + + /** Finds an animation by comparing each animation's name. It is more efficient to cache the results of this method than to + * call it multiple times. + * @returns May be null. */ + findAnimation(animationName: string) { + if (animationName == null) throw new Error("animationName cannot be null."); + const animations = this.animations; + for (let i = 0, n = animations.length; i < n; i++) { + const animation = animations[i]; + if (animation.name == animationName) return animation; + } + return null; + } + + /** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method + * than to call it multiple times. + * @return May be null. */ + findIkConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const ikConstraints = this.ikConstraints; + for (let i = 0, n = ikConstraints.length; i < n; i++) { + const constraint = ikConstraints[i]; + if (constraint.name == constraintName) return constraint; + } + return null; + } + + /** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of + * this method than to call it multiple times. + * @return May be null. */ + findTransformConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const transformConstraints = this.transformConstraints; + for (let i = 0, n = transformConstraints.length; i < n; i++) { + const constraint = transformConstraints[i]; + if (constraint.name == constraintName) return constraint; + } + return null; + } + + /** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method + * than to call it multiple times. + * @return May be null. */ + findPathConstraint(constraintName: string) { + if (constraintName == null) throw new Error("constraintName cannot be null."); + const pathConstraints = this.pathConstraints; + for (let i = 0, n = pathConstraints.length; i < n; i++) { + const constraint = pathConstraints[i]; + if (constraint.name == constraintName) return constraint; + } + return null; + } + + findPathConstraintIndex(pathConstraintName: string) { + if (pathConstraintName == null) throw new Error("pathConstraintName cannot be null."); + const pathConstraints = this.pathConstraints; + for (let i = 0, n = pathConstraints.length; i < n; i++) if (pathConstraints[i].name == pathConstraintName) return i; + return -1; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SkeletonJson.ts b/packages/spine-core-3.8/src/spine-core/SkeletonJson.ts new file mode 100644 index 0000000000..9c55054752 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SkeletonJson.ts @@ -0,0 +1,906 @@ +import { AttachmentLoader } from "./attachments/AttachmentLoader"; +import { SkeletonData } from "./SkeletonData"; +import { BoneData, TransformMode } from "./BoneData"; +import { SlotData } from "./SlotData"; +import { Color, Utils, ArrayLike } from "./Utils"; +import { IkConstraintData } from "./IkConstraintData"; +import { TransformConstraintData } from "./TransformConstraintData"; +import { PathConstraintData, PositionMode, SpacingMode, RotateMode } from "./PathConstraintData"; +import { Skin } from "./Skin"; +import { VertexAttachment, Attachment } from "./attachments/Attachment"; +import { MeshAttachment } from "./attachments/MeshAttachment"; +import { EventData } from "./EventData"; +import { + Timeline, + AttachmentTimeline, + ColorTimeline, + TwoColorTimeline, + RotateTimeline, + TranslateTimeline, + ScaleTimeline, + ShearTimeline, + IkConstraintTimeline, + TransformConstraintTimeline, + PathConstraintPositionTimeline, + PathConstraintSpacingTimeline, + PathConstraintMixTimeline, + DeformTimeline, + DrawOrderTimeline, + EventTimeline, + CurveTimeline +} from "./Animation"; +import { BlendMode } from "./BlendMode"; +import { Event } from "./Event"; +import { Animation } from "./Animation"; + +/** Loads skeleton data in the Spine JSON format. + * + * See [Spine JSON format](http://esotericsoftware.com/spine-json-format) and + * [JSON and binary data](http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data) in the Spine + * Runtimes Guide. */ +export class SkeletonJson { + attachmentLoader: AttachmentLoader; + + /** Scales bone positions, image sizes, and translations as they are loaded. This allows different size images to be used at + * runtime than were used in Spine. + * + * See [Scaling](http://esotericsoftware.com/spine-loading-skeleton-data#Scaling) in the Spine Runtimes Guide. */ + scale = 1; + private linkedMeshes = new Array(); + + constructor(attachmentLoader: AttachmentLoader) { + this.attachmentLoader = attachmentLoader; + } + + readSkeletonData(json: string | any): SkeletonData { + const scale = this.scale; + const skeletonData = new SkeletonData(); + const root = typeof json === "string" ? JSON.parse(json) : json; + + // Skeleton + const skeletonMap = root.skeleton; + if (skeletonMap != null) { + skeletonData.hash = skeletonMap.hash; + skeletonData.version = skeletonMap.spine; + if ("3.8.75" == skeletonData.version) + throw new Error("Unsupported skeleton data, please export with a newer version of Spine."); + skeletonData.x = skeletonMap.x; + skeletonData.y = skeletonMap.y; + skeletonData.width = skeletonMap.width; + skeletonData.height = skeletonMap.height; + skeletonData.fps = skeletonMap.fps; + skeletonData.imagesPath = skeletonMap.images; + } + + // Bones + if (root.bones) { + for (let i = 0; i < root.bones.length; i++) { + const boneMap = root.bones[i]; + + let parent: BoneData = null; + const parentName: string = this.getValue(boneMap, "parent", null); + if (parentName != null) { + parent = skeletonData.findBone(parentName); + if (parent == null) throw new Error("Parent bone not found: " + parentName); + } + const data = new BoneData(skeletonData.bones.length, boneMap.name, parent); + data.length = this.getValue(boneMap, "length", 0) * scale; + data.x = this.getValue(boneMap, "x", 0) * scale; + data.y = this.getValue(boneMap, "y", 0) * scale; + data.rotation = this.getValue(boneMap, "rotation", 0); + data.scaleX = this.getValue(boneMap, "scaleX", 1); + data.scaleY = this.getValue(boneMap, "scaleY", 1); + data.shearX = this.getValue(boneMap, "shearX", 0); + data.shearY = this.getValue(boneMap, "shearY", 0); + data.transformMode = SkeletonJson.transformModeFromString(this.getValue(boneMap, "transform", "normal")); + data.skinRequired = this.getValue(boneMap, "skin", false); + + skeletonData.bones.push(data); + } + } + + // Slots. + if (root.slots) { + for (let i = 0; i < root.slots.length; i++) { + const slotMap = root.slots[i]; + const slotName: string = slotMap.name; + const boneName: string = slotMap.bone; + const boneData = skeletonData.findBone(boneName); + if (boneData == null) throw new Error("Slot bone not found: " + boneName); + const data = new SlotData(skeletonData.slots.length, slotName, boneData); + + const color: string = this.getValue(slotMap, "color", null); + if (color != null) data.color.setFromString(color); + + const dark: string = this.getValue(slotMap, "dark", null); + if (dark != null) { + data.darkColor = new Color(1, 1, 1, 1); + data.darkColor.setFromString(dark); + } + + data.attachmentName = this.getValue(slotMap, "attachment", null); + data.blendMode = SkeletonJson.blendModeFromString(this.getValue(slotMap, "blend", "normal")); + skeletonData.slots.push(data); + } + } + + // IK constraints + if (root.ik) { + for (let i = 0; i < root.ik.length; i++) { + const constraintMap = root.ik[i]; + const data = new IkConstraintData(constraintMap.name); + data.order = this.getValue(constraintMap, "order", 0); + data.skinRequired = this.getValue(constraintMap, "skin", false); + + for (let j = 0; j < constraintMap.bones.length; j++) { + const boneName = constraintMap.bones[j]; + const bone = skeletonData.findBone(boneName); + if (bone == null) throw new Error("IK bone not found: " + boneName); + data.bones.push(bone); + } + + const targetName: string = constraintMap.target; + data.target = skeletonData.findBone(targetName); + if (data.target == null) throw new Error("IK target bone not found: " + targetName); + + data.mix = this.getValue(constraintMap, "mix", 1); + data.softness = this.getValue(constraintMap, "softness", 0) * scale; + data.bendDirection = this.getValue(constraintMap, "bendPositive", true) ? 1 : -1; + data.compress = this.getValue(constraintMap, "compress", false); + data.stretch = this.getValue(constraintMap, "stretch", false); + data.uniform = this.getValue(constraintMap, "uniform", false); + + skeletonData.ikConstraints.push(data); + } + } + + // Transform constraints. + if (root.transform) { + for (let i = 0; i < root.transform.length; i++) { + const constraintMap = root.transform[i]; + const data = new TransformConstraintData(constraintMap.name); + data.order = this.getValue(constraintMap, "order", 0); + data.skinRequired = this.getValue(constraintMap, "skin", false); + + for (let j = 0; j < constraintMap.bones.length; j++) { + const boneName = constraintMap.bones[j]; + const bone = skeletonData.findBone(boneName); + if (bone == null) throw new Error("Transform constraint bone not found: " + boneName); + data.bones.push(bone); + } + + const targetName: string = constraintMap.target; + data.target = skeletonData.findBone(targetName); + if (data.target == null) throw new Error("Transform constraint target bone not found: " + targetName); + + data.local = this.getValue(constraintMap, "local", false); + data.relative = this.getValue(constraintMap, "relative", false); + data.offsetRotation = this.getValue(constraintMap, "rotation", 0); + data.offsetX = this.getValue(constraintMap, "x", 0) * scale; + data.offsetY = this.getValue(constraintMap, "y", 0) * scale; + data.offsetScaleX = this.getValue(constraintMap, "scaleX", 0); + data.offsetScaleY = this.getValue(constraintMap, "scaleY", 0); + data.offsetShearY = this.getValue(constraintMap, "shearY", 0); + + data.rotateMix = this.getValue(constraintMap, "rotateMix", 1); + data.translateMix = this.getValue(constraintMap, "translateMix", 1); + data.scaleMix = this.getValue(constraintMap, "scaleMix", 1); + data.shearMix = this.getValue(constraintMap, "shearMix", 1); + + skeletonData.transformConstraints.push(data); + } + } + + // Path constraints. + if (root.path) { + for (let i = 0; i < root.path.length; i++) { + const constraintMap = root.path[i]; + const data = new PathConstraintData(constraintMap.name); + data.order = this.getValue(constraintMap, "order", 0); + data.skinRequired = this.getValue(constraintMap, "skin", false); + + for (let j = 0; j < constraintMap.bones.length; j++) { + const boneName = constraintMap.bones[j]; + const bone = skeletonData.findBone(boneName); + if (bone == null) throw new Error("Transform constraint bone not found: " + boneName); + data.bones.push(bone); + } + + const targetName: string = constraintMap.target; + data.target = skeletonData.findSlot(targetName); + if (data.target == null) throw new Error("Path target slot not found: " + targetName); + + data.positionMode = SkeletonJson.positionModeFromString( + this.getValue(constraintMap, "positionMode", "percent") + ); + data.spacingMode = SkeletonJson.spacingModeFromString(this.getValue(constraintMap, "spacingMode", "length")); + data.rotateMode = SkeletonJson.rotateModeFromString(this.getValue(constraintMap, "rotateMode", "tangent")); + data.offsetRotation = this.getValue(constraintMap, "rotation", 0); + data.position = this.getValue(constraintMap, "position", 0); + if (data.positionMode == PositionMode.Fixed) data.position *= scale; + data.spacing = this.getValue(constraintMap, "spacing", 0); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; + data.rotateMix = this.getValue(constraintMap, "rotateMix", 1); + data.translateMix = this.getValue(constraintMap, "translateMix", 1); + + skeletonData.pathConstraints.push(data); + } + } + + // Skins. + if (root.skins) { + for (let i = 0; i < root.skins.length; i++) { + const skinMap = root.skins[i]; + const skin = new Skin(skinMap.name); + + if (skinMap.bones) { + for (let ii = 0; ii < skinMap.bones.length; ii++) { + const bone = skeletonData.findBone(skinMap.bones[ii]); + if (bone == null) throw new Error("Skin bone not found: " + skinMap.bones[i]); + skin.bones.push(bone); + } + } + + if (skinMap.ik) { + for (let ii = 0; ii < skinMap.ik.length; ii++) { + const constraint = skeletonData.findIkConstraint(skinMap.ik[ii]); + if (constraint == null) throw new Error("Skin IK constraint not found: " + skinMap.ik[i]); + skin.constraints.push(constraint); + } + } + + if (skinMap.transform) { + for (let ii = 0; ii < skinMap.transform.length; ii++) { + const constraint = skeletonData.findTransformConstraint(skinMap.transform[ii]); + if (constraint == null) throw new Error("Skin transform constraint not found: " + skinMap.transform[i]); + skin.constraints.push(constraint); + } + } + + if (skinMap.path) { + for (let ii = 0; ii < skinMap.path.length; ii++) { + const constraint = skeletonData.findPathConstraint(skinMap.path[ii]); + if (constraint == null) throw new Error("Skin path constraint not found: " + skinMap.path[i]); + skin.constraints.push(constraint); + } + } + + for (const slotName in skinMap.attachments) { + const slot = skeletonData.findSlot(slotName); + if (slot == null) throw new Error("Slot not found: " + slotName); + const slotMap = skinMap.attachments[slotName]; + for (const entryName in slotMap) { + const attachment = this.readAttachment(slotMap[entryName], skin, slot.index, entryName, skeletonData); + if (attachment != null) skin.setAttachment(slot.index, entryName, attachment); + } + } + skeletonData.skins.push(skin); + if (skin.name == "default") skeletonData.defaultSkin = skin; + } + } + + // Linked meshes. + for (let i = 0, n = this.linkedMeshes.length; i < n; i++) { + const linkedMesh = this.linkedMeshes[i]; + const skin = linkedMesh.skin == null ? skeletonData.defaultSkin : skeletonData.findSkin(linkedMesh.skin); + if (skin == null) throw new Error("Skin not found: " + linkedMesh.skin); + const parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent); + if (parent == null) throw new Error("Parent mesh not found: " + linkedMesh.parent); + linkedMesh.mesh.deformAttachment = linkedMesh.inheritDeform + ? parent + : linkedMesh.mesh; + linkedMesh.mesh.setParentMesh(parent); + linkedMesh.mesh.updateUVs(); + } + this.linkedMeshes.length = 0; + + // Events. + if (root.events) { + for (const eventName in root.events) { + const eventMap = root.events[eventName]; + const data = new EventData(eventName); + data.intValue = this.getValue(eventMap, "int", 0); + data.floatValue = this.getValue(eventMap, "float", 0); + data.stringValue = this.getValue(eventMap, "string", ""); + data.audioPath = this.getValue(eventMap, "audio", null); + if (data.audioPath != null) { + data.volume = this.getValue(eventMap, "volume", 1); + data.balance = this.getValue(eventMap, "balance", 0); + } + skeletonData.events.push(data); + } + } + + // Animations. + if (root.animations) { + for (const animationName in root.animations) { + const animationMap = root.animations[animationName]; + this.readAnimation(animationMap, animationName, skeletonData); + } + } + + return skeletonData; + } + + readAttachment(map: any, skin: Skin, slotIndex: number, name: string, skeletonData: SkeletonData): Attachment { + const scale = this.scale; + name = this.getValue(map, "name", name); + + const type = this.getValue(map, "type", "region"); + + switch (type) { + case "region": { + const path = this.getValue(map, "path", name); + const region = this.attachmentLoader.newRegionAttachment(skin, name, path); + if (region == null) return null; + region.path = path; + region.x = this.getValue(map, "x", 0) * scale; + region.y = this.getValue(map, "y", 0) * scale; + region.scaleX = this.getValue(map, "scaleX", 1); + region.scaleY = this.getValue(map, "scaleY", 1); + region.rotation = this.getValue(map, "rotation", 0); + region.width = map.width * scale; + region.height = map.height * scale; + + const color: string = this.getValue(map, "color", null); + if (color != null) region.color.setFromString(color); + + region.updateOffset(); + return region; + } + case "boundingbox": { + const box = this.attachmentLoader.newBoundingBoxAttachment(skin, name); + if (box == null) return null; + this.readVertices(map, box, map.vertexCount << 1); + const color: string = this.getValue(map, "color", null); + if (color != null) box.color.setFromString(color); + return box; + } + case "mesh": + case "linkedmesh": { + const path = this.getValue(map, "path", name); + const mesh = this.attachmentLoader.newMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.path = path; + + const color = this.getValue(map, "color", null); + if (color != null) mesh.color.setFromString(color); + + mesh.width = this.getValue(map, "width", 0) * scale; + mesh.height = this.getValue(map, "height", 0) * scale; + + const parent: string = this.getValue(map, "parent", null); + if (parent != null) { + this.linkedMeshes.push( + new LinkedMesh( + mesh, + this.getValue(map, "skin", null), + slotIndex, + parent, + this.getValue(map, "deform", true) + ) + ); + return mesh; + } + + const uvs: Array = map.uvs; + this.readVertices(map, mesh, uvs.length); + mesh.triangles = map.triangles; + mesh.regionUVs = uvs; + mesh.updateUVs(); + + mesh.edges = this.getValue(map, "edges", null); + mesh.hullLength = this.getValue(map, "hull", 0) * 2; + return mesh; + } + case "path": { + const path = this.attachmentLoader.newPathAttachment(skin, name); + if (path == null) return null; + path.closed = this.getValue(map, "closed", false); + path.constantSpeed = this.getValue(map, "constantSpeed", true); + + const vertexCount = map.vertexCount; + this.readVertices(map, path, vertexCount << 1); + + const lengths: Array = Utils.newArray(vertexCount / 3, 0); + for (let i = 0; i < map.lengths.length; i++) lengths[i] = map.lengths[i] * scale; + path.lengths = lengths; + + const color: string = this.getValue(map, "color", null); + if (color != null) path.color.setFromString(color); + return path; + } + case "point": { + const point = this.attachmentLoader.newPointAttachment(skin, name); + if (point == null) return null; + point.x = this.getValue(map, "x", 0) * scale; + point.y = this.getValue(map, "y", 0) * scale; + point.rotation = this.getValue(map, "rotation", 0); + + const color = this.getValue(map, "color", null); + if (color != null) point.color.setFromString(color); + return point; + } + case "clipping": { + const clip = this.attachmentLoader.newClippingAttachment(skin, name); + if (clip == null) return null; + + const end = this.getValue(map, "end", null); + if (end != null) { + const slot = skeletonData.findSlot(end); + if (slot == null) throw new Error("Clipping end slot not found: " + end); + clip.endSlot = slot; + } + + const vertexCount = map.vertexCount; + this.readVertices(map, clip, vertexCount << 1); + + const color: string = this.getValue(map, "color", null); + if (color != null) clip.color.setFromString(color); + return clip; + } + } + return null; + } + + readVertices(map: any, attachment: VertexAttachment, verticesLength: number) { + const scale = this.scale; + attachment.worldVerticesLength = verticesLength; + const vertices: Array = map.vertices; + if (verticesLength == vertices.length) { + const scaledVertices = Utils.toFloatArray(vertices); + if (scale != 1) { + for (let i = 0, n = vertices.length; i < n; i++) scaledVertices[i] *= scale; + } + attachment.vertices = scaledVertices; + return; + } + const weights = new Array(); + const bones = new Array(); + for (let i = 0, n = vertices.length; i < n; ) { + const boneCount = vertices[i++]; + bones.push(boneCount); + for (let nn = i + boneCount * 4; i < nn; i += 4) { + bones.push(vertices[i]); + weights.push(vertices[i + 1] * scale); + weights.push(vertices[i + 2] * scale); + weights.push(vertices[i + 3]); + } + } + attachment.bones = bones; + attachment.vertices = Utils.toFloatArray(weights); + } + + readAnimation(map: any, name: string, skeletonData: SkeletonData) { + const scale = this.scale; + const timelines = new Array(); + let duration = 0; + + // Slot timelines. + if (map.slots) { + for (const slotName in map.slots) { + const slotMap = map.slots[slotName]; + const slotIndex = skeletonData.findSlotIndex(slotName); + if (slotIndex == -1) throw new Error("Slot not found: " + slotName); + for (const timelineName in slotMap) { + const timelineMap = slotMap[timelineName]; + if (timelineName == "attachment") { + const timeline = new AttachmentTimeline(timelineMap.length); + timeline.slotIndex = slotIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + timeline.setFrame(frameIndex++, this.getValue(valueMap, "time", 0), valueMap.name); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() - 1]); + } else if (timelineName == "color") { + const timeline = new ColorTimeline(timelineMap.length); + timeline.slotIndex = slotIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + const color = new Color(); + color.setFromString(valueMap.color); + timeline.setFrame(frameIndex, this.getValue(valueMap, "time", 0), color.r, color.g, color.b, color.a); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * ColorTimeline.ENTRIES]); + } else if (timelineName == "twoColor") { + const timeline = new TwoColorTimeline(timelineMap.length); + timeline.slotIndex = slotIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + const light = new Color(); + const dark = new Color(); + light.setFromString(valueMap.light); + dark.setFromString(valueMap.dark); + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + light.r, + light.g, + light.b, + light.a, + dark.r, + dark.g, + dark.b + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * TwoColorTimeline.ENTRIES]); + } else throw new Error("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")"); + } + } + } + + // Bone timelines. + if (map.bones) { + for (const boneName in map.bones) { + const boneMap = map.bones[boneName]; + const boneIndex = skeletonData.findBoneIndex(boneName); + if (boneIndex == -1) throw new Error("Bone not found: " + boneName); + for (const timelineName in boneMap) { + const timelineMap = boneMap[timelineName]; + if (timelineName === "rotate") { + const timeline = new RotateTimeline(timelineMap.length); + timeline.boneIndex = boneIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + timeline.setFrame(frameIndex, this.getValue(valueMap, "time", 0), this.getValue(valueMap, "angle", 0)); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * RotateTimeline.ENTRIES]); + } else if (timelineName === "translate" || timelineName === "scale" || timelineName === "shear") { + let timeline: TranslateTimeline = null; + let timelineScale = 1, + defaultValue = 0; + if (timelineName === "scale") { + timeline = new ScaleTimeline(timelineMap.length); + defaultValue = 1; + } else if (timelineName === "shear") timeline = new ShearTimeline(timelineMap.length); + else { + timeline = new TranslateTimeline(timelineMap.length); + timelineScale = scale; + } + timeline.boneIndex = boneIndex; + + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + const x = this.getValue(valueMap, "x", defaultValue), + y = this.getValue(valueMap, "y", defaultValue); + timeline.setFrame(frameIndex, this.getValue(valueMap, "time", 0), x * timelineScale, y * timelineScale); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * TranslateTimeline.ENTRIES]); + } else throw new Error("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")"); + } + } + } + + // IK constraint timelines. + if (map.ik) { + for (const constraintName in map.ik) { + const constraintMap = map.ik[constraintName]; + const constraint = skeletonData.findIkConstraint(constraintName); + const timeline = new IkConstraintTimeline(constraintMap.length); + timeline.ikConstraintIndex = skeletonData.ikConstraints.indexOf(constraint); + let frameIndex = 0; + for (let i = 0; i < constraintMap.length; i++) { + const valueMap = constraintMap[i]; + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + this.getValue(valueMap, "mix", 1), + this.getValue(valueMap, "softness", 0) * scale, + this.getValue(valueMap, "bendPositive", true) ? 1 : -1, + this.getValue(valueMap, "compress", false), + this.getValue(valueMap, "stretch", false) + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[(timeline.getFrameCount() - 1) * IkConstraintTimeline.ENTRIES]); + } + } + + // Transform constraint timelines. + if (map.transform) { + for (const constraintName in map.transform) { + const constraintMap = map.transform[constraintName]; + const constraint = skeletonData.findTransformConstraint(constraintName); + const timeline = new TransformConstraintTimeline(constraintMap.length); + timeline.transformConstraintIndex = skeletonData.transformConstraints.indexOf(constraint); + let frameIndex = 0; + for (let i = 0; i < constraintMap.length; i++) { + const valueMap = constraintMap[i]; + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + this.getValue(valueMap, "rotateMix", 1), + this.getValue(valueMap, "translateMix", 1), + this.getValue(valueMap, "scaleMix", 1), + this.getValue(valueMap, "shearMix", 1) + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max( + duration, + timeline.frames[(timeline.getFrameCount() - 1) * TransformConstraintTimeline.ENTRIES] + ); + } + } + + // Path constraint timelines. + if (map.path) { + for (const constraintName in map.path) { + const constraintMap = map.path[constraintName]; + const index = skeletonData.findPathConstraintIndex(constraintName); + if (index == -1) throw new Error("Path constraint not found: " + constraintName); + const data = skeletonData.pathConstraints[index]; + for (const timelineName in constraintMap) { + const timelineMap = constraintMap[timelineName]; + if (timelineName === "position" || timelineName === "spacing") { + let timeline: PathConstraintPositionTimeline = null; + let timelineScale = 1; + if (timelineName === "spacing") { + timeline = new PathConstraintSpacingTimeline(timelineMap.length); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) + timelineScale = scale; + } else { + timeline = new PathConstraintPositionTimeline(timelineMap.length); + if (data.positionMode == PositionMode.Fixed) timelineScale = scale; + } + timeline.pathConstraintIndex = index; + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + this.getValue(valueMap, timelineName, 0) * timelineScale + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max( + duration, + timeline.frames[(timeline.getFrameCount() - 1) * PathConstraintPositionTimeline.ENTRIES] + ); + } else if (timelineName === "mix") { + const timeline = new PathConstraintMixTimeline(timelineMap.length); + timeline.pathConstraintIndex = index; + let frameIndex = 0; + for (let i = 0; i < timelineMap.length; i++) { + const valueMap = timelineMap[i]; + timeline.setFrame( + frameIndex, + this.getValue(valueMap, "time", 0), + this.getValue(valueMap, "rotateMix", 1), + this.getValue(valueMap, "translateMix", 1) + ); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max( + duration, + timeline.frames[(timeline.getFrameCount() - 1) * PathConstraintMixTimeline.ENTRIES] + ); + } + } + } + } + + // Deform timelines. + if (map.deform) { + for (const deformName in map.deform) { + const deformMap = map.deform[deformName]; + const skin = skeletonData.findSkin(deformName); + if (skin == null) throw new Error("Skin not found: " + deformName); + for (const slotName in deformMap) { + const slotMap = deformMap[slotName]; + const slotIndex = skeletonData.findSlotIndex(slotName); + if (slotIndex == -1) throw new Error("Slot not found: " + slotMap.name); + for (const timelineName in slotMap) { + const timelineMap = slotMap[timelineName]; + const attachment = skin.getAttachment(slotIndex, timelineName); + if (attachment == null) throw new Error("Deform attachment not found: " + timelineMap.name); + const weighted = attachment.bones != null; + const vertices = attachment.vertices; + const deformLength = weighted ? (vertices.length / 3) * 2 : vertices.length; + + const timeline = new DeformTimeline(timelineMap.length); + timeline.slotIndex = slotIndex; + timeline.attachment = attachment; + + let frameIndex = 0; + for (let j = 0; j < timelineMap.length; j++) { + const valueMap = timelineMap[j]; + let deform: ArrayLike; + const verticesValue: Array = this.getValue(valueMap, "vertices", null); + if (verticesValue == null) deform = weighted ? Utils.newFloatArray(deformLength) : vertices; + else { + deform = Utils.newFloatArray(deformLength); + const start = this.getValue(valueMap, "offset", 0); + Utils.arrayCopy(verticesValue, 0, deform, start, verticesValue.length); + if (scale != 1) { + for (let i = start, n = i + verticesValue.length; i < n; i++) deform[i] *= scale; + } + if (!weighted) { + for (let i = 0; i < deformLength; i++) deform[i] += vertices[i]; + } + } + + timeline.setFrame(frameIndex, this.getValue(valueMap, "time", 0), deform); + this.readCurve(valueMap, timeline, frameIndex); + frameIndex++; + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() - 1]); + } + } + } + } + + // Draw order timeline. + let drawOrderNode = map.drawOrder; + if (drawOrderNode == null) drawOrderNode = map.draworder; + if (drawOrderNode != null) { + const timeline = new DrawOrderTimeline(drawOrderNode.length); + const slotCount = skeletonData.slots.length; + let frameIndex = 0; + for (let j = 0; j < drawOrderNode.length; j++) { + const drawOrderMap = drawOrderNode[j]; + let drawOrder: Array = null; + const offsets = this.getValue(drawOrderMap, "offsets", null); + if (offsets != null) { + drawOrder = Utils.newArray(slotCount, -1); + const unchanged = Utils.newArray(slotCount - offsets.length, 0); + let originalIndex = 0, + unchangedIndex = 0; + for (let i = 0; i < offsets.length; i++) { + const offsetMap = offsets[i]; + const slotIndex = skeletonData.findSlotIndex(offsetMap.slot); + if (slotIndex == -1) throw new Error("Slot not found: " + offsetMap.slot); + // Collect unchanged items. + while (originalIndex != slotIndex) unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + drawOrder[originalIndex + offsetMap.offset] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (let i = slotCount - 1; i >= 0; i--) if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex]; + } + timeline.setFrame(frameIndex++, this.getValue(drawOrderMap, "time", 0), drawOrder); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() - 1]); + } + + // Event timeline. + if (map.events) { + const timeline = new EventTimeline(map.events.length); + let frameIndex = 0; + for (let i = 0; i < map.events.length; i++) { + const eventMap = map.events[i]; + const eventData = skeletonData.findEvent(eventMap.name); + if (eventData == null) throw new Error("Event not found: " + eventMap.name); + const event = new Event(Utils.toSinglePrecision(this.getValue(eventMap, "time", 0)), eventData); + event.intValue = this.getValue(eventMap, "int", eventData.intValue); + event.floatValue = this.getValue(eventMap, "float", eventData.floatValue); + event.stringValue = this.getValue(eventMap, "string", eventData.stringValue); + if (event.data.audioPath != null) { + event.volume = this.getValue(eventMap, "volume", 1); + event.balance = this.getValue(eventMap, "balance", 0); + } + timeline.setFrame(frameIndex++, event); + } + timelines.push(timeline); + duration = Math.max(duration, timeline.frames[timeline.getFrameCount() - 1]); + } + + if (isNaN(duration)) { + throw new Error("Error while parsing animation, duration is NaN"); + } + + skeletonData.animations.push(new Animation(name, timelines, duration)); + } + + readCurve(map: any, timeline: CurveTimeline, frameIndex: number) { + if (!map.hasOwnProperty("curve")) return; + if (map.curve == "stepped") timeline.setStepped(frameIndex); + else { + const curve: number = map.curve; + timeline.setCurve( + frameIndex, + curve, + this.getValue(map, "c2", 0), + this.getValue(map, "c3", 1), + this.getValue(map, "c4", 1) + ); + } + } + + getValue(map: any, prop: string, defaultValue: any) { + return map[prop] !== undefined ? map[prop] : defaultValue; + } + + static blendModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "normal") return BlendMode.Normal; + if (str == "additive") return BlendMode.Additive; + if (str == "multiply") return BlendMode.Multiply; + if (str == "screen") return BlendMode.Screen; + throw new Error(`Unknown blend mode: ${str}`); + } + + static positionModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "fixed") return PositionMode.Fixed; + if (str == "percent") return PositionMode.Percent; + throw new Error(`Unknown position mode: ${str}`); + } + + static spacingModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "length") return SpacingMode.Length; + if (str == "fixed") return SpacingMode.Fixed; + if (str == "percent") return SpacingMode.Percent; + throw new Error(`Unknown position mode: ${str}`); + } + + static rotateModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "tangent") return RotateMode.Tangent; + if (str == "chain") return RotateMode.Chain; + if (str == "chainscale") return RotateMode.ChainScale; + throw new Error(`Unknown rotate mode: ${str}`); + } + + static transformModeFromString(str: string) { + str = str.toLowerCase(); + if (str == "normal") return TransformMode.Normal; + if (str == "onlytranslation") return TransformMode.OnlyTranslation; + if (str == "norotationorreflection") return TransformMode.NoRotationOrReflection; + if (str == "noscale") return TransformMode.NoScale; + if (str == "noscaleorreflection") return TransformMode.NoScaleOrReflection; + throw new Error(`Unknown transform mode: ${str}`); + } +} + +class LinkedMesh { + parent: string; + skin: string; + slotIndex: number; + mesh: MeshAttachment; + inheritDeform: boolean; + + constructor(mesh: MeshAttachment, skin: string, slotIndex: number, parent: string, inheritDeform: boolean) { + this.mesh = mesh; + this.skin = skin; + this.slotIndex = slotIndex; + this.parent = parent; + this.inheritDeform = inheritDeform; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Skin.ts b/packages/spine-core-3.8/src/spine-core/Skin.ts new file mode 100644 index 0000000000..3081742665 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Skin.ts @@ -0,0 +1,182 @@ +import { Attachment } from "./attachments/Attachment"; +import { BoneData } from "./BoneData"; +import { ConstraintData } from "./ConstraintData"; +import { Skeleton } from "./Skeleton"; +import { MeshAttachment } from "./attachments/MeshAttachment"; +import { Map } from "./Utils"; + +/** Stores an entry in the skin consisting of the slot index, name, and attachment **/ +export class SkinEntry { + constructor( + public slotIndex: number, + public name: string, + public attachment: Attachment + ) {} +} + +/** Stores attachments by slot index and attachment name. + * + * See SkeletonData {@link SkeletonData#defaultSkin}, Skeleton {@link Skeleton#skin}, and + * [Runtime skins](http://esotericsoftware.com/spine-runtime-skins) in the Spine Runtimes Guide. */ +export class Skin { + /** The skin's name, which is unique across all skins in the skeleton. */ + name: string; + + attachments = new Array>(); + bones = Array(); + constraints = new Array(); + + constructor(name: string) { + if (name == null) throw new Error("name cannot be null."); + this.name = name; + } + + /** Adds an attachment to the skin for the specified slot index and name. */ + setAttachment(slotIndex: number, name: string, attachment: Attachment) { + if (attachment == null) throw new Error("attachment cannot be null."); + const attachments = this.attachments; + if (slotIndex >= attachments.length) attachments.length = slotIndex + 1; + if (!attachments[slotIndex]) attachments[slotIndex] = {}; + attachments[slotIndex][name] = attachment; + } + + /** Adds all attachments, bones, and constraints from the specified skin to this skin. */ + addSkin(skin: Skin) { + for (let i = 0; i < skin.bones.length; i++) { + const bone = skin.bones[i]; + let contained = false; + for (let j = 0; j < this.bones.length; j++) { + if (this.bones[j] == bone) { + contained = true; + break; + } + } + if (!contained) this.bones.push(bone); + } + + for (let i = 0; i < skin.constraints.length; i++) { + const constraint = skin.constraints[i]; + let contained = false; + for (let j = 0; j < this.constraints.length; j++) { + if (this.constraints[j] == constraint) { + contained = true; + break; + } + } + if (!contained) this.constraints.push(constraint); + } + + const attachments = skin.getAttachments(); + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i]; + this.setAttachment(attachment.slotIndex, attachment.name, attachment.attachment); + } + } + + /** Adds all bones and constraints and copies of all attachments from the specified skin to this skin. Mesh attachments are not + * copied, instead a new linked mesh is created. The attachment copies can be modified without affecting the originals. */ + copySkin(skin: Skin) { + for (let i = 0; i < skin.bones.length; i++) { + const bone = skin.bones[i]; + let contained = false; + for (let j = 0; j < this.bones.length; j++) { + if (this.bones[j] == bone) { + contained = true; + break; + } + } + if (!contained) this.bones.push(bone); + } + + for (let i = 0; i < skin.constraints.length; i++) { + const constraint = skin.constraints[i]; + let contained = false; + for (let j = 0; j < this.constraints.length; j++) { + if (this.constraints[j] == constraint) { + contained = true; + break; + } + } + if (!contained) this.constraints.push(constraint); + } + + const attachments = skin.getAttachments(); + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i]; + if (attachment.attachment == null) continue; + if (attachment.attachment instanceof MeshAttachment) { + attachment.attachment = attachment.attachment.newLinkedMesh(); + this.setAttachment(attachment.slotIndex, attachment.name, attachment.attachment); + } else { + attachment.attachment = attachment.attachment.copy(); + this.setAttachment(attachment.slotIndex, attachment.name, attachment.attachment); + } + } + } + + /** Returns the attachment for the specified slot index and name, or null. */ + getAttachment(slotIndex: number, name: string): Attachment { + const dictionary = this.attachments[slotIndex]; + return dictionary ? dictionary[name] : null; + } + + /** Removes the attachment in the skin for the specified slot index and name, if any. */ + removeAttachment(slotIndex: number, name: string) { + const dictionary = this.attachments[slotIndex]; + if (dictionary) dictionary[name] = null; + } + + /** Returns all attachments in this skin. */ + getAttachments(): Array { + const entries = new Array(); + for (let i = 0; i < this.attachments.length; i++) { + const slotAttachments = this.attachments[i]; + if (slotAttachments) { + for (const name in slotAttachments) { + const attachment = slotAttachments[name]; + if (attachment) entries.push(new SkinEntry(i, name, attachment)); + } + } + } + return entries; + } + + /** Returns all attachments in this skin for the specified slot index. */ + getAttachmentsForSlot(slotIndex: number, attachments: Array) { + const slotAttachments = this.attachments[slotIndex]; + if (slotAttachments) { + for (const name in slotAttachments) { + const attachment = slotAttachments[name]; + if (attachment) attachments.push(new SkinEntry(slotIndex, name, attachment)); + } + } + } + + /** Clears all attachments, bones, and constraints. */ + clear() { + this.attachments.length = 0; + this.bones.length = 0; + this.constraints.length = 0; + } + + /** Attach each attachment in this skin if the corresponding attachment in the old skin is currently attached. */ + attachAll(skeleton: Skeleton, oldSkin: Skin) { + let slotIndex = 0; + for (let i = 0; i < skeleton.slots.length; i++) { + const slot = skeleton.slots[i]; + const slotAttachment = slot.getAttachment(); + if (slotAttachment && slotIndex < oldSkin.attachments.length) { + const dictionary = oldSkin.attachments[slotIndex]; + for (const key in dictionary) { + const skinAttachment: Attachment = dictionary[key]; + if (slotAttachment == skinAttachment) { + const attachment = this.getAttachment(slotIndex, key); + if (attachment != null) slot.setAttachment(attachment); + break; + } + } + } + slotIndex++; + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Slot.ts b/packages/spine-core-3.8/src/spine-core/Slot.ts new file mode 100644 index 0000000000..5e4903193e --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Slot.ts @@ -0,0 +1,86 @@ +import { SlotData } from "./SlotData"; +import { Bone } from "./Bone"; +import { Color } from "./Utils"; +import { Attachment } from "./attachments/Attachment"; +import { Skeleton } from "./Skeleton"; + +/** Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store + * state for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared + * across multiple skeletons. */ +export class Slot { + /** The slot's setup pose data. */ + data: SlotData; + + /** The bone this slot belongs to. */ + bone: Bone; + + /** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two + * color tinting. */ + color: Color; + + /** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark + * color's alpha is not used. */ + darkColor: Color; + + attachment: Attachment; + + private attachmentTime: number; + + attachmentState: number; + + /** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a + * weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions. + * + * See {@link VertexAttachment#computeWorldVertices()} and {@link DeformTimeline}. */ + deform = new Array(); + + constructor(data: SlotData, bone: Bone) { + if (data == null) throw new Error("data cannot be null."); + if (bone == null) throw new Error("bone cannot be null."); + this.data = data; + this.bone = bone; + this.color = new Color(); + this.darkColor = data.darkColor == null ? null : new Color(); + this.setToSetupPose(); + } + + /** The skeleton this slot belongs to. */ + getSkeleton(): Skeleton { + return this.bone.skeleton; + } + + /** The current attachment for the slot, or null if the slot has no attachment. */ + getAttachment(): Attachment { + return this.attachment; + } + + /** Sets the slot's attachment and, if the attachment changed, resets {@link #attachmentTime} and clears {@link #deform}. + * @param attachment May be null. */ + setAttachment(attachment: Attachment) { + if (this.attachment == attachment) return; + this.attachment = attachment; + this.attachmentTime = this.bone.skeleton.time; + this.deform.length = 0; + } + + setAttachmentTime(time: number) { + this.attachmentTime = this.bone.skeleton.time - time; + } + + /** The time that has elapsed since the last time the attachment was set or cleared. Relies on Skeleton + * {@link Skeleton#time}. */ + getAttachmentTime(): number { + return this.bone.skeleton.time - this.attachmentTime; + } + + /** Sets this slot to the setup pose. */ + setToSetupPose() { + this.color.setFromColor(this.data.color); + if (this.darkColor != null) this.darkColor.setFromColor(this.data.darkColor); + if (this.data.attachmentName == null) this.attachment = null; + else { + this.attachment = null; + this.setAttachment(this.bone.skeleton.getAttachment(this.data.index, this.data.attachmentName)); + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/SlotData.ts b/packages/spine-core-3.8/src/spine-core/SlotData.ts new file mode 100644 index 0000000000..c5d3705b79 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/SlotData.ts @@ -0,0 +1,38 @@ +import { BoneData } from "./BoneData"; +import { Color } from "./Utils"; +import { BlendMode } from "./BlendMode"; + +/** Stores the setup pose for a {@link Slot}. */ +export class SlotData { + /** The index of the slot in {@link Skeleton#getSlots()}. */ + index: number; + + /** The name of the slot, which is unique across all slots in the skeleton. */ + name: string; + + /** The bone this slot belongs to. */ + boneData: BoneData; + + /** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two + * color tinting. */ + color = new Color(1, 1, 1, 1); + + /** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark + * color's alpha is not used. */ + darkColor: Color; + + /** The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. */ + attachmentName: string; + + /** The blend mode for drawing the slot's attachment. */ + blendMode: BlendMode; + + constructor(index: number, name: string, boneData: BoneData) { + if (index < 0) throw new Error("index must be >= 0."); + if (name == null) throw new Error("name cannot be null."); + if (boneData == null) throw new Error("boneData cannot be null."); + this.index = index; + this.name = name; + this.boneData = boneData; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Texture.ts b/packages/spine-core-3.8/src/spine-core/Texture.ts new file mode 100644 index 0000000000..f911a5625c --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Texture.ts @@ -0,0 +1,96 @@ +import { Engine, Texture2D } from "@galacean/engine"; + +export abstract class Texture { + protected _texture: Texture2D; + + constructor(texture: Texture2D) { + this._texture = texture; + } + + get width(): number { + return this._texture.width; + } + + get height(): number { + return this._texture.height; + } + + get texture(): Texture2D { + return this._texture; + } + + abstract setFilters(minFilter: TextureFilter, magFilter: TextureFilter): void; + abstract setWraps(uWrap: TextureWrap, vWrap: TextureWrap): void; + abstract dispose(): void; + + public static filterFromString(text: string): TextureFilter { + switch (text.toLowerCase()) { + case "nearest": + return TextureFilter.Nearest; + case "linear": + return TextureFilter.Linear; + case "mipmap": + return TextureFilter.MipMap; + case "mipmapnearestnearest": + return TextureFilter.MipMapNearestNearest; + case "mipmaplinearnearest": + return TextureFilter.MipMapLinearNearest; + case "mipmapnearestlinear": + return TextureFilter.MipMapNearestLinear; + case "mipmaplinearlinear": + return TextureFilter.MipMapLinearLinear; + default: + throw new Error(`Unknown texture filter ${text}`); + } + } + + public static wrapFromString(text: string): TextureWrap { + switch (text.toLowerCase()) { + case "mirroredtepeat": + return TextureWrap.MirroredRepeat; + case "clamptoedge": + return TextureWrap.ClampToEdge; + case "repeat": + return TextureWrap.Repeat; + default: + throw new Error(`Unknown texture wrap ${text}`); + } + } +} + +export enum TextureFilter { + Nearest = 9728, // WebGLRenderingContext.NEAREST + Linear = 9729, // WebGLRenderingContext.LINEAR + MipMap = 9987, // WebGLRenderingContext.LINEAR_MIPMAP_LINEAR + MipMapNearestNearest = 9984, // WebGLRenderingContext.NEAREST_MIPMAP_NEAREST + MipMapLinearNearest = 9985, // WebGLRenderingContext.LINEAR_MIPMAP_NEAREST + MipMapNearestLinear = 9986, // WebGLRenderingContext.NEAREST_MIPMAP_LINEAR + MipMapLinearLinear = 9987 // WebGLRenderingContext.LINEAR_MIPMAP_LINEAR +} + +export enum TextureWrap { + MirroredRepeat = 33648, // WebGLRenderingContext.MIRRORED_REPEAT + ClampToEdge = 33071, // WebGLRenderingContext.CLAMP_TO_EDGE + Repeat = 10497 // WebGLRenderingContext.REPEAT +} + +export class TextureRegion { + renderObject: any; + u = 0; + v = 0; + u2 = 0; + v2 = 0; + width = 0; + height = 0; + rotate = false; + offsetX = 0; + offsetY = 0; + originalWidth = 0; + originalHeight = 0; +} + +export class FakeTexture extends Texture { + setFilters(minFilter: TextureFilter, magFilter: TextureFilter) {} + setWraps(uWrap: TextureWrap, vWrap: TextureWrap) {} + dispose() {} +} diff --git a/packages/spine-core-3.8/src/spine-core/TextureAtlas.ts b/packages/spine-core-3.8/src/spine-core/TextureAtlas.ts new file mode 100644 index 0000000000..299e9f94be --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/TextureAtlas.ts @@ -0,0 +1,187 @@ +import { Disposable } from "./Utils"; +import { Texture, TextureFilter } from "./Texture"; +import { TextureWrap, TextureRegion } from "./Texture"; + +export class TextureAtlas implements Disposable { + pages = new Array(); + regions = new Array(); + + constructor(atlasText: string, textureLoader: (path: string, width?: number, height?: number) => any) { + this.load(atlasText, textureLoader); + } + + private load(atlasText: string, textureLoader: (path: string, width?: number, height?: number) => any) { + if (textureLoader == null) throw new Error("textureLoader cannot be null."); + + const reader = new TextureAtlasReader(atlasText); + const tuple = new Array(4); + let page: TextureAtlasPage = null; + while (true) { + let line = reader.readLine(); + if (line == null) break; + line = line.trim(); + if (line.length == 0) page = null; + else if (!page) { + page = new TextureAtlasPage(); + page.name = line; + + if (reader.readTuple(tuple) == 2) { + // size is only optional for an atlas packed with an old TexturePacker. + page.width = parseInt(tuple[0]); + page.height = parseInt(tuple[1]); + reader.readTuple(tuple); + } + // page.format = Format[tuple[0]]; we don't need format in WebGL + + reader.readTuple(tuple); + page.minFilter = Texture.filterFromString(tuple[0]); + page.magFilter = Texture.filterFromString(tuple[1]); + + const direction = reader.readValue(); + page.uWrap = TextureWrap.ClampToEdge; + page.vWrap = TextureWrap.ClampToEdge; + if (direction == "x") page.uWrap = TextureWrap.Repeat; + else if (direction == "y") page.vWrap = TextureWrap.Repeat; + else if (direction == "xy") page.uWrap = page.vWrap = TextureWrap.Repeat; + + page.texture = textureLoader(line); + page.texture.setFilters(page.minFilter, page.magFilter); + page.texture.setWraps(page.uWrap, page.vWrap); + page.width = page.texture.width; + page.height = page.texture.height; + this.pages.push(page); + } else { + const region: TextureAtlasRegion = new TextureAtlasRegion(); + region.name = line; + region.page = page; + + const rotateValue = reader.readValue(); + if (rotateValue.toLocaleLowerCase() == "true") { + region.degrees = 90; + } else if (rotateValue.toLocaleLowerCase() == "false") { + region.degrees = 0; + } else { + region.degrees = parseFloat(rotateValue); + } + region.rotate = region.degrees == 90; + + reader.readTuple(tuple); + const x = parseInt(tuple[0]); + const y = parseInt(tuple[1]); + + reader.readTuple(tuple); + const width = parseInt(tuple[0]); + const height = parseInt(tuple[1]); + + region.u = x / page.width; + region.v = y / page.height; + if (region.rotate) { + region.u2 = (x + height) / page.width; + region.v2 = (y + width) / page.height; + } else { + region.u2 = (x + width) / page.width; + region.v2 = (y + height) / page.height; + } + region.x = x; + region.y = y; + region.width = Math.abs(width); + region.height = Math.abs(height); + + if (reader.readTuple(tuple) == 4) { + // split is optional + // region.splits = new Vector.(parseInt(tuple[0]), parseInt(tuple[1]), parseInt(tuple[2]), parseInt(tuple[3])); + if (reader.readTuple(tuple) == 4) { + // pad is optional, but only present with splits + //region.pads = Vector.(parseInt(tuple[0]), parseInt(tuple[1]), parseInt(tuple[2]), parseInt(tuple[3])); + reader.readTuple(tuple); + } + } + + region.originalWidth = parseInt(tuple[0]); + region.originalHeight = parseInt(tuple[1]); + + reader.readTuple(tuple); + region.offsetX = parseInt(tuple[0]); + region.offsetY = parseInt(tuple[1]); + + region.index = parseInt(reader.readValue()); + + region.texture = page.texture; + this.regions.push(region); + } + } + } + + findRegion(name: string): TextureAtlasRegion { + for (let i = 0; i < this.regions.length; i++) { + if (this.regions[i].name == name) { + return this.regions[i]; + } + } + return null; + } + + dispose() { + for (let i = 0; i < this.pages.length; i++) { + this.pages[i].texture.dispose(); + } + } +} + +class TextureAtlasReader { + lines: Array; + index: number = 0; + + constructor(text: string) { + this.lines = text.split(/\r\n|\r|\n/); + } + + readLine(): string { + if (this.index >= this.lines.length) return null; + return this.lines[this.index++]; + } + + readValue(): string { + const line = this.readLine(); + const colon = line.indexOf(":"); + if (colon == -1) throw new Error("Invalid line: " + line); + return line.substring(colon + 1).trim(); + } + + readTuple(tuple: Array): number { + const line = this.readLine(); + const colon = line.indexOf(":"); + if (colon == -1) throw new Error("Invalid line: " + line); + let i = 0, + lastMatch = colon + 1; + for (; i < 3; i++) { + const comma = line.indexOf(",", lastMatch); + if (comma == -1) break; + tuple[i] = line.substr(lastMatch, comma - lastMatch).trim(); + lastMatch = comma + 1; + } + tuple[i] = line.substring(lastMatch).trim(); + return i + 1; + } +} + +export class TextureAtlasPage { + name: string; + minFilter: TextureFilter; + magFilter: TextureFilter; + uWrap: TextureWrap; + vWrap: TextureWrap; + texture: Texture; + width: number; + height: number; +} + +export class TextureAtlasRegion extends TextureRegion { + page: TextureAtlasPage; + name: string; + x: number; + y: number; + index: number; + degrees: number; + texture: Texture; +} diff --git a/packages/spine-core-3.8/src/spine-core/TransformConstraint.ts b/packages/spine-core-3.8/src/spine-core/TransformConstraint.ts new file mode 100644 index 0000000000..ab841eb62e --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/TransformConstraint.ts @@ -0,0 +1,296 @@ +import { Updatable } from "./Updatable"; +import { TransformConstraintData } from "./TransformConstraintData"; +import { Bone } from "./Bone"; +import { Skeleton } from "./Skeleton"; +import { MathUtils, Vector2 } from "./Utils"; + +/** Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained + * bones to match that of the target bone. + * + * See [Transform constraints](http://esotericsoftware.com/spine-transform-constraints) in the Spine User Guide. */ +export class TransformConstraint implements Updatable { + /** The transform constraint's setup pose data. */ + data: TransformConstraintData; + + /** The bones that will be modified by this transform constraint. */ + bones: Array; + + /** The target bone whose world transform will be copied to the constrained bones. */ + target: Bone; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + rotateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained translations. */ + translateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained scales. */ + scaleMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained scales. */ + shearMix = 0; + + temp = new Vector2(); + active = false; + + constructor(data: TransformConstraintData, skeleton: Skeleton) { + if (data == null) throw new Error("data cannot be null."); + if (skeleton == null) throw new Error("skeleton cannot be null."); + this.data = data; + this.rotateMix = data.rotateMix; + this.translateMix = data.translateMix; + this.scaleMix = data.scaleMix; + this.shearMix = data.shearMix; + this.bones = new Array(); + for (let i = 0; i < data.bones.length; i++) this.bones.push(skeleton.findBone(data.bones[i].name)); + this.target = skeleton.findBone(data.target.name); + } + + isActive() { + return this.active; + } + + /** Applies the constraint to the constrained bones. */ + apply() { + this.update(); + } + + update() { + if (this.data.local) { + if (this.data.relative) this.applyRelativeLocal(); + else this.applyAbsoluteLocal(); + } else { + if (this.data.relative) this.applyRelativeWorld(); + else this.applyAbsoluteWorld(); + } + } + + applyAbsoluteWorld() { + const rotateMix = this.rotateMix, + translateMix = this.translateMix, + scaleMix = this.scaleMix, + shearMix = this.shearMix; + const target = this.target; + const ta = target.a, + tb = target.b, + tc = target.c, + td = target.d; + const degRadReflect = ta * td - tb * tc > 0 ? MathUtils.degRad : -MathUtils.degRad; + const offsetRotation = this.data.offsetRotation * degRadReflect; + const offsetShearY = this.data.offsetShearY * degRadReflect; + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + let modified = false; + + if (rotateMix != 0) { + const a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d; + let r = Math.atan2(tc, ta) - Math.atan2(c, a) + offsetRotation; + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + r *= rotateMix; + const cos = Math.cos(r), + sin = Math.sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + modified = true; + } + + if (translateMix != 0) { + const temp = this.temp; + target.localToWorld(temp.set(this.data.offsetX, this.data.offsetY)); + bone.worldX += (temp.x - bone.worldX) * translateMix; + bone.worldY += (temp.y - bone.worldY) * translateMix; + modified = true; + } + + if (scaleMix > 0) { + let s = Math.sqrt(bone.a * bone.a + bone.c * bone.c); + let ts = Math.sqrt(ta * ta + tc * tc); + if (s > 0.00001) s = (s + (ts - s + this.data.offsetScaleX) * scaleMix) / s; + bone.a *= s; + bone.c *= s; + s = Math.sqrt(bone.b * bone.b + bone.d * bone.d); + ts = Math.sqrt(tb * tb + td * td); + if (s > 0.00001) s = (s + (ts - s + this.data.offsetScaleY) * scaleMix) / s; + bone.b *= s; + bone.d *= s; + modified = true; + } + + if (shearMix > 0) { + const b = bone.b, + d = bone.d; + const by = Math.atan2(d, b); + let r = Math.atan2(td, tb) - Math.atan2(tc, ta) - (by - Math.atan2(bone.c, bone.a)); + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + r = by + (r + offsetShearY) * shearMix; + const s = Math.sqrt(b * b + d * d); + bone.b = Math.cos(r) * s; + bone.d = Math.sin(r) * s; + modified = true; + } + + if (modified) bone.appliedValid = false; + } + } + + applyRelativeWorld() { + const rotateMix = this.rotateMix, + translateMix = this.translateMix, + scaleMix = this.scaleMix, + shearMix = this.shearMix; + const target = this.target; + const ta = target.a, + tb = target.b, + tc = target.c, + td = target.d; + const degRadReflect = ta * td - tb * tc > 0 ? MathUtils.degRad : -MathUtils.degRad; + const offsetRotation = this.data.offsetRotation * degRadReflect, + offsetShearY = this.data.offsetShearY * degRadReflect; + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + let modified = false; + + if (rotateMix != 0) { + const a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d; + let r = Math.atan2(tc, ta) + offsetRotation; + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + r *= rotateMix; + const cos = Math.cos(r), + sin = Math.sin(r); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + modified = true; + } + + if (translateMix != 0) { + const temp = this.temp; + target.localToWorld(temp.set(this.data.offsetX, this.data.offsetY)); + bone.worldX += temp.x * translateMix; + bone.worldY += temp.y * translateMix; + modified = true; + } + + if (scaleMix > 0) { + let s = (Math.sqrt(ta * ta + tc * tc) - 1 + this.data.offsetScaleX) * scaleMix + 1; + bone.a *= s; + bone.c *= s; + s = (Math.sqrt(tb * tb + td * td) - 1 + this.data.offsetScaleY) * scaleMix + 1; + bone.b *= s; + bone.d *= s; + modified = true; + } + + if (shearMix > 0) { + let r = Math.atan2(td, tb) - Math.atan2(tc, ta); + if (r > MathUtils.PI) r -= MathUtils.PI2; + else if (r < -MathUtils.PI) r += MathUtils.PI2; + const b = bone.b, + d = bone.d; + r = Math.atan2(d, b) + (r - MathUtils.PI / 2 + offsetShearY) * shearMix; + const s = Math.sqrt(b * b + d * d); + bone.b = Math.cos(r) * s; + bone.d = Math.sin(r) * s; + modified = true; + } + + if (modified) bone.appliedValid = false; + } + } + + applyAbsoluteLocal() { + const rotateMix = this.rotateMix, + translateMix = this.translateMix, + scaleMix = this.scaleMix, + shearMix = this.shearMix; + const target = this.target; + if (!target.appliedValid) target.updateAppliedTransform(); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (!bone.appliedValid) bone.updateAppliedTransform(); + + let rotation = bone.arotation; + if (rotateMix != 0) { + let r = target.arotation - rotation + this.data.offsetRotation; + r -= (16384 - ((16384.499999999996 - r / 360) | 0)) * 360; + rotation += r * rotateMix; + } + + let x = bone.ax, + y = bone.ay; + if (translateMix != 0) { + x += (target.ax - x + this.data.offsetX) * translateMix; + y += (target.ay - y + this.data.offsetY) * translateMix; + } + + let scaleX = bone.ascaleX, + scaleY = bone.ascaleY; + if (scaleMix != 0) { + if (scaleX > 0.00001) + scaleX = (scaleX + (target.ascaleX - scaleX + this.data.offsetScaleX) * scaleMix) / scaleX; + if (scaleY > 0.00001) + scaleY = (scaleY + (target.ascaleY - scaleY + this.data.offsetScaleY) * scaleMix) / scaleY; + } + + const shearY = bone.ashearY; + if (shearMix != 0) { + let r = target.ashearY - shearY + this.data.offsetShearY; + r -= (16384 - ((16384.499999999996 - r / 360) | 0)) * 360; + bone.shearY += r * shearMix; + } + + bone.updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } + + applyRelativeLocal() { + const rotateMix = this.rotateMix, + translateMix = this.translateMix, + scaleMix = this.scaleMix, + shearMix = this.shearMix; + const target = this.target; + if (!target.appliedValid) target.updateAppliedTransform(); + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) { + const bone = bones[i]; + if (!bone.appliedValid) bone.updateAppliedTransform(); + + let rotation = bone.arotation; + if (rotateMix != 0) rotation += (target.arotation + this.data.offsetRotation) * rotateMix; + + let x = bone.ax, + y = bone.ay; + if (translateMix != 0) { + x += (target.ax + this.data.offsetX) * translateMix; + y += (target.ay + this.data.offsetY) * translateMix; + } + + let scaleX = bone.ascaleX, + scaleY = bone.ascaleY; + if (scaleMix != 0) { + if (scaleX > 0.00001) scaleX *= (target.ascaleX - 1 + this.data.offsetScaleX) * scaleMix + 1; + if (scaleY > 0.00001) scaleY *= (target.ascaleY - 1 + this.data.offsetScaleY) * scaleMix + 1; + } + + let shearY = bone.ashearY; + if (shearMix != 0) shearY += (target.ashearY + this.data.offsetShearY) * shearMix; + + bone.updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/TransformConstraintData.ts b/packages/spine-core-3.8/src/spine-core/TransformConstraintData.ts new file mode 100644 index 0000000000..c135697c08 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/TransformConstraintData.ts @@ -0,0 +1,50 @@ +import { ConstraintData } from "./ConstraintData"; +import { BoneData } from "./BoneData"; + +/** Stores the setup pose for a {@link TransformConstraint}. + * + * See [Transform constraints](http://esotericsoftware.com/spine-transform-constraints) in the Spine User Guide. */ +export class TransformConstraintData extends ConstraintData { + /** The bones that will be modified by this transform constraint. */ + bones = new Array(); + + /** The target bone whose world transform will be copied to the constrained bones. */ + target: BoneData; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ + rotateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained translations. */ + translateMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained scales. */ + scaleMix = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained shears. */ + shearMix = 0; + + /** An offset added to the constrained bone rotation. */ + offsetRotation = 0; + + /** An offset added to the constrained bone X translation. */ + offsetX = 0; + + /** An offset added to the constrained bone Y translation. */ + offsetY = 0; + + /** An offset added to the constrained bone scaleX. */ + offsetScaleX = 0; + + /** An offset added to the constrained bone scaleY. */ + offsetScaleY = 0; + + /** An offset added to the constrained bone shearY. */ + offsetShearY = 0; + + relative = false; + local = false; + + constructor(name: string) { + super(name, 0, false); + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Triangulator.ts b/packages/spine-core-3.8/src/spine-core/Triangulator.ts new file mode 100644 index 0000000000..bd6efff41d --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Triangulator.ts @@ -0,0 +1,269 @@ +import { Pool } from "./Utils"; + +export class Triangulator { + private convexPolygons = new Array>(); + private convexPolygonsIndices = new Array>(); + + private indicesArray = new Array(); + private isConcaveArray = new Array(); + private triangles = new Array(); + + private polygonPool = new Pool>(() => { + return new Array(); + }); + + private polygonIndicesPool = new Pool>(() => { + return new Array(); + }); + + public triangulate(verticesArray: ArrayLike): Array { + const vertices = verticesArray; + let vertexCount = verticesArray.length >> 1; + + const indices = this.indicesArray; + indices.length = 0; + for (let i = 0; i < vertexCount; i++) indices[i] = i; + + const isConcave = this.isConcaveArray; + isConcave.length = 0; + for (let i = 0, n = vertexCount; i < n; ++i) + isConcave[i] = Triangulator.isConcave(i, vertexCount, vertices, indices); + + const triangles = this.triangles; + triangles.length = 0; + + while (vertexCount > 3) { + // Find ear tip. + let previous = vertexCount - 1, + i = 0, + next = 1; + while (true) { + outer: if (!isConcave[i]) { + const p1 = indices[previous] << 1, + p2 = indices[i] << 1, + p3 = indices[next] << 1; + const p1x = vertices[p1], + p1y = vertices[p1 + 1]; + const p2x = vertices[p2], + p2y = vertices[p2 + 1]; + const p3x = vertices[p3], + p3y = vertices[p3 + 1]; + for (let ii = (next + 1) % vertexCount; ii != previous; ii = (ii + 1) % vertexCount) { + if (!isConcave[ii]) continue; + const v = indices[ii] << 1; + const vx = vertices[v], + vy = vertices[v + 1]; + if (Triangulator.positiveArea(p3x, p3y, p1x, p1y, vx, vy)) { + if (Triangulator.positiveArea(p1x, p1y, p2x, p2y, vx, vy)) { + if (Triangulator.positiveArea(p2x, p2y, p3x, p3y, vx, vy)) break outer; + } + } + } + break; + } + + if (next == 0) { + do { + if (!isConcave[i]) break; + i--; + } while (i > 0); + break; + } + + previous = i; + i = next; + next = (next + 1) % vertexCount; + } + + // Cut ear tip. + triangles.push(indices[(vertexCount + i - 1) % vertexCount]); + triangles.push(indices[i]); + triangles.push(indices[(i + 1) % vertexCount]); + indices.splice(i, 1); + isConcave.splice(i, 1); + vertexCount--; + + const previousIndex = (vertexCount + i - 1) % vertexCount; + const nextIndex = i == vertexCount ? 0 : i; + isConcave[previousIndex] = Triangulator.isConcave(previousIndex, vertexCount, vertices, indices); + isConcave[nextIndex] = Triangulator.isConcave(nextIndex, vertexCount, vertices, indices); + } + + if (vertexCount == 3) { + triangles.push(indices[2]); + triangles.push(indices[0]); + triangles.push(indices[1]); + } + + return triangles; + } + + decompose(verticesArray: Array, triangles: Array): Array> { + const vertices = verticesArray; + const convexPolygons = this.convexPolygons; + this.polygonPool.freeAll(convexPolygons); + convexPolygons.length = 0; + + const convexPolygonsIndices = this.convexPolygonsIndices; + this.polygonIndicesPool.freeAll(convexPolygonsIndices); + convexPolygonsIndices.length = 0; + + let polygonIndices = this.polygonIndicesPool.obtain(); + polygonIndices.length = 0; + + let polygon = this.polygonPool.obtain(); + polygon.length = 0; + + // Merge subsequent triangles if they form a triangle fan. + let fanBaseIndex = -1, + lastWinding = 0; + for (let i = 0, n = triangles.length; i < n; i += 3) { + const t1 = triangles[i] << 1, + t2 = triangles[i + 1] << 1, + t3 = triangles[i + 2] << 1; + const x1 = vertices[t1], + y1 = vertices[t1 + 1]; + const x2 = vertices[t2], + y2 = vertices[t2 + 1]; + const x3 = vertices[t3], + y3 = vertices[t3 + 1]; + + // If the base of the last triangle is the same as this triangle, check if they form a convex polygon (triangle fan). + let merged = false; + if (fanBaseIndex == t1) { + const o = polygon.length - 4; + const winding1 = Triangulator.winding(polygon[o], polygon[o + 1], polygon[o + 2], polygon[o + 3], x3, y3); + const winding2 = Triangulator.winding(x3, y3, polygon[0], polygon[1], polygon[2], polygon[3]); + if (winding1 == lastWinding && winding2 == lastWinding) { + polygon.push(x3); + polygon.push(y3); + polygonIndices.push(t3); + merged = true; + } + } + + // Otherwise make this triangle the new base. + if (!merged) { + if (polygon.length > 0) { + convexPolygons.push(polygon); + convexPolygonsIndices.push(polygonIndices); + } else { + this.polygonPool.free(polygon); + this.polygonIndicesPool.free(polygonIndices); + } + polygon = this.polygonPool.obtain(); + polygon.length = 0; + polygon.push(x1); + polygon.push(y1); + polygon.push(x2); + polygon.push(y2); + polygon.push(x3); + polygon.push(y3); + polygonIndices = this.polygonIndicesPool.obtain(); + polygonIndices.length = 0; + polygonIndices.push(t1); + polygonIndices.push(t2); + polygonIndices.push(t3); + lastWinding = Triangulator.winding(x1, y1, x2, y2, x3, y3); + fanBaseIndex = t1; + } + } + + if (polygon.length > 0) { + convexPolygons.push(polygon); + convexPolygonsIndices.push(polygonIndices); + } + + // Go through the list of polygons and try to merge the remaining triangles with the found triangle fans. + for (let i = 0, n = convexPolygons.length; i < n; i++) { + polygonIndices = convexPolygonsIndices[i]; + if (polygonIndices.length == 0) continue; + const firstIndex = polygonIndices[0]; + const lastIndex = polygonIndices[polygonIndices.length - 1]; + + polygon = convexPolygons[i]; + const o = polygon.length - 4; + let prevPrevX = polygon[o], + prevPrevY = polygon[o + 1]; + let prevX = polygon[o + 2], + prevY = polygon[o + 3]; + const firstX = polygon[0], + firstY = polygon[1]; + const secondX = polygon[2], + secondY = polygon[3]; + const winding = Triangulator.winding(prevPrevX, prevPrevY, prevX, prevY, firstX, firstY); + + for (let ii = 0; ii < n; ii++) { + if (ii == i) continue; + const otherIndices = convexPolygonsIndices[ii]; + if (otherIndices.length != 3) continue; + const otherFirstIndex = otherIndices[0]; + const otherSecondIndex = otherIndices[1]; + const otherLastIndex = otherIndices[2]; + + const otherPoly = convexPolygons[ii]; + const x3 = otherPoly[otherPoly.length - 2], + y3 = otherPoly[otherPoly.length - 1]; + + if (otherFirstIndex != firstIndex || otherSecondIndex != lastIndex) continue; + const winding1 = Triangulator.winding(prevPrevX, prevPrevY, prevX, prevY, x3, y3); + const winding2 = Triangulator.winding(x3, y3, firstX, firstY, secondX, secondY); + if (winding1 == winding && winding2 == winding) { + otherPoly.length = 0; + otherIndices.length = 0; + polygon.push(x3); + polygon.push(y3); + polygonIndices.push(otherLastIndex); + prevPrevX = prevX; + prevPrevY = prevY; + prevX = x3; + prevY = y3; + ii = 0; + } + } + } + + // Remove empty polygons that resulted from the merge step above. + for (let i = convexPolygons.length - 1; i >= 0; i--) { + polygon = convexPolygons[i]; + if (polygon.length == 0) { + convexPolygons.splice(i, 1); + this.polygonPool.free(polygon); + polygonIndices = convexPolygonsIndices[i]; + convexPolygonsIndices.splice(i, 1); + this.polygonIndicesPool.free(polygonIndices); + } + } + + return convexPolygons; + } + + private static isConcave( + index: number, + vertexCount: number, + vertices: ArrayLike, + indices: ArrayLike + ): boolean { + const previous = indices[(vertexCount + index - 1) % vertexCount] << 1; + const current = indices[index] << 1; + const next = indices[(index + 1) % vertexCount] << 1; + return !this.positiveArea( + vertices[previous], + vertices[previous + 1], + vertices[current], + vertices[current + 1], + vertices[next], + vertices[next + 1] + ); + } + + private static positiveArea(p1x: number, p1y: number, p2x: number, p2y: number, p3x: number, p3y: number): boolean { + return p1x * (p3y - p2y) + p2x * (p1y - p3y) + p3x * (p2y - p1y) >= 0; + } + + private static winding(p1x: number, p1y: number, p2x: number, p2y: number, p3x: number, p3y: number): number { + const px = p2x - p1x, + py = p2y - p1y; + return p3x * py - p3y * px + px * p1y - p1x * py >= 0 ? 1 : -1; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/Updatable.ts b/packages/spine-core-3.8/src/spine-core/Updatable.ts new file mode 100644 index 0000000000..64e1965de1 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Updatable.ts @@ -0,0 +1,10 @@ +/** The interface for items updated by {@link Skeleton#updateWorldTransform()}. */ +export interface Updatable { + update(): void; + + /** Returns false when this item has not been updated because a skin is required and the {@link Skeleton#skin active skin} + * does not contain this item. + * @see Skin#getBones() + * @see Skin#getConstraints() */ + isActive(): boolean; +} diff --git a/packages/spine-core-3.8/src/spine-core/Utils.ts b/packages/spine-core-3.8/src/spine-core/Utils.ts new file mode 100644 index 0000000000..510e4cbba4 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/Utils.ts @@ -0,0 +1,417 @@ +import { MixBlend } from "./Animation"; +import { Skeleton } from "./Skeleton"; + +export interface Map { + [key: string]: T; +} + +export class IntSet { + array = new Array(); + + add(value: number): boolean { + const contains = this.contains(value); + this.array[value | 0] = value | 0; + return !contains; + } + + contains(value: number) { + return this.array[value | 0] != undefined; + } + + remove(value: number) { + this.array[value | 0] = undefined; + } + + clear() { + this.array.length = 0; + } +} + +export interface Disposable { + dispose(): void; +} + +export interface Restorable { + restore(): void; +} + +export class Color { + public static WHITE = new Color(1, 1, 1, 1); + public static RED = new Color(1, 0, 0, 1); + public static GREEN = new Color(0, 1, 0, 1); + public static BLUE = new Color(0, 0, 1, 1); + public static MAGENTA = new Color(1, 0, 1, 1); + + constructor( + public r: number = 0, + public g: number = 0, + public b: number = 0, + public a: number = 0 + ) {} + + set(r: number, g: number, b: number, a: number) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + this.clamp(); + return this; + } + + setFromColor(c: Color) { + this.r = c.r; + this.g = c.g; + this.b = c.b; + this.a = c.a; + return this; + } + + setFromString(hex: string) { + hex = hex.charAt(0) == "#" ? hex.substr(1) : hex; + this.r = parseInt(hex.substr(0, 2), 16) / 255.0; + this.g = parseInt(hex.substr(2, 2), 16) / 255.0; + this.b = parseInt(hex.substr(4, 2), 16) / 255.0; + this.a = (hex.length != 8 ? 255 : parseInt(hex.substr(6, 2), 16)) / 255.0; + return this; + } + + add(r: number, g: number, b: number, a: number) { + this.r += r; + this.g += g; + this.b += b; + this.a += a; + this.clamp(); + return this; + } + + clamp() { + if (this.r < 0) this.r = 0; + else if (this.r > 1) this.r = 1; + + if (this.g < 0) this.g = 0; + else if (this.g > 1) this.g = 1; + + if (this.b < 0) this.b = 0; + else if (this.b > 1) this.b = 1; + + if (this.a < 0) this.a = 0; + else if (this.a > 1) this.a = 1; + return this; + } + + static rgba8888ToColor(color: Color, value: number) { + color.r = ((value & 0xff000000) >>> 24) / 255; + color.g = ((value & 0x00ff0000) >>> 16) / 255; + color.b = ((value & 0x0000ff00) >>> 8) / 255; + color.a = (value & 0x000000ff) / 255; + } + + static rgb888ToColor(color: Color, value: number) { + color.r = ((value & 0x00ff0000) >>> 16) / 255; + color.g = ((value & 0x0000ff00) >>> 8) / 255; + color.b = (value & 0x000000ff) / 255; + } +} + +export class MathUtils { + static PI = 3.1415927; + static PI2 = MathUtils.PI * 2; + static radiansToDegrees = 180 / MathUtils.PI; + static radDeg = MathUtils.radiansToDegrees; + static degreesToRadians = MathUtils.PI / 180; + static degRad = MathUtils.degreesToRadians; + + static clamp(value: number, min: number, max: number) { + if (value < min) return min; + if (value > max) return max; + return value; + } + + static cosDeg(degrees: number) { + return Math.cos(degrees * MathUtils.degRad); + } + + static sinDeg(degrees: number) { + return Math.sin(degrees * MathUtils.degRad); + } + + static signum(value: number): number { + return value > 0 ? 1 : value < 0 ? -1 : 0; + } + + static toInt(x: number) { + return x > 0 ? Math.floor(x) : Math.ceil(x); + } + + static cbrt(x: number) { + const y = Math.pow(Math.abs(x), 1 / 3); + return x < 0 ? -y : y; + } + + static randomTriangular(min: number, max: number): number { + return MathUtils.randomTriangularWith(min, max, (min + max) * 0.5); + } + + static randomTriangularWith(min: number, max: number, mode: number): number { + const u = Math.random(); + const d = max - min; + if (u <= (mode - min) / d) return min + Math.sqrt(u * d * (mode - min)); + return max - Math.sqrt((1 - u) * d * (max - mode)); + } +} + +export abstract class Interpolation { + protected abstract applyInternal(a: number): number; + apply(start: number, end: number, a: number): number { + return start + (end - start) * this.applyInternal(a); + } +} + +export class Pow extends Interpolation { + protected power = 2; + + constructor(power: number) { + super(); + this.power = power; + } + + applyInternal(a: number): number { + if (a <= 0.5) return Math.pow(a * 2, this.power) / 2; + return Math.pow((a - 1) * 2, this.power) / (this.power % 2 == 0 ? -2 : 2) + 1; + } +} + +export class PowOut extends Pow { + constructor(power: number) { + super(power); + } + + override applyInternal(a: number): number { + return Math.pow(a - 1, this.power) * (this.power % 2 == 0 ? -1 : 1) + 1; + } +} + +export class Utils { + static SUPPORTS_TYPED_ARRAYS = typeof Float32Array !== "undefined"; + + static arrayCopy( + source: ArrayLike, + sourceStart: number, + dest: ArrayLike, + destStart: number, + numElements: number + ) { + for (let i = sourceStart, j = destStart; i < sourceStart + numElements; i++, j++) { + dest[j] = source[i]; + } + } + + static setArraySize(array: Array, size: number, value: any = 0): Array { + const oldSize = array.length; + if (oldSize == size) return array; + array.length = size; + if (oldSize < size) { + for (let i = oldSize; i < size; i++) array[i] = value; + } + return array; + } + + static ensureArrayCapacity(array: Array, size: number, value: any = 0): Array { + if (array.length >= size) return array; + return Utils.setArraySize(array, size, value); + } + + static newArray(size: number, defaultValue: T): Array { + const array = new Array(size); + for (let i = 0; i < size; i++) array[i] = defaultValue; + return array; + } + + static newFloatArray(size: number): ArrayLike { + if (Utils.SUPPORTS_TYPED_ARRAYS) { + return new Float32Array(size); + } else { + const array = new Array(size); + for (let i = 0; i < array.length; i++) array[i] = 0; + return array; + } + } + + static newShortArray(size: number): ArrayLike { + if (Utils.SUPPORTS_TYPED_ARRAYS) { + return new Int16Array(size); + } else { + const array = new Array(size); + for (let i = 0; i < array.length; i++) array[i] = 0; + return array; + } + } + + static toFloatArray(array: Array) { + return Utils.SUPPORTS_TYPED_ARRAYS ? new Float32Array(array) : array; + } + + static toSinglePrecision(value: number) { + return Utils.SUPPORTS_TYPED_ARRAYS ? Math.fround(value) : value; + } + + // This function is used to fix WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109 + static webkit602BugfixHelper(alpha: number, blend: MixBlend) {} + + static contains(array: Array, element: T, identity = true) { + for (let i = 0; i < array.length; i++) { + if (array[i] == element) return true; + } + return false; + } +} + +export class DebugUtils { + static logBones(skeleton: Skeleton) { + for (let i = 0; i < skeleton.bones.length; i++) { + const bone = skeleton.bones[i]; + console.log( + bone.data.name + + ", " + + bone.a + + ", " + + bone.b + + ", " + + bone.c + + ", " + + bone.d + + ", " + + bone.worldX + + ", " + + bone.worldY + ); + } + } +} + +export class Pool { + private items = new Array(); + private instantiator: () => T; + + constructor(instantiator: () => T) { + this.instantiator = instantiator; + } + + obtain() { + return this.items.length > 0 ? this.items.pop() : this.instantiator(); + } + + free(item: T) { + if ((item as any).reset) (item as any).reset(); + this.items.push(item); + } + + freeAll(items: ArrayLike) { + for (let i = 0; i < items.length; i++) { + this.free(items[i]); + } + } + + clear() { + this.items.length = 0; + } +} + +export class Vector2 { + constructor( + public x = 0, + public y = 0 + ) {} + + set(x: number, y: number): Vector2 { + this.x = x; + this.y = y; + return this; + } + + length() { + const x = this.x; + const y = this.y; + return Math.sqrt(x * x + y * y); + } + + normalize() { + const len = this.length(); + if (len != 0) { + this.x /= len; + this.y /= len; + } + return this; + } +} + +export class TimeKeeper { + maxDelta = 0.064; + framesPerSecond = 0; + delta = 0; + totalTime = 0; + + private lastTime = Date.now() / 1000; + private frameCount = 0; + private frameTime = 0; + + update() { + const now = Date.now() / 1000; + this.delta = now - this.lastTime; + this.frameTime += this.delta; + this.totalTime += this.delta; + if (this.delta > this.maxDelta) this.delta = this.maxDelta; + this.lastTime = now; + + this.frameCount++; + if (this.frameTime > 1) { + this.framesPerSecond = this.frameCount / this.frameTime; + this.frameTime = 0; + this.frameCount = 0; + } + } +} + +export interface ArrayLike { + length: number; + [n: number]: T; +} + +export class WindowedMean { + values: Array; + addedValues = 0; + lastValue = 0; + mean = 0; + dirty = true; + + constructor(windowSize: number = 32) { + this.values = new Array(windowSize); + } + + hasEnoughData() { + return this.addedValues >= this.values.length; + } + + addValue(value: number) { + if (this.addedValues < this.values.length) this.addedValues++; + this.values[this.lastValue++] = value; + if (this.lastValue > this.values.length - 1) this.lastValue = 0; + this.dirty = true; + } + + getMean() { + if (this.hasEnoughData()) { + if (this.dirty) { + let mean = 0; + for (let i = 0; i < this.values.length; i++) { + mean += this.values[i]; + } + this.mean = mean / this.values.length; + this.dirty = false; + } + return this.mean; + } else { + return 0; + } + } +} diff --git a/packages/spine-core-3.8/src/spine-core/VertexEffect.ts b/packages/spine-core-3.8/src/spine-core/VertexEffect.ts new file mode 100644 index 0000000000..ae23cd16d2 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/VertexEffect.ts @@ -0,0 +1,8 @@ +import { Skeleton } from "./Skeleton"; +import { Color, Vector2 } from "./Utils"; + +export interface VertexEffect { + begin(skeleton: Skeleton): void; + transform(position: Vector2, uv: Vector2, light: Color, dark: Color): void; + end(): void; +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/Attachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/Attachment.ts new file mode 100644 index 0000000000..e13cf7b936 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/Attachment.ts @@ -0,0 +1,147 @@ +import { Slot } from "../Slot"; +import { Utils, ArrayLike } from "../Utils"; + +/** The base class for all attachments. */ +export abstract class Attachment { + name: string; + + constructor(name: string) { + if (name == null) throw new Error("name cannot be null."); + this.name = name; + } + + abstract copy(): Attachment; +} + +/** Base class for an attachment with vertices that are transformed by one or more bones and can be deformed by a slot's + * {@link Slot#deform}. */ +export abstract class VertexAttachment extends Attachment { + private static nextID = 0; + + /** The unique ID for this attachment. */ + id = (VertexAttachment.nextID++ & 65535) << 11; + + /** The bones which affect the {@link #getVertices()}. The array entries are, for each vertex, the number of bones affecting + * the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton#bones}. Will be null + * if this attachment has no weights. */ + bones: Array; + + /** The vertex positions in the bone's coordinate system. For a non-weighted attachment, the values are `x,y` + * entries for each vertex. For a weighted attachment, the values are `x,y,weight` entries for each bone affecting + * each vertex. */ + vertices: ArrayLike; + + /** The maximum number of world vertex values that can be output by + * {@link #computeWorldVertices()} using the `count` parameter. */ + worldVerticesLength = 0; + + /** Deform keys for the deform attachment are also applied to this attachment. May be null if no deform keys should be applied. */ + deformAttachment: VertexAttachment = this; + + constructor(name: string) { + super(name); + } + + /** Transforms the attachment's local {@link vertices} to world coordinates. If the slot's {@link Slot#deform} is + * not empty, it is used to deform the vertices. + * + * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine + * Runtimes Guide. + * @param start The index of the first {@link #vertices} value to transform. Each vertex has 2 values, x and y. + * @param count The number of world vertex values to output. Must be <= {@link #worldVerticesLength} - `start`. + * @param worldVertices The output world vertices. Must have a length >= `offset` + `count` * + * `stride` / 2. + * @param offset The `worldVertices` index to begin writing values. + * @param stride The number of `worldVertices` entries between the value pairs written. */ + computeWorldVertices( + slot: Slot, + start: number, + count: number, + worldVertices: ArrayLike, + offset: number, + stride: number + ) { + count = offset + (count >> 1) * stride; + const skeleton = slot.bone.skeleton; + const deformArray = slot.deform; + let vertices = this.vertices; + const bones = this.bones; + if (bones == null) { + if (deformArray.length > 0) vertices = deformArray; + const bone = slot.bone; + const x = bone.worldX; + const y = bone.worldY; + const a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d; + for (let v = start, w = offset; w < count; v += 2, w += stride) { + const vx = vertices[v], + vy = vertices[v + 1]; + worldVertices[w] = vx * a + vy * b + x; + worldVertices[w + 1] = vx * c + vy * d + y; + } + return; + } + let v = 0, + skip = 0; + for (let i = 0; i < start; i += 2) { + const n = bones[v]; + v += n + 1; + skip += n; + } + const skeletonBones = skeleton.bones; + if (deformArray.length == 0) { + for (let w = offset, b = skip * 3; w < count; w += stride) { + let wx = 0, + wy = 0; + let n = bones[v++]; + n += v; + for (; v < n; v++, b += 3) { + const bone = skeletonBones[bones[v]]; + const vx = vertices[b], + vy = vertices[b + 1], + weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } else { + const deform = deformArray; + for (let w = offset, b = skip * 3, f = skip << 1; w < count; w += stride) { + let wx = 0, + wy = 0; + let n = bones[v++]; + n += v; + for (; v < n; v++, b += 3, f += 2) { + const bone = skeletonBones[bones[v]]; + const vx = vertices[b] + deform[f], + vy = vertices[b + 1] + deform[f + 1], + weight = vertices[b + 2]; + wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight; + wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight; + } + worldVertices[w] = wx; + worldVertices[w + 1] = wy; + } + } + } + + /** Does not copy id (generated) or name (set on construction). **/ + copyTo(attachment: VertexAttachment) { + if (this.bones != null) { + attachment.bones = new Array(this.bones.length); + Utils.arrayCopy(this.bones, 0, attachment.bones, 0, this.bones.length); + } else attachment.bones = null; + + if (this.vertices != null) { + attachment.vertices = Utils.newFloatArray(this.vertices.length); + Utils.arrayCopy(this.vertices, 0, attachment.vertices, 0, this.vertices.length); + } else attachment.vertices = null; + + attachment.worldVerticesLength = this.worldVerticesLength; + attachment.deformAttachment = this.deformAttachment; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/AttachmentLoader.ts b/packages/spine-core-3.8/src/spine-core/attachments/AttachmentLoader.ts new file mode 100644 index 0000000000..7d14c9f22c --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/AttachmentLoader.ts @@ -0,0 +1,31 @@ +import { RegionAttachment } from "./RegionAttachment"; +import { Skin } from "../Skin"; +import { BoundingBoxAttachment } from "./BoundingBoxAttachment"; +import { PathAttachment } from "./PathAttachment"; +import { PointAttachment } from "./PointAttachment"; +import { ClippingAttachment } from "./ClippingAttachment"; +import { MeshAttachment } from "./MeshAttachment"; + +/** The interface which can be implemented to customize creating and populating attachments. + * + * See [Loading skeleton data](http://esotericsoftware.com/spine-loading-skeleton-data#AttachmentLoader) in the Spine + * Runtimes Guide. */ +export interface AttachmentLoader { + /** @return May be null to not load an attachment. */ + newRegionAttachment(skin: Skin, name: string, path: string): RegionAttachment; + + /** @return May be null to not load an attachment. */ + newMeshAttachment(skin: Skin, name: string, path: string): MeshAttachment; + + /** @return May be null to not load an attachment. */ + newBoundingBoxAttachment(skin: Skin, name: string): BoundingBoxAttachment; + + /** @return May be null to not load an attachment */ + newPathAttachment(skin: Skin, name: string): PathAttachment; + + /** @return May be null to not load an attachment */ + newPointAttachment(skin: Skin, name: string): PointAttachment; + + /** @return May be null to not load an attachment */ + newClippingAttachment(skin: Skin, name: string): ClippingAttachment; +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/AttachmentType.ts b/packages/spine-core-3.8/src/spine-core/attachments/AttachmentType.ts new file mode 100644 index 0000000000..193fa62007 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/AttachmentType.ts @@ -0,0 +1,9 @@ +export enum AttachmentType { + Region, + BoundingBox, + Mesh, + LinkedMesh, + Path, + Point, + Clipping +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/BoundingBoxAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/BoundingBoxAttachment.ts new file mode 100644 index 0000000000..b23cec9733 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/BoundingBoxAttachment.ts @@ -0,0 +1,22 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { Color } from "../Utils"; + +/** An attachment with vertices that make up a polygon. Can be used for hit detection, creating physics bodies, spawning particle + * effects, and more. + * + * See {@link SkeletonBounds} and [Bounding Boxes](http://esotericsoftware.com/spine-bounding-boxes) in the Spine User + * Guide. */ +export class BoundingBoxAttachment extends VertexAttachment { + color = new Color(1, 1, 1, 1); + + constructor(name: string) { + super(name); + } + + copy(): Attachment { + const copy = new BoundingBoxAttachment(this.name); + this.copyTo(copy); + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/ClippingAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/ClippingAttachment.ts new file mode 100644 index 0000000000..8ebc184346 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/ClippingAttachment.ts @@ -0,0 +1,27 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { SlotData } from "../SlotData"; +import { Color } from "../Utils"; + +/** An attachment with vertices that make up a polygon used for clipping the rendering of other attachments. */ +export class ClippingAttachment extends VertexAttachment { + /** Clipping is performed between the clipping polygon's slot and the end slot. Returns null if clipping is done until the end of + * the skeleton's rendering. */ + endSlot: SlotData; + + // Nonessential. + /** The color of the clipping polygon as it was in Spine. Available only when nonessential data was exported. Clipping polygons + * are not usually rendered at runtime. */ + color = new Color(0.2275, 0.2275, 0.8078, 1); // ce3a3aff + + constructor(name: string) { + super(name); + } + + copy(): Attachment { + const copy = new ClippingAttachment(this.name); + this.copyTo(copy); + copy.endSlot = this.endSlot; + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/MeshAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/MeshAttachment.ts new file mode 100644 index 0000000000..02deaa68ff --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/MeshAttachment.ts @@ -0,0 +1,175 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { TextureRegion } from "../Texture"; +import { Color, Utils, ArrayLike } from "../Utils"; +import { TextureAtlasRegion } from "../TextureAtlas"; + +/** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not + * supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh. + * + * See [Mesh attachments](http://esotericsoftware.com/spine-meshes) in the Spine User Guide. */ +export class MeshAttachment extends VertexAttachment { + region: TextureRegion; + + /** The name of the texture region for this attachment. */ + path: string; + + /** The UV pair for each vertex, normalized within the texture region. */ + regionUVs: ArrayLike; + + /** The UV pair for each vertex, normalized within the entire texture. + * + * See {@link #updateUVs}. */ + uvs: ArrayLike; + + /** Triplets of vertex indices which describe the mesh's triangulation. */ + triangles: Array; + + /** The color to tint the mesh. */ + color = new Color(1, 1, 1, 1); + + /** The width of the mesh's image. Available only when nonessential data was exported. */ + width: number; + + /** The height of the mesh's image. Available only when nonessential data was exported. */ + height: number; + + /** The number of entries at the beginning of {@link #vertices} that make up the mesh hull. */ + hullLength: number; + + /** Vertex index pairs describing edges for controling triangulation. Mesh triangles will never cross edges. Only available if + * nonessential data was exported. Triangulation is not performed at runtime. */ + edges: Array; + + private parentMesh: MeshAttachment; + tempColor = new Color(0, 0, 0, 0); + + constructor(name: string) { + super(name); + } + + /** Calculates {@link #uvs} using {@link #regionUVs} and the {@link #region}. Must be called after changing the region UVs or + * region. */ + updateUVs() { + const regionUVs = this.regionUVs; + if (this.uvs == null || this.uvs.length != regionUVs.length) this.uvs = Utils.newFloatArray(regionUVs.length); + const uvs = this.uvs; + const n = this.uvs.length; + let u = this.region.u, + v = this.region.v, + width = 0, + height = 0; + if (this.region instanceof TextureAtlasRegion) { + const region = this.region; + const textureWidth = region.texture.width, + textureHeight = region.texture.height; + switch (region.degrees) { + case 90: + u -= (region.originalHeight - region.offsetY - region.height) / textureWidth; + v -= (region.originalWidth - region.offsetX - region.width) / textureHeight; + width = region.originalHeight / textureWidth; + height = region.originalWidth / textureHeight; + for (let i = 0; i < n; i += 2) { + uvs[i] = u + regionUVs[i + 1] * width; + uvs[i + 1] = v + (1 - regionUVs[i]) * height; + } + return; + case 180: + u -= (region.originalWidth - region.offsetX - region.width) / textureWidth; + v -= region.offsetY / textureHeight; + width = region.originalWidth / textureWidth; + height = region.originalHeight / textureHeight; + for (let i = 0; i < n; i += 2) { + uvs[i] = u + (1 - regionUVs[i]) * width; + uvs[i + 1] = v + (1 - regionUVs[i + 1]) * height; + } + return; + case 270: + u -= region.offsetY / textureWidth; + v -= region.offsetX / textureHeight; + width = region.originalHeight / textureWidth; + height = region.originalWidth / textureHeight; + for (let i = 0; i < n; i += 2) { + uvs[i] = u + (1 - regionUVs[i + 1]) * width; + uvs[i + 1] = v + regionUVs[i] * height; + } + return; + } + u -= region.offsetX / textureWidth; + v -= (region.originalHeight - region.offsetY - region.height) / textureHeight; + width = region.originalWidth / textureWidth; + height = region.originalHeight / textureHeight; + } else if (this.region == null) { + u = v = 0; + width = height = 1; + } else { + width = this.region.u2 - u; + height = this.region.v2 - v; + } + + for (let i = 0; i < n; i += 2) { + uvs[i] = u + regionUVs[i] * width; + uvs[i + 1] = v + regionUVs[i + 1] * height; + } + } + + /** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices}, + * {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the + * parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */ + getParentMesh() { + return this.parentMesh; + } + + /** @param parentMesh May be null. */ + setParentMesh(parentMesh: MeshAttachment) { + this.parentMesh = parentMesh; + if (parentMesh != null) { + this.bones = parentMesh.bones; + this.vertices = parentMesh.vertices; + this.worldVerticesLength = parentMesh.worldVerticesLength; + this.regionUVs = parentMesh.regionUVs; + this.triangles = parentMesh.triangles; + this.hullLength = parentMesh.hullLength; + this.worldVerticesLength = parentMesh.worldVerticesLength; + } + } + + copy(): Attachment { + if (this.parentMesh != null) return this.newLinkedMesh(); + + const copy = new MeshAttachment(this.name); + copy.region = this.region; + copy.path = this.path; + copy.color.setFromColor(this.color); + + this.copyTo(copy); + copy.regionUVs = new Array(this.regionUVs.length); + Utils.arrayCopy(this.regionUVs, 0, copy.regionUVs, 0, this.regionUVs.length); + copy.uvs = new Array(this.uvs.length); + Utils.arrayCopy(this.uvs, 0, copy.uvs, 0, this.uvs.length); + copy.triangles = new Array(this.triangles.length); + Utils.arrayCopy(this.triangles, 0, copy.triangles, 0, this.triangles.length); + copy.hullLength = this.hullLength; + + // Nonessential. + if (this.edges != null) { + copy.edges = new Array(this.edges.length); + Utils.arrayCopy(this.edges, 0, copy.edges, 0, this.edges.length); + } + copy.width = this.width; + copy.height = this.height; + + return copy; + } + + /** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. **/ + newLinkedMesh(): MeshAttachment { + const copy = new MeshAttachment(this.name); + copy.region = this.region; + copy.path = this.path; + copy.color.setFromColor(this.color); + copy.deformAttachment = this.deformAttachment; + copy.setParentMesh(this.parentMesh != null ? this.parentMesh : this); + copy.updateUVs(); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/PathAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/PathAttachment.ts new file mode 100644 index 0000000000..53562b7657 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/PathAttachment.ts @@ -0,0 +1,36 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { Color, Utils } from "../Utils"; + +/** An attachment whose vertices make up a composite Bezier curve. + * + * See {@link PathConstraint} and [Paths](http://esotericsoftware.com/spine-paths) in the Spine User Guide. */ +export class PathAttachment extends VertexAttachment { + /** The lengths along the path in the setup pose from the start of the path to the end of each Bezier curve. */ + lengths: Array; + + /** If true, the start and end knots are connected. */ + closed = false; + + /** If true, additional calculations are performed to make calculating positions along the path more accurate. If false, fewer + * calculations are performed but calculating positions along the path is less accurate. */ + constantSpeed = false; + + /** The color of the path as it was in Spine. Available only when nonessential data was exported. Paths are not usually + * rendered at runtime. */ + color = new Color(1, 1, 1, 1); + + constructor(name: string) { + super(name); + } + + copy(): Attachment { + const copy = new PathAttachment(this.name); + this.copyTo(copy); + copy.lengths = new Array(this.lengths.length); + Utils.arrayCopy(this.lengths, 0, copy.lengths, 0, this.lengths.length); + copy.closed = closed; + copy.constantSpeed = this.constantSpeed; + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/PointAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/PointAttachment.ts new file mode 100644 index 0000000000..74de51a2ef --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/PointAttachment.ts @@ -0,0 +1,46 @@ +import { VertexAttachment, Attachment } from "./Attachment"; +import { Color, MathUtils, Vector2 } from "../Utils"; +import { Bone } from "../Bone"; + +/** An attachment which is a single point and a rotation. This can be used to spawn projectiles, particles, etc. A bone can be + * used in similar ways, but a PointAttachment is slightly less expensive to compute and can be hidden, shown, and placed in a + * skin. + * + * See [Point Attachments](http://esotericsoftware.com/spine-point-attachments) in the Spine User Guide. */ +export class PointAttachment extends VertexAttachment { + x: number; + y: number; + rotation: number; + + /** The color of the point attachment as it was in Spine. Available only when nonessential data was exported. Point attachments + * are not usually rendered at runtime. */ + color = new Color(0.38, 0.94, 0, 1); + + constructor(name: string) { + super(name); + this.name = name; + } + + computeWorldPosition(bone: Bone, point: Vector2) { + point.x = this.x * bone.a + this.y * bone.b + bone.worldX; + point.y = this.x * bone.c + this.y * bone.d + bone.worldY; + return point; + } + + computeWorldRotation(bone: Bone) { + const cos = MathUtils.cosDeg(this.rotation), + sin = MathUtils.sinDeg(this.rotation); + const x = cos * bone.a + sin * bone.b; + const y = cos * bone.c + sin * bone.d; + return Math.atan2(y, x) * MathUtils.radDeg; + } + + copy(): Attachment { + const copy = new PointAttachment(this.name); + copy.x = this.x; + copy.y = this.y; + copy.rotation = this.rotation; + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/attachments/RegionAttachment.ts b/packages/spine-core-3.8/src/spine-core/attachments/RegionAttachment.ts new file mode 100644 index 0000000000..1730e3880a --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/attachments/RegionAttachment.ts @@ -0,0 +1,211 @@ +import { Attachment } from "./Attachment"; +import { Color, Utils, ArrayLike } from "../Utils"; +import { TextureRegion } from "../Texture"; +import { Bone } from "../Bone"; + +/** An attachment that displays a textured quadrilateral. + * + * See [Region attachments](http://esotericsoftware.com/spine-regions) in the Spine User Guide. */ +export class RegionAttachment extends Attachment { + static OX1 = 0; + static OY1 = 1; + static OX2 = 2; + static OY2 = 3; + static OX3 = 4; + static OY3 = 5; + static OX4 = 6; + static OY4 = 7; + + static X1 = 0; + static Y1 = 1; + static C1R = 2; + static C1G = 3; + static C1B = 4; + static C1A = 5; + static U1 = 6; + static V1 = 7; + + static X2 = 8; + static Y2 = 9; + static C2R = 10; + static C2G = 11; + static C2B = 12; + static C2A = 13; + static U2 = 14; + static V2 = 15; + + static X3 = 16; + static Y3 = 17; + static C3R = 18; + static C3G = 19; + static C3B = 20; + static C3A = 21; + static U3 = 22; + static V3 = 23; + + static X4 = 24; + static Y4 = 25; + static C4R = 26; + static C4G = 27; + static C4B = 28; + static C4A = 29; + static U4 = 30; + static V4 = 31; + + /** The local x translation. */ + x = 0; + + /** The local y translation. */ + y = 0; + + /** The local scaleX. */ + scaleX = 1; + + /** The local scaleY. */ + scaleY = 1; + + /** The local rotation. */ + rotation = 0; + + /** The width of the region attachment in Spine. */ + width = 0; + + /** The height of the region attachment in Spine. */ + height = 0; + + /** The color to tint the region attachment. */ + color = new Color(1, 1, 1, 1); + + /** The name of the texture region for this attachment. */ + path: string; + + rendererObject: any; + region: TextureRegion; + + /** For each of the 4 vertices, a pair of x,y values that is the local position of the vertex. + * + * See {@link #updateOffset()}. */ + offset = Utils.newFloatArray(8); + + uvs = Utils.newFloatArray(8); + + tempColor = new Color(1, 1, 1, 1); + + constructor(name: string) { + super(name); + } + + /** Calculates the {@link #offset} using the region settings. Must be called after changing region settings. */ + updateOffset(): void { + const regionScaleX = (this.width / this.region.originalWidth) * this.scaleX; + const regionScaleY = (this.height / this.region.originalHeight) * this.scaleY; + const localX = (-this.width / 2) * this.scaleX + this.region.offsetX * regionScaleX; + const localY = (-this.height / 2) * this.scaleY + this.region.offsetY * regionScaleY; + const localX2 = localX + this.region.width * regionScaleX; + const localY2 = localY + this.region.height * regionScaleY; + const radians = (this.rotation * Math.PI) / 180; + const cos = Math.cos(radians); + const sin = Math.sin(radians); + const localXCos = localX * cos + this.x; + const localXSin = localX * sin; + const localYCos = localY * cos + this.y; + const localYSin = localY * sin; + const localX2Cos = localX2 * cos + this.x; + const localX2Sin = localX2 * sin; + const localY2Cos = localY2 * cos + this.y; + const localY2Sin = localY2 * sin; + const offset = this.offset; + offset[RegionAttachment.OX1] = localXCos - localYSin; + offset[RegionAttachment.OY1] = localYCos + localXSin; + offset[RegionAttachment.OX2] = localXCos - localY2Sin; + offset[RegionAttachment.OY2] = localY2Cos + localXSin; + offset[RegionAttachment.OX3] = localX2Cos - localY2Sin; + offset[RegionAttachment.OY3] = localY2Cos + localX2Sin; + offset[RegionAttachment.OX4] = localX2Cos - localYSin; + offset[RegionAttachment.OY4] = localYCos + localX2Sin; + } + + setRegion(region: TextureRegion): void { + this.region = region; + const uvs = this.uvs; + if (region.rotate) { + uvs[2] = region.u; + uvs[3] = region.v2; + uvs[4] = region.u; + uvs[5] = region.v; + uvs[6] = region.u2; + uvs[7] = region.v; + uvs[0] = region.u2; + uvs[1] = region.v2; + } else { + uvs[0] = region.u; + uvs[1] = region.v2; + uvs[2] = region.u; + uvs[3] = region.v; + uvs[4] = region.u2; + uvs[5] = region.v; + uvs[6] = region.u2; + uvs[7] = region.v2; + } + } + + /** Transforms the attachment's four vertices to world coordinates. + * + * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine + * Runtimes Guide. + * @param worldVertices The output world vertices. Must have a length >= `offset` + 8. + * @param offset The `worldVertices` index to begin writing values. + * @param stride The number of `worldVertices` entries between the value pairs written. */ + computeWorldVertices(bone: Bone, worldVertices: ArrayLike, offset: number, stride: number) { + const vertexOffset = this.offset; + const x = bone.worldX, + y = bone.worldY; + const a = bone.a, + b = bone.b, + c = bone.c, + d = bone.d; + let offsetX = 0, + offsetY = 0; + + offsetX = vertexOffset[RegionAttachment.OX1]; + offsetY = vertexOffset[RegionAttachment.OY1]; + worldVertices[offset] = offsetX * a + offsetY * b + x; // br + worldVertices[offset + 1] = offsetX * c + offsetY * d + y; + offset += stride; + + offsetX = vertexOffset[RegionAttachment.OX2]; + offsetY = vertexOffset[RegionAttachment.OY2]; + worldVertices[offset] = offsetX * a + offsetY * b + x; // bl + worldVertices[offset + 1] = offsetX * c + offsetY * d + y; + offset += stride; + + offsetX = vertexOffset[RegionAttachment.OX3]; + offsetY = vertexOffset[RegionAttachment.OY3]; + worldVertices[offset] = offsetX * a + offsetY * b + x; // ul + worldVertices[offset + 1] = offsetX * c + offsetY * d + y; + offset += stride; + + offsetX = vertexOffset[RegionAttachment.OX4]; + offsetY = vertexOffset[RegionAttachment.OY4]; + worldVertices[offset] = offsetX * a + offsetY * b + x; // ur + worldVertices[offset + 1] = offsetX * c + offsetY * d + y; + } + + copy(): Attachment { + const copy = new RegionAttachment(this.name); + copy.region = this.region; + copy.rendererObject = this.rendererObject; + copy.path = this.path; + copy.x = this.x; + copy.y = this.y; + copy.scaleX = this.scaleX; + copy.scaleY = this.scaleY; + copy.rotation = this.rotation; + copy.width = this.width; + copy.height = this.height; + Utils.arrayCopy(this.uvs, 0, copy.uvs, 0, 8); + Utils.arrayCopy(this.offset, 0, copy.offset, 0, 8); + copy.color.setFromColor(this.color); + return copy; + } +} diff --git a/packages/spine-core-3.8/src/spine-core/index.ts b/packages/spine-core-3.8/src/spine-core/index.ts new file mode 100644 index 0000000000..9dfa8aee05 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/index.ts @@ -0,0 +1,50 @@ +// Barrel for the vendored spine 3.8 runtime. There is no `@esotericsoftware/spine-core` npm +// release for the 3.8 line (npm starts at 4.0.1), so this directory vendors the runtime source +// instead of depending on a package; this file is the re-export surface the 4.2 package gets for +// free from `export * from "@esotericsoftware/spine-core"`. +import "./polyfills"; + +export * from "./Animation"; +export * from "./AnimationState"; +export * from "./AnimationStateData"; +export * from "./AtlasAttachmentLoader"; +export * from "./BlendMode"; +export * from "./Bone"; +export * from "./BoneData"; +export * from "./ConstraintData"; +export * from "./Event"; +export * from "./EventData"; +export * from "./IkConstraint"; +export * from "./IkConstraintData"; +export * from "./PathConstraint"; +export * from "./PathConstraintData"; +export * from "./Skeleton"; +export * from "./SkeletonBinary"; +export * from "./SkeletonBounds"; +export * from "./SkeletonClipping"; +export * from "./SkeletonData"; +export * from "./SkeletonJson"; +export * from "./Skin"; +export * from "./Slot"; +export * from "./SlotData"; +export * from "./Texture"; +export * from "./TextureAtlas"; +export * from "./TransformConstraint"; +export * from "./TransformConstraintData"; +export * from "./Triangulator"; +export * from "./Updatable"; +export * from "./Utils"; +export * from "./VertexEffect"; + +export * from "./attachments/Attachment"; +export * from "./attachments/AttachmentLoader"; +export * from "./attachments/AttachmentType"; +export * from "./attachments/BoundingBoxAttachment"; +export * from "./attachments/ClippingAttachment"; +export * from "./attachments/MeshAttachment"; +export * from "./attachments/PathAttachment"; +export * from "./attachments/PointAttachment"; +export * from "./attachments/RegionAttachment"; + +export * from "./vertexeffects/JitterEffect"; +export * from "./vertexeffects/SwirlEffect"; diff --git a/packages/spine-core-3.8/src/spine-core/polyfills.ts b/packages/spine-core-3.8/src/spine-core/polyfills.ts new file mode 100644 index 0000000000..0531725d1c --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/polyfills.ts @@ -0,0 +1,13 @@ +interface Math { + fround(n: number): number; +} + +(() => { + if (!Math.fround) { + Math.fround = (function (array) { + return function (x: number) { + return (array[0] = x), array[0]; + }; + })(new Float32Array(1)); + } +})(); diff --git a/packages/spine-core-3.8/src/spine-core/vertexeffects/JitterEffect.ts b/packages/spine-core-3.8/src/spine-core/vertexeffects/JitterEffect.ts new file mode 100644 index 0000000000..f7060b06ba --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/vertexeffects/JitterEffect.ts @@ -0,0 +1,22 @@ +import { VertexEffect } from "../VertexEffect"; +import { Skeleton } from "../Skeleton"; +import { Color, MathUtils, Vector2 } from "../Utils"; + +export class JitterEffect implements VertexEffect { + jitterX = 0; + jitterY = 0; + + constructor(jitterX: number, jitterY: number) { + this.jitterX = jitterX; + this.jitterY = jitterY; + } + + begin(skeleton: Skeleton): void {} + + transform(position: Vector2, uv: Vector2, light: Color, dark: Color): void { + position.x += MathUtils.randomTriangular(-this.jitterX, this.jitterY); + position.y += MathUtils.randomTriangular(-this.jitterX, this.jitterY); + } + + end(): void {} +} diff --git a/packages/spine-core-3.8/src/spine-core/vertexeffects/SwirlEffect.ts b/packages/spine-core-3.8/src/spine-core/vertexeffects/SwirlEffect.ts new file mode 100644 index 0000000000..52f0169be5 --- /dev/null +++ b/packages/spine-core-3.8/src/spine-core/vertexeffects/SwirlEffect.ts @@ -0,0 +1,38 @@ +import { VertexEffect } from "../VertexEffect"; +import { PowOut, Color, MathUtils, Vector2 } from "../Utils"; +import { Skeleton } from "../Skeleton"; + +export class SwirlEffect implements VertexEffect { + static interpolation = new PowOut(2); + centerX = 0; + centerY = 0; + radius = 0; + angle = 0; + private worldX = 0; + private worldY = 0; + + constructor(radius: number) { + this.radius = radius; + } + + begin(skeleton: Skeleton): void { + this.worldX = skeleton.x + this.centerX; + this.worldY = skeleton.y + this.centerY; + } + + transform(position: Vector2, uv: Vector2, light: Color, dark: Color): void { + const radAngle = this.angle * MathUtils.degreesToRadians; + const x = position.x - this.worldX; + const y = position.y - this.worldY; + const dist = Math.sqrt(x * x + y * y); + if (dist < this.radius) { + const theta = SwirlEffect.interpolation.apply(0, radAngle, (this.radius - dist) / this.radius); + const cos = Math.cos(theta); + const sin = Math.sin(theta); + position.x = cos * x - sin * y + this.worldX; + position.y = sin * x + cos * y + this.worldY; + } + } + + end(): void {} +} diff --git a/packages/spine-core-3.8/src/util/ClearablePool.ts b/packages/spine-core-3.8/src/util/ClearablePool.ts new file mode 100644 index 0000000000..ff8d6ccc6a --- /dev/null +++ b/packages/spine-core-3.8/src/util/ClearablePool.ts @@ -0,0 +1,26 @@ +export class ClearablePool { + private _type: new () => T; + private _elements: T[]; + private _usedElementCount: number = 0; + + constructor(type: new () => T) { + this._type = type; + this._elements = []; + } + + get(): T { + const { _usedElementCount: usedElementCount, _elements: elements } = this; + this._usedElementCount++; + if (elements.length === usedElementCount) { + const element = new this._type(); + elements.push(element); + return element; + } else { + return elements[usedElementCount]; + } + } + + clear(): void { + this._usedElementCount = 0; + } +} diff --git a/packages/spine-core-3.8/src/util/ReturnablePool.ts b/packages/spine-core-3.8/src/util/ReturnablePool.ts new file mode 100644 index 0000000000..77c1bcaa47 --- /dev/null +++ b/packages/spine-core-3.8/src/util/ReturnablePool.ts @@ -0,0 +1,25 @@ +export class ReturnablePool { + private _type: new () => T; + private _elements: T[]; + private _lastElementIndex: number; + + constructor(type: new () => T, initializeCount: number = 1) { + this._type = type; + this._lastElementIndex = initializeCount - 1; + const elements = (this._elements = new Array(initializeCount)); + for (let i = 0; i < initializeCount; ++i) { + elements[i] = new type(); + } + } + + get(): T { + if (this._lastElementIndex < 0) { + return new this._type(); + } + return this._elements[this._lastElementIndex--]; + } + + return(element: T): void { + this._elements[++this._lastElementIndex] = element; + } +} diff --git a/packages/spine-core-3.8/tsconfig.json b/packages/spine-core-3.8/tsconfig.json new file mode 100644 index 0000000000..588b0055c3 --- /dev/null +++ b/packages/spine-core-3.8/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "declaration": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "declarationDir": "types", + "emitDeclarationOnly": true, + "noImplicitOverride": true, + "sourceMap": true, + "incremental": false, + "skipLibCheck": true, + "stripInternal": true + }, + "include": ["src/**/*"] +} diff --git a/packages/spine-core-4.2/package.json b/packages/spine-core-4.2/package.json new file mode 100644 index 0000000000..020c4d1474 --- /dev/null +++ b/packages/spine-core-4.2/package.json @@ -0,0 +1,41 @@ +{ + "name": "@galacean/engine-spine-core-4.2", + "version": "2.0.0-alpha.38", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "url": "https://github.com/galacean/engine.git" + }, + "license": "MIT", + "main": "dist/main.js", + "module": "dist/module.js", + "debug": "src/index.ts", + "browser": "dist/browser.js", + "types": "types/index.d.ts", + "scripts": { + "b:types": "tsc" + }, + "umd": { + "name": "Galacean.SpineCore42", + "globals": { + "@galacean/engine": "Galacean", + "@galacean/engine-spine": "Galacean.Spine" + } + }, + "files": [ + "dist/**/*", + "types/**/*" + ], + "dependencies": { + "@galacean/engine-spine": "workspace:*" + }, + "devDependencies": { + "@esotericsoftware/spine-core": "~4.2.66", + "@galacean/engine": "workspace:*" + }, + "peerDependencies": { + "@galacean/engine": "workspace:*" + } +} diff --git a/packages/spine-core-4.2/src/Spine42Runtime.ts b/packages/spine-core-4.2/src/Spine42Runtime.ts new file mode 100644 index 0000000000..03082b3589 --- /dev/null +++ b/packages/spine-core-4.2/src/Spine42Runtime.ts @@ -0,0 +1,81 @@ +import { + AnimationState, + AnimationStateData, + AtlasAttachmentLoader, + Physics, + Skeleton, + SkeletonBinary, + SkeletonData, + SkeletonJson, + TextureAtlas +} from "@esotericsoftware/spine-core"; +import type { Texture2D } from "@galacean/engine"; +import type { ISpineRenderTarget, ISpineRuntime } from "@galacean/engine-spine"; +import { SpineGenerator } from "./SpineGenerator"; +import { SpineTexture } from "./SpineTexture"; + +/** + * Spine 4.2 runtime backend. + * + * @remarks + * Owns everything specific to the spine 4.2 runtime line: parsing (`SkeletonJson`/`SkeletonBinary`/ + * `TextureAtlas`), object-model construction, world-transform stepping (4.2's `Physics.update`), and + * the attachment-reading mesh generator. Registered automatically when this package is imported. + */ +export class Spine42Runtime implements ISpineRuntime { + private _generator = new SpineGenerator(); + + parseAtlasPageNames(atlasText: string): string[] { + return new TextureAtlas(atlasText).pages.map((page) => page.name); + } + + createTextureAtlas(atlasText: string, textures: Texture2D[]): TextureAtlas { + const textureAtlas = new TextureAtlas(atlasText); + textureAtlas.pages.forEach((page, index) => { + const engineTexture = textures.find((item) => item.name === page.name) || textures[index]; + const texture = new SpineTexture(engineTexture); + page.setTexture(texture); + }); + return textureAtlas; + } + + createSkeletonData( + skeletonRawData: string | ArrayBuffer, + textureAtlas: TextureAtlas, + skeletonDataScale: number + ): SkeletonData { + const atlasLoader = new AtlasAttachmentLoader(textureAtlas); + if (typeof skeletonRawData === "string") { + const skeletonJson = new SkeletonJson(atlasLoader); + skeletonJson.scale = skeletonDataScale; + return skeletonJson.readSkeletonData(skeletonRawData); + } else { + const skeletonBinary = new SkeletonBinary(atlasLoader); + skeletonBinary.scale = skeletonDataScale; + return skeletonBinary.readSkeletonData(new Uint8Array(skeletonRawData as ArrayBuffer)); + } + } + + createAnimationStateData(skeletonData: SkeletonData): AnimationStateData { + return new AnimationStateData(skeletonData); + } + + createSkeleton(skeletonData: SkeletonData): Skeleton { + return new Skeleton(skeletonData); + } + + createAnimationState(stateData: AnimationStateData): AnimationState { + return new AnimationState(stateData); + } + + updateState(skeleton: Skeleton, state: AnimationState, delta: number): void { + state.update(delta); + state.apply(skeleton); + skeleton.update(delta); + skeleton.updateWorldTransform(Physics.update); + } + + buildPrimitive(skeleton: Skeleton, target: ISpineRenderTarget): void { + this._generator.buildPrimitive(skeleton, target); + } +} diff --git a/packages/spine-core-4.2/src/SpineGenerator.ts b/packages/spine-core-4.2/src/SpineGenerator.ts new file mode 100644 index 0000000000..7e45d6f98e --- /dev/null +++ b/packages/spine-core-4.2/src/SpineGenerator.ts @@ -0,0 +1,342 @@ +import { + ArrayLike, + BlendMode, + ClippingAttachment, + Color, + MeshAttachment, + NumberArrayLike, + RegionAttachment, + Skeleton, + SkeletonClipping +} from "@esotericsoftware/spine-core"; +import { BoundingBox, SubPrimitive } from "@galacean/engine"; +import { SpineTexture } from "./SpineTexture"; +import { ClearablePool } from "./util/ClearablePool"; +import { ReturnablePool } from "./util/ReturnablePool"; +import type { ISpineRenderTarget } from "@galacean/engine-spine"; +import { SpineBlendMode, SpineVertexStride } from "@galacean/engine-spine"; + +class SubRenderItem { + subPrimitive: SubPrimitive; + blendMode: BlendMode; + texture: any; +} + +/** + * @internal + */ +export class SpineGenerator { + static tempDark = new Color(); + static tempColor = new Color(); + static tempVerts = new Array(8); + static QUAD_TRIANGLES = [0, 1, 2, 2, 3, 0]; + static subPrimitivePool = new ReturnablePool(SubPrimitive); + static subRenderItemPool = new ClearablePool(SubRenderItem); + + private _subRenderItems: SubRenderItem[] = []; + private _clipper: SkeletonClipping = new SkeletonClipping(); + + buildPrimitive(skeleton: Skeleton, renderer: ISpineRenderTarget) { + const { _indices, _vertices, _localBounds, _vertexCount, _subPrimitives, zSpacing, premultipliedAlpha, tintBlack } = + renderer; + + _localBounds.min.set(Infinity, Infinity, Infinity); + _localBounds.max.set(-Infinity, -Infinity, -Infinity); + + const { _clipper, _subRenderItems } = this; + + const { tempVerts, subRenderItemPool, subPrimitivePool } = SpineGenerator; + const { withTint: vertexStrideWithTint, withoutTint: vertexStrideWithoutTint } = SpineVertexStride; + + _subRenderItems.length = 0; + subRenderItemPool.clear(); + + let triangles: Array; + let uvs: NumberArrayLike; + + let verticesLength = 0; + let indicesLength = 0; + let start = 0; + let count = 0; + + let blend = BlendMode.Normal; + let texture: SpineTexture = null; + let tempBlendMode: BlendMode | null = null; + let tempTexture: SpineTexture | null = null; + + let primitiveIndex = 0; + + const drawOrder = skeleton.drawOrder; + for (let slotIndex = 0, n = drawOrder.length; slotIndex < n; ++slotIndex) { + const slot = drawOrder[slotIndex]; + if (!slot.bone.active) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const attachment = slot.getAttachment(); + if (!attachment) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const z = zSpacing * slotIndex; + const isClipping = _clipper.isClipping(); + let numFloats = 0; + let attachmentColor: Color = null; + + // This vertexSize will be passed to spine-core's computeWorldVertices function. + // + // Expected format by computeWorldVertices: + // - Without tintBlack: [x, y, u, v, r, g, b, a] = 8 components + // - With tintBlack: [x, y, u, v, r, g, b, a, dr, dg, db, da] = 12 components + // + // Our actual vertex buffer format: + // - vertexStrideWithoutTint: [x, y, z, u, v, r, g, b, a] = 9 components + // - vertexStrideWithTint: [x, y, z, u, v, r, g, b, a, dr, dg, db] = 12 components + // (Note: we optimized 'da' as uniform instead of buffer attribute) + // + // Calculation: + // - Without tintBlack: 9 - 1 (remove z) = 8 ✓ + // - With tintBlack: 12 - 1 (remove z) + 1 (add back da) = 12 ✓ + const vertexSize = tintBlack ? vertexStrideWithTint : vertexStrideWithoutTint - 1; + const clippedVertexSize = isClipping ? 2 : vertexSize; + + switch (attachment.constructor) { + case RegionAttachment: + const 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 = 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: + const clip = attachment; + _clipper.clipStart(slot, clip); + continue; + default: + _clipper.clipEndWithSlot(slot); + continue; + } + + if (texture != null) { + let finalVertices: ArrayLike; + let finalVerticesLength: number; + let finalIndices: ArrayLike; + let finalIndicesLength: number; + + const skeleton = slot.bone.skeleton; + const skeletonColor = skeleton.color; + const slotColor = slot.color; + const finalColor = SpineGenerator.tempColor; + const finalAlpha = skeletonColor.a * slotColor.a * attachmentColor.a; + + finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r; + finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g; + finalColor.b = skeletonColor.b * slotColor.b * attachmentColor.b; + finalColor.a = finalAlpha; + + if (premultipliedAlpha) { + finalColor.r *= finalAlpha; + finalColor.g *= finalAlpha; + finalColor.b *= finalAlpha; + } + + const darkColor = SpineGenerator.tempDark; + const slotDarkColor = slot.darkColor; + if (!slotDarkColor) { + darkColor.set(0, 0, 0, 1); + } else { + if (premultipliedAlpha) { + darkColor.r = slotDarkColor.r * finalAlpha; + darkColor.g = slotDarkColor.g * finalAlpha; + darkColor.b = slotDarkColor.b * finalAlpha; + } else { + darkColor.setFromColor(slotDarkColor); + } + } + + if (isClipping) { + _clipper.clipTriangles(tempVerts, triangles, triangles.length, uvs, finalColor, darkColor, tintBlack); + finalVertices = _clipper.clippedVertices; + finalVerticesLength = finalVertices.length; + finalIndices = _clipper.clippedTriangles; + finalIndicesLength = finalIndices.length; + } else { + const { r, g, b, a } = finalColor; + for (let v = 2, u = 0, n = numFloats; v < n; v += vertexSize, u += 2) { + tempVerts[v] = r; + tempVerts[v + 1] = g; + tempVerts[v + 2] = b; + tempVerts[v + 3] = a; + tempVerts[v + 4] = uvs[u]; + tempVerts[v + 5] = uvs[u + 1]; + if (tintBlack) { + tempVerts[v + 6] = darkColor.r; + tempVerts[v + 7] = darkColor.g; + tempVerts[v + 8] = darkColor.b; + tempVerts[v + 9] = darkColor.a; + } + } + finalVertices = tempVerts; + finalVerticesLength = numFloats; + finalIndices = triangles; + finalIndicesLength = triangles.length; + } + + if (finalVerticesLength == 0 || finalIndicesLength == 0) { + _clipper.clipEndWithSlot(slot); + continue; + } + + const stride = tintBlack ? vertexStrideWithTint : vertexStrideWithoutTint; + const indexStart = verticesLength / stride; + let i = verticesLength; + let j = 0; + for (; j < finalVerticesLength; ) { + const x = finalVertices[j++]; + const y = finalVertices[j++]; + _vertices[i++] = x; + _vertices[i++] = y; + _vertices[i++] = z; + _vertices[i++] = finalVertices[j++]; // u + _vertices[i++] = finalVertices[j++]; // v + _vertices[i++] = finalVertices[j++]; // r + _vertices[i++] = finalVertices[j++]; // g + _vertices[i++] = finalVertices[j++]; // b + _vertices[i++] = finalVertices[j++]; // a + if (tintBlack) { + _vertices[i++] = finalVertices[j++]; // darkR + _vertices[i++] = finalVertices[j++]; // darkG + _vertices[i++] = finalVertices[j++]; // darkB + j++; // darkA + } + this._expandBounds(x, y, z, _localBounds); + } + verticesLength = i; + + for (i = indicesLength, j = 0; j < finalIndicesLength; i++, j++) { + _indices[i] = finalIndices[j] + indexStart; + } + indicesLength += finalIndicesLength; + + const textureChanged = tempTexture !== null && tempTexture !== texture; + blend = slot.data.blendMode; + const blendModeChanged = tempBlendMode !== null && tempBlendMode !== blend; + + if (blendModeChanged || textureChanged) { + // Finish accumulated count first + if (count > 0) { + primitiveIndex = this._createRenderItem( + _subPrimitives, + primitiveIndex, + start, + count, + tempTexture, + tempBlendMode + ); + start += count; + count = 0; + } + } + count += finalIndicesLength; + tempTexture = texture; + tempBlendMode = blend; + } + + _clipper.clipEndWithSlot(slot); + } // slot traverse end + + // add reset sub primitive + if (count > 0) { + primitiveIndex = this._createRenderItem(_subPrimitives, primitiveIndex, start, count, texture, blend); + count = 0; + } + + _clipper.clipEnd(); + + const lastLen = _subPrimitives.length; + const curLen = _subRenderItems.length; + for (let i = curLen; i < lastLen; i++) { + const item = _subPrimitives[i]; + subPrimitivePool.return(item); + } + + renderer._clearSubPrimitives(); + for (let i = 0, l = curLen; i < l; ++i) { + const item = _subRenderItems[i]; + const { blendMode, texture } = item; + renderer._addSubPrimitive(item.subPrimitive); + const material = renderer._getMaterial(texture.getImage(), blendMode as unknown as SpineBlendMode); + renderer.setMaterial(i, material); + } + + if (indicesLength > _vertexCount || renderer._needResizeBuffer) { + renderer._createAndBindBuffer(indicesLength); + renderer._needResizeBuffer = false; + this.buildPrimitive(skeleton, renderer); + return; + } + + if (renderer._vertexBuffer) { + renderer._vertexBuffer.setData(_vertices); + renderer._indexBuffer.setData(_indices); + } + } + + private _createRenderItem( + subPrimitives: SubPrimitive[], + primitiveIndex: number, + start: number, + count: number, + texture: SpineTexture, + blend: BlendMode + ): number { + const { subPrimitivePool, subRenderItemPool } = SpineGenerator; + const origin = subPrimitives[primitiveIndex]; + + if (origin) { + primitiveIndex++; + } + + const subPrimitive = origin || subPrimitivePool.get(); + subPrimitive.start = start; + subPrimitive.count = count; + + const renderItem = subRenderItemPool.get(); + renderItem.blendMode = blend; + renderItem.subPrimitive = subPrimitive; + renderItem.texture = texture; + + this._subRenderItems.push(renderItem); + + return primitiveIndex; + } + + private _expandBounds(x: number, y: number, z: number, localBounds: BoundingBox) { + const { min, max } = localBounds; + min.set(Math.min(min.x, x), Math.min(min.y, y), Math.min(min.z, z)); + max.set(Math.max(max.x, x), Math.max(max.y, y), Math.max(max.z, z)); + } +} diff --git a/packages/spine-core-4.2/src/SpineTexture.ts b/packages/spine-core-4.2/src/SpineTexture.ts new file mode 100644 index 0000000000..5297919e1b --- /dev/null +++ b/packages/spine-core-4.2/src/SpineTexture.ts @@ -0,0 +1,49 @@ +import { Texture, TextureFilter, TextureWrap } from "@esotericsoftware/spine-core"; +import { Texture2D, TextureFilterMode, TextureWrapMode } from "@galacean/engine"; + +/** + * @internal + */ +export class SpineTexture extends Texture { + constructor(image: Texture2D) { + super(image); + image.generateMipmaps(); + } + + // rewrite getImage function, return galacean texture2D, then attachment can get size of texture + override getImage(): Texture2D { + return this._image; + } + + setFilters(minFilter: TextureFilter, magFilter: TextureFilter) { + if (minFilter === TextureFilter.Nearest) { + this._image.filterMode = TextureFilterMode.Point; + } else if (minFilter === TextureFilter.Linear) { + this._image.filterMode = TextureFilterMode.Bilinear; + } else { + // The remaining min filters are the MipMap* family (mag filters are never MipMap*); + // mipmaps are generated in the constructor. + this._image.filterMode = TextureFilterMode.Trilinear; + } + } + + setWraps(uWrap: TextureWrap, vWrap: TextureWrap) { + this._image.wrapModeU = this._convertWrapMode(uWrap); + this._image.wrapModeV = this._convertWrapMode(vWrap); + } + + dispose() {} + + private _convertWrapMode(wrap: TextureWrap): TextureWrapMode { + switch (wrap) { + case TextureWrap.ClampToEdge: + return TextureWrapMode.Clamp; + case TextureWrap.Repeat: + return TextureWrapMode.Repeat; + case TextureWrap.MirroredRepeat: + return TextureWrapMode.Mirror; + default: + throw new Error("Unsupported texture wrap mode."); + } + } +} diff --git a/packages/spine-core-4.2/src/index.ts b/packages/spine-core-4.2/src/index.ts new file mode 100644 index 0000000000..8822f0a362 --- /dev/null +++ b/packages/spine-core-4.2/src/index.ts @@ -0,0 +1,15 @@ +import { registerSpineRuntime } from "@galacean/engine-spine"; +import { Spine42Runtime } from "./Spine42Runtime"; + +// Self-register on import: `import "@galacean/engine-spine-core-4.2"` wires the 4.2 backend +// into the shared `@galacean/engine-spine` package. +registerSpineRuntime(new Spine42Runtime()); + +export { Spine42Runtime }; + +// Re-export the spine 4.2 runtime API. Power users that construct spine-core objects directly +// (custom attachments, manual skeleton parsing) import them from this core package. +export * from "@esotericsoftware/spine-core"; + +export const version = `__buildVersion`; +console.log(`Galacean spine-core-4.2 version: ${version}`); diff --git a/packages/spine-core-4.2/src/util/ClearablePool.ts b/packages/spine-core-4.2/src/util/ClearablePool.ts new file mode 100644 index 0000000000..ff8d6ccc6a --- /dev/null +++ b/packages/spine-core-4.2/src/util/ClearablePool.ts @@ -0,0 +1,26 @@ +export class ClearablePool { + private _type: new () => T; + private _elements: T[]; + private _usedElementCount: number = 0; + + constructor(type: new () => T) { + this._type = type; + this._elements = []; + } + + get(): T { + const { _usedElementCount: usedElementCount, _elements: elements } = this; + this._usedElementCount++; + if (elements.length === usedElementCount) { + const element = new this._type(); + elements.push(element); + return element; + } else { + return elements[usedElementCount]; + } + } + + clear(): void { + this._usedElementCount = 0; + } +} diff --git a/packages/spine-core-4.2/src/util/ReturnablePool.ts b/packages/spine-core-4.2/src/util/ReturnablePool.ts new file mode 100644 index 0000000000..77c1bcaa47 --- /dev/null +++ b/packages/spine-core-4.2/src/util/ReturnablePool.ts @@ -0,0 +1,25 @@ +export class ReturnablePool { + private _type: new () => T; + private _elements: T[]; + private _lastElementIndex: number; + + constructor(type: new () => T, initializeCount: number = 1) { + this._type = type; + this._lastElementIndex = initializeCount - 1; + const elements = (this._elements = new Array(initializeCount)); + for (let i = 0; i < initializeCount; ++i) { + elements[i] = new type(); + } + } + + get(): T { + if (this._lastElementIndex < 0) { + return new this._type(); + } + return this._elements[this._lastElementIndex--]; + } + + return(element: T): void { + this._elements[++this._lastElementIndex] = element; + } +} diff --git a/packages/spine-core-4.2/tsconfig.json b/packages/spine-core-4.2/tsconfig.json new file mode 100644 index 0000000000..588b0055c3 --- /dev/null +++ b/packages/spine-core-4.2/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "declaration": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "declarationDir": "types", + "emitDeclarationOnly": true, + "noImplicitOverride": true, + "sourceMap": true, + "incremental": false, + "skipLibCheck": true, + "stripInternal": true + }, + "include": ["src/**/*"] +} diff --git a/packages/spine/package.json b/packages/spine/package.json new file mode 100644 index 0000000000..1c3c28bb1f --- /dev/null +++ b/packages/spine/package.json @@ -0,0 +1,37 @@ +{ + "name": "@galacean/engine-spine", + "version": "2.0.0-alpha.38", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "repository": { + "url": "https://github.com/galacean/engine.git" + }, + "license": "MIT", + "main": "dist/main.js", + "module": "dist/module.js", + "debug": "src/index.ts", + "browser": "dist/browser.js", + "types": "types/index.d.ts", + "scripts": { + "b:types": "tsc" + }, + "umd": { + "name": "Galacean.Spine", + "globals": { + "@galacean/engine": "Galacean" + } + }, + "files": [ + "dist/**/*", + "types/**/*" + ], + "devDependencies": { + "@esotericsoftware/spine-core": "~4.2.66", + "@galacean/engine": "workspace:*" + }, + "peerDependencies": { + "@galacean/engine": "workspace:*" + } +} diff --git a/packages/spine/src/SpineConstant.ts b/packages/spine/src/SpineConstant.ts new file mode 100644 index 0000000000..679d01d2e9 --- /dev/null +++ b/packages/spine/src/SpineConstant.ts @@ -0,0 +1,13 @@ +/** + * Vertex stride (float count per vertex) of the spine vertex layout owned by `SpineAnimationRenderer`. + * + * - `withoutTint`: [x, y, z, u, v, r, g, b, a] = 9 floats + * - `withTint`: [x, y, z, u, v, r, g, b, a, dr, dg, db] = 12 floats + * + * Shared between the renderer (which declares the vertex elements and allocates buffers) and the + * version-specific generator in a spine core package (which fills them), so the two never drift. + */ +export const SpineVertexStride = { + withoutTint: 9, + withTint: 12 +} as const; diff --git a/packages/spine/src/enums/SpineBlendMode.ts b/packages/spine/src/enums/SpineBlendMode.ts new file mode 100644 index 0000000000..509a2b60af --- /dev/null +++ b/packages/spine/src/enums/SpineBlendMode.ts @@ -0,0 +1,14 @@ +/** + * Blend mode of a spine slot. + * + * @remarks + * Version-neutral mirror of spine-core's `BlendMode` enum. The values match spine-core across all + * supported runtime versions (3.8 / 4.2), so a core package can pass its `BlendMode` straight through. + * Owning it here keeps the shared package free of any runtime spine-core dependency. + */ +export enum SpineBlendMode { + Normal = 0, + Additive = 1, + Multiply = 2, + Screen = 3 +} diff --git a/packages/spine/src/index.ts b/packages/spine/src/index.ts new file mode 100644 index 0000000000..6cdc5bf196 --- /dev/null +++ b/packages/spine/src/index.ts @@ -0,0 +1,21 @@ +import * as LoaderObject from "./loader"; +import * as RendererObject from "./renderer"; +import { Loader } from "@galacean/engine"; + +for (const key in RendererObject) { + Loader.registerClass(key, RendererObject[key]); +} +for (const key in LoaderObject) { + Loader.registerClass(key, LoaderObject[key]); +} + +export * from "./loader/index"; +export * from "./renderer/index"; +export { SpineBlendMode } from "./enums/SpineBlendMode"; +export { SpineVertexStride } from "./SpineConstant"; +export { registerSpineRuntime, getSpineRuntime } from "./runtime/SpineRuntimeRegistry"; +export type { ISpineRuntime } from "./runtime/ISpineRuntime"; +export type { ISpineRenderTarget } from "./runtime/ISpineRenderTarget"; + +export const version = `__buildVersion`; +console.log(`Galacean spine version: ${version}`); diff --git a/packages/spine/src/loader/LoaderUtils.ts b/packages/spine/src/loader/LoaderUtils.ts new file mode 100644 index 0000000000..967aafa5a0 --- /dev/null +++ b/packages/spine/src/loader/LoaderUtils.ts @@ -0,0 +1,96 @@ +import type { SkeletonData, TextureAtlas } from "@esotericsoftware/spine-core"; +import { AssetPromise, AssetType, Engine, Texture2D } from "@galacean/engine"; +import { getSpineRuntime } from "../runtime/SpineRuntimeRegistry"; + +/** + * @internal + */ +export class LoaderUtils { + static createSkeletonData( + skeletonRawData: string | ArrayBuffer, + textureAtlas: TextureAtlas, + skeletonDataScale: number + ): SkeletonData { + return getSpineRuntime().createSkeletonData(skeletonRawData, textureAtlas, skeletonDataScale); + } + + static createTextureAtlas(atlasText: string, textures: Texture2D[]): TextureAtlas { + return getSpineRuntime().createTextureAtlas(atlasText, textures); + } + + static loadTexturesByPaths( + imagePaths: string[], + imageExtensions: string[], + engine: Engine, + reject: (reason?: any) => void + ): Promise { + const resourceManager = engine.resourceManager; + // @ts-ignore + const virtualPathResourceMap = resourceManager._virtualPathResourceMap; + const texturePromises: AssetPromise[] = imagePaths.map((imagePath, index) => { + const ext = imageExtensions[index]; + let imageLoaderType = AssetType.Texture; + const virtualElement = virtualPathResourceMap[imagePath]; + if (virtualElement) { + imageLoaderType = virtualElement.type; + } else if (ext === "ktx") { + imageLoaderType = AssetType.KTX; + } else if (ext === "ktx2") { + imageLoaderType = AssetType.KTX2; + } + return resourceManager.load({ + url: imagePath, + type: imageLoaderType + }); + }); + return Promise.all(texturePromises).catch((error) => { + reject(error); + return []; + }); + } + + static loadTextureAtlas(atlasPath: string, engine: Engine, reject: (reason?: any) => void): Promise { + const baseUrl = LoaderUtils.getBaseUrl(atlasPath); + const resourceManager = engine.resourceManager; + let atlasText: string; + return ( + resourceManager + // @ts-ignore + ._request(atlasPath, { type: "text" }) + .then((text: string) => { + atlasText = text; + const runtime = getSpineRuntime(); + const pageNames = runtime.parseAtlasPageNames(atlasText); + const loadTexturePromises = pageNames.map((name) => { + const textureUrl = baseUrl + name; + return resourceManager.load({ + url: textureUrl, + type: AssetType.Texture + }) as unknown as Promise; + }); + return Promise.all(loadTexturePromises).then((textures) => { + return runtime.createTextureAtlas(atlasText, textures); + }); + }) + .catch((err) => { + reject(new Error(`Spine Atlas: ${atlasPath} load error: ${err}`)); + return null; + }) + ); + } + + static getBaseUrl(url: string): string { + const isLocalPath = !/^(http|https|ftp):\/\/.*/i.test(url); + if (isLocalPath) { + const lastSlashIndex = url.lastIndexOf("/"); + if (lastSlashIndex === -1) { + return ""; + } + return url.substring(0, lastSlashIndex + 1); + } else { + const parsedUrl = new URL(url); + const basePath = parsedUrl.origin + parsedUrl.pathname; + return basePath.endsWith("/") ? basePath : basePath.substring(0, basePath.lastIndexOf("/") + 1); + } + } +} diff --git a/packages/spine/src/loader/SpineAtlasLoader.ts b/packages/spine/src/loader/SpineAtlasLoader.ts new file mode 100644 index 0000000000..a33f4d6298 --- /dev/null +++ b/packages/spine/src/loader/SpineAtlasLoader.ts @@ -0,0 +1,107 @@ +import type { TextureAtlas } from "@esotericsoftware/spine-core"; +import { AssetPromise, Loader, LoadItem, resourceLoader, ResourceManager } from "@galacean/engine"; +import { LoaderUtils } from "./LoaderUtils"; +import { SpineLoader } from "./SpineLoader"; + +interface SpineAtlasAsset { + atlasPath: string; + imagePaths?: string[]; + imageExtensions?: string[]; +} + +@resourceLoader("SpineAtlas", ["atlas"], false) +export class SpineAtlasLoader extends Loader { + private static _groupAssetsByExtension(url: string, assetPath: SpineAtlasAsset, resourceManager: ResourceManager) { + const ext = SpineLoader._getUrlExtension(url); + if (!ext) return; + + if (ext === "atlas") { + assetPath.atlasPath = url; + } + if (["png", "jpg", "webp", "jpeg", "ktx", "ktx2"].includes(ext)) { + assetPath.imagePaths.push(url); + } + } + + private static _assignAssetPathsFromUrl(url: string, assetPath: SpineAtlasAsset, resourceManager: ResourceManager) { + const ext = SpineLoader._getUrlExtension(url); + if (ext === "atlas") { + assetPath.atlasPath = url; + // @ts-ignore + const atlasDependency = resourceManager?._virtualPathResourceMap?.[url]?.dependentAssetMap; + if (atlasDependency) { + for (const key in atlasDependency) { + const imageVirtualPath = atlasDependency[key]; + assetPath.imagePaths.push(imageVirtualPath); + } + } + } + } + + load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { + return new AssetPromise((resolve, reject) => { + const engine = resourceManager.engine; + const spineAtlasAsset = { + atlasPath: "", + imagePaths: [], + imageExtensions: [] + }; + + if (!item.urls) { + SpineAtlasLoader._assignAssetPathsFromUrl(item.url, spineAtlasAsset, resourceManager); + } else { + const urls = item.urls; + for (let i = 0, len = urls.length; i < len; i += 1) { + const url = urls[i]; + SpineAtlasLoader._groupAssetsByExtension(url, spineAtlasAsset, resourceManager); + } + } + + const { atlasPath } = spineAtlasAsset; + if (!atlasPath) { + reject( + new Error("Failed to load spine atlas. Please check the file path and ensure the file extension is included.") + ); + return; + } + + const imagePaths = spineAtlasAsset.imagePaths; + if (imagePaths.length === 0) { + // Use the parsed atlas path: `item.url` is undefined when the asset came in via `item.urls`. + LoaderUtils.loadTextureAtlas(atlasPath, engine, reject) + .then((textureAtlas) => { + resolve(textureAtlas); + }) + .catch((err) => { + reject(err); + }); + } else { + const { atlasPath, imagePaths, imageExtensions } = spineAtlasAsset; + if (imagePaths.length > 0) { + Promise.all([ + // @ts-ignore + resourceManager._request(atlasPath, { + type: "text" + }) as Promise, + LoaderUtils.loadTexturesByPaths(imagePaths, imageExtensions, engine, reject) + ]) + .then(([atlasText, textures]) => { + const textureAtlas = LoaderUtils.createTextureAtlas(atlasText, textures); + resolve(textureAtlas); + }) + .catch((err) => { + reject(err); + }); + } else { + LoaderUtils.loadTextureAtlas(atlasPath, engine, reject) + .then((textureAtlas) => { + resolve(textureAtlas); + }) + .catch((err) => { + reject(err); + }); + } + } + }); + } +} diff --git a/packages/spine/src/loader/SpineLoader.ts b/packages/spine/src/loader/SpineLoader.ts new file mode 100644 index 0000000000..41d613e822 --- /dev/null +++ b/packages/spine/src/loader/SpineLoader.ts @@ -0,0 +1,144 @@ +import type { TextureAtlas } from "@esotericsoftware/spine-core"; +import { AssetPromise, Loader, LoadItem, resourceLoader, ResourceManager } from "@galacean/engine"; +import { LoaderUtils } from "./LoaderUtils"; +import { SpineResource } from "./SpineResource"; + +type SpineAssetPath = { + atlasPath: string; + skeletonPath: string; + extraPaths: string[]; + fileExtensions?: string | string[]; +}; + +type SpineLoadContext = { + fileName: string; + spineAssetPath: SpineAssetPath; + skeletonRawData: string | ArrayBuffer; +}; + +export type SpineLoaderParams = { + fileExtensions?: string | string[]; +}; + +// Registering "json" makes this loader the default for bare `.json` urls (extension mappings are +// last-registration-wins, overriding the core JSONLoader) — matches the historical engine-spine +// behavior. Pass an explicit `type` (e.g. AssetType.JSON) to load plain JSON once this package +// is imported. +@resourceLoader("Spine", ["json", "bin", "skel"]) +export class SpineLoader extends Loader { + private static _decoder = new TextDecoder("utf-8"); + + private static _groupAssetsByExtension(url: string, assetPath: SpineAssetPath) { + const ext = SpineLoader._getUrlExtension(url); + if (!ext) return; + + if (["skel", "json", "bin"].includes(ext)) { + assetPath.skeletonPath = url; + } else if (ext === "atlas") { + assetPath.atlasPath = url; + } else { + assetPath.extraPaths.push(url); + } + } + + private static _assignAssetPathsFromUrl(url: string, assetPath: SpineAssetPath, resourceManager: ResourceManager) { + const ext = SpineLoader._getUrlExtension(url); + if (!ext) return; + assetPath.skeletonPath = url; + + // @ts-ignore + const skeletonDependency = resourceManager?._virtualPathResourceMap?.[url]?.dependentAssetMap; + if (skeletonDependency) { + assetPath.atlasPath = skeletonDependency.atlas; + } else { + const extensionPattern: RegExp = /\.(json|bin|skel)$/; + let baseUrl: string; + if (extensionPattern.test(url)) { + baseUrl = url.replace(extensionPattern, ""); + } + if (baseUrl) { + const atlasUrl = baseUrl + ".atlas"; + assetPath.atlasPath = atlasUrl; + } + } + } + + static _getUrlExtension(url: string): string | null { + const regex = /\.(\w+)(\?|$)/; + const match = url.match(regex); + return match ? match[1] : null; + } + + load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { + return new AssetPromise((resolve, reject) => { + const spineLoadContext: SpineLoadContext = { + fileName: "", + skeletonRawData: "", + spineAssetPath: { + atlasPath: null, + skeletonPath: null, + extraPaths: [] + } + }; + const { spineAssetPath } = spineLoadContext; + if (!item.urls) { + SpineLoader._assignAssetPathsFromUrl(item.url, spineAssetPath, resourceManager); + } else { + const urls = item.urls; + for (let i = 0, len = urls.length; i < len; i += 1) { + const url = urls[i]; + SpineLoader._groupAssetsByExtension(url, spineAssetPath); + } + } + + const { skeletonPath, atlasPath } = spineAssetPath; + if (!skeletonPath || !atlasPath) { + reject( + new Error( + "Failed to load spine assets. Please check the file path and ensure the file extension is included." + ) + ); + return; + } + + resourceManager + // @ts-ignore + ._request(skeletonPath, { type: "arraybuffer" }) + .then((skeletonRawData: ArrayBuffer) => { + spineLoadContext.skeletonRawData = skeletonRawData; + const skeletonString = SpineLoader._decoder.decode(skeletonRawData); + if (skeletonString.startsWith("{")) { + spineLoadContext.skeletonRawData = skeletonString; + } + return this._loadAndCreateSpineResource(spineLoadContext, resourceManager); + }) + .then(resolve) + .catch(reject); + }); + } + + private _loadAndCreateSpineResource( + spineLoadContext: SpineLoadContext, + resourceManager: ResourceManager + ): Promise { + const { engine } = resourceManager; + const { skeletonRawData, spineAssetPath } = spineLoadContext; + const { skeletonPath, atlasPath, extraPaths } = spineAssetPath; + + const atlasLoadPromise = + extraPaths.length === 0 + ? (resourceManager.load({ + url: atlasPath, + type: "SpineAtlas" + }) as unknown as Promise) + : (resourceManager.load({ + urls: [atlasPath].concat(extraPaths), + type: "SpineAtlas" + }) as unknown as Promise); + + return atlasLoadPromise.then((textureAtlas) => { + const skeletonData = LoaderUtils.createSkeletonData(skeletonRawData, textureAtlas, 0.01); + return new SpineResource(engine, skeletonData, skeletonPath); + }); + } +} diff --git a/packages/spine/src/loader/SpineResource.ts b/packages/spine/src/loader/SpineResource.ts new file mode 100644 index 0000000000..a1532ad606 --- /dev/null +++ b/packages/spine/src/loader/SpineResource.ts @@ -0,0 +1,112 @@ +import type { + AnimationState, + AnimationStateData, + MeshAttachment, + RegionAttachment, + Skeleton, + SkeletonData +} from "@esotericsoftware/spine-core"; +import { Engine, Entity, ReferResource, Texture2D } from "@galacean/engine"; +import { SpineAnimationRenderer } from "../renderer/SpineAnimationRenderer"; +import { getSpineRuntime } from "../runtime/SpineRuntimeRegistry"; + +/** + * Represents a resource that manages Spine animation data, textures, and entity templates for the Galacean engine. + * + */ +export class SpineResource extends ReferResource { + /** The url of spine resource. */ + readonly url: string; + + private _texturesInSpineAtlas: Texture2D[] = []; + private _skeletonData: SkeletonData; + private _stateData: AnimationStateData; + + private _template: Entity; + + constructor(engine: Engine, skeletonData: SkeletonData, url?: string) { + super(engine); + this.url = url; + this._skeletonData = skeletonData; + this._stateData = getSpineRuntime().createAnimationStateData(skeletonData); + this._associationTextureInSkeletonData(skeletonData); + this._createTemplate(); + } + + /** + * The skeleton data associated with this Spine resource. + */ + get skeletonData(): SkeletonData { + return this._skeletonData; + } + + /** + * The animation state data associated with this Spine resource. + */ + get stateData(): AnimationStateData { + return this._stateData; + } + + /** + * Creates and returns a new instance of the spine entity template. + * @returns A instance of the spine entity template + */ + instantiate(): Entity { + return this._template.clone(); + } + + protected override _onDestroy(): void { + super._onDestroy(); + this._disassociationSuperResource(); + this._skeletonData = null; + this._stateData = null; + } + + private _createTemplate(): void { + const name = this._extractFileName(this.url); + const spineEntity = new Entity(this.engine, name); + const spineAnimationRenderer = spineEntity.addComponent(SpineAnimationRenderer); + const runtime = getSpineRuntime(); + const skeleton = runtime.createSkeleton(this._skeletonData); + const state = runtime.createAnimationState(this._stateData); + spineAnimationRenderer._setSkeleton(skeleton); + spineAnimationRenderer._setState(state); + // @ts-ignore + spineEntity._markAsTemplate(this); + this._template = spineEntity; + } + + private _associationTextureInSkeletonData(skeletonData: SkeletonData): void { + const { skins, slots } = skeletonData; + const textures = this._texturesInSpineAtlas; + + 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 = (attachment)?.region?.texture.getImage(); + if (texture) { + if (!textures.includes(texture)) { + textures.push(texture); + // @ts-ignore + texture._associationSuperResource(this); + } + } + } + } + } + + private _disassociationSuperResource(): void { + const textures = this._texturesInSpineAtlas; + for (let i = 0, n = textures.length; i < n; i++) { + // @ts-ignore + textures[i]._disassociationSuperResource(this); + } + } + + private _extractFileName(url: string): string { + if (!url) return "Spine Entity"; + const match = url.match(/\/([^\/]+?)(\.[^\/]*)?$/); + return match ? match[1] : "Spine Entity"; + } +} diff --git a/packages/spine/src/loader/index.ts b/packages/spine/src/loader/index.ts new file mode 100644 index 0000000000..b7367ba692 --- /dev/null +++ b/packages/spine/src/loader/index.ts @@ -0,0 +1,5 @@ +import "./SpineLoader"; +import "./SpineAtlasLoader"; + +export { SpineResource } from "./SpineResource"; +export { LoaderUtils } from "./LoaderUtils"; diff --git a/packages/spine/src/renderer/SpineAnimationRenderer.ts b/packages/spine/src/renderer/SpineAnimationRenderer.ts new file mode 100644 index 0000000000..2be54a129c --- /dev/null +++ b/packages/spine/src/renderer/SpineAnimationRenderer.ts @@ -0,0 +1,422 @@ +import type { AnimationState, Skeleton } from "@esotericsoftware/spine-core"; +import { + assignmentClone, + BoundingBox, + Buffer, + BufferBindFlag, + BufferUsage, + deepClone, + Entity, + ignoreClone, + IndexBufferBinding, + IndexFormat, + Material, + Primitive, + Renderer, + SubPrimitive, + Texture2D, + Vector3, + VertexBufferBinding, + VertexElement, + VertexElementFormat +} from "@galacean/engine"; +import { SpineResource } from "../loader/SpineResource"; +import { SpineMaterial } from "./SpineMaterial"; +import { SpineBlendMode } from "../enums/SpineBlendMode"; +import { SpineVertexStride } from "../SpineConstant"; +import { getSpineRuntime } from "../runtime/SpineRuntimeRegistry"; +import type { ISpineRenderTarget } from "../runtime/ISpineRenderTarget"; + +/** + * Spine animation renderer, capable of rendering spine animations and providing functions for animation and skeleton manipulation. + */ +export class SpineAnimationRenderer extends Renderer implements ISpineRenderTarget { + private static _positionVertexElement = new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0); + private static _lightColorVertexElement = new VertexElement("LIGHT_COLOR", 12, VertexElementFormat.Vector4, 0); + private static _uvVertexElement = new VertexElement("TEXCOORD_0", 28, VertexElementFormat.Vector2, 0); + private static _darkColorVertexElement = new VertexElement("DARK_COLOR", 36, VertexElementFormat.Vector3, 0); + + /** @internal */ + static _materialCacheMap = new Map(); + + /** + * The spacing between z layers in world units. + */ + @assignmentClone + zSpacing = 0.001; + + /** + * Whether to use premultiplied alpha mode for rendering. + * When enabled, vertex color values are multiplied by the alpha channel. + * @remarks + If this option is enabled, the Spine editor must export textures with "Premultiply Alpha" checked. + */ + @assignmentClone + premultipliedAlpha = false; + + @assignmentClone + private _tintBlack = false; + + /** + * Whether to enable tint black feature for dark color tinting. + * + * @remarks Should be enabled when using "Tint Black" feature in Spine editor. + */ + get tintBlack(): boolean { + return this._tintBlack; + } + + set tintBlack(value: boolean) { + if (this._tintBlack !== value) { + this._tintBlack = value; + this._needResizeBuffer = true; + } + } + + /** + * Default state for spine animation. + * Contains the default animation name to be played, whether this animation should loop, the default skin name. + */ + @deepClone + readonly defaultConfig: SpineAnimationDefaultConfig = new SpineAnimationDefaultConfig(); + + /** @internal */ + @ignoreClone + _primitive: Primitive; + /** @internal */ + @ignoreClone + _subPrimitives: SubPrimitive[] = []; + /** @internal */ + @ignoreClone + _indexBuffer: Buffer; + /** @internal */ + @ignoreClone + _vertexBuffer: Buffer; + /** @internal */ + @ignoreClone + _vertices = new Float32Array(); + /** @internal */ + @ignoreClone + _indices = new Uint16Array(); + /** @internal */ + @ignoreClone + _needResizeBuffer = false; + /** @internal */ + @ignoreClone + _vertexCount = 0; + /** @internal */ + @ignoreClone + _resource: SpineResource; + /** @internal */ + @ignoreClone + _localBounds = new BoundingBox( + new Vector3(Infinity, Infinity, Infinity), + new Vector3(-Infinity, -Infinity, -Infinity) + ); + + @ignoreClone + private _skeleton: Skeleton; + @ignoreClone + private _state: AnimationState; + + /** + * The Spine.AnimationState object of this SpineAnimationRenderer. + * Manage, blend, and transition between multiple simultaneous animations effectively. + */ + get state(): AnimationState { + return this._state; + } + + /** + * The Spine.Skeleton object of this SpineAnimationRenderer. + * Manipulate bone positions, rotations, scaling + * and change spine attachment to customize character appearances dynamically during runtime. + */ + get skeleton(): Skeleton { + return this._skeleton; + } + + /** + * @internal + */ + constructor(entity: Entity) { + super(entity); + const primitive = new Primitive(this.engine); + this._primitive = primitive; + this._primitive._addReferCount(1); + primitive.addVertexElement(SpineAnimationRenderer._positionVertexElement); + primitive.addVertexElement(SpineAnimationRenderer._lightColorVertexElement); + primitive.addVertexElement(SpineAnimationRenderer._uvVertexElement); + primitive.addVertexElement(SpineAnimationRenderer._darkColorVertexElement); + } + + /** + * @internal + */ + // @ts-ignore + override _onEnable(): void { + this._applyDefaultConfig(); + } + + /** + * @internal + */ + override update(delta: number): void { + const { _state: state, _skeleton: skeleton } = this; + if (!state || !skeleton) return; + const runtime = getSpineRuntime(); + runtime.updateState(skeleton, state, delta); + runtime.buildPrimitive(skeleton, this); + this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; + } + + /** + * @internal + */ + // @ts-ignore + override _render(context: any): void { + const { _primitive, _subPrimitives, _materials: materials } = this; + if (!_subPrimitives) return; + // Engine 2.0 render-element model: one RenderElement per sub-primitive (no sub-element pool). + const engine = this.engine as any; + const priority = this.priority; + const distanceForSort = (this as any)._distanceForSort; + const renderElementPool = engine._renderElementPool; + const renderPipeline = context.camera._renderPipeline; + for (let i = 0, n = _subPrimitives.length; i < n; i++) { + let material = materials[i]; + if (!material) { + continue; + } + if (material.destroyed || material.shader.destroyed) { + material = engine._basicResources.meshMagentaMaterial; + } + const renderElement = renderElementPool.get(); + renderElement.set(this, material, _primitive, _subPrimitives[i]); + renderElement.priority = priority; + renderElement.distanceForSort = distanceForSort; + renderPipeline.pushRenderElement(context, renderElement); + } + } + + /** + * @internal + */ + // @ts-ignore + override _updateBounds(worldBounds: BoundingBox): void { + BoundingBox.transform(this._localBounds, this.entity.transform.worldMatrix, worldBounds); + } + + /** + * @internal + */ + _setSkeleton(skeleton: Skeleton) { + this._skeleton = skeleton; + } + + /** + * @internal + */ + _setState(state: AnimationState) { + this._state = state; + } + + /** + * @internal + */ + // @ts-ignore + override _cloneTo(target: SpineAnimationRenderer): void { + // A renderer added manually and never bound to a resource has no skeleton/state to clone. + if (!this._skeleton || !this._state) return; + const runtime = getSpineRuntime(); + // Clones share the source's immutable SkeletonData and AnimationStateData (mix config), + // so mix times configured on the resource propagate to every instance; only the + // per-instance Skeleton / AnimationState are fresh. + const skeleton = runtime.createSkeleton(this._skeleton.data); + const state = runtime.createAnimationState(this._state.data); + target._setSkeleton(skeleton); + target._setState(state); + } + + /** + * @internal + */ + override _onDestroy(): void { + this._clearMaterialCache(); + this._subPrimitives.length = 0; + const primitive = this._primitive; + if (primitive) { + primitive._addReferCount(-1); + primitive.destroy(); + this._primitive = null; + } + // destroy() is refCount-guarded; the primitive released its buffer references above. + this._vertexBuffer?.destroy(); + this._indexBuffer?.destroy(); + this._vertexBuffer = null; + this._indexBuffer = null; + this._resource = null; + this._skeleton = null; + this._state = null; + super._onDestroy(); + } + + /** + * @internal + */ + _createAndBindBuffer(vertexCount: number): void { + const { engine: _engine, _primitive } = this; + const oldVertexBuffer = this._vertexBuffer; + const oldIndexBuffer = this._indexBuffer; + this._vertexCount = vertexCount; + const stride = this.tintBlack ? SpineVertexStride.withTint : SpineVertexStride.withoutTint; + this._vertices = new Float32Array(vertexCount * stride); + this._indices = new Uint16Array(vertexCount); + const vertexStride = stride << 2; + const vertexBuffer = new Buffer(_engine, BufferBindFlag.VertexBuffer, this._vertices, BufferUsage.Dynamic); + const indexBuffer = new Buffer(_engine, BufferBindFlag.IndexBuffer, this._indices, BufferUsage.Dynamic); + this._indexBuffer = indexBuffer; + this._vertexBuffer = vertexBuffer; + const vertexBufferBinding = new VertexBufferBinding(vertexBuffer, vertexStride); + this._primitive.setVertexBufferBinding(0, vertexBufferBinding); + const indexBufferBinding = new IndexBufferBinding(indexBuffer, IndexFormat.UInt16); + _primitive.setIndexBufferBinding(indexBufferBinding); + // Rebinding released the primitive's references to the old buffers; destroy() is + // refCount-guarded, so this frees their GPU resources without waiting for gc(). + oldVertexBuffer?.destroy(); + oldIndexBuffer?.destroy(); + } + + /** + * @internal + */ + _addSubPrimitive(subPrimitive: SubPrimitive): void { + this._subPrimitives.push(subPrimitive); + } + + /** + * @internal + */ + _clearSubPrimitives(): void { + this._subPrimitives.length = 0; + } + + /** + * @internal + */ + _getMaterial(texture: Texture2D, blendMode: SpineBlendMode): Material { + const engine = this.engine; + const premultipliedAlpha = this.premultipliedAlpha; + const tintBlack = this.tintBlack; + + // tintBlack must be part of the key: it toggles a per-material macro, so renderers that + // differ only in tintBlack cannot share a material. + 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; + cached._cacheKey = key; + 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 materials = this._materials; + for (let i = 0, len = materials.length; i < len; i += 1) { + const material = materials[i]; + // `setMaterial` is public API, so entries may be user materials or null holes; a cached + // SpineMaterial is removed by the exact key it was registered under (recomputing the key + // from the renderer's current state would miss materials cached under older settings). + if (material instanceof SpineMaterial) { + materialCache.delete(material._cacheKey); + } + } + } + + private _applyDefaultConfig(): void { + const { skeleton, state } = this; + if (skeleton && state) { + const { animationName, skinName, loop } = this.defaultConfig; + if (skinName !== "default") { + skeleton.setSkinByName(skinName); + skeleton.setToSetupPose(); + } + if (animationName) { + state.setAnimation(0, animationName, loop); + } + } + } + + /** + * * @deprecated This property is deprecated and will be removed in future releases. + * Spine resource of current spine animation. + */ + get resource(): SpineResource { + return this._resource; + } + + /** + * * @deprecated This property is deprecated and will be removed in future releases. + * Sets the Spine resource for the current animation. This property allows switching to a different `SpineResource`. + * + * @param value - The new `SpineResource` to be used for the current animation + */ + set resource(value: SpineResource) { + if (!value) { + this._state = null; + this._skeleton = null; + this._resource = null; + return; + } + this._resource = value; + const { skeletonData, stateData } = value; + const runtime = getSpineRuntime(); + const skeleton = runtime.createSkeleton(skeletonData); + const state = runtime.createAnimationState(stateData); + this._setSkeleton(skeleton); + this._setState(state); + this._applyDefaultConfig(); + } +} + +/** + * @internal + */ +export enum RendererUpdateFlags { + /** Include world position and world bounds. */ + WorldVolume = 0x1 +} + +/** + * Default state for spine animation. + * Contains the default animation name to be played, whether this animation should loop, + * the default skin name, and the default scale of the skeleton. + */ +export class SpineAnimationDefaultConfig { + /** + * Creates an instance of default config + */ + constructor( + /** + * Whether the default animation should loop @defaultValue `true. The default animation should loop` + */ + public loop: boolean = true, + + /** + * The name of the default animation @defaultValue `null. Do not play any animation by default` + */ + public animationName: string | null = null, + + /** + * The name of the default skin @defaultValue `default` + */ + public skinName: string = "default" + ) {} +} diff --git a/packages/spine/src/renderer/SpineMaterial.ts b/packages/spine/src/renderer/SpineMaterial.ts new file mode 100644 index 0000000000..bf4bec3b97 --- /dev/null +++ b/packages/spine/src/renderer/SpineMaterial.ts @@ -0,0 +1,106 @@ +import { BlendFactor, Engine, Material, Shader, ShaderProperty, Texture2D } from "@galacean/engine"; +import { SpineBlendMode } from "../enums/SpineBlendMode"; + +const { SourceAlpha, One, DestinationColor, OneMinusSourceColor, OneMinusSourceAlpha } = BlendFactor; + +/** + * Material for spine rendering. + * + * @remarks + * The shader lives as a built-in engine shader (`2D/Spine`, registered by the engine's + * `ShaderPool`). Per-material blend factors are driven through shader data properties + * (`sourceColorBlendFactor` etc.), which the `2D/Spine` shader binds into its `BlendState`. + * The constant render state (transparent queue, depth-write off, cull off) is declared in + * the shader, so this material only varies the blend factors per blend mode. + */ +export class SpineMaterial extends Material { + private static _sourceColorBlendFactorProp = ShaderProperty.getByName("sourceColorBlendFactor"); + private static _destinationColorBlendFactorProp = ShaderProperty.getByName("destinationColorBlendFactor"); + private static _sourceAlphaBlendFactorProp = ShaderProperty.getByName("sourceAlphaBlendFactor"); + private static _destinationAlphaBlendFactorProp = ShaderProperty.getByName("destinationAlphaBlendFactor"); + + /** + * @internal + * The key this material is registered under in `SpineAnimationRenderer._materialCacheMap`. + */ + _cacheKey: string; + + private _blendMode: SpineBlendMode = SpineBlendMode.Normal; + + /** + * @internal + */ + _setTintBlack(enabled: boolean) { + if (enabled) { + this.shaderData.enableMacro("RENDERER_TINT_BLACK"); + } else { + this.shaderData.disableMacro("RENDERER_TINT_BLACK"); + } + } + + /** + * @internal + */ + _setPremultipliedAlpha(enabled: boolean) { + this.shaderData.setFloat("renderer_PremultipliedAlpha", enabled ? 1 : 0); + } + + /** + * @internal + */ + _setTexture(value: Texture2D) { + this.shaderData.setTexture("material_SpineTexture", value); + } + + constructor(engine: Engine) { + super(engine, Shader.find("2D/Spine")); + this._setBlendMode(SpineBlendMode.Normal, false); + } + + /** + * @internal + */ + _setBlendMode(blendMode: SpineBlendMode, premultipliedAlpha: boolean) { + const { shaderData } = this; + const { + _sourceColorBlendFactorProp: srcColor, + _destinationColorBlendFactorProp: dstColor, + _sourceAlphaBlendFactorProp: srcAlpha, + _destinationAlphaBlendFactorProp: dstAlpha + } = SpineMaterial; + this._blendMode = blendMode; + switch (blendMode) { + case SpineBlendMode.Additive: + shaderData.setInt(srcColor, premultipliedAlpha ? One : SourceAlpha); + shaderData.setInt(dstColor, One); + shaderData.setInt(srcAlpha, One); + shaderData.setInt(dstAlpha, One); + break; + case SpineBlendMode.Multiply: + shaderData.setInt(srcColor, DestinationColor); + shaderData.setInt(dstColor, OneMinusSourceAlpha); + shaderData.setInt(srcAlpha, One); + shaderData.setInt(dstAlpha, OneMinusSourceAlpha); + break; + case SpineBlendMode.Screen: + shaderData.setInt(srcColor, One); + shaderData.setInt(dstColor, OneMinusSourceColor); + shaderData.setInt(srcAlpha, One); + shaderData.setInt(dstAlpha, OneMinusSourceColor); + break; + default: // Normal + shaderData.setInt(srcColor, premultipliedAlpha ? One : SourceAlpha); + shaderData.setInt(dstColor, OneMinusSourceAlpha); + shaderData.setInt(srcAlpha, One); + shaderData.setInt(dstAlpha, OneMinusSourceAlpha); + break; + } + } + + /** + * @internal + */ + _getBlendMode(): SpineBlendMode { + return this._blendMode; + } +} diff --git a/packages/spine/src/renderer/index.ts b/packages/spine/src/renderer/index.ts new file mode 100644 index 0000000000..aca750bda7 --- /dev/null +++ b/packages/spine/src/renderer/index.ts @@ -0,0 +1,2 @@ +export { SpineAnimationRenderer } from "./SpineAnimationRenderer"; +export { SpineMaterial } from "./SpineMaterial"; diff --git a/packages/spine/src/runtime/ISpineRenderTarget.ts b/packages/spine/src/runtime/ISpineRenderTarget.ts new file mode 100644 index 0000000000..50133ae9f8 --- /dev/null +++ b/packages/spine/src/runtime/ISpineRenderTarget.ts @@ -0,0 +1,30 @@ +import type { BoundingBox, Buffer, Material, SubPrimitive, Texture2D } from "@galacean/engine"; +import type { SpineBlendMode } from "../enums/SpineBlendMode"; + +/** + * The write target a spine core package's generator fills while building the renderable primitive. + * + * @remarks + * Implemented by `SpineAnimationRenderer`. This is the narrow, spine-core-free contract that lets the + * version-specific generator live in a core package while the buffer/material/bounds bookkeeping stays + * in the shared renderer — the analogue of how engine physics colliders expose a native handle. + */ +export interface ISpineRenderTarget { + readonly _vertices: Float32Array; + readonly _indices: Uint16Array; + readonly _vertexCount: number; + readonly _localBounds: BoundingBox; + readonly _subPrimitives: SubPrimitive[]; + readonly _vertexBuffer: Buffer; + readonly _indexBuffer: Buffer; + readonly zSpacing: number; + readonly premultipliedAlpha: boolean; + readonly tintBlack: boolean; + _needResizeBuffer: boolean; + + _createAndBindBuffer(vertexCount: number): void; + _addSubPrimitive(subPrimitive: SubPrimitive): void; + _clearSubPrimitives(): void; + _getMaterial(texture: Texture2D, blendMode: SpineBlendMode): Material; + setMaterial(index: number, material: Material): void; +} diff --git a/packages/spine/src/runtime/ISpineRuntime.ts b/packages/spine/src/runtime/ISpineRuntime.ts new file mode 100644 index 0000000000..8f3d4202ea --- /dev/null +++ b/packages/spine/src/runtime/ISpineRuntime.ts @@ -0,0 +1,52 @@ +import type { + AnimationState, + AnimationStateData, + Skeleton, + SkeletonData, + TextureAtlas +} from "@esotericsoftware/spine-core"; +import type { Texture2D } from "@galacean/engine"; +import type { ISpineRenderTarget } from "./ISpineRenderTarget"; + +/** + * The version-specific spine runtime backend. + * + * @remarks + * The engine-spine analogue of `IPhysics`: a narrow factory + stepping interface that a spine core + * package (e.g. `@galacean/engine-spine-core-4.2`) implements and registers via {@link registerSpineRuntime}. + * It owns everything that differs between spine runtime versions — parsing, object-model construction, + * world-transform stepping (e.g. spine 4.2's `Physics` argument), and the attachment-reading mesh + * generator — so the shared renderer/loader never reference a concrete spine-core version at runtime. + * + * The spine-core types in these signatures are the 4.2 line, used as the canonical type contract; a + * 3.8 backend returns structurally-3.8 objects typed against the same contract. + */ +export interface ISpineRuntime { + /** Parse the page names declared in an atlas text, used to resolve which page textures to load. */ + parseAtlasPageNames(atlasText: string): string[]; + + /** Build a `TextureAtlas`, binding the given engine textures to its pages (matched by page name). */ + createTextureAtlas(atlasText: string, textures: Texture2D[]): TextureAtlas; + + /** Parse skeleton data (json or binary) against an atlas. */ + createSkeletonData( + skeletonRawData: string | ArrayBuffer, + textureAtlas: TextureAtlas, + skeletonDataScale: number + ): SkeletonData; + + /** Create the shared animation state data for a skeleton data. */ + createAnimationStateData(skeletonData: SkeletonData): AnimationStateData; + + /** Create a skeleton instance. */ + createSkeleton(skeletonData: SkeletonData): Skeleton; + + /** Create an animation state instance. */ + createAnimationState(stateData: AnimationStateData): AnimationState; + + /** Advance the animation state and apply it to the skeleton, then update world transforms. */ + updateState(skeleton: Skeleton, state: AnimationState, delta: number): void; + + /** Build the renderable primitive for the current skeleton pose into the given target. */ + buildPrimitive(skeleton: Skeleton, target: ISpineRenderTarget): void; +} diff --git a/packages/spine/src/runtime/SpineRuntimeRegistry.ts b/packages/spine/src/runtime/SpineRuntimeRegistry.ts new file mode 100644 index 0000000000..df865bc687 --- /dev/null +++ b/packages/spine/src/runtime/SpineRuntimeRegistry.ts @@ -0,0 +1,25 @@ +import type { ISpineRuntime } from "./ISpineRuntime"; + +let _runtime: ISpineRuntime | null = null; + +/** + * Register the spine runtime backend. Called for its side effect when a spine core package + * (e.g. `@galacean/engine-spine-core-4.2`) is imported. The last registration wins. + */ +export function registerSpineRuntime(runtime: ISpineRuntime): void { + _runtime = runtime; +} + +/** + * Get the registered spine runtime backend. + * @throws If no core package has been imported. + */ +export function getSpineRuntime(): ISpineRuntime { + if (!_runtime) { + throw new Error( + "@galacean/engine-spine: no spine runtime registered. Import a spine core package, " + + 'e.g. `import "@galacean/engine-spine-core-4.2"`.' + ); + } + return _runtime; +} diff --git a/packages/spine/tsconfig.json b/packages/spine/tsconfig.json new file mode 100644 index 0000000000..588b0055c3 --- /dev/null +++ b/packages/spine/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "declaration": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "declarationDir": "types", + "emitDeclarationOnly": true, + "noImplicitOverride": true, + "sourceMap": true, + "incremental": false, + "skipLibCheck": true, + "stripInternal": true + }, + "include": ["src/**/*"] +} diff --git a/packages/ui/src/Utils.ts b/packages/ui/src/Utils.ts index f47e0a8053..90d0bda7a7 100644 --- a/packages/ui/src/Utils.ts +++ b/packages/ui/src/Utils.ts @@ -1,10 +1,67 @@ -import { Entity } from "@galacean/engine"; +import { Entity, Matrix, Plane, Ray, Vector2, Vector3 } from "@galacean/engine"; +import { UITransform } from "./component"; import { RootCanvasModifyFlags, UICanvas } from "./component/UICanvas"; import { GroupModifyFlags, UIGroup } from "./component/UIGroup"; +import { CanvasRenderMode } from "./enums/CanvasRenderMode"; import { IElement } from "./interface/IElement"; import { IGroupAble } from "./interface/IGroupAble"; export class Utils { + static _tempRay: Ray = new Ray(); + static _tempPlane: Plane = new Plane(); + static _tempVec3: Vector3 = new Vector3(); + static _tempMat: Matrix = new Matrix(); + + /** + * Local position of a screen point in the component + */ + static screenToLocalPoint(position: Vector2, transform: UITransform, out: Vector3): boolean { + const engine = transform.engine; + // Get root canvas + let entity = transform.entity; + let rootCanvas: UICanvas; + while (entity) { + // @ts-ignore + const components = entity._components; + for (let i = 0, n = components.length; i < n; i++) { + const component = components[i]; + if (component.enabled && component instanceof UICanvas && component._isRootCanvas) { + rootCanvas = component; + } + } + entity = entity.parent; + } + if (!rootCanvas) return false; + // Calculate ray + const ray = this._tempRay; + switch (rootCanvas._realRenderMode) { + case CanvasRenderMode.ScreenSpaceOverlay: + // Screen to world ( Assume that world units have a one-to-one relationship with pixel units ) + ray.origin.set(position.x, engine.canvas.height - position.y, 1); + ray.direction.set(0, 0, -1); + break; + case CanvasRenderMode.ScreenSpaceCamera: + rootCanvas.renderCamera.screenPointToRay(position, ray); + break; + default: + // World space not yet supported, see issue #2793 + return false; + } + // Intersect ray with UI plane to get local coordinates + const plane = this._tempPlane; + const normal = plane.normal.copyFrom(transform.worldForward); + plane.distance = -Vector3.dot(normal, transform.worldPosition); + const curDistance = ray.intersectPlane(plane); + if (curDistance >= 0 && curDistance < Number.MAX_SAFE_INTEGER) { + const hitPointWorld = ray.getPoint(curDistance, this._tempVec3); + const worldMatrixInv = this._tempMat; + Matrix.invert(transform.worldMatrix, worldMatrixInv); + Vector3.transformCoordinate(hitPointWorld, worldMatrixInv, out); + return true; + } + return false; + } + static setRootCanvasDirty(element: IElement): void { if (element._isRootCanvasDirty) return; element._isRootCanvasDirty = true; diff --git a/packages/ui/src/component/UICanvas.ts b/packages/ui/src/component/UICanvas.ts index a69cd846bf..48e733151a 100644 --- a/packages/ui/src/component/UICanvas.ts +++ b/packages/ui/src/component/UICanvas.ts @@ -26,6 +26,7 @@ import { ResolutionAdaptationMode } from "../enums/ResolutionAdaptationMode"; import { UIHitResult } from "../input/UIHitResult"; import { IElement } from "../interface/IElement"; import { IGroupAble } from "../interface/IGroupAble"; +import { RectMask2D } from "./advanced/RectMask2D"; import { UIGroup } from "./UIGroup"; import { UIRenderer } from "./UIRenderer"; import { UITransform } from "./UITransform"; @@ -40,6 +41,7 @@ export class UICanvas extends Component implements IElement { /** @internal */ static _hierarchyCounter: number = 1; private static _tempGroupAbleList: IGroupAble[] = []; + private static _tempRectMaskList: RectMask2D[] = []; private static _tempVec3: Vector3 = new Vector3(); private static _tempMat: Matrix = new Matrix(); @@ -435,7 +437,8 @@ export class UICanvas extends Component implements IElement { const { _orderedRenderers: renderers, entity } = this; const uiHierarchyVersion = entity._uiHierarchyVersion; if (this._hierarchyVersion !== uiHierarchyVersion) { - renderers.length = this._walk(this.entity, renderers); + UICanvas._tempRectMaskList.length = 0; + renderers.length = this._walk(this.entity, renderers, 0, null, 0); UICanvas._tempGroupAbleList.length = 0; this._hierarchyVersion = uiHierarchyVersion; ++UICanvas._hierarchyCounter; @@ -517,10 +520,18 @@ export class UICanvas extends Component implements IElement { transform.size.set(curWidth / expectX, curHeight / expectY); } - private _walk(entity: Entity, renderers: UIRenderer[], depth = 0, group: UIGroup = null): number { + private _walk( + entity: Entity, + renderers: UIRenderer[], + depth = 0, + group: UIGroup = null, + rectMaskCount: number = 0 + ): number { // @ts-ignore const components: Component[] = entity._components; const tempGroupAbleList = UICanvas._tempGroupAbleList; + const tempRectMaskList = UICanvas._tempRectMaskList; + let rectMask: RectMask2D = null; let groupAbleCount = 0; for (let i = 0, n = components.length; i < n; i++) { const component = components[i]; @@ -532,11 +543,14 @@ export class UICanvas extends Component implements IElement { if (component._isGroupDirty) { tempGroupAbleList[groupAbleCount++] = component; } + component._setRectMasks(tempRectMaskList, rectMaskCount); } else if (component instanceof UIInteractive) { component._isRootCanvasDirty && Utils.setRootCanvas(component, this); if (component._isGroupDirty) { tempGroupAbleList[groupAbleCount++] = component; } + } else if (component instanceof RectMask2D) { + rectMask = component; } else if (component instanceof UIGroup) { component._isRootCanvasDirty && Utils.setRootCanvas(component, this); component._isGroupDirty && Utils.setGroup(component, group); @@ -546,10 +560,13 @@ export class UICanvas extends Component implements IElement { for (let i = 0; i < groupAbleCount; i++) { Utils.setGroup(tempGroupAbleList[i], group); } + if (rectMask) { + tempRectMaskList[rectMaskCount++] = rectMask; + } const children = entity.children; for (let i = 0, n = children.length; i < n; i++) { const child = children[i]; - child.isActive && (depth = this._walk(child, renderers, depth, group)); + child.isActive && (depth = this._walk(child, renderers, depth, group, rectMaskCount)); } return depth; } diff --git a/packages/ui/src/component/UIRenderer.ts b/packages/ui/src/component/UIRenderer.ts index 82af6480bd..527ad3bd23 100644 --- a/packages/ui/src/component/UIRenderer.ts +++ b/packages/ui/src/component/UIRenderer.ts @@ -11,22 +11,28 @@ import { RendererUpdateFlags, ShaderMacroCollection, ShaderProperty, + SpriteMaskInteraction, + SpriteMaskLayer, Vector3, Vector4, assignmentClone, deepClone, dependentComponents, - ignoreClone + ignoreClone, + Vector2 } from "@galacean/engine"; import { Utils } from "../Utils"; import { UIHitResult } from "../input/UIHitResult"; import { IGraphics } from "../interface/IGraphics"; +import { RectMask2D } from "./advanced/RectMask2D"; import { EntityUIModifyFlags, UICanvas } from "./UICanvas"; import { GroupModifyFlags, UIGroup } from "./UIGroup"; import { UITransform } from "./UITransform"; @dependentComponents(UITransform, DependentMode.AutoAdd) export class UIRenderer extends Renderer implements IGraphics { + /** @internal */ + static _tempVec20: Vector2 = new Vector2(); /** @internal */ static _tempVec30: Vector3 = new Vector3(); /** @internal */ @@ -37,6 +43,16 @@ export class UIRenderer extends Renderer implements IGraphics { static _tempPlane: Plane = new Plane(); /** @internal */ static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_UITexture"); + /** @internal */ + static _rectClipRectProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipRect"); + /** @internal */ + static _rectClipEnabledProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipEnabled"); + /** @internal */ + static _rectClipSoftnessProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipSoftness"); + /** @internal */ + static _rectClipHardClipProperty: ShaderProperty = ShaderProperty.getByName("renderer_UIRectClipHardClip"); + /** @internal */ + static _tempRect: Vector4 = new Vector4(); /** * Custom boundary for raycast detection. @@ -69,9 +85,24 @@ export class UIRenderer extends Renderer implements IGraphics { /** @internal */ @ignoreClone _subChunk; + /** @internal */ + @ignoreClone + _rectMasks: RectMask2D[] = []; + /** @internal */ + @ignoreClone + _rectMaskRect: Vector4 = new Vector4(); + /** @internal */ + @ignoreClone + _rectMaskEnabled: boolean = false; + /** @internal */ + @ignoreClone + _rectMaskSoftness: Vector4 = new Vector4(); + /** @internal */ + @ignoreClone + _rectMaskHardClip: boolean = false; @assignmentClone - private _raycastEnabled: boolean = true; + private _raycastEnabled: boolean = false; @deepClone protected _color: Color = new Color(1, 1, 1, 1); @@ -88,6 +119,30 @@ export class UIRenderer extends Renderer implements IGraphics { } } + /** + * The mask layer the ui renderer belongs to. + */ + get maskLayer(): SpriteMaskLayer { + return this._maskLayer; + } + + set maskLayer(value: SpriteMaskLayer) { + this._maskLayer = value; + } + + /** + * Interacts with the masks. + */ + get maskInteraction(): SpriteMaskInteraction { + return this._maskInteraction; + } + + set maskInteraction(value: SpriteMaskInteraction) { + if (this._maskInteraction !== value) { + this._maskInteraction = value; + } + } + /** * Whether this renderer be picked up by raycast. */ @@ -110,6 +165,9 @@ export class UIRenderer extends Renderer implements IGraphics { this._color._onValueChanged = this._onColorChanged; this._groupListener = this._groupListener.bind(this); this._rootCanvasListener = this._rootCanvasListener.bind(this); + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 0); + this.shaderData.setVector4(UIRenderer._rectClipSoftnessProperty, this._rectMaskSoftness); + this.shaderData.setFloat(UIRenderer._rectClipHardClipProperty, 0); } // @ts-ignore @@ -135,6 +193,7 @@ export class UIRenderer extends Renderer implements IGraphics { this._update(context); } + this._updateRectMaskClipState(); this._render(context); // union camera global macro and renderer macro. @@ -237,6 +296,17 @@ export class UIRenderer extends Renderer implements IGraphics { return this.engine._batcherManager.primitiveChunkManagerUI; } + /** + * @internal + */ + _setRectMasks(rectMasks: RectMask2D[], count: number): void { + const targetMasks = this._rectMasks; + targetMasks.length = count; + for (let i = 0; i < count; i++) { + targetMasks[i] = rectMasks[i]; + } + } + /** * @internal */ @@ -252,7 +322,11 @@ export class UIRenderer extends Renderer implements IGraphics { Matrix.invert(transform.worldMatrix, worldMatrixInv); const localPosition = UIRenderer._tempVec31; Vector3.transformCoordinate(hitPointWorld, worldMatrixInv, localPosition); - if (this._hitTest(localPosition)) { + if ( + this._hitTest(localPosition) && + this._isRaycastVisibleByRectMask(hitPointWorld) && + this._isRaycastVisibleByMask(hitPointWorld) + ) { out.component = this; out.distance = curDistance; out.entity = this.entity; @@ -278,6 +352,143 @@ export class UIRenderer extends Renderer implements IGraphics { ); } + private _isRaycastVisibleByMask(hitPointWorld: Vector3): boolean { + const maskInteraction = this._maskInteraction; + if (maskInteraction === SpriteMaskInteraction.None) { + return true; + } + // @ts-ignore + return this.scene._maskManager.isVisibleByMask(maskInteraction, this._maskLayer, hitPointWorld); + } + + private _isRaycastVisibleByRectMask(hitPointWorld: Vector3): boolean { + const rectMasks = this._rectMasks; + for (let i = 0, n = rectMasks.length; i < n; i++) { + const rectMask = rectMasks[i]; + if (!rectMask.enabled || !rectMask.entity.isActiveInHierarchy) { + continue; + } + if (!rectMask._containsWorldPoint(hitPointWorld)) { + return false; + } + } + return true; + } + + private _updateRectMaskClipState(): void { + const rectMasks = this._rectMasks; + const count = rectMasks.length; + if (count <= 0) { + this._resetRectMaskClipState(); + return; + } + + let minX = Number.NEGATIVE_INFINITY; + let minY = Number.NEGATIVE_INFINITY; + let maxX = Number.POSITIVE_INFINITY; + let maxY = Number.POSITIVE_INFINITY; + let clipSoftnessLeft = 0; + let clipSoftnessBottom = 0; + let clipSoftnessRight = 0; + let clipSoftnessTop = 0; + let clipHardClip = false; + let hasActiveMask = false; + const tempRect = UIRenderer._tempRect; + for (let i = 0; i < count; i++) { + const rectMask = rectMasks[i]; + if (!rectMask.enabled || !rectMask.entity.isActiveInHierarchy) { + continue; + } + hasActiveMask = true; + const softness = rectMask.softness; + if (!clipHardClip && rectMask.alphaClip) { + clipHardClip = true; + } + if (!rectMask._getWorldRect(tempRect)) { + minX = 1; + minY = 1; + maxX = 0; + maxY = 0; + break; + } + if (tempRect.x > minX) { + minX = tempRect.x; + clipSoftnessLeft = softness.x; + } + if (tempRect.y > minY) { + minY = tempRect.y; + clipSoftnessBottom = softness.y; + } + if (tempRect.z < maxX) { + maxX = tempRect.z; + clipSoftnessRight = softness.x; + } + if (tempRect.w < maxY) { + maxY = tempRect.w; + clipSoftnessTop = softness.y; + } + } + + if (!hasActiveMask) { + this._resetRectMaskClipState(); + return; + } + + if (minX >= maxX || minY >= maxY) { + minX = 1; + minY = 1; + maxX = 0; + maxY = 0; + clipSoftnessLeft = 0; + clipSoftnessBottom = 0; + clipSoftnessRight = 0; + clipSoftnessTop = 0; + } + + const rectMaskRect = this._rectMaskRect; + if (rectMaskRect.x !== minX || rectMaskRect.y !== minY || rectMaskRect.z !== maxX || rectMaskRect.w !== maxY) { + rectMaskRect.set(minX, minY, maxX, maxY); + this.shaderData.setVector4(UIRenderer._rectClipRectProperty, rectMaskRect); + } + + const rectMaskSoftness = this._rectMaskSoftness; + if ( + rectMaskSoftness.x !== clipSoftnessLeft || + rectMaskSoftness.y !== clipSoftnessBottom || + rectMaskSoftness.z !== clipSoftnessRight || + rectMaskSoftness.w !== clipSoftnessTop + ) { + rectMaskSoftness.set(clipSoftnessLeft, clipSoftnessBottom, clipSoftnessRight, clipSoftnessTop); + this.shaderData.setVector4(UIRenderer._rectClipSoftnessProperty, rectMaskSoftness); + } + + if (this._rectMaskHardClip !== clipHardClip) { + this._rectMaskHardClip = clipHardClip; + this.shaderData.setFloat(UIRenderer._rectClipHardClipProperty, clipHardClip ? 1 : 0); + } + + if (!this._rectMaskEnabled) { + this._rectMaskEnabled = true; + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 1); + } + } + + private _resetRectMaskClipState(): void { + if (this._rectMaskEnabled) { + this._rectMaskEnabled = false; + this.shaderData.setFloat(UIRenderer._rectClipEnabledProperty, 0); + } + const rectMaskSoftness = this._rectMaskSoftness; + if (rectMaskSoftness.x !== 0 || rectMaskSoftness.y !== 0 || rectMaskSoftness.z !== 0 || rectMaskSoftness.w !== 0) { + rectMaskSoftness.set(0, 0, 0, 0); + this.shaderData.setVector4(UIRenderer._rectClipSoftnessProperty, rectMaskSoftness); + } + if (this._rectMaskHardClip) { + this._rectMaskHardClip = false; + this.shaderData.setFloat(UIRenderer._rectClipHardClipProperty, 0); + } + } + protected override _onDestroy(): void { if (this._subChunk) { this._getChunkManager().freeSubChunk(this._subChunk); @@ -287,6 +498,8 @@ export class UIRenderer extends Renderer implements IGraphics { //@ts-ignore this._color._onValueChanged = null; this._color = null; + this._rectMasks = null; + this._rectMaskSoftness = null; } } diff --git a/packages/ui/src/component/advanced/Image.ts b/packages/ui/src/component/advanced/Image.ts index b364a2e303..5485cdae20 100644 --- a/packages/ui/src/component/advanced/Image.ts +++ b/packages/ui/src/component/advanced/Image.ts @@ -1,6 +1,7 @@ import { BoundingBox, Entity, + FilledSpriteAssembler, ISpriteAssembler, ISpriteRenderer, MathUtil, @@ -9,6 +10,8 @@ import { SlicedSpriteAssembler, Sprite, SpriteDrawMode, + SpriteFilledMode, + SpriteFilledOrigin, SpriteModifyFlags, SpriteTileMode, TiledSpriteAssembler, @@ -34,6 +37,14 @@ export class Image extends UIRenderer implements ISpriteRenderer { private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; @assignmentClone private _tiledAdaptiveThreshold: number = 0.5; + @assignmentClone + private _filledMode: SpriteFilledMode = SpriteFilledMode.Radial360; + @assignmentClone + private _filledAmount: number = 1; + @assignmentClone + private _filledOrigin: SpriteFilledOrigin = SpriteFilledOrigin.Bottom; + @assignmentClone + private _filledClockWise: boolean = true; /** * The draw mode of the image. @@ -55,6 +66,9 @@ export class Image extends UIRenderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._assembler = TiledSpriteAssembler; break; + case SpriteDrawMode.Filled: + this._assembler = FilledSpriteAssembler; + break; default: break; } @@ -96,6 +110,73 @@ export class Image extends UIRenderer implements ISpriteRenderer { } } + /** + * The fill amount of the image, range from 0 to 1. (Only works in filled mode.) + */ + get filledAmount(): number { + return this._filledAmount; + } + + set filledAmount(value: number) { + value = MathUtil.clamp(value, 0, 1); + if (this._filledAmount !== value) { + this._filledAmount = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill mode of the image. (Only works in filled mode.) + */ + get filledMode(): SpriteFilledMode { + return this._filledMode; + } + + set filledMode(value: SpriteFilledMode) { + if (this._filledMode !== value) { + this._filledMode = value; + this._filledOrigin = Image._correctOrigin(value, this._filledOrigin); + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * The fill origin of the image. (Only works in filled mode.) + */ + get filledOrigin(): SpriteFilledOrigin { + return this._filledOrigin; + } + + set filledOrigin(value: SpriteFilledOrigin) { + value = Image._correctOrigin(this._filledMode, value); + if (this._filledOrigin !== value) { + this._filledOrigin = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + + /** + * Whether the fill is clockwise. (Only works in filled radial mode.) + */ + get filledClockWise(): boolean { + return this._filledClockWise; + } + + set filledClockWise(value: boolean) { + if (this._filledClockWise !== value) { + this._filledClockWise = value; + if (this._drawMode === SpriteDrawMode.Filled) { + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; + } + } + } + /** * The Sprite to render. */ @@ -247,7 +328,10 @@ export class Image extends UIRenderer implements ISpriteRenderer { @ignoreClone protected override _onTransformChanged(type: number): void { - if (type & UITransformModifyFlags.Size && this._drawMode === SpriteDrawMode.Tiled) { + if ( + type & UITransformModifyFlags.Size && + (this._drawMode === SpriteDrawMode.Tiled || this._drawMode === SpriteDrawMode.Filled) + ) { this._dirtyUpdateFlag |= ImageUpdateFlags.All; } this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; @@ -278,6 +362,9 @@ export class Image extends UIRenderer implements ISpriteRenderer { case SpriteDrawMode.Tiled: this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; break; + case SpriteDrawMode.Filled: + this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeUVAndColor; + break; default: break; } @@ -299,13 +386,41 @@ export class Image extends UIRenderer implements ISpriteRenderer { this._dirtyUpdateFlag |= ImageUpdateFlags.WorldVolumeAndUV; break; case SpriteModifyFlags.atlasRegion: - this._dirtyUpdateFlag |= ImageUpdateFlags.UV; + this._dirtyUpdateFlag |= + this._drawMode === SpriteDrawMode.Filled ? ImageUpdateFlags.WorldVolumeAndUV : ImageUpdateFlags.UV; break; case SpriteModifyFlags.destroy: this.sprite = null; break; } } + + private static _correctOrigin(mode: SpriteFilledMode, origin: SpriteFilledOrigin): SpriteFilledOrigin { + switch (mode) { + case SpriteFilledMode.Horizontal: + return origin === SpriteFilledOrigin.Left || origin === SpriteFilledOrigin.Right + ? origin + : SpriteFilledOrigin.Left; + case SpriteFilledMode.Vertical: + return origin === SpriteFilledOrigin.Top || origin === SpriteFilledOrigin.Bottom + ? origin + : SpriteFilledOrigin.Bottom; + case SpriteFilledMode.Radial90: + return origin === SpriteFilledOrigin.TopLeft || + origin === SpriteFilledOrigin.TopRight || + origin === SpriteFilledOrigin.BottomLeft || + origin === SpriteFilledOrigin.BottomRight + ? origin + : SpriteFilledOrigin.BottomLeft; + default: + return origin === SpriteFilledOrigin.Top || + origin === SpriteFilledOrigin.Bottom || + origin === SpriteFilledOrigin.Left || + origin === SpriteFilledOrigin.Right + ? origin + : SpriteFilledOrigin.Bottom; + } + } } /** diff --git a/packages/ui/src/component/advanced/Mask.ts b/packages/ui/src/component/advanced/Mask.ts new file mode 100644 index 0000000000..cff2fd85ab --- /dev/null +++ b/packages/ui/src/component/advanced/Mask.ts @@ -0,0 +1,80 @@ +import { BoundingBox, Entity, MaskRenderable, Vector2 } from "@galacean/engine"; +import type { IMaskRenderable } from "@galacean/engine"; +import { UIRenderer } from "../UIRenderer"; +import { UITransform } from "../UITransform"; + +/** + * UI component that uses a sprite to mask child UI renderers via stencil. + */ +export class Mask extends MaskRenderable(UIRenderer) { + /** + * @internal + */ + override _getChunkManager() { + // @ts-ignore + return this.engine._batcherManager.primitiveChunkManagerMask; + } + + /** + * @internal + */ + constructor(entity: Entity) { + super(entity); + this._initMask(); + this.raycastEnabled = false; + } + + /** + * @internal + */ + // @ts-ignore + _cloneTo(target: Mask): void { + // @ts-ignore + super._cloneTo(target); + this._cloneMaskData(target); + } + + protected override _updateBounds(worldBounds: BoundingBox): void { + const rootCanvas = this._getRootCanvas(); + if (this.sprite && rootCanvas) { + this._updateMaskBounds(worldBounds); + } else { + const { worldPosition } = this._transformEntity.transform; + worldBounds.min.copyFrom(worldPosition); + worldBounds.max.copyFrom(worldPosition); + } + } + + /** + * @inheritdoc + */ + protected override _render(context): void { + this._renderMask(0); + } + + /** + * @inheritdoc + */ + protected override _onDestroy(): void { + this._destroyMaskResources(); + + super._onDestroy(); + + if (this._subChunk) { + this._getChunkManager().freeSubChunk(this._subChunk); + this._subChunk = null; + } + } + + override _getSpriteWidth(): number { + return (this._transformEntity.transform).size.x; + } + + override _getSpriteHeight(): number { + return (this._transformEntity.transform).size.y; + } + + override _getSpritePivot(): Vector2 { + return (this._transformEntity.transform).pivot; + } +} diff --git a/packages/ui/src/component/advanced/RectMask2D.ts b/packages/ui/src/component/advanced/RectMask2D.ts new file mode 100644 index 0000000000..9cba135e84 --- /dev/null +++ b/packages/ui/src/component/advanced/RectMask2D.ts @@ -0,0 +1,157 @@ +import { + Component, + DependentMode, + Entity, + Vector2, + Vector3, + Vector4, + assignmentClone, + deepClone, + dependentComponents +} from "@galacean/engine"; +import { UICanvas } from "../UICanvas"; +import { UITransform } from "../UITransform"; + +/** + * UI component that clips descendant graphics by an axis-aligned rectangle. + */ +@dependentComponents(UITransform, DependentMode.AutoAdd) +export class RectMask2D extends Component { + private static _tempRect: Vector4 = new Vector4(); + private static _tempCorner0: Vector3 = new Vector3(); + private static _tempCorner1: Vector3 = new Vector3(); + private static _tempCorner2: Vector3 = new Vector3(); + private static _tempCorner3: Vector3 = new Vector3(); + + @deepClone + private _softness: Vector2 = new Vector2(0, 0); + @assignmentClone + private _alphaClip: boolean = false; + + /** + * Soft clipping width on X/Y axis in world space. + */ + get softness(): Vector2 { + return this._softness; + } + + set softness(value: Vector2) { + const softness = this._softness; + if (softness === value) { + return; + } + if (softness.x !== value.x || softness.y !== value.y) { + softness.copyFrom(value); + this._clampSoftness(); + } + } + + /** + * Whether to enable hard clip (discard) when outside the rect. + */ + get alphaClip(): boolean { + return this._alphaClip; + } + + set alphaClip(value: boolean) { + this._alphaClip = value; + } + + /** + * @internal + */ + _getWorldRect(out: Vector4): boolean { + const transform = this.entity.transform; + const { x: width, y: height } = transform.size; + if (!width || !height) { + return false; + } + + const { x: pivotX, y: pivotY } = transform.pivot; + const left = -width * pivotX; + const right = width * (1 - pivotX); + const bottom = -height * pivotY; + const top = height * (1 - pivotY); + + const worldMatrix = transform.worldMatrix; + const corner0 = RectMask2D._tempCorner0; + const corner1 = RectMask2D._tempCorner1; + const corner2 = RectMask2D._tempCorner2; + const corner3 = RectMask2D._tempCorner3; + Vector3.transformCoordinate(corner0.set(left, bottom, 0), worldMatrix, corner0); + Vector3.transformCoordinate(corner1.set(left, top, 0), worldMatrix, corner1); + Vector3.transformCoordinate(corner2.set(right, bottom, 0), worldMatrix, corner2); + Vector3.transformCoordinate(corner3.set(right, top, 0), worldMatrix, corner3); + + const minX = Math.min(corner0.x, corner1.x, corner2.x, corner3.x); + const minY = Math.min(corner0.y, corner1.y, corner2.y, corner3.y); + const maxX = Math.max(corner0.x, corner1.x, corner2.x, corner3.x); + const maxY = Math.max(corner0.y, corner1.y, corner2.y, corner3.y); + out.set(minX, minY, maxX, maxY); + return true; + } + + /** + * @internal + */ + _containsWorldPoint(worldPoint: Vector3): boolean { + const worldRect = RectMask2D._tempRect; + if (!this._getWorldRect(worldRect)) { + return false; + } + const { x, y } = worldPoint; + return x >= worldRect.x && x <= worldRect.z && y >= worldRect.y && y <= worldRect.w; + } + + constructor(entity: Entity) { + super(entity); + this._onSoftnessChanged = this._onSoftnessChanged.bind(this); + // @ts-ignore + this._softness._onValueChanged = this._onSoftnessChanged; + } + + // @ts-ignore + override _onEnableInScene(): void { + this.entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter); + } + + // @ts-ignore + override _onDisableInScene(): void { + this.entity._updateUIHierarchyVersion(UICanvas._hierarchyCounter); + } + + // @ts-ignore + override _cloneTo(target: RectMask2D): void { + // RectMask2D extends Component directly; Component.prototype 上没有 _cloneTo, + // 不能 super._cloneTo(target) — 会拿到 undefined 报 "Cannot read properties of undefined (reading 'call')"。 + // (Image/Mask 走 Renderer 链路所以能 super;RectMask2D 不在 Renderer 链路里。) + const targetSoftness = target._softness; + // @ts-ignore + targetSoftness._onValueChanged = null; + targetSoftness.copyFrom(this._softness); + target._clampSoftness(); + // @ts-ignore + targetSoftness._onValueChanged = target._onSoftnessChanged; + } + + protected override _onDestroy(): void { + // @ts-ignore + this._softness._onValueChanged = null; + this._softness = null; + super._onDestroy(); + } + + private _onSoftnessChanged(): void { + this._clampSoftness(); + } + + private _clampSoftness(): void { + const softness = this._softness; + if (softness.x < 0) { + softness.x = 0; + } + if (softness.y < 0) { + softness.y = 0; + } + } +} diff --git a/packages/ui/src/component/advanced/Text.ts b/packages/ui/src/component/advanced/Text.ts index b534253a9a..329902f9e7 100644 --- a/packages/ui/src/component/advanced/Text.ts +++ b/packages/ui/src/component/advanced/Text.ts @@ -1,6 +1,7 @@ import { BoundingBox, CharRenderInfo, + Color, Engine, Entity, Font, @@ -17,10 +18,11 @@ import { TextVerticalAlignment, Texture2D, Vector3, + VertexMergeBatcher, assignmentClone, + deepClone, ignoreClone } from "@galacean/engine"; -import { CanvasRenderMode } from "../../enums/CanvasRenderMode"; import { RootCanvasModifyFlags } from "../UICanvas"; import { UIRenderer, UIRendererUpdateFlags } from "../UIRenderer"; import { UITransform, UITransformModifyFlags } from "../UITransform"; @@ -30,6 +32,9 @@ import { UITransform, UITransformModifyFlags } from "../UITransform"; */ export class Text extends UIRenderer implements ITextRenderer { private static _textTextureProperty = ShaderProperty.getByName("renderElement_TextTexture"); + private static _textTextureSizeProperty = ShaderProperty.getByName("renderElement_TextTextureSize"); + private static _outlineColorProperty = ShaderProperty.getByName("renderer_OutlineColor"); + private static _outlineWidthProperty = ShaderProperty.getByName("renderer_OutlineWidth"); private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; private static _charRenderInfos: CharRenderInfo[] = []; @@ -59,6 +64,10 @@ export class Text extends UIRenderer implements ITextRenderer { private _enableWrapping: boolean = false; @assignmentClone private _overflowMode: OverflowMode = OverflowMode.Overflow; + @deepClone + private _outlineColor: Color = new Color(0, 0, 0, 1); + @ignoreClone + private _outlineWidth: number = 0; /** * Rendering string for the Text. @@ -205,14 +214,32 @@ export class Text extends UIRenderer implements ITextRenderer { } /** - * The mask layer the sprite renderer belongs to. + * The outline width in pixels. 0 means outline is disabled. Clamped to [0, 8]. */ - get maskLayer(): number { - return this._maskLayer; + get outlineWidth(): number { + return this._outlineWidth; } - set maskLayer(value: number) { - this._maskLayer = value; + set outlineWidth(value: number) { + value = Math.max(0, Math.min(value, 3)); + if (this._outlineWidth !== value) { + this._outlineWidth = value; + this.shaderData.setFloat(Text._outlineWidthProperty, value); + this._setDirtyFlagTrue(DirtyFlag.Position); + } + } + + /** + * The outline color. Only effective when outlineWidth > 0. + */ + get outlineColor(): Color { + return this._outlineColor; + } + + set outlineColor(value: Color) { + if (this._outlineColor !== value) { + this._outlineColor.copyFrom(value); + } } /** @@ -246,6 +273,11 @@ export class Text extends UIRenderer implements ITextRenderer { this.raycastEnabled = false; // @ts-ignore this.setMaterial(engine._basicResources.textDefaultMaterial); + const shaderData = this.shaderData; + shaderData.setFloat(Text._outlineWidthProperty, this._outlineWidth); + shaderData.setColor(Text._outlineColorProperty, this._outlineColor); + // @ts-ignore + this._outlineColor._onValueChanged = this._onOutlineColorChanged.bind(this); } /** @@ -271,6 +303,7 @@ export class Text extends UIRenderer implements ITextRenderer { super._cloneTo(target); target.font = this._font; target._subFont = this._subFont; + target.outlineWidth = this._outlineWidth; } /** @@ -313,13 +346,15 @@ export class Text extends UIRenderer implements ITextRenderer { } } + /** + * @internal + */ + override _canBatch(preElement, curElement): boolean { + return VertexMergeBatcher.canBatchText(preElement, curElement); + } + protected override _updateBounds(worldBounds: BoundingBox): void { - const transform = this._transformEntity.transform; - const { x: width, y: height } = transform.size; - const { x: pivotX, y: pivotY } = transform.pivot; - worldBounds.min.set(-width * pivotX, -height * pivotY, 0); - worldBounds.max.set(width * (1 - pivotX), height * (1 - pivotY), 0); - BoundingBox.transform(worldBounds, this._transformEntity.transform.worldMatrix, worldBounds); + BoundingBox.transform(this._localBounds, this._transformEntity.transform.worldMatrix, worldBounds); } protected override _render(context): void { @@ -356,6 +391,7 @@ export class Text extends UIRenderer implements ITextRenderer { const distanceForSort = canvas._sortDistance; const textChunks = this._textChunks; const subShader = material.shader.subShaders[0]; + const textTextureSize = Text._tempVec20; for (let i = 0, n = textChunks.length; i < n; ++i) { const { subChunk, texture } = textChunks[i]; const renderElement = textRenderElementPool.get(); @@ -363,6 +399,10 @@ export class Text extends UIRenderer implements ITextRenderer { // @ts-ignore renderElement.shaderData ||= new ShaderData(ShaderDataGroup.RenderElement); renderElement.shaderData.setTexture(Text._textTextureProperty, texture); + renderElement.shaderData.setVector2( + Text._textTextureSizeProperty, + textTextureSize.set(texture.width, texture.height) + ); renderElement.subShader = subShader; renderElement.priority = priority; renderElement.distanceForSort = distanceForSort; @@ -377,6 +417,17 @@ export class Text extends UIRenderer implements ITextRenderer { this._subFont.nativeFontString = TextUtils.getNativeFontString(font.name, this.fontSize, this.fontStyle); } + /** + * Switch the sub font to a specific font size, used by the SHRINK overflow measurement. + */ + private _applyFontSizeForShrink(fontSize: number): void { + const font = this._font; + // @ts-ignore + const subFont = font._getSubFont(fontSize, this._fontStyle); + subFont.nativeFontString = TextUtils.getNativeFontString(font.name, fontSize, this._fontStyle); + this._subFont = subFont; + } + private _updatePosition(): void { const e = this._transformEntity.transform.worldMatrix.elements; @@ -447,27 +498,45 @@ export class Text extends UIRenderer implements ITextRenderer { const pixelsPerResolution = Engine._pixelsPerUnit / this._getRootCanvas().referenceResolutionPerUnit; const { min, max } = this._localBounds; const charRenderInfos = Text._charRenderInfos; - const charFont = this._getSubFont(); const { size, pivot } = this._transformEntity.transform; let rendererWidth = size.x; let rendererHeight = size.y; const offsetWidth = rendererWidth * (0.5 - pivot.x); const offsetHeight = rendererHeight * (0.5 - pivot.y); - const characterSpacing = this._characterSpacing * this._fontSize; - const textMetrics = this.enableWrapping - ? TextUtils.measureTextWithWrap( - this, - rendererWidth * pixelsPerResolution, - rendererHeight * pixelsPerResolution, - this._lineSpacing * this._fontSize, - characterSpacing - ) - : TextUtils.measureTextWithoutWrap( - this, - rendererHeight * pixelsPerResolution, - this._lineSpacing * this._fontSize, - characterSpacing - ); + let fontSize = this._fontSize; + let textMetrics: ReturnType; + if (this._overflowMode === OverflowMode.Shrink) { + const result = TextUtils.measureTextWithShrink( + this, + rendererWidth * pixelsPerResolution, + rendererHeight * pixelsPerResolution, + this._fontSize, + this._lineSpacing, + this._characterSpacing, + this.enableWrapping, + (sizeValue) => this._applyFontSizeForShrink(sizeValue) + ); + fontSize = result.fontSize; + textMetrics = result.metrics; + } else { + const characterSpacing = this._characterSpacing * fontSize; + textMetrics = this.enableWrapping + ? TextUtils.measureTextWithWrap( + this, + rendererWidth * pixelsPerResolution, + rendererHeight * pixelsPerResolution, + this._lineSpacing * fontSize, + characterSpacing + ) + : TextUtils.measureTextWithoutWrap( + this, + rendererHeight * pixelsPerResolution, + this._lineSpacing * fontSize, + characterSpacing + ); + } + const charFont = this._getSubFont(); + const characterSpacing = this._characterSpacing * fontSize; const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics; // @ts-ignore const charRenderInfoPool = this.engine._charRenderInfoPool; @@ -490,7 +559,10 @@ export class Text extends UIRenderer implements ITextRenderer { startY = rendererHeight * 0.5 - halfLineHeight + topDiff; break; case TextVerticalAlignment.Center: - startY = height * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; + // Center the text block (lineHeight * lineCount) within the renderer, independent of + // `height` — which equals the renderer height for Truncate/Shrink and would otherwise + // push the text upward by (rendererHeight - blockHeight) / 2 when the box is taller. + startY = lineHeight * linesLen * 0.5 - halfLineHeight - (bottomDiff - topDiff) * 0.5; break; case TextVerticalAlignment.Bottom: startY = height - rendererHeight * 0.5 - halfLineHeight - bottomDiff; @@ -532,10 +604,11 @@ export class Text extends UIRenderer implements ITextRenderer { charRenderInfo.texture = charFont._getTextureByIndex(charInfo.index); charRenderInfo.uvs = charInfo.uvs; const { w, ascent, descent } = charInfo; - const left = (startX + offsetWidth) * pixelsPerUnitReciprocal; - const right = (startX + w + offsetWidth) * pixelsPerUnitReciprocal; - const top = (startY + ascent + offsetHeight) * pixelsPerUnitReciprocal; - const bottom = (startY - descent + offsetHeight) * pixelsPerUnitReciprocal; + const ow = this._outlineWidth * pixelsPerUnitReciprocal; + const left = (startX + offsetWidth) * pixelsPerUnitReciprocal - ow; + const right = (startX + w + offsetWidth) * pixelsPerUnitReciprocal + ow; + const top = (startY + ascent + offsetHeight) * pixelsPerUnitReciprocal + ow; + const bottom = (startY - descent + offsetHeight) * pixelsPerUnitReciprocal - ow; localPositions.set(left, top, right, bottom); i === firstLine && (maxY = Math.max(maxY, top)); minY = Math.min(minY, bottom); @@ -618,7 +691,7 @@ export class Text extends UIRenderer implements ITextRenderer { this._text === "" || this._fontSize === 0 || (this.enableWrapping && size.x <= 0) || - (this.overflowMode === OverflowMode.Truncate && size.y <= 0) || + ((this.overflowMode === OverflowMode.Truncate || this.overflowMode === OverflowMode.Shrink) && size.y <= 0) || !this._getRootCanvas() ); } @@ -632,6 +705,10 @@ export class Text extends UIRenderer implements ITextRenderer { const vertices = subChunk.chunk.vertices; const indices = (subChunk.indices = []); const charRenderInfos = textChunk.charRenderInfos; + const ow = this._outlineWidth; + const texture = textChunk.texture; + const owU = ow > 0 ? ow / texture.width : 0; + const owV = ow > 0 ? ow / texture.height : 0; for (let i = 0, ii = 0, io = 0, vo = subChunk.vertexArea.start + 3; i < count; ++i, io += 4) { const charRenderInfo = charRenderInfos[i]; charRenderInfo.indexInChunk = i; @@ -641,10 +718,13 @@ export class Text extends UIRenderer implements ITextRenderer { indices[ii++] = tempIndices[j] + io; } - // Set uv and color for vertices + // Set uv and color for vertices, expand uv outward by outline width for (let j = 0; j < 4; ++j, vo += 9) { const uv = charRenderInfo.uvs[j]; - uv.copyToArray(vertices, vo); + const su = j === 1 || j === 2 ? 1 : -1; + const sv = j >= 2 ? 1 : -1; + vertices[vo] = uv.x + owU * su; + vertices[vo + 1] = uv.y + owV * sv; vertices[vo + 2] = r; vertices[vo + 3] = g; vertices[vo + 4] = b; @@ -673,6 +753,11 @@ export class Text extends UIRenderer implements ITextRenderer { } textChunks.length = 0; } + + @ignoreClone + private _onOutlineColorChanged(): void { + this.shaderData.setColor(Text._outlineColorProperty, this._outlineColor); + } } class TextChunk { diff --git a/packages/ui/src/component/index.ts b/packages/ui/src/component/index.ts index 1f89431265..8329d1f39a 100644 --- a/packages/ui/src/component/index.ts +++ b/packages/ui/src/component/index.ts @@ -1,11 +1,13 @@ -export { UICanvas } from "./UICanvas"; -export { UIGroup } from "./UIGroup"; -export { UIRenderer } from "./UIRenderer"; -export { UITransform } from "./UITransform"; export { Button } from "./advanced/Button"; export { Image } from "./advanced/Image"; +export { Mask } from "./advanced/Mask"; +export { RectMask2D } from "./advanced/RectMask2D"; export { Text } from "./advanced/Text"; export { ColorTransition } from "./interactive/transition/ColorTransition"; export { ScaleTransition } from "./interactive/transition/ScaleTransition"; export { SpriteTransition } from "./interactive/transition/SpriteTransition"; export { Transition } from "./interactive/transition/Transition"; +export { UICanvas } from "./UICanvas"; +export { UIGroup } from "./UIGroup"; +export { UIRenderer } from "./UIRenderer"; +export { UITransform } from "./UITransform"; diff --git a/packages/ui/src/input/UIPointerEventEmitter.ts b/packages/ui/src/input/UIPointerEventEmitter.ts index 83bf8f1484..5ea25e23c3 100644 --- a/packages/ui/src/input/UIPointerEventEmitter.ts +++ b/packages/ui/src/input/UIPointerEventEmitter.ts @@ -91,11 +91,11 @@ export class UIPointerEventEmitter extends PointerEventEmitter { } } if (camera.clearFlags & CameraClearFlags.Color) { - this._updateRaycast(null); + this._updateRaycast(null, pointer); return; } } - this._updateRaycast(null); + this._updateRaycast(null, pointer); } } @@ -128,10 +128,7 @@ export class UIPointerEventEmitter extends PointerEventEmitter { if (pressedPath.length > 0) { const common = UIPointerEventEmitter._tempArray0; if (this._findCommonInPath(enteredPath, pressedPath, common)) { - const eventData = this._createEventData(pointer); - for (let i = 0, n = common.length; i < n; i++) { - this._fireClick(common[i], eventData); - } + this._bubble(common, pointer, this._fireClick); common.length = 0; } } @@ -170,18 +167,17 @@ export class UIPointerEventEmitter extends PointerEventEmitter { this._enteredPath.length = this._pressedPath.length = this._draggedPath.length = 0; } - private _updateRaycast(element: UIRenderer, pointer: Pointer = null): void { + private _updateRaycast(element: UIRenderer | null, pointer: Pointer): void { const enteredPath = this._enteredPath; const curPath = this._composedPath(element, UIPointerEventEmitter._path); const add = UIPointerEventEmitter._tempArray0; const del = UIPointerEventEmitter._tempArray1; if (this._findDiffInPath(enteredPath, curPath, add, del)) { - const eventData = this._createEventData(pointer); for (let i = 0, n = add.length; i < n; i++) { - this._fireEnter(add[i], eventData); + this._fireEnter(add[i], this._createEventData(pointer, add[i])); } for (let i = 0, n = del.length; i < n; i++) { - this._fireExit(del[i], eventData); + this._fireExit(del[i], this._createEventData(pointer, del[i])); } const length = (enteredPath.length = curPath.length); @@ -193,7 +189,7 @@ export class UIPointerEventEmitter extends PointerEventEmitter { curPath.length = 0; } - private _composedPath(element: UIRenderer, path: Entity[]): Entity[] { + private _composedPath(element: UIRenderer | null, path: Entity[]): Entity[] { if (!element) { path.length = 0; return path; @@ -256,8 +252,9 @@ export class UIPointerEventEmitter extends PointerEventEmitter { private _bubble(path: Entity[], pointer: Pointer, fireEvent: FireEvent): void { const length = path.length; if (length <= 0) return; - const eventData = this._createEventData(pointer); + const eventData = this._createEventData(pointer, path[0]); for (let i = 0; i < length; i++) { + eventData.currentTarget = path[i]; fireEvent(path[i], eventData); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c82cc287c..778352b1c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,6 +140,12 @@ importers: '@galacean/engine-shader-compiler': specifier: workspace:* version: link:../packages/shader-compiler + '@galacean/engine-spine': + specifier: workspace:* + version: link:../packages/spine + '@galacean/engine-spine-core-4.2': + specifier: workspace:* + version: link:../packages/spine-core-4.2 '@galacean/engine-ui': specifier: workspace:* version: link:../packages/ui @@ -179,6 +185,15 @@ importers: '@galacean/engine-shader-compiler': specifier: workspace:* version: link:../packages/shader-compiler + '@galacean/engine-spine': + specifier: workspace:* + version: link:../packages/spine + '@galacean/engine-spine-core-3.8': + specifier: workspace:* + version: link:../packages/spine-core-3.8 + '@galacean/engine-spine-core-4.2': + specifier: workspace:* + version: link:../packages/spine-core-4.2 '@galacean/engine-toolkit': specifier: latest version: 1.6.0(@galacean/engine-ui@packages+ui)(@galacean/engine@packages+galacean) @@ -288,6 +303,38 @@ importers: specifier: workspace:* version: link:../design + packages/spine: + devDependencies: + '@esotericsoftware/spine-core': + specifier: ~4.2.66 + version: 4.2.119 + '@galacean/engine': + specifier: workspace:* + version: link:../galacean + + packages/spine-core-3.8: + dependencies: + '@galacean/engine-spine': + specifier: workspace:* + version: link:../spine + devDependencies: + '@galacean/engine': + specifier: workspace:* + version: link:../galacean + + packages/spine-core-4.2: + dependencies: + '@galacean/engine-spine': + specifier: workspace:* + version: link:../spine + devDependencies: + '@esotericsoftware/spine-core': + specifier: ~4.2.66 + version: 4.2.119 + '@galacean/engine': + specifier: workspace:* + version: link:../galacean + packages/ui: devDependencies: '@galacean/engine': @@ -345,10 +392,19 @@ importers: '@galacean/engine-shader-compiler': specifier: workspace:* version: link:../packages/shader-compiler + '@galacean/engine-spine': + specifier: workspace:* + version: link:../packages/spine + '@galacean/engine-spine-core-4.2': + specifier: workspace:* + version: link:../packages/spine-core-4.2 '@galacean/engine-ui': specifier: workspace:* version: link:../packages/ui devDependencies: + '@esotericsoftware/spine-core': + specifier: ~4.2.66 + version: 4.2.119 '@vitest/browser': specifier: 3.2.6 version: 3.2.6(msw@2.6.5(@types/node@18.19.64)(typescript@5.6.3))(playwright@1.55.0)(vite@5.4.11(@types/node@18.19.64)(sass@1.81.0)(terser@5.44.1))(vitest@3.2.6) @@ -833,6 +889,9 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@esotericsoftware/spine-core@4.2.119': + resolution: {integrity: sha512-lvaRECaQDScO758ZSAR1Fj+GWkBKVZxPdgT/gCiKqdkrjZCDu2UzgbZtdPhxnLPcKG/zsaGJkfbN4OS0wHsxZQ==} + '@fastify/deepmerge@1.3.0': resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} @@ -4436,6 +4495,8 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 + '@esotericsoftware/spine-core@4.2.119': {} + '@fastify/deepmerge@1.3.0': {} '@galacean/engine-toolkit-auxiliary-lines@1.6.0(@galacean/engine@packages+galacean)': diff --git a/rollup.config.js b/rollup.config.js index 1831d6fb4e..04e9b4f06f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -44,7 +44,10 @@ const commonPlugins = [ swc( defineRollupSwcOption({ include: /\.[mc]?[jt]sx?$/, - exclude: /node_modules/, + // Transpile bundled @esotericsoftware/spine-core to es5 too, so spine core packages' + // es5 subclasses (e.g. SpineTexture extends Texture) can extend it without the + // "Class constructor cannot be invoked without 'new'" es5-extends-es6 error. + exclude: /node_modules\/(?!\.pnpm\/@esotericsoftware\+|@esotericsoftware\/)/, jsc: { loose: true, externalHelpers: true, diff --git a/tests/package.json b/tests/package.json index 3740665d09..33779e4ba4 100644 --- a/tests/package.json +++ b/tests/package.json @@ -24,9 +24,12 @@ "@galacean/engine-shader-compiler": "workspace:*", "@galacean/engine-physics-physx": "workspace:*", "@galacean/engine-ui": "workspace:*", - "@galacean/engine-shader": "workspace:*" + "@galacean/engine-shader": "workspace:*", + "@galacean/engine-spine": "workspace:*", + "@galacean/engine-spine-core-4.2": "workspace:*" }, "devDependencies": { + "@esotericsoftware/spine-core": "~4.2.66", "@vitest/browser": "3.2.6" } } diff --git a/tests/src/core/2d/text/TextUtils.test.ts b/tests/src/core/2d/text/TextUtils.test.ts index 54a5a57820..640cb4713c 100644 --- a/tests/src/core/2d/text/TextUtils.test.ts +++ b/tests/src/core/2d/text/TextUtils.test.ts @@ -564,6 +564,90 @@ describe("TextUtils", () => { ); }); + it("measureTextWithShrink", () => { + // @ts-ignore + const { _pixelsPerUnit } = Engine; + const r = textRendererTruncate; + r.overflowMode = OverflowMode.Shrink; + r.enableWrapping = false; + r.lineSpacing = 0; + r.characterSpacing = 0; + r.text = "15"; + // applyFontSize: switch the sub font for each measurement during the shrink search. + const apply = (fs: number) => { + // @ts-ignore + r._applyFontSizeForShrink(fs); + }; + const measure = (w: number, h: number, fontSize: number) => { + r.width = w; + r.height = h; + r.fontSize = fontSize; + r.bounds; + return TextUtils.measureTextWithShrink( + r, + w * _pixelsPerUnit, + h * _pixelsPerUnit, + fontSize, + r.lineSpacing, + r.characterSpacing, + r.enableWrapping, + apply + ); + }; + + // Fits at original size → keep it (never shrinks unnecessarily). + let res = measure(3, 1, 24); + expect(res.fontSize).to.be.equal(24); + expect(res.metrics.width).to.be.at.most(300); + expect(res.metrics.lineHeight * res.metrics.lines.length).to.be.at.most(100); + + // Box too short → shrink the font size until the content height fits. + res = measure(3, 0.3, 80); + expect(res.fontSize).to.be.below(80); + expect(res.metrics.lineHeight * res.metrics.lines.length).to.be.at.most(30); + + // Box too narrow (no wrap) → shrink the font size until the width fits. + res = measure(0.3, 3, 80); + expect(res.fontSize).to.be.below(80); + expect(res.metrics.width).to.be.at.most(30); + + // Only shrinks, never enlarges: even a huge box keeps the original font size. + res = measure(10, 10, 24); + expect(res.fontSize).to.be.equal(24); + + // Wrapping path: a long text that wraps to multiple lines and overflows the height + // must shrink until the real content height (lineHeight * lineCount) fits the box. + r.enableWrapping = true; + r.text = "这是一段会换行的较长文本"; + res = measure(2, 0.5, 60); // 200x50px box + expect(res.fontSize).to.be.below(60); + expect(res.metrics.width).to.be.at.most(200); + expect(res.metrics.lineHeight * res.metrics.lines.length).to.be.at.most(50); + }); + + it("_getCharInfo writes the atlas only when uploadTexture is true (used by SHRINK search)", () => { + const r = wrap2TextRenderer; + // @ts-ignore + const subFont = r._getSubFont(); + const fontString = subFont.nativeFontString; + const char = "Q"; // a glyph not measured by the other cases above + + // Sanity: the glyph is not in the atlas yet. + // @ts-ignore + expect(subFont._getCharInfo(char)).to.be.null; + + // measure-only path: returns valid metrics but must NOT upload to the atlas. + const info = TextUtils._getCharInfo(char, fontString, subFont, false); + expect(info.w).to.be.greaterThan(0); + // @ts-ignore + expect(subFont._getCharInfo(char)).to.be.null; + + // full path: uploads the glyph to the atlas. + TextUtils._getCharInfo(char, fontString, subFont, true); + // @ts-ignore + expect(subFont._getCharInfo(char)).to.not.be.null; + }); + afterAll(() => { engine.destroy(); }); diff --git a/tests/src/core/CloneUtils.test.ts b/tests/src/core/CloneUtils.test.ts index 8cf3dd9d55..841399d21c 100644 --- a/tests/src/core/CloneUtils.test.ts +++ b/tests/src/core/CloneUtils.test.ts @@ -79,6 +79,26 @@ class NestedObjectScript extends Script { config: { target: Entity; label: string } = { target: null, label: "" }; } +/** Script with UNDECORATED array of entities (no @deepClone) */ +class UndecoratedArrayScript extends Script { + entities: Entity[] = []; +} + +/** Script with UNDECORATED nested object containing entity refs */ +class UndecoratedObjectScript extends Script { + config: { target: Entity; label: string } = { target: null, label: "" }; +} + +/** Script with UNDECORATED nested array of arrays containing entities */ +class NestedArrayScript extends Script { + groups: Entity[][] = []; +} + +/** Script with UNDECORATED Map containing entity values */ +class MapRefScript extends Script { + entityMap: Map = new Map(); +} + /** Script for testing multiple same-type components on one entity */ class CounterScript extends Script { value: number = 0; @@ -236,7 +256,9 @@ describe("Clone remap", async () => { expect(clonedScript.speed).eq(42); expect(clonedScript.name2).eq("test"); expect(clonedScript.flag).eq(true); - expect(clonedScript.data).eq(obj); + // Plain objects are now deep cloned (independent copy) for undecorated properties + expect(clonedScript.data).not.eq(obj); + expect((clonedScript.data).x).eq(1); rootEntity.destroy(); }); @@ -867,4 +889,142 @@ describe("Clone remap", async () => { rootEntity.destroy(); }); }); + + describe("Undecorated array auto clone + remap (type inference)", () => { + it("undecorated entity array should create new array and remap elements", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const script = parent.addComponent(UndecoratedArrayScript); + script.entities = [childA, childB]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedArrayScript); + + expect(cs.entities).not.eq(script.entities); + expect(cs.entities.length).eq(2); + expect(cs.entities[0]).not.eq(childA); + expect(cs.entities[1]).not.eq(childB); + expect(cs.entities[0]).eq(cloned.children[0]); + expect(cs.entities[1]).eq(cloned.children[1]); + + rootEntity.destroy(); + }); + + it("undecorated entity array with external ref keeps original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(UndecoratedArrayScript); + script.entities = [child, external]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedArrayScript); + + expect(cs.entities[0]).eq(cloned.children[0]); + expect(cs.entities[1]).eq(external); + + rootEntity.destroy(); + }); + + it("undecorated empty array stays empty with independent reference", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(UndecoratedArrayScript); + script.entities = []; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedArrayScript); + + expect(cs.entities).not.eq(script.entities); + expect(cs.entities.length).eq(0); + + rootEntity.destroy(); + }); + }); + + describe("Undecorated nested object auto clone + remap (type inference)", () => { + it("undecorated object with entity ref should deep clone and remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(UndecoratedObjectScript); + script.config = { target: child, label: "hello" }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedObjectScript); + + expect(cs.config).not.eq(script.config); + expect(cs.config.label).eq("hello"); + expect(cs.config.target).not.eq(child); + expect(cs.config.target).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + + it("undecorated object with external entity ref keeps original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(UndecoratedObjectScript); + script.config = { target: external, label: "ext" }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(UndecoratedObjectScript); + + expect(cs.config.target).eq(external); + expect(cs.config.label).eq("ext"); + + rootEntity.destroy(); + }); + }); + + describe("Nested array of arrays with entity refs (type inference)", () => { + it("undecorated nested entity arrays should recursively clone and remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const childC = parent.createChild("childC"); + const script = parent.addComponent(NestedArrayScript); + script.groups = [[childA, childB], [childC]]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(NestedArrayScript); + + expect(cs.groups).not.eq(script.groups); + expect(cs.groups.length).eq(2); + expect(cs.groups[0]).not.eq(script.groups[0]); + expect(cs.groups[1]).not.eq(script.groups[1]); + expect(cs.groups[0][0]).eq(cloned.children[0]); + expect(cs.groups[0][1]).eq(cloned.children[1]); + expect(cs.groups[1][0]).eq(cloned.children[2]); + + rootEntity.destroy(); + }); + }); + + describe("Map with entity values (type inference)", () => { + it("undecorated Map should create new Map and remap entity values", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(MapRefScript); + script.entityMap.set("internal", child); + script.entityMap.set("external", external); + + const cloned = parent.clone(); + const cs = cloned.getComponent(MapRefScript); + + expect(cs.entityMap).not.eq(script.entityMap); + expect(cs.entityMap.size).eq(2); + expect(cs.entityMap.get("internal")).eq(cloned.children[0]); + expect(cs.entityMap.get("external")).eq(external); + + rootEntity.destroy(); + }); + }); }); diff --git a/tests/src/core/Entity.test.ts b/tests/src/core/Entity.test.ts index b34a94cb0a..f1e1dad3f1 100644 --- a/tests/src/core/Entity.test.ts +++ b/tests/src/core/Entity.test.ts @@ -328,8 +328,29 @@ describe("Entity", async () => { child.parent = parent; const child2 = new Entity(engine, "child2"); child2.parent = parent; + + const parentModifyCount = [0, 0, 0]; + const childModifyCount = [0, 0, 0]; + const child2ModifyCount = [0, 0, 0]; + // @ts-ignore + parent._registerModifyListener((flag: EntityModifyFlags) => ++parentModifyCount[flag]); + // @ts-ignore + child._registerModifyListener((flag: EntityModifyFlags) => ++childModifyCount[flag]); + // @ts-ignore + child2._registerModifyListener((flag: EntityModifyFlags) => ++child2ModifyCount[flag]); + parent.clearChildren(); expect(parent.children.length).eq(0); + + // Parent should receive a single `Child` modify event for the whole clear so + // listeners (e.g. UICanvas) can invalidate their cached state. + expect(parentModifyCount[EntityModifyFlags.Child]).eq(1); + // Each detached child should receive a `Parent` modify event. + expect(childModifyCount[EntityModifyFlags.Parent]).eq(1); + expect(child2ModifyCount[EntityModifyFlags.Parent]).eq(1); + // Sibling index must be reset so the entity is treated as lonely afterwards. + expect(child.siblingIndex).eq(-1); + expect(child2.siblingIndex).eq(-1); }); it("sibling index", () => { const root = scene.createRootEntity(); @@ -395,12 +416,13 @@ describe("Entity", async () => { }; expect(siblingIndexBadFn).to.throw(); - // thorw error when set lonely entity + // setting sibling index on a lonely entity (no parent, not in scene root) warns instead of throwing const entityX = new Entity(engine, "entityX"); - var lonelyBadFn = function () { + var lonelyFn = function () { entityX.siblingIndex = 1; }; - expect(lonelyBadFn).to.throw(); + expect(lonelyFn).not.to.throw(); + expect(entityX.siblingIndex).eq(-1); }); it("isRoot", () => { diff --git a/tests/src/core/Scene.test.ts b/tests/src/core/Scene.test.ts index c932fbd9db..22c3dbbf55 100644 --- a/tests/src/core/Scene.test.ts +++ b/tests/src/core/Scene.test.ts @@ -1,6 +1,6 @@ import { BackgroundMode, Engine, Entity, Scene, TextureFormat, Texture2D } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine"; -import { describe, beforeAll, beforeEach, expect, it } from "vitest"; +import { describe, beforeAll, beforeEach, expect, it, vi } from "vitest"; describe("Scene", () => { let engine: Engine; @@ -202,4 +202,37 @@ describe("Scene", () => { expect(scene.rootEntitiesCount).eq(0); }); }); + + describe("loadScene", () => { + it("destroyOldScene destroys every previous scene during safe iteration", async () => { + // Dedicated engine so destroying old scenes does not disturb the shared one. + const localEngine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + const sceneManager = localEngine.sceneManager; + + // Engine starts with one default scene; add two more so multiple scenes exist. + const old0 = sceneManager.scenes[0]; + const old1 = new Scene(localEngine, "old1"); + const old2 = new Scene(localEngine, "old2"); + sceneManager.addScene(old1); + sceneManager.addScene(old2); + expect(sceneManager.scenes.length).eq(3); + + // Resolve the load with a fresh scene without hitting real resources. + const newScene = new Scene(localEngine, "newScene"); + vi.spyOn(localEngine.resourceManager, "load").mockReturnValue(Promise.resolve(newScene) as any); + + await sceneManager.loadScene("mock://scene", true); + + // Destroying a scene removes it from `_scenes` mid-loop; iterating the live array + // (`getArray`) would skip scenes, so every previous scene must still be destroyed. + expect(old0.destroyed).eq(true); + expect(old1.destroyed).eq(true); + expect(old2.destroyed).eq(true); + // Only the newly loaded scene remains. + expect(sceneManager.scenes.length).eq(1); + expect(sceneManager.scenes[0]).eq(newScene); + + localEngine.destroy(); + }); + }); }); diff --git a/tests/src/core/SpriteRenderer.test.ts b/tests/src/core/SpriteRenderer.test.ts index db08e68b08..370582ba77 100644 --- a/tests/src/core/SpriteRenderer.test.ts +++ b/tests/src/core/SpriteRenderer.test.ts @@ -2,6 +2,8 @@ import { RendererUpdateFlags, Sprite, SpriteDrawMode, + SpriteFilledMode, + SpriteFilledOrigin, SpriteMaskInteraction, SpriteMaskLayer, SpriteRenderer, @@ -1578,6 +1580,436 @@ describe("SpriteRenderer", async () => { expect(spriteRenderer.bounds.min).to.deep.eq(new Vector3(-0.5, -1, 0)); expect(spriteRenderer.bounds.max).to.deep.eq(new Vector3(0.5, 1, 0)); }); + + it("get set filled properties", () => { + const rootEntity = scene.getRootEntity(); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.drawMode = SpriteDrawMode.Filled; + + // filledAmount is clamped to [0, 1] + spriteRenderer.filledAmount = 2; + expect(spriteRenderer.filledAmount).to.eq(1); + spriteRenderer.filledAmount = -1; + expect(spriteRenderer.filledAmount).to.eq(0); + spriteRenderer.filledAmount = 0.5; + expect(spriteRenderer.filledAmount).to.eq(0.5); + + spriteRenderer.filledClockWise = false; + expect(spriteRenderer.filledClockWise).to.eq(false); + + // Changing filledMode resets filledOrigin to a valid default for the new mode + spriteRenderer.filledMode = SpriteFilledMode.Horizontal; + expect(spriteRenderer.filledMode).to.eq(SpriteFilledMode.Horizontal); + expect(spriteRenderer.filledOrigin).to.eq(SpriteFilledOrigin.Left); + spriteRenderer.filledMode = SpriteFilledMode.Vertical; + expect(spriteRenderer.filledOrigin).to.eq(SpriteFilledOrigin.Bottom); + spriteRenderer.filledMode = SpriteFilledMode.Radial90; + expect(spriteRenderer.filledOrigin).to.eq(SpriteFilledOrigin.BottomLeft); + spriteRenderer.filledMode = SpriteFilledMode.Radial180; + expect(spriteRenderer.filledOrigin).to.eq(SpriteFilledOrigin.Bottom); + spriteRenderer.filledMode = SpriteFilledMode.Radial360; + expect(spriteRenderer.filledOrigin).to.eq(SpriteFilledOrigin.Bottom); + + // A valid origin for the current mode (Radial360 accepts edges) is kept + spriteRenderer.filledOrigin = SpriteFilledOrigin.Top; + expect(spriteRenderer.filledOrigin).to.eq(SpriteFilledOrigin.Top); + // An origin invalid for the current mode snaps to the mode's default + spriteRenderer.filledOrigin = SpriteFilledOrigin.TopRight; + expect(spriteRenderer.filledOrigin).to.eq(SpriteFilledOrigin.Bottom); + + // Corner-based modes validate origin the same way + spriteRenderer.filledMode = SpriteFilledMode.Radial90; + spriteRenderer.filledOrigin = SpriteFilledOrigin.Top; // edge origin invalid for Radial90 → snap default + expect(spriteRenderer.filledOrigin).to.eq(SpriteFilledOrigin.BottomLeft); + spriteRenderer.filledOrigin = SpriteFilledOrigin.TopRight; // valid corner → kept + expect(spriteRenderer.filledOrigin).to.eq(SpriteFilledOrigin.TopRight); + }); + + it("draw Filled Sprite (linear)", () => { + const rootEntity = scene.getRootEntity(); + const texture2D = new Texture2D(engine, 200, 300, TextureFormat.R8G8B8A8, false); + const sprite = new Sprite(engine, texture2D); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = sprite; + spriteRenderer.drawMode = SpriteDrawMode.Filled; + spriteRenderer.width = 4; + spriteRenderer.height = 5; + sprite.pivot = new Vector2(0, 0); + // @ts-ignore + const subChunk = spriteRenderer._subChunk; + const vertices = subChunk.chunk.vertices; + const context = { camera: { engine: engine, _renderPipeline: { pushRenderElement: () => {} } } }; + const readVertex = (i: number) => { + const o = subChunk.vertexArea.start + i * 9; + return { + pos: new Vector3(vertices[o], vertices[o + 1], vertices[o + 2]), + uv: new Vector2(vertices[o + 3], vertices[o + 4]) + }; + }; + + // Horizontal, default origin Left → fill the left half + spriteRenderer.filledMode = SpriteFilledMode.Horizontal; + spriteRenderer.filledAmount = 0.5; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length).to.eq(6); + expect(Vector3.equals(readVertex(0).pos, new Vector3(0, 0, 0))).to.eq(true); + expect(Vector3.equals(readVertex(1).pos, new Vector3(2, 0, 0))).to.eq(true); + expect(Vector3.equals(readVertex(2).pos, new Vector3(0, 5, 0))).to.eq(true); + expect(Vector3.equals(readVertex(3).pos, new Vector3(2, 5, 0))).to.eq(true); + expect(Vector2.equals(readVertex(0).uv, new Vector2(0, 1))).to.eq(true); + expect(Vector2.equals(readVertex(1).uv, new Vector2(0.5, 1))).to.eq(true); + expect(Vector2.equals(readVertex(2).uv, new Vector2(0, 0))).to.eq(true); + expect(Vector2.equals(readVertex(3).uv, new Vector2(0.5, 0))).to.eq(true); + expect([...subChunk.indices]).to.eql([0, 1, 2, 2, 1, 3]); + + // Vertical, default origin Bottom → fill the bottom half + spriteRenderer.filledMode = SpriteFilledMode.Vertical; + spriteRenderer.filledAmount = 0.5; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length).to.eq(6); + expect(Vector3.equals(readVertex(0).pos, new Vector3(0, 0, 0))).to.eq(true); + expect(Vector3.equals(readVertex(1).pos, new Vector3(4, 0, 0))).to.eq(true); + expect(Vector3.equals(readVertex(2).pos, new Vector3(0, 2.5, 0))).to.eq(true); + expect(Vector3.equals(readVertex(3).pos, new Vector3(4, 2.5, 0))).to.eq(true); + expect(Vector2.equals(readVertex(0).uv, new Vector2(0, 1))).to.eq(true); + expect(Vector2.equals(readVertex(1).uv, new Vector2(1, 1))).to.eq(true); + expect(Vector2.equals(readVertex(2).uv, new Vector2(0, 0.5))).to.eq(true); + expect(Vector2.equals(readVertex(3).uv, new Vector2(1, 0.5))).to.eq(true); + }); + + it("draw Filled Sprite (radial amount bounds)", () => { + const rootEntity = scene.getRootEntity(); + const texture2D = new Texture2D(engine, 200, 300, TextureFormat.R8G8B8A8, false); + const sprite = new Sprite(engine, texture2D); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = sprite; + spriteRenderer.drawMode = SpriteDrawMode.Filled; + spriteRenderer.width = 4; + spriteRenderer.height = 5; + sprite.pivot = new Vector2(0, 0); + // @ts-ignore + const subChunk = spriteRenderer._subChunk; + const context = { camera: { engine: engine, _renderPipeline: { pushRenderElement: () => {} } } }; + + // Radial360 full fill → 4 quadrants (4 quads × 6 indices) + spriteRenderer.filledMode = SpriteFilledMode.Radial360; + spriteRenderer.filledAmount = 1; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length).to.eq(24); + + // amount 0 → nothing is drawn + spriteRenderer.filledAmount = 0; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length).to.eq(0); + + // Radial90 full fill → single quad + spriteRenderer.filledMode = SpriteFilledMode.Radial90; + spriteRenderer.filledAmount = 1; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length).to.eq(6); + }); + + it("draw Filled Sprite falls back to the default origin for an unsupported origin", () => { + const rootEntity = scene.getRootEntity(); + const texture2D = new Texture2D(engine, 200, 300, TextureFormat.R8G8B8A8, false); + const sprite = new Sprite(engine, texture2D); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = sprite; + spriteRenderer.drawMode = SpriteDrawMode.Filled; + spriteRenderer.width = 4; + spriteRenderer.height = 5; + sprite.pivot = new Vector2(0, 0); + // @ts-ignore + const subChunk = spriteRenderer._subChunk; + const vertices = subChunk.chunk.vertices; + const context = { camera: { engine: engine, _renderPipeline: { pushRenderElement: () => {} } } }; + // The radial cut depends only on amount/clockwise, so all origins emit one triangle here. + const snapshotGeom = () => { + const start = subChunk.vertexArea.start; + const out: number[] = []; + for (let i = 0; i < 3; ++i) { + const o = start + i * 9; + out.push(vertices[o], vertices[o + 1], vertices[o + 2], vertices[o + 3], vertices[o + 4]); + } + return out; + }; + + // Radial90 with its default corner origin (BottomLeft) + spriteRenderer.filledMode = SpriteFilledMode.Radial90; + spriteRenderer.filledAmount = 0.5; + // @ts-ignore + spriteRenderer._render(context); + const bottomLeftGeom = snapshotGeom(); + + // A different valid corner origin produces different geometry + spriteRenderer.filledOrigin = SpriteFilledOrigin.TopRight; + // @ts-ignore + spriteRenderer._render(context); + expect(snapshotGeom()).to.not.eql(bottomLeftGeom); + + // An unsupported edge origin must fall back to the default (BottomLeft), + // not silently reuse the previous (TopRight) geometry. + spriteRenderer.filledOrigin = SpriteFilledOrigin.Top; + // @ts-ignore + spriteRenderer._render(context); + expect(snapshotGeom()).to.eql(bottomLeftGeom); + }); + + it("filled mode refreshes UV when the atlas region changes", () => { + const rootEntity = scene.getRootEntity(); + const texture2D = new Texture2D(engine, 200, 300, TextureFormat.R8G8B8A8, false); + const sprite = new Sprite(engine, texture2D); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = sprite; + spriteRenderer.drawMode = SpriteDrawMode.Filled; + spriteRenderer.filledMode = SpriteFilledMode.Horizontal; + spriteRenderer.filledAmount = 1; + spriteRenderer.width = 4; + spriteRenderer.height = 5; + sprite.pivot = new Vector2(0, 0); + // @ts-ignore + const subChunk = spriteRenderer._subChunk; + const vertices = subChunk.chunk.vertices; + const context = { camera: { engine: engine, _renderPipeline: { pushRenderElement: () => {} } } }; + // U of the right (RB) vertex, which equals the sprite's right UV for a full horizontal fill. + const rightU = () => vertices[subChunk.vertexArea.start + 9 + 3]; + + // @ts-ignore + spriteRenderer._render(context); + const uvBefore = rightU(); + + // Changing the atlas region dispatches SpriteModifyFlags.atlasRegion. Filled mode bakes + // UVs inside updatePositions, so the renderer must re-run it instead of a no-op updateUVs. + sprite.atlasRegion = new Rect(0, 0, 0.5, 0.5); + // @ts-ignore + spriteRenderer._render(context); + expect(rightU()).to.not.eq(uvBefore); + }); + + it("draw Filled Sprite (linear, non-default origin)", () => { + const rootEntity = scene.getRootEntity(); + const texture2D = new Texture2D(engine, 200, 300, TextureFormat.R8G8B8A8, false); + const sprite = new Sprite(engine, texture2D); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = sprite; + spriteRenderer.drawMode = SpriteDrawMode.Filled; + spriteRenderer.width = 4; + spriteRenderer.height = 5; + sprite.pivot = new Vector2(0, 0); + // @ts-ignore + const subChunk = spriteRenderer._subChunk; + const vertices = subChunk.chunk.vertices; + const context = { camera: { engine: engine, _renderPipeline: { pushRenderElement: () => {} } } }; + const readVertex = (i: number) => { + const o = subChunk.vertexArea.start + i * 9; + return { + pos: new Vector3(vertices[o], vertices[o + 1], vertices[o + 2]), + uv: new Vector2(vertices[o + 3], vertices[o + 4]) + }; + }; + + // Horizontal with origin Right → fill the right half (originIsStart === false branch) + spriteRenderer.filledMode = SpriteFilledMode.Horizontal; + spriteRenderer.filledOrigin = SpriteFilledOrigin.Right; + spriteRenderer.filledAmount = 0.5; + // @ts-ignore + spriteRenderer._render(context); + expect(Vector3.equals(readVertex(0).pos, new Vector3(2, 0, 0))).to.eq(true); + expect(Vector3.equals(readVertex(1).pos, new Vector3(4, 0, 0))).to.eq(true); + expect(Vector3.equals(readVertex(2).pos, new Vector3(2, 5, 0))).to.eq(true); + expect(Vector3.equals(readVertex(3).pos, new Vector3(4, 5, 0))).to.eq(true); + expect(Vector2.equals(readVertex(0).uv, new Vector2(0.5, 1))).to.eq(true); + expect(Vector2.equals(readVertex(1).uv, new Vector2(1, 1))).to.eq(true); + expect(Vector2.equals(readVertex(2).uv, new Vector2(0.5, 0))).to.eq(true); + expect(Vector2.equals(readVertex(3).uv, new Vector2(1, 0))).to.eq(true); + + // Vertical with origin Top → fill the top half + spriteRenderer.filledMode = SpriteFilledMode.Vertical; + spriteRenderer.filledOrigin = SpriteFilledOrigin.Top; + spriteRenderer.filledAmount = 0.5; + // @ts-ignore + spriteRenderer._render(context); + expect(Vector3.equals(readVertex(0).pos, new Vector3(0, 2.5, 0))).to.eq(true); + expect(Vector3.equals(readVertex(1).pos, new Vector3(4, 2.5, 0))).to.eq(true); + expect(Vector3.equals(readVertex(2).pos, new Vector3(0, 5, 0))).to.eq(true); + expect(Vector3.equals(readVertex(3).pos, new Vector3(4, 5, 0))).to.eq(true); + expect(Vector2.equals(readVertex(0).uv, new Vector2(0, 0.5))).to.eq(true); + expect(Vector2.equals(readVertex(1).uv, new Vector2(1, 0.5))).to.eq(true); + expect(Vector2.equals(readVertex(2).uv, new Vector2(0, 0))).to.eq(true); + expect(Vector2.equals(readVertex(3).uv, new Vector2(1, 0))).to.eq(true); + }); + + it("draw Filled Sprite (Radial180 + unsupported origin fallback)", () => { + const rootEntity = scene.getRootEntity(); + const texture2D = new Texture2D(engine, 200, 300, TextureFormat.R8G8B8A8, false); + const sprite = new Sprite(engine, texture2D); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = sprite; + spriteRenderer.drawMode = SpriteDrawMode.Filled; + spriteRenderer.width = 4; + spriteRenderer.height = 5; + sprite.pivot = new Vector2(0, 0); + // @ts-ignore + const subChunk = spriteRenderer._subChunk; + const vertices = subChunk.chunk.vertices; + const context = { camera: { engine: engine, _renderPipeline: { pushRenderElement: () => {} } } }; + const readPos = (i: number) => { + const o = subChunk.vertexArea.start + i * 9; + return new Vector3(vertices[o], vertices[o + 1], vertices[o + 2]); + }; + const snapshot = (n: number) => { + const start = subChunk.vertexArea.start; + const out: number[] = []; + for (let i = 0; i < n; ++i) { + const o = start + i * 9; + out.push(vertices[o], vertices[o + 1], vertices[o + 2], vertices[o + 3], vertices[o + 4]); + } + return out; + }; + + // Radial180, default origin Bottom, full fill → two quads (12 indices) + spriteRenderer.filledMode = SpriteFilledMode.Radial180; + spriteRenderer.filledAmount = 1; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length).to.eq(12); + // The first emitted vertex is the fill center = bottom-edge midpoint. + expect(Vector3.equals(readPos(0), new Vector3(2, 0, 0))).to.eq(true); + const bottomGeom = snapshot(8); + + // A valid non-default origin (Top, center = top-edge midpoint) produces different geometry. + spriteRenderer.filledOrigin = SpriteFilledOrigin.Top; + // @ts-ignore + spriteRenderer._render(context); + expect(Vector3.equals(readPos(0), new Vector3(2, 5, 0))).to.eq(true); + expect(snapshot(8)).to.not.eql(bottomGeom); + + // An unsupported corner origin (BottomLeft) must fall back to the default Bottom, + // not silently reuse the previous Top geometry. + spriteRenderer.filledOrigin = SpriteFilledOrigin.BottomLeft; + // @ts-ignore + spriteRenderer._render(context); + expect(snapshot(8)).to.eql(bottomGeom); + }); + + it("draw Filled Sprite (counter-clockwise radial differs from clockwise)", () => { + const rootEntity = scene.getRootEntity(); + const texture2D = new Texture2D(engine, 200, 300, TextureFormat.R8G8B8A8, false); + const sprite = new Sprite(engine, texture2D); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = sprite; + spriteRenderer.drawMode = SpriteDrawMode.Filled; + spriteRenderer.width = 4; + spriteRenderer.height = 5; + sprite.pivot = new Vector2(0, 0); + // @ts-ignore + const subChunk = spriteRenderer._subChunk; + const vertices = subChunk.chunk.vertices; + const context = { camera: { engine: engine, _renderPipeline: { pushRenderElement: () => {} } } }; + const snapshot3 = () => { + const start = subChunk.vertexArea.start; + const out: number[] = []; + for (let i = 0; i < 3; ++i) { + const o = start + i * 9; + out.push(vertices[o], vertices[o + 1], vertices[o + 2], vertices[o + 3], vertices[o + 4]); + } + return out; + }; + + spriteRenderer.filledMode = SpriteFilledMode.Radial90; + spriteRenderer.filledAmount = 0.5; + + // Clockwise (default) cuts the [45, 90] sector → one triangle. + spriteRenderer.filledClockWise = true; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length).to.eq(3); + const cwGeom = snapshot3(); + + // Counter-clockwise cuts the [0, 45] sector → a different triangle. + spriteRenderer.filledClockWise = false; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length).to.eq(3); + expect(snapshot3()).to.not.eql(cwGeom); + }); + + it("draw Filled Sprite (flipX mirrors positions, keeps UVs)", () => { + const rootEntity = scene.getRootEntity(); + const texture2D = new Texture2D(engine, 200, 300, TextureFormat.R8G8B8A8, false); + const sprite = new Sprite(engine, texture2D); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = sprite; + spriteRenderer.drawMode = SpriteDrawMode.Filled; + spriteRenderer.width = 4; + spriteRenderer.height = 5; + sprite.pivot = new Vector2(0, 0); + // @ts-ignore + const subChunk = spriteRenderer._subChunk; + const vertices = subChunk.chunk.vertices; + const context = { camera: { engine: engine, _renderPipeline: { pushRenderElement: () => {} } } }; + const readVertex = (i: number) => { + const o = subChunk.vertexArea.start + i * 9; + return { + pos: new Vector3(vertices[o], vertices[o + 1], vertices[o + 2]), + uv: new Vector2(vertices[o + 3], vertices[o + 4]) + }; + }; + + // Horizontal (origin Left) half fill with flipX → positions mirror to negative x, UVs unchanged. + spriteRenderer.filledMode = SpriteFilledMode.Horizontal; + spriteRenderer.filledAmount = 0.5; + spriteRenderer.flipX = true; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length).to.eq(6); + expect(Vector3.equals(readVertex(0).pos, new Vector3(0, 0, 0))).to.eq(true); + expect(Vector3.equals(readVertex(1).pos, new Vector3(-2, 0, 0))).to.eq(true); + expect(Vector3.equals(readVertex(2).pos, new Vector3(0, 5, 0))).to.eq(true); + expect(Vector3.equals(readVertex(3).pos, new Vector3(-2, 5, 0))).to.eq(true); + expect(Vector2.equals(readVertex(0).uv, new Vector2(0, 1))).to.eq(true); + expect(Vector2.equals(readVertex(1).uv, new Vector2(0.5, 1))).to.eq(true); + }); + + it("filled indices count across modes and amounts", () => { + const rootEntity = scene.getRootEntity(); + const texture2D = new Texture2D(engine, 200, 300, TextureFormat.R8G8B8A8, false); + const sprite = new Sprite(engine, texture2D); + const spriteRenderer = rootEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = sprite; + spriteRenderer.drawMode = SpriteDrawMode.Filled; + spriteRenderer.width = 4; + spriteRenderer.height = 5; + sprite.pivot = new Vector2(0, 0); + // @ts-ignore + const subChunk = spriteRenderer._subChunk; + const context = { camera: { engine: engine, _renderPipeline: { pushRenderElement: () => {} } } }; + + // Each mode runs with its default origin and clockwise=true; indices count locks the + // triangle/quad topology produced at amount 0 / 0.25 / 0.5 / 0.75 / 1. + const amounts = [0, 0.25, 0.5, 0.75, 1]; + const cases: Array<[SpriteFilledMode, number[]]> = [ + [SpriteFilledMode.Horizontal, [0, 6, 6, 6, 6]], + [SpriteFilledMode.Vertical, [0, 6, 6, 6, 6]], + [SpriteFilledMode.Radial90, [0, 3, 3, 6, 6]], + [SpriteFilledMode.Radial180, [0, 3, 6, 9, 12]], + [SpriteFilledMode.Radial360, [0, 6, 12, 18, 24]] + ]; + for (const [mode, expected] of cases) { + spriteRenderer.filledMode = mode; + for (let i = 0; i < amounts.length; ++i) { + spriteRenderer.filledAmount = amounts[i]; + // @ts-ignore + spriteRenderer._render(context); + expect(subChunk.indices.length, `mode=${mode} amount=${amounts[i]}`).to.eq(expected[i]); + } + } + }); }); /** diff --git a/tests/src/core/input/InputManager.test.ts b/tests/src/core/input/InputManager.test.ts index c552d39465..a7b21634f1 100644 --- a/tests/src/core/input/InputManager.test.ts +++ b/tests/src/core/input/InputManager.test.ts @@ -173,6 +173,41 @@ describe("InputManager", async () => { engine.update(); }); + it("pointer pressedPosition", () => { + // @ts-ignore + const { _pointerManager: pointerManager } = inputManager; + const { _target: target } = pointerManager; + const { left, top } = target.getBoundingClientRect(); + + // Frame 1: a pointerdown and a move to a different position are batched into one frame. + target.dispatchEvent(generatePointerEvent("pointerdown", 5, left + 1, top + 1)); + target.dispatchEvent(generatePointerEvent("pointermove", 5, left + 3, top + 3)); + engine.update(); + const pointer = inputManager.pointers[0]; + // @ts-ignore + const uniqueID = pointer._uniqueID; + const position1 = pointer.position.clone(); + const pressedPosition1 = pointer.pressedPosition.clone(); + + // Frame 2: keep dragging to yet another position. + target.dispatchEvent(generatePointerEvent("pointermove", 5, left + 6, top + 6)); + engine.update(); + const position2 = pointer.position.clone(); + const pressedPosition2 = pointer.pressedPosition.clone(); + + // Release the pointer BEFORE asserting, so a failing expectation cannot leak it into later tests. + target.dispatchEvent(generatePointerEvent("pointerleave", 5, left + 6, top + 6, -1, 0)); + engine.update(); + + expect(uniqueID).to.eq(5); + // `position` always tracks the frame's latest event... + expect(position1).to.deep.eq(new Vector2(3 * 2, 3 * 2)); + expect(position2).to.deep.eq(new Vector2(6 * 2, 6 * 2)); + // ...while `pressedPosition` stays at the pointerdown location regardless of later moves. + expect(pressedPosition1).to.deep.eq(new Vector2(1 * 2, 1 * 2)); + expect(pressedPosition2).to.deep.eq(new Vector2(1 * 2, 1 * 2)); + }); + it("keyboard", () => { // @ts-ignore const { _keyboardManager: keyboardManager } = inputManager; @@ -247,6 +282,31 @@ describe("InputManager", async () => { expect(inputManager.wheelDelta).to.deep.eq(new Vector3(1, 2, 3)); }); + it("gc reclaims the pointer event data pool", () => { + // @ts-ignore + const { _pointerManager: pointerManager } = inputManager; + const { _target: target } = pointerManager; + // @ts-ignore + const elements = pointerManager._eventPool._elements; + const { left, top } = target.getBoundingClientRect(); + + // Pressing on the box allocates event data into the pool. + target.dispatchEvent(generatePointerEvent("pointerdown", 6, left + 2, top + 2)); + engine.update(); + expect(elements.length).to.be.greaterThan(0); + + // Engine gc reclaims the pool, releasing the entity references it held. + // @ts-ignore + engine._pendingGC(); + expect(elements.length).to.eq(0); + + // The pool keeps working after gc. + target.dispatchEvent(generatePointerEvent("pointerup", 6, left + 2, top + 2, 0, 0)); + target.dispatchEvent(generatePointerEvent("pointerleave", 6, left + 2, top + 2, -1, 0)); + engine.update(); + expect(elements.length).to.be.greaterThan(0); + }); + it("destroy", () => { engine.destroy(); // @ts-ignore diff --git a/tests/src/core/particle/ParticleStopResume.test.ts b/tests/src/core/particle/ParticleStopResume.test.ts new file mode 100644 index 0000000000..0e0db6ac05 --- /dev/null +++ b/tests/src/core/particle/ParticleStopResume.test.ts @@ -0,0 +1,142 @@ +import { + Burst, + Camera, + ParticleCompositeCurve, + ParticleRenderer, + ParticleStopMode, + Scene +} from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { beforeAll, describe, expect, it } from "vitest"; + +describe("ParticleGenerator stop/resume timeline", () => { + let engine: WebGLEngine; + let scene: Scene; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity("root"); + const camera = root.createChild("Camera"); + camera.addComponent(Camera); + camera.transform.setPosition(0, 0, 10); + }); + + /** + * Drive `generator._update(dt)` directly so we control the timeline. + * `ParticleRenderer._update` would call this with `engine.time.deltaTime`, + * which is wall-clock and not reproducible. + */ + function tick(generator: any, frames: number, dt: number): void { + for (let i = 0; i < frames; i++) { + generator._update(dt); + } + } + + it("rate-over-time: stop -> idle -> play emits a catch-up batch in one frame", () => { + const entity = scene.createRootEntity("rate"); + const renderer = entity.addComponent(ParticleRenderer); + const generator = renderer.generator; + generator.useAutoRandomSeed = false; + generator.main.duration = 1; + generator.main.startLifetime.constant = 1; + generator.main.maxParticles = 10000; + generator.emission.rateOverTime.constant = 100; + + let totalEmitted = 0; + const origEmit = (generator as any)._emit.bind(generator); + (generator as any)._emit = (playTime: number, count: number) => { + totalEmitted += count; + origEmit(playTime, count); + }; + + generator.play(); + // Run 0.5s at 60fps -> ~50 particles + tick(generator, 30, 1 / 60); + const emittedDuringPlay = totalEmitted; + const playTimeAfterPlay = generator._playTime; + + generator.stop(true, ParticleStopMode.StopEmitting); + const playTimeAtStop = generator._playTime; + const emittedAtStop = totalEmitted; + + // Idle 4.5s while stopped -> _emit must not run, but _playTime drifts + tick(generator, 270, 1 / 60); + const playTimeAfterIdle = generator._playTime; + const emittedDuringIdle = totalEmitted - emittedAtStop; + + generator.play(); + // Single frame after resume + tick(generator, 1, 1 / 60); + const emittedFirstFrameAfterResume = totalEmitted - emittedAtStop; + + entity.destroy(); + + // eslint-disable-next-line no-console + console.log("[bug-repro/rate]", { + emittedDuringPlay, + playTimeAfterPlay, + playTimeAtStop, + playTimeAfterIdle, + emittedDuringIdle, + emittedFirstFrameAfterResume + }); + + expect(emittedDuringIdle).toBe(0); + // Buggy behavior: emits a large catch-up batch (~ idleSeconds * rate). + expect(emittedFirstFrameAfterResume).toBeGreaterThan(100); + expect(playTimeAfterIdle - playTimeAtStop).toBeGreaterThan(4); + }); + + it("burst: stop -> idle -> play replays multiple cycles of bursts", () => { + const entity = scene.createRootEntity("burst"); + const renderer = entity.addComponent(ParticleRenderer); + const generator = renderer.generator; + generator.useAutoRandomSeed = false; + generator.main.duration = 1; + generator.main.isLoop = true; + generator.main.startLifetime.constant = 1; + generator.main.maxParticles = 10000; + generator.emission.rateOverTime.constant = 0; + generator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(10))); + + let totalEmitted = 0; + const origEmit = (generator as any)._emit.bind(generator); + (generator as any)._emit = (playTime: number, count: number) => { + totalEmitted += count; + origEmit(playTime, count); + }; + + generator.play(); + // 1 full cycle -> burst at t=0 fires once (10 particles at frame 0) + tick(generator, 60, 1 / 60); + const emittedAfterOneSecond = totalEmitted; + + generator.stop(true, ParticleStopMode.StopEmitting); + const playTimeAtStop = generator._playTime; + const emittedAtStop = totalEmitted; + + // Idle 4 cycles + tick(generator, 240, 1 / 60); + const playTimeAfterIdle = generator._playTime; + + generator.play(); + tick(generator, 1, 1 / 60); // first frame after resume + const emittedFirstFrameAfterResume = totalEmitted - emittedAtStop; + + entity.destroy(); + + // eslint-disable-next-line no-console + console.log("[bug-repro/burst]", { + emittedAfterOneSecond, + playTimeAtStop, + playTimeAfterIdle, + emittedFirstFrameAfterResume + }); + + // After fix this should be 0 (next burst at t=0 of the new cycle hasn't reached yet). + // Buggy behavior: replays the burst once because `_currentBurstIndex` is 0 and + // `_emitBySubBurst(lastPlayTime, playTime, ...)` sees burst.time === startTime. + expect(emittedFirstFrameAfterResume).toBe(10); + }); +}); diff --git a/tests/src/core/resource/SceneLoaderCache.test.ts b/tests/src/core/resource/SceneLoaderCache.test.ts new file mode 100644 index 0000000000..2a9ee568f1 --- /dev/null +++ b/tests/src/core/resource/SceneLoaderCache.test.ts @@ -0,0 +1,98 @@ +import { AssetPromise, AssetType, ResourceManager, Scene } from "@galacean/engine-core"; +import "@galacean/engine-loader"; +import { WebGLEngine } from "@galacean/engine"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; + +describe("SceneLoader cache policy", () => { + let engine: WebGLEngine; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + afterAll(() => { + engine.destroy(); + }); + + it("SceneLoader should have useCache disabled", () => { + const sceneLoader = ResourceManager._loaders[AssetType.Scene]; + expect(sceneLoader).to.not.be.undefined; + expect(sceneLoader.useCache).to.eq(false); + }); + + it("PrimitiveMeshLoader should also have useCache disabled (existing convention)", () => { + const loader = ResourceManager._loaders[AssetType.PrimitiveMesh]; + expect(loader).to.not.be.undefined; + expect(loader.useCache).to.eq(false); + }); + + it("Texture loader should still have useCache enabled (immutable asset)", () => { + const loader = ResourceManager._loaders[AssetType.Texture]; + expect(loader).to.not.be.undefined; + expect(loader.useCache).to.eq(true); + }); + + describe("loadScene behavior", () => { + function mockSceneLoad() { + const loader = ResourceManager._loaders[AssetType.Scene]; + return vi.spyOn(loader, "load").mockImplementation( + () => + new AssetPromise((resolve) => { + resolve(new Scene(engine, "mock")); + }) + ); + } + + it("should activate a fresh Scene instance and destroy the old one on sequential loads of the same url", async () => { + mockSceneLoad(); + const sceneManager = engine.sceneManager; + + const first = await sceneManager.loadScene("/mock-sequential.scene"); + const second = await sceneManager.loadScene("/mock-sequential.scene"); + + expect(second).not.toBe(first); + expect(first.destroyed).toBe(true); + expect(second.destroyed).toBe(false); + expect(sceneManager.scenes[0]).toBe(second); + }); + + it("should destroy every old scene when multiple scenes are active", async () => { + mockSceneLoad(); + const sceneManager = engine.sceneManager; + sceneManager.addScene(new Scene(engine, "extra1")); + sceneManager.addScene(new Scene(engine, "extra2")); + const oldScenes = [...sceneManager.scenes]; + expect(oldScenes.length).toBeGreaterThanOrEqual(2); + + const scene = await sceneManager.loadScene("/mock-multi.scene"); + + for (const oldScene of oldScenes) { + expect(oldScene.destroyed).toBe(true); + } + expect(scene.destroyed).toBe(false); + expect(sceneManager.scenes.length).toBe(1); + expect(sceneManager.scenes[0]).toBe(scene); + }); + + it("should not destroy the activated scene when concurrent loads of the same url share one loading promise", async () => { + const loadSpy = mockSceneLoad(); + const sceneManager = engine.sceneManager; + + const [first, second] = await Promise.all([ + sceneManager.loadScene("/mock-concurrent.scene"), + sceneManager.loadScene("/mock-concurrent.scene") + ]); + + // The in-flight loading promise is shared regardless of useCache + expect(loadSpy).toHaveBeenCalledTimes(1); + expect(second).toBe(first); + expect(first.destroyed).toBe(false); + expect(sceneManager.scenes.length).toBe(1); + expect(sceneManager.scenes[0]).toBe(first); + }); + }); +}); diff --git a/tests/src/loader/ScenePhysics.test.ts b/tests/src/loader/ScenePhysics.test.ts new file mode 100644 index 0000000000..8b4e889df0 --- /dev/null +++ b/tests/src/loader/ScenePhysics.test.ts @@ -0,0 +1,53 @@ +import { AssetType, BackgroundMode, Scene } from "@galacean/engine"; +import "@galacean/engine-loader"; +import { PhysXPhysics } from "@galacean/engine-physics-physx"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +let engine: WebGLEngine; + +beforeAll(async () => { + engine = await WebGLEngine.create({ + canvas: document.createElement("canvas"), + physics: new PhysXPhysics() + }); +}); + +afterAll(() => { + engine?.destroy(); +}); + +describe("SceneLoader physics settings", () => { + it("applies serialized scene physics settings to PhysicsScene", async () => { + const sceneData = { + version: "2.0", + refs: [], + entities: [], + components: [], + scene: { + name: "physics-scene", + rootEntities: [], + background: { + mode: BackgroundMode.SolidColor, + color: [0, 0, 0, 1] + }, + physics: { + gravity: [0, -3200, 0], + fixedTimeStep: 1 / 120 + } + } + }; + const sceneUrl = + URL.createObjectURL(new Blob([JSON.stringify(sceneData)], { type: "application/json" })) + "#.scene"; + + const scene = await engine.resourceManager.load({ + url: sceneUrl, + type: AssetType.Scene + }); + + expect(scene.physics.gravity.x).toBe(0); + expect(scene.physics.gravity.y).toBe(-3200); + expect(scene.physics.gravity.z).toBe(0); + expect(scene.physics.fixedTimeStep).toBe(1 / 120); + }); +}); diff --git a/tests/src/spine/LoaderUtils.test.ts b/tests/src/spine/LoaderUtils.test.ts new file mode 100644 index 0000000000..fa97f9afcf --- /dev/null +++ b/tests/src/spine/LoaderUtils.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { LoaderUtils } from "../../../packages/spine/src/loader/LoaderUtils"; + +describe("LoaderUtils.getBaseUrl", () => { + it("strips the file name from a local path", () => { + expect(LoaderUtils.getBaseUrl("/assets/spine/data.json")).to.equal("/assets/spine/"); + }); + + it("returns empty string for a bare file name", () => { + expect(LoaderUtils.getBaseUrl("data.json")).to.equal(""); + }); + + it("strips the file name from an http url", () => { + expect(LoaderUtils.getBaseUrl("https://cdn.example.com/a/b/data.json")).to.equal("https://cdn.example.com/a/b/"); + }); +}); diff --git a/tests/src/spine/Pool.test.ts b/tests/src/spine/Pool.test.ts new file mode 100644 index 0000000000..0d835b408a --- /dev/null +++ b/tests/src/spine/Pool.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { ClearablePool } from "../../../packages/spine-core-4.2/src/util/ClearablePool"; +import { ReturnablePool } from "../../../packages/spine-core-4.2/src/util/ReturnablePool"; + +class Item { + value = 0; +} + +describe("ClearablePool", () => { + it("hands out distinct elements, then reuses them after clear", () => { + const pool = new ClearablePool(Item); + const a = pool.get(); + const b = pool.get(); + expect(a).to.not.equal(b); + + pool.clear(); + // clear() only resets the cursor; existing elements are reused in order. + expect(pool.get()).to.equal(a); + expect(pool.get()).to.equal(b); + }); +}); + +describe("ReturnablePool", () => { + it("pre-fills initializeCount and reuses returned elements", () => { + const pool = new ReturnablePool(Item, 2); + const a = pool.get(); + const b = pool.get(); + expect(a).to.not.equal(b); + + pool.return(a); + expect(pool.get()).to.equal(a); + }); + + it("allocates a fresh element when the pool is exhausted", () => { + const pool = new ReturnablePool(Item, 1); + const a = pool.get(); + const fresh = pool.get(); + expect(fresh).to.be.instanceOf(Item); + expect(fresh).to.not.equal(a); + }); +}); diff --git a/tests/src/spine/SpineAnimationRenderer.test.ts b/tests/src/spine/SpineAnimationRenderer.test.ts new file mode 100644 index 0000000000..48de10c2e3 --- /dev/null +++ b/tests/src/spine/SpineAnimationRenderer.test.ts @@ -0,0 +1,72 @@ +import { describe, beforeAll, expect, it } from "vitest"; +import { Entity, Material, Shader, Texture2D, WebGLEngine } from "@galacean/engine"; +import { SpineAnimationRenderer } from "../../../packages/spine/src/renderer/SpineAnimationRenderer"; +import { SpineMaterial } from "../../../packages/spine/src/renderer/SpineMaterial"; +import { SpineBlendMode } from "../../../packages/spine/src/enums/SpineBlendMode"; + +describe("SpineAnimationRenderer", () => { + let engine: WebGLEngine; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + }); + + it("initializes with default config", () => { + const renderer = new Entity(engine).addComponent(SpineAnimationRenderer); + expect(renderer.defaultConfig.animationName).to.be.null; + expect(renderer.defaultConfig.skinName).to.equal("default"); + expect(renderer.premultipliedAlpha).to.be.false; + expect(renderer.tintBlack).to.be.false; + }); + + it("tintBlack setter flags a buffer resize", () => { + const renderer = new Entity(engine).addComponent(SpineAnimationRenderer); + renderer.tintBlack = true; + expect(renderer.tintBlack).to.be.true; + expect((renderer as any)._needResizeBuffer).to.be.true; + }); + + it("_getMaterial caches by texture + blendMode", () => { + const renderer = new Entity(engine).addComponent(SpineAnimationRenderer); + const texture = new Texture2D(engine, 4, 4); + const normal = renderer._getMaterial(texture, SpineBlendMode.Normal); + // Same texture + blendMode must hit the cache (regression guard for the Map[key] bug). + expect(renderer._getMaterial(texture, SpineBlendMode.Normal)).to.equal(normal); + // Different blendMode must produce a different material. + expect(renderer._getMaterial(texture, SpineBlendMode.Additive)).to.not.equal(normal); + }); + + it("_getMaterial does not share a material across tintBlack variants", () => { + const renderer = new Entity(engine).addComponent(SpineAnimationRenderer); + const texture = new Texture2D(engine, 4, 4); + const plain = renderer._getMaterial(texture, SpineBlendMode.Normal); + renderer.tintBlack = true; + const tinted = renderer._getMaterial(texture, SpineBlendMode.Normal); + // Sharing one material would let renderers that differ only in tintBlack clobber + // each other's RENDERER_TINT_BLACK macro every frame. + expect(tinted).to.not.equal(plain); + expect(renderer._getMaterial(texture, SpineBlendMode.Normal)).to.equal(tinted); + renderer.tintBlack = false; + expect(renderer._getMaterial(texture, SpineBlendMode.Normal)).to.equal(plain); + }); + + it("destroy tolerates user-set materials and removes cached entries by key", () => { + const entity = new Entity(engine); + const renderer = entity.addComponent(SpineAnimationRenderer); + const texture = new Texture2D(engine, 4, 4); + const cached = renderer._getMaterial(texture, SpineBlendMode.Normal) as SpineMaterial; + renderer.setMaterial(0, cached); + // setMaterial is public API — destroy must not assume every entry is a SpineMaterial. + renderer.setMaterial(1, new Material(engine, Shader.find("2D/Spine"))); + const cacheKey = cached._cacheKey; + expect(SpineAnimationRenderer._materialCacheMap.has(cacheKey)).to.be.true; + expect(() => entity.destroy()).to.not.throw(); + expect(SpineAnimationRenderer._materialCacheMap.has(cacheKey)).to.be.false; + }); + + it("cloning an entity whose renderer has no skeleton does not throw", () => { + const entity = new Entity(engine); + entity.addComponent(SpineAnimationRenderer); + expect(() => entity.clone()).to.not.throw(); + }); +}); diff --git a/tests/src/spine/SpineConstant.test.ts b/tests/src/spine/SpineConstant.test.ts new file mode 100644 index 0000000000..38113402c6 --- /dev/null +++ b/tests/src/spine/SpineConstant.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { SpineVertexStride } from "../../../packages/spine/src/SpineConstant"; +import { SpineBlendMode } from "../../../packages/spine/src/enums/SpineBlendMode"; + +describe("SpineVertexStride", () => { + it("is 9 floats without tint and 12 with tint black", () => { + // [x,y,z,u,v,r,g,b,a] vs [...,dr,dg,db] — must match the renderer's vertex layout. + expect(SpineVertexStride.withoutTint).to.equal(9); + expect(SpineVertexStride.withTint).to.equal(12); + }); +}); + +describe("SpineBlendMode", () => { + it("ordinals match the spine-core BlendMode order", () => { + expect(SpineBlendMode.Normal).to.equal(0); + expect(SpineBlendMode.Additive).to.equal(1); + expect(SpineBlendMode.Multiply).to.equal(2); + expect(SpineBlendMode.Screen).to.equal(3); + }); +}); diff --git a/tests/src/spine/SpineLoader.test.ts b/tests/src/spine/SpineLoader.test.ts new file mode 100644 index 0000000000..3e2903f986 --- /dev/null +++ b/tests/src/spine/SpineLoader.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { SpineLoader } from "../../../packages/spine/src/loader/SpineLoader"; + +describe("SpineLoader._getUrlExtension", () => { + it("extracts the extension for spine asset types", () => { + expect(SpineLoader._getUrlExtension("path/to/spine.json")).to.equal("json"); + expect(SpineLoader._getUrlExtension("path/to/spine.skel")).to.equal("skel"); + expect(SpineLoader._getUrlExtension("path/to/spine.atlas")).to.equal("atlas"); + }); + + it("ignores query strings", () => { + expect(SpineLoader._getUrlExtension("spine.json?v=2")).to.equal("json"); + }); + + it("returns null when there is no extension", () => { + expect(SpineLoader._getUrlExtension("spine")).to.be.null; + }); +}); diff --git a/tests/src/spine/SpineMaterial.test.ts b/tests/src/spine/SpineMaterial.test.ts new file mode 100644 index 0000000000..2bb4e42bcd --- /dev/null +++ b/tests/src/spine/SpineMaterial.test.ts @@ -0,0 +1,36 @@ +import { describe, beforeAll, expect, it } from "vitest"; +import { BlendFactor, Shader, ShaderProperty, WebGLEngine } from "@galacean/engine"; +import { SpineMaterial } from "../../../packages/spine/src/renderer/SpineMaterial"; +import { SpineBlendMode } from "../../../packages/spine/src/enums/SpineBlendMode"; + +describe("SpineMaterial", () => { + let engine: WebGLEngine; + const srcColor = ShaderProperty.getByName("sourceColorBlendFactor"); + const dstColor = ShaderProperty.getByName("destinationColorBlendFactor"); + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + }); + + it("uses the built-in 2D/Spine shader and defaults to Normal blend", () => { + const material = new SpineMaterial(engine); + expect(material.shader).to.equal(Shader.find("2D/Spine")); + expect(material._getBlendMode()).to.equal(SpineBlendMode.Normal); + }); + + it("additive blend sets source=SourceAlpha, destination=One", () => { + const material = new SpineMaterial(engine); + material._setBlendMode(SpineBlendMode.Additive, false); + expect(material._getBlendMode()).to.equal(SpineBlendMode.Additive); + expect(material.shaderData.getInt(srcColor)).to.equal(BlendFactor.SourceAlpha); + expect(material.shaderData.getInt(dstColor)).to.equal(BlendFactor.One); + }); + + it("premultipliedAlpha switches the source color factor to One", () => { + const material = new SpineMaterial(engine); + material._setBlendMode(SpineBlendMode.Normal, true); + expect(material.shaderData.getInt(srcColor)).to.equal(BlendFactor.One); + material._setBlendMode(SpineBlendMode.Normal, false); + expect(material.shaderData.getInt(srcColor)).to.equal(BlendFactor.SourceAlpha); + }); +}); diff --git a/tests/src/spine/SpineResource.test.ts b/tests/src/spine/SpineResource.test.ts new file mode 100644 index 0000000000..6f281f81ba --- /dev/null +++ b/tests/src/spine/SpineResource.test.ts @@ -0,0 +1,33 @@ +import { Texture2D, WebGLEngine } from "@galacean/engine"; +import { beforeAll, describe, expect, it } from "vitest"; +import { SpineResource } from "../../../packages/spine/src/loader/SpineResource"; +import { SpineTexture } from "../../../packages/spine-core-4.2/src/SpineTexture"; + +describe("SpineResource._associationTextureInSkeletonData", () => { + let engine: WebGLEngine; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + }); + + it("associates the attachment's Texture2D and protects it from GC while the resource is alive", () => { + const texture2D = new Texture2D(engine, 4, 4); + const attachment = { region: { texture: new SpineTexture(texture2D) } }; + const skeletonData = { + skins: [{ getAttachment: () => attachment }], + slots: [{ index: 0, name: "slot0" }] + }; + // Stands in for the SpineResource instance: only the fields `_associationTextureInSkeletonData` + // reads/writes (`_texturesInSpineAtlas`) and the super-resource GC check reads (`refCount`). + const fakeResource = { _texturesInSpineAtlas: [] as Texture2D[], refCount: 1 }; + + // @ts-ignore + SpineResource.prototype._associationTextureInSkeletonData.call(fakeResource, skeletonData); + + expect(fakeResource._texturesInSpineAtlas).to.deep.equal([texture2D]); + // Regression guard: region.texture is already the SpineTexture, so reading `.texture` off it again + // (instead of `.getImage()`) resolved to `undefined` on the 4.2 backend, and the association above + // silently never happened - leaving the texture unprotected from a GC pass while still in use. + expect(texture2D.destroy(false, true)).to.equal(false); + }); +}); diff --git a/tests/src/spine/SpineRuntimeRegistry.test.ts b/tests/src/spine/SpineRuntimeRegistry.test.ts new file mode 100644 index 0000000000..d9234a9b8f --- /dev/null +++ b/tests/src/spine/SpineRuntimeRegistry.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { registerSpineRuntime, getSpineRuntime } from "../../../packages/spine/src/runtime/SpineRuntimeRegistry"; +import type { ISpineRuntime } from "../../../packages/spine/src/runtime/ISpineRuntime"; + +describe("SpineRuntimeRegistry", () => { + // Must run first: the module singleton starts null and there is no reset. + it("throws when no runtime is registered", () => { + expect(() => getSpineRuntime()).to.throw(/no spine runtime registered/); + }); + + it("returns the registered runtime", () => { + const runtime = {} as ISpineRuntime; + registerSpineRuntime(runtime); + expect(getSpineRuntime()).to.equal(runtime); + }); + + it("last registration wins", () => { + const a = {} as ISpineRuntime; + const b = {} as ISpineRuntime; + registerSpineRuntime(a); + registerSpineRuntime(b); + expect(getSpineRuntime()).to.equal(b); + }); +}); diff --git a/tests/src/spine/SpineTexture.test.ts b/tests/src/spine/SpineTexture.test.ts new file mode 100644 index 0000000000..ac6b76e5f6 --- /dev/null +++ b/tests/src/spine/SpineTexture.test.ts @@ -0,0 +1,44 @@ +import { describe, beforeAll, expect, it } from "vitest"; +import { Texture2D, TextureFilterMode, WebGLEngine } from "@galacean/engine"; +import { TextureFilter as TextureFilter42 } from "@esotericsoftware/spine-core"; +import { SpineTexture as SpineTexture42 } from "../../../packages/spine-core-4.2/src/SpineTexture"; +import { SpineTexture as SpineTexture38 } from "../../../packages/spine-core-3.8/src/SpineTexture"; +import { TextureFilter as TextureFilter38 } from "../../../packages/spine-core-3.8/src/spine-core/Texture"; + +describe("SpineTexture.setFilters", () => { + let engine: WebGLEngine; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + }); + + it("maps atlas min filters to engine filter modes (4.2)", () => { + const texture = new Texture2D(engine, 4, 4); + const spineTexture = new SpineTexture42(texture); + + spineTexture.setFilters(TextureFilter42.Nearest, TextureFilter42.Nearest); + expect(texture.filterMode).to.equal(TextureFilterMode.Point); + + spineTexture.setFilters(TextureFilter42.Linear, TextureFilter42.Linear); + expect(texture.filterMode).to.equal(TextureFilterMode.Bilinear); + + // Atlases pair a MipMap* min filter with a plain Linear mag filter — the min filter + // alone decides whether mipmap sampling (Trilinear) is requested. + spineTexture.setFilters(TextureFilter42.MipMapLinearLinear, TextureFilter42.Linear); + expect(texture.filterMode).to.equal(TextureFilterMode.Trilinear); + }); + + it("maps atlas min filters to engine filter modes (3.8)", () => { + const texture = new Texture2D(engine, 4, 4); + const spineTexture = new SpineTexture38(texture); + + spineTexture.setFilters(TextureFilter38.Nearest, TextureFilter38.Nearest); + expect(texture.filterMode).to.equal(TextureFilterMode.Point); + + spineTexture.setFilters(TextureFilter38.Linear, TextureFilter38.Linear); + expect(texture.filterMode).to.equal(TextureFilterMode.Bilinear); + + spineTexture.setFilters(TextureFilter38.MipMapLinearLinear, TextureFilter38.Linear); + expect(texture.filterMode).to.equal(TextureFilterMode.Trilinear); + }); +}); diff --git a/tests/src/ui/Mask.test.ts b/tests/src/ui/Mask.test.ts new file mode 100644 index 0000000000..747a9309d8 --- /dev/null +++ b/tests/src/ui/Mask.test.ts @@ -0,0 +1,75 @@ +import { Sprite, SpriteMaskInteraction, SpriteMaskLayer, Texture2D } from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { CanvasRenderMode, Image, Mask, UICanvas, UITransform } from "@galacean/engine-ui"; +import { describe, expect, it } from "vitest"; + +describe("Mask", async () => { + const canvas = document.createElement("canvas"); + const engine = await WebGLEngine.create({ canvas }); + const webCanvas = engine.canvas; + webCanvas.setResolution(300, 300); + const scene = engine.sceneManager.scenes[0]; + const root = scene.createRootEntity("root"); + + const canvasEntity = root.createChild("canvas"); + const rootCanvas = canvasEntity.addComponent(UICanvas); + rootCanvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay; + rootCanvas.referenceResolutionPerUnit = 50; + rootCanvas.referenceResolution.set(300, 300); + + const imageEntity = canvasEntity.createChild("image"); + const image = imageEntity.addComponent(Image); + (imageEntity.transform).size.set(300, 300); + + const maskEntity = canvasEntity.createChild("mask"); + const mask = maskEntity.addComponent(Mask); + (maskEntity.transform).size.set(100, 100); + mask.sprite = createSolidSprite(engine); + + it("Set and Get sprite", () => { + const texture = new Texture2D(engine, 1, 1); + const sprite = new Sprite(engine, texture); + mask.sprite = sprite; + expect(mask.sprite).to.eq(sprite); + + mask.sprite = null; + expect(mask.sprite).to.eq(null); + + mask.sprite = createSolidSprite(engine); + expect(mask.sprite).not.to.eq(null); + }); + + it("Set and Get alphaCutoff", () => { + expect(mask.alphaCutoff).to.eq(0.5); + mask.alphaCutoff = 0.2; + expect(mask.alphaCutoff).to.eq(0.2); + }); + + it("Set and Get influenceLayers", () => { + expect(mask.influenceLayers).to.eq(SpriteMaskLayer.Everything); + mask.influenceLayers = SpriteMaskLayer.Layer1; + expect(mask.influenceLayers).to.eq(SpriteMaskLayer.Layer1); + mask.influenceLayers = SpriteMaskLayer.Everything; + }); + + it("Set and Get flipX/flipY", () => { + mask.flipX = true; + expect(mask.flipX).to.eq(true); + mask.flipY = true; + expect(mask.flipY).to.eq(true); + mask.flipX = false; + mask.flipY = false; + }); + + it("UI image maskInteraction default is None", () => { + expect(image.maskInteraction).to.eq(SpriteMaskInteraction.None); + image.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + expect(image.maskInteraction).to.eq(SpriteMaskInteraction.VisibleInsideMask); + }); +}); + +function createSolidSprite(engine: WebGLEngine): Sprite { + const texture = new Texture2D(engine, 1, 1); + texture.setPixelBuffer(new Uint8Array([255, 255, 255, 255])); + return new Sprite(engine, texture); +} diff --git a/tests/src/ui/RectMask2D.test.ts b/tests/src/ui/RectMask2D.test.ts new file mode 100644 index 0000000000..872d4b56b3 --- /dev/null +++ b/tests/src/ui/RectMask2D.test.ts @@ -0,0 +1,78 @@ +import { Vector2, Vector3, Vector4 } from "@galacean/engine-math"; +import { WebGLEngine } from "@galacean/engine-rhi-webgl"; +import { CanvasRenderMode, RectMask2D, UICanvas, UITransform } from "@galacean/engine-ui"; +import { describe, expect, it } from "vitest"; + +describe("RectMask2D", async () => { + const canvas = document.createElement("canvas"); + const engine = await WebGLEngine.create({ canvas }); + const webCanvas = engine.canvas; + webCanvas.setResolution(300, 300); + const scene = engine.sceneManager.scenes[0]; + const root = scene.createRootEntity("root"); + + const canvasEntity = root.createChild("canvas"); + const rootCanvas = canvasEntity.addComponent(UICanvas); + rootCanvas.renderMode = CanvasRenderMode.ScreenSpaceOverlay; + rootCanvas.referenceResolutionPerUnit = 1; + rootCanvas.referenceResolution.set(300, 300); + + it("should return false when mask size is zero", () => { + const maskEntity = canvasEntity.createChild("mask-zero"); + const rectMask = maskEntity.addComponent(RectMask2D); + const transform = maskEntity.transform as UITransform; + transform.size.set(0, 100); + const worldRect = new Vector4(); + + expect(rectMask._getWorldRect(worldRect)).to.eq(false); + }); + + it("should clamp negative softness values", () => { + const rectMask = canvasEntity.createChild("mask-softness").addComponent(RectMask2D); + + rectMask.softness.set(-4, 6); + expect(rectMask.softness.x).to.eq(0); + expect(rectMask.softness.y).to.eq(6); + + rectMask.softness = new Vector2(5, -3); + expect(rectMask.softness.x).to.eq(5); + expect(rectMask.softness.y).to.eq(0); + }); + + it("should toggle alphaClip", () => { + const rectMask = canvasEntity.createChild("mask-alphaclip").addComponent(RectMask2D); + expect(rectMask.alphaClip).to.eq(false); + rectMask.alphaClip = true; + expect(rectMask.alphaClip).to.eq(true); + }); + + it("should compute world rect when size and pivot set", () => { + const maskEntity = canvasEntity.createChild("mask-rect"); + const transform = maskEntity.transform as UITransform; + transform.pivot.set(0.5, 0.5); + transform.size.set(100, 80); + transform.setPosition(0, 0, 0); + const rectMask = maskEntity.addComponent(RectMask2D); + const worldRect = new Vector4(); + expect(rectMask._getWorldRect(worldRect)).to.eq(true); + // The canvas applies adaptation; just verify width/height match + expect(worldRect.z - worldRect.x).to.be.closeTo(100, 1); + expect(worldRect.w - worldRect.y).to.be.closeTo(80, 1); + }); + + it("should test contains world point", () => { + const maskEntity = canvasEntity.createChild("mask-contains"); + const transform = maskEntity.transform as UITransform; + transform.pivot.set(0.5, 0.5); + transform.size.set(100, 100); + transform.setPosition(0, 0, 0); + const rectMask = maskEntity.addComponent(RectMask2D); + + const rect = new Vector4(); + rectMask._getWorldRect(rect); + const cx = (rect.x + rect.z) * 0.5; + const cy = (rect.y + rect.w) * 0.5; + expect(rectMask._containsWorldPoint(new Vector3(cx, cy, 0))).to.eq(true); + expect(rectMask._containsWorldPoint(new Vector3(rect.x - 5, rect.y - 5, 0))).to.eq(false); + }); +}); diff --git a/tests/src/ui/UIEvent.test.ts b/tests/src/ui/UIEvent.test.ts index cbc87ba359..81056a705a 100644 --- a/tests/src/ui/UIEvent.test.ts +++ b/tests/src/ui/UIEvent.test.ts @@ -1,4 +1,4 @@ -import { PointerEventData, Script } from "@galacean/engine-core"; +import { Entity, PointerEventData, Script } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine"; import { CanvasRenderMode, Image, UICanvas, UITransform } from "@galacean/engine-ui"; import { describe, expect, it } from "vitest"; @@ -34,6 +34,8 @@ describe("UIEvent", async () => { endDragCount = 0; upCount = 0; dropCount = 0; + downTarget: Entity = null; + downCurrentTarget: Entity = null; onPointerEnter(eventData: PointerEventData): void { ++this.enterCount; } @@ -44,6 +46,8 @@ describe("UIEvent", async () => { onPointerDown(eventData: PointerEventData): void { ++this.downCount; + this.downTarget = eventData.target; + this.downCurrentTarget = eventData.currentTarget; } onPointerClick(eventData: PointerEventData): void { @@ -73,17 +77,20 @@ describe("UIEvent", async () => { // Add Image const imageEntity1 = canvasEntity.createChild("Image1"); - imageEntity1.addComponent(Image); + const image1 = imageEntity1.addComponent(Image); + image1.raycastEnabled = true; (imageEntity1.transform).size.set(300, 300); const script1 = imageEntity1.addComponent(TestScript); const imageEntity2 = imageEntity1.createChild("Image2"); - imageEntity2.addComponent(Image); + const image2 = imageEntity2.addComponent(Image); + image2.raycastEnabled = true; (imageEntity2.transform).size.set(200, 200); const script2 = imageEntity2.addComponent(TestScript); const imageEntity3 = imageEntity2.createChild("Image3"); const image3 = imageEntity3.addComponent(Image); + image3.raycastEnabled = true; (imageEntity3.transform).size.set(100, 100); const script3 = imageEntity3.addComponent(TestScript); @@ -289,6 +296,42 @@ describe("UIEvent", async () => { expect(script3.upCount).toBe(0); expect(script3.dropCount).toBe(0); }); + + it("ui event target and currentTarget", () => { + // @ts-ignore + const { _pointerManager: pointerManager } = inputManager; + const { _target: target } = pointerManager; + const { left, top } = target.getBoundingClientRect(); + + // Re-enable the deepest image (disabled by the previous test) and release the leftover pointer. + image3.enabled = true; + target.dispatchEvent(generatePointerEvent("pointerleave", 2, left + 8, top + 8, -1, 0)); + engine.update(); + + // Reset the records touched by the previous test. + script1.downCount = script2.downCount = script3.downCount = 0; + script1.downTarget = script2.downTarget = script3.downTarget = null; + script1.downCurrentTarget = script2.downCurrentTarget = script3.downCurrentTarget = null; + + // Press at the center so the deepest image (Image3) is hit; the event bubbles Image3 -> Image2 -> Image1. + target.dispatchEvent(generatePointerEvent("pointerdown", 3, left + 8, top + 8)); + engine.update(); + + // Down bubbles through all three nested images. + expect(script1.downCount).toBe(1); + expect(script2.downCount).toBe(1); + expect(script3.downCount).toBe(1); + + // `target` stays the deepest hit entity (Image3) for every handler on the bubble path. + expect(script1.downTarget).toBe(imageEntity3); + expect(script2.downTarget).toBe(imageEntity3); + expect(script3.downTarget).toBe(imageEntity3); + + // `currentTarget` is the entity actually handling the event at each bubble step. + expect(script1.downCurrentTarget).toBe(imageEntity1); + expect(script2.downCurrentTarget).toBe(imageEntity2); + expect(script3.downCurrentTarget).toBe(imageEntity3); + }); }); function generatePointerEvent( diff --git a/tests/src/ui/UIInteractive.test.ts b/tests/src/ui/UIInteractive.test.ts index b44f7a50ec..5780463ec0 100644 --- a/tests/src/ui/UIInteractive.test.ts +++ b/tests/src/ui/UIInteractive.test.ts @@ -22,6 +22,7 @@ class ClickHandler extends Script { } handleClickWithPrefix(event: PointerEventData, prefix: string) { + void event; this.callCount++; this.lastPrefix = prefix; } From 79dbaa54aa0cf2314fd82eb94197386ce9b3bc66 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Mon, 20 Jul 2026 00:52:50 +0800 Subject: [PATCH 17/23] refactor(clone): adopt type-driven clone defaults --- docs/en/core/clone.mdx | 136 +- docs/en/how-to-contribute.mdx | 4 +- docs/zh/core/clone.mdx | 139 +- docs/zh/how-to-contribute.mdx | 4 +- e2e/case/spine-spineboy.ts | 1 - e2e/case/spine-tint-black.ts | 1 - e2e/case/sprite-filled.ts | 1 - packages/core/src/2d/sprite/MaskRenderable.ts | 6 +- packages/core/src/2d/sprite/SpriteMask.ts | 4 +- packages/core/src/2d/sprite/SpriteRenderer.ts | 13 +- packages/core/src/2d/text/TextRenderer.ts | 31 +- packages/core/src/Camera.ts | 20 +- packages/core/src/Component.ts | 11 +- packages/core/src/Entity.ts | 34 +- packages/core/src/Renderer.ts | 8 +- packages/core/src/Signal.ts | 32 +- packages/core/src/Transform.ts | 3 +- packages/core/src/UpdateFlag.ts | 2 +- packages/core/src/UpdateFlagManager.ts | 8 +- packages/core/src/VirtualCamera.ts | 3 +- packages/core/src/animation/AnimationClip.ts | 4 +- packages/core/src/animation/Animator.ts | 9 +- .../core/src/animation/AnimatorController.ts | 4 +- packages/core/src/audio/AudioSource.ts | 15 +- packages/core/src/base/DataObject.ts | 6 + packages/core/src/base/index.ts | 1 + packages/core/src/clone/CloneManager.ts | 293 +-- packages/core/src/clone/CloneUtil.ts | 277 ++ packages/core/src/clone/CloneUtils.ts | 46 - packages/core/src/clone/ComponentCloner.ts | 40 +- packages/core/src/clone/enums/CloneMode.ts | 14 +- packages/core/src/index.ts | 3 +- packages/core/src/lighting/Light.ts | 3 +- packages/core/src/mesh/Skin.ts | 12 +- packages/core/src/mesh/SkinnedMeshRenderer.ts | 13 +- .../core/src/particle/ParticleGenerator.ts | 18 +- .../core/src/particle/ParticleRenderer.ts | 4 +- packages/core/src/particle/modules/Burst.ts | 6 +- .../modules/ColorOverLifetimeModule.ts | 3 +- .../src/particle/modules/CustomDataModule.ts | 23 - .../src/particle/modules/EmissionModule.ts | 12 +- .../modules/ForceOverLifetimeModule.ts | 5 +- .../LimitVelocityOverLifetimeModule.ts | 6 +- .../core/src/particle/modules/MainModule.ts | 20 +- .../core/src/particle/modules/NoiseModule.ts | 5 +- .../modules/ParticleCompositeCurve.ts | 9 +- .../modules/ParticleCompositeGradient.ts | 17 +- .../src/particle/modules/ParticleCurve.ts | 12 +- .../modules/ParticleGeneratorModule.ts | 4 +- .../src/particle/modules/ParticleGradient.ts | 14 +- .../modules/RotationOverLifetimeModule.ts | 5 +- .../modules/SizeOverLifetimeModule.ts | 5 +- .../core/src/particle/modules/SubEmitter.ts | 4 +- .../src/particle/modules/SubEmittersModule.ts | 14 +- .../modules/TextureSheetAnimationModule.ts | 5 +- .../modules/VelocityOverLifetimeModule.ts | 10 +- .../src/particle/modules/shape/BaseShape.ts | 18 +- .../src/particle/modules/shape/BoxShape.ts | 2 - .../src/particle/modules/shape/MeshShape.ts | 8 +- .../core/src/physics/CharacterController.ts | 3 +- packages/core/src/physics/Collider.ts | 10 +- packages/core/src/physics/joint/HingeJoint.ts | 5 +- packages/core/src/physics/joint/Joint.ts | 9 +- .../core/src/physics/joint/JointLimits.ts | 5 +- packages/core/src/physics/joint/JointMotor.ts | 5 +- .../src/physics/shape/BoxColliderShape.ts | 3 +- .../core/src/physics/shape/ColliderShape.ts | 26 +- .../src/physics/shape/MeshColliderShape.ts | 26 +- packages/core/src/postProcess/PostProcess.ts | 2 - .../core/src/postProcess/PostProcessEffect.ts | 5 +- .../postProcess/PostProcessEffectParameter.ts | 4 +- packages/core/src/shader/ShaderData.ts | 33 +- packages/core/src/shader/state/BlendState.ts | 6 +- packages/core/src/shader/state/DepthState.ts | 3 +- packages/core/src/shader/state/RasterState.ts | 3 +- packages/core/src/shader/state/RenderState.ts | 8 +- .../shader/state/RenderTargetBlendState.ts | 3 +- .../core/src/shader/state/StencilState.ts | 3 +- packages/core/src/trail/TrailRenderer.ts | 6 +- packages/math/src/Ray.ts | 25 +- .../src/renderer/SpineAnimationRenderer.ts | 13 +- packages/ui/src/component/UICanvas.ts | 9 - packages/ui/src/component/UIGroup.ts | 6 +- packages/ui/src/component/UIRenderer.ts | 5 - packages/ui/src/component/UITransform.ts | 12 +- packages/ui/src/component/advanced/Button.ts | 3 +- packages/ui/src/component/advanced/Image.ts | 7 - .../ui/src/component/advanced/RectMask2D.ts | 14 +- packages/ui/src/component/advanced/Text.ts | 33 +- .../component/interactive/UIInteractive.ts | 12 +- .../transition/SpriteTransition.ts | 29 - .../interactive/transition/Transition.ts | 31 +- tests/src/core/CloneManager.test.ts | 2243 +++++++++++++++++ tests/src/core/CloneTextureRefCount.test.ts | 120 + tests/src/core/CloneUtils.test.ts | 1030 -------- tests/src/core/RefCountContract.test.ts | 266 ++ tests/src/core/ShaderDataRefCount.test.ts | 183 ++ tests/src/core/Signal.test.ts | 23 +- tests/src/core/SkinnedMeshRenderer.test.ts | 53 + tests/src/core/Trail.test.ts | 56 + tests/src/core/particle/CustomData.test.ts | 53 +- .../core/particle/ParticleStopResume.test.ts | 142 -- tests/src/core/physics/ColliderShape.test.ts | 34 + .../core/physics/MeshColliderShape.test.ts | 32 + .../src/core/postProcess/PostProcess.test.ts | 51 + tests/src/math/Ray.test.ts | 20 + tests/src/ui/Image.test.ts | 23 + tests/src/ui/UIInteractive.test.ts | 97 +- 108 files changed, 4078 insertions(+), 2145 deletions(-) create mode 100644 packages/core/src/base/DataObject.ts create mode 100644 packages/core/src/clone/CloneUtil.ts delete mode 100644 packages/core/src/clone/CloneUtils.ts create mode 100644 tests/src/core/CloneManager.test.ts create mode 100644 tests/src/core/CloneTextureRefCount.test.ts delete mode 100644 tests/src/core/CloneUtils.test.ts create mode 100644 tests/src/core/RefCountContract.test.ts create mode 100644 tests/src/core/ShaderDataRefCount.test.ts delete mode 100644 tests/src/core/particle/ParticleStopResume.test.ts diff --git a/docs/en/core/clone.mdx b/docs/en/core/clone.mdx index 2453b0531d..1d9d8de532 100644 --- a/docs/en/core/clone.mdx +++ b/docs/en/core/clone.mdx @@ -8,24 +8,28 @@ label: Core Node cloning is a common runtime feature, and node cloning also includes cloning its bound components. For example, during the initialization phase, dynamically create a certain number of identical entities based on configuration, and then place them in different positions in the scene according to logical rules. Here, the details of script cloning will be explained in detail. ## Entity Cloning + It's very simple, just call the entity's [clone()](/apis/design/#IClone-clone) method to complete the cloning of the entity and its attached components. + ```typescript const cloneEntity = entity.clone(); ``` ## Script Cloning -Scripts are essentially components, so when we call the entity's [clone()](/apis/design/#IClone-clone) function, the engine will not only clone the built-in components but also clone custom scripts. The cloning rules for built-in components have been customized by the official team, and similarly, we have also opened up the cloning capabilities and rules for scripts to developers. The default cloning method for script fields is shallow copy. For example, if we modify the field values of the script and then clone it, the cloned script will retain the modified values without any additional coding. Below is an example of custom script cloning: + +Scripts are essentially components, so when we call the entity's [clone()](/apis/design/#IClone-clone) function, the engine will not only clone the built-in components but also clone custom scripts. The cloning rules for built-in components have been customized by the official team, and similarly, we have also opened up the cloning capabilities and rules for scripts to developers. Script fields are cloned automatically, and how each field value is cloned is resolved by the value's type (see the default rules below). For example, if we modify the field values of the script and then clone it, the cloned script will retain the modified values without any additional coding. Below is an example of custom script cloning: + ```typescript // define a custom script -class CustomScript extends Script{ +class CustomScript extends Script { /** boolean type.*/ - a:boolean = false; - + a: boolean = false; + /** number type.*/ - b:number = 1; - + b: number = 1; + /** class type.*/ - c:Vector3 = new Vector3(0,0,0); + c: Vector3 = new Vector3(0, 0, 0); } // Init entity and script @@ -33,44 +37,99 @@ const entity = engine.createEntity(); const script = entity.addComponent(CustomScript); script.a = true; script.b = 2; -script.c.set(1,1,1); +script.c.set(1, 1, 1); // Clone logic const cloneEntity = entity.clone(); const cloneScript = cloneEntity.getComponent(CustomScript); console.log(cloneScript.a); // output is true. console.log(cloneScript.b); // output is 2. -console.log(cloneScript.c); // output is (1,1,1). +console.log(cloneScript.c); // output is (1,1,1), and it is an independent copy — Vector3 is a math value type and is deep cloned by default. ``` + +### Default Cloning Rules + +When a field has no clone decorator, the engine resolves how to clone its value by the value's type: + +| Value Type | Default Cloning Behavior | +| :-- | :-- | +| Primitive types (`number`, `string`, `boolean`, ...) | Copy the value. | +| Assets (`Texture`, `Mesh`, `Material`, `Sprite`, `Font` and other `ReferResource` types) | Share the reference — the clone uses the same asset as the source. | +| `Entity` / `Component` references | Automatically remapped to the corresponding clone within the cloned subtree; references pointing outside the subtree keep the original reference. | +| Math value types (`Vector2`, `Vector3`, `Vector4`, `Quaternion`, `Matrix`, `Color`, ...) and other value-semantic data | Deep cloned — the clone gets a fully independent copy. | +| Containers (`Array`, `Map`, `Set`, TypedArray, `DataView`, plain objects) | Deep cloned — a fresh container is created, and each member (for `Map`, keys included) is cloned again according to its own type semantics. | +| Runtime containers (internal transient state such as `UpdateFlagManager`) | Ignored — the clone keeps its own constructor-built value. | +| Function values | The clone keeps its own constructor-built function; if it has none, the source's function is shared. | +| Other objects | Share the reference (assignment). | + +A class opts into the deep-clone default in one of two ways: extend [DataObject](/apis/core/#DataObject) (data classes — the engine clones them field by field), or expose a `copyFrom` method (value types like the math classes — the engine copies through it). Either way the type must support argument-less construction: when a slot has no reusable preset instance, the engine bare-constructs one with `new Type()` before populating it, and a constructor that cannot run bare throws. + +So without any decorator, plain data objects and arrays get independent deep copies, assets remain shared, and entity references are automatically remapped: + +```typescript +// define a custom script +class CustomScript extends Script { + /** Entity reference.*/ + target: Entity; + + /** Plain data object.*/ + config = { speed: 1, offsets: [0, 1, 2] }; + + /** Asset.*/ + texture: Texture2D; +} + +// Init entity and script +const entity = engine.createEntity(); +const child = entity.createChild("child"); +const script = entity.addComponent(CustomScript); +script.target = child; +script.texture = new Texture2D(engine, 128, 128); + +// Clone logic +const cloneEntity = entity.clone(); +const cloneScript = cloneEntity.getComponent(CustomScript); +console.log(cloneScript.target === cloneEntity.findByName("child")); // output is true, entity references are remapped to the corresponding clone in the cloned subtree. +console.log(cloneScript.config === script.config); // output is false, plain data objects are deep cloned into independent copies. +console.log(cloneScript.texture === script.texture); // output is true, assets are shared between the source and the clone. +``` + ### Clone Decorators -In addition to the default cloning method, the engine also provides "clone decorators" to customize the cloning method for script fields. The engine has four built-in clone decorators: + +In addition to the default type-driven rules, the engine also provides "clone decorators" to customize the cloning method for script fields. Field decorators have the highest priority — they override the value type's default cloning behavior and apply wherever the clone walks fields: at the component's top level and inside any object that is itself deep cloned (no field walk happens inside a value that stays shared, so no decorator is consulted there). The engine has three built-in clone decorators: | Decorator Name | Decorator Description | -| :--- | :--- | -| [ignoreClone](/apis/core/#ignoreClone) | Ignore the field during cloning. | -| [assignmentClone](/apis/core/#assignmentClone) | (Default value, equivalent to not adding any clone decorator) Assign the field during cloning. If it is a basic type, the value will be copied; if it is a reference type, the reference address will be copied. | -| [shallowClone](/apis/core/#shallowClone) | Shallow clone the field during cloning. After cloning, it will maintain its own independent reference and clone all its internal fields by assignment (if the internal field is a basic type, the value will be copied; if the internal field is a reference type, the reference address will be copied). | -| [deepClone](/apis/core/#deepClone) | Deep clone the field during cloning. After cloning, it will maintain its own independent reference, and all its internal deep fields will remain completely independent. | +| :-- | :-- | +| [ignoreClone](/apis/core/#ignoreClone) | Ignore the field during cloning; the clone keeps its own constructor-built value. | +| [assignmentClone](/apis/core/#assignmentClone) | Assign the field during cloning. If it is a basic type, the value will be copied; if it is a reference type, the clone will share the reference with the source. | +| [deepClone](/apis/core/#deepClone) | Deep clone the field's whole subtree. The deep intent carries into members, while assets inside stay shared, entity references remap, and runtime state keeps the clone's own. Engine-bound values cannot be deep cloned: `@deepClone` on an `Entity` / `Component` reference, an asset, engine runtime state (such as `UpdateFlagManager`), or a function throws — remove the decorator to get the default behavior, or copy an asset via its own clone API. | + + + `@shallowClone` has been removed. Its old semantics — copy the container itself but share its members — no longer + exist. Migrate by intent: use `@deepClone` if the field should be independent, or `@assignmentClone` if it should be + shared. + + +We slightly modify the above example and add different "clone decorators" to the four fields in `CustomScript`. Since arrays are already deep cloned by default, we use `assignmentClone` on field `c` to force sharing, and `deepClone` on field `d` to make the default behavior explicit, with additional print output for further explanation. -We slightly modify the above example and add different "clone decorators" to the four fields in `CustomScript`. Since `shallowClone` and `deepClone` are more complex, we add additional print output to the fields `c` and `d` for further explanation. ```typescript // define a custom script -class CustomScript extends Script{ +class CustomScript extends Script { /** boolean type.*/ @ignoreClone - a:boolean = false; - + a: boolean = false; + /** number type.*/ @assignmentClone - b:number = 1; - + b: number = 1; + /** class type.*/ - @shallowClone - c:Vector3[] = [new Vector3(0,0,0)]; - + @assignmentClone + c: Vector3[] = [new Vector3(0, 0, 0)]; + /** class type.*/ @deepClone - d:Vector3[] = [new Vector3(0,0,0)]; + d: Vector3[] = [new Vector3(0, 0, 0)]; } // Init entity and script @@ -78,26 +137,31 @@ const entity = engine.createEntity(); const script = entity.addComponent(CustomScript); script.a = true; script.b = 2; -script.c[0].set(1,1,1); -script.d[0].set(1,1,1); +script.c[0].set(1, 1, 1); +script.d[0].set(1, 1, 1); // Clone logic const cloneEntity = entity.clone(); const cloneScript = cloneEntity.getComponent(CustomScript); -console.log(cloneScript.a); // output is false,ignoreClone will ignore the value. +console.log(cloneScript.a); // output is false,ignoreClone keeps the clone's own constructor-built value. console.log(cloneScript.b); // output is 2,assignmentClone is just assignment the origin value. -console.log(cloneScript.c[0]); // output is Vector3(1,1,1),shallowClone clone the array shell,but use the same element. +console.log(cloneScript.c[0]); // output is Vector3(1,1,1),assignmentClone shares the same array instance with the source. console.log(cloneScript.d[0]); // output is Vector3(1,1,1),deepClone clone the array shell and also clone the element. -cloneScript.c[0].set(2,2,2); // change the field c[0] value to (2,2,2). -cloneScript.d[0].set(2,2,2); // change the field d[0] value to (2,2,2). +cloneScript.c[0].set(2, 2, 2); // change the field c[0] value to (2,2,2). +cloneScript.d[0].set(2, 2, 2); // change the field d[0] value to (2,2,2). -console.log(script.c[0]); // output is (2,2,2). bacause shallowClone let c[0] use the same reference with cloneScript's c[0]. +console.log(script.c[0]); // output is (2,2,2). bacause assignmentClone let c use the same array reference with cloneScript's c. console.log(script.d[0]); // output is (1,1,1). bacause deepClone let d[0] use the different reference with cloneScript's d[0]. ``` + - Note: - - `shallowClone` and `deepClone` are usually used for *Object*, *Array*, and *Class* types. - - `shallowClone` will maintain its own independent reference after cloning and clone all its internal fields by assignment (if the internal field is a basic type, the value will be copied; if the internal field is a reference type, the reference address will be copied). - - `deepClone` is a deep clone that will recursively clone the properties deeply. How the sub-properties of the properties are cloned depends on the decorators of the sub-properties. - - If the clone decorators do not meet the requirements, you can implement the [_cloneTo()](/apis/design/#IClone-cloneTo) method to add custom cloning. + - Field decorators have the highest priority and also apply to the fields of nested objects, wherever the clone walks fields. + - `deepClone` deep-clones the whole subtree: the intent carries into members, so a member class instance with no deep default of its own is copied too (it must support argument-less construction). Assets inside stay shared, entity references are remapped, runtime state keeps the clone's own, and nested field decorators still win. + - `deepClone` cannot deep clone engine-bound objects: `@deepClone` on an `Entity` / `Component` reference, an asset, engine runtime state, or a function throws, because the explicit intent can't be honored. Remove the decorator to fall back to the default, or use the asset's own clone API for a real copy. + - If the clone decorators do not meet the requirements, you can implement the [\_cloneTo()](/apis/design/#IClone-cloneTo) method to add custom cloning. + +## Cloning and Asset Reference Counting + +When a component's top-level field shares a ref-counted asset (a `ReferResource` such as `Texture`, `Mesh`, or `Material`) during cloning, the engine automatically adds one reference for the clone, so destroying the source never pulls the asset out from under it. Built-in components release their references when destroyed. An asset referenced by custom script fields is kept alive by its clones — destroy the asset itself via its `destroy()` when it is no longer needed. diff --git a/docs/en/how-to-contribute.mdx b/docs/en/how-to-contribute.mdx index 988d96a68e..b5f5e6319a 100644 --- a/docs/en/how-to-contribute.mdx +++ b/docs/en/how-to-contribute.mdx @@ -120,7 +120,7 @@ class CustomScript extends Script { a:boolean = false; @assignmentClone b:number = 1; - @shallowClone + @deepClone c:Vector3[] = [new Vector3(0,0,0)]; } ``` @@ -133,7 +133,7 @@ class CustomScript extends Script{ a:boolean = false; @assignmentClone b:number = 1; - @shallowClone + @deepClone c:Vector3[] = [new Vector3(0,0,0)]; } diff --git a/docs/zh/core/clone.mdx b/docs/zh/core/clone.mdx index 4cf66c382d..dc5ca73704 100644 --- a/docs/zh/core/clone.mdx +++ b/docs/zh/core/clone.mdx @@ -5,28 +5,31 @@ type: 核心 label: Core --- - 节点克隆是运行时的常用功能,同时节点克隆也会附带克隆其绑定的组件。例如在初始化阶段根据配置动态创建一定数量相同的实体,然后根据逻辑规则摆放到场景不同的位置。这里会对脚本的克隆细节进行详细讲解。 ## 实体的克隆 -非常简单,直接调用实体的 [clone()](/apis/design/#IClone-clone) 方法即可完成实体以及附属组件的克隆。 + +非常简单,直接调用实体的 [clone()](/apis/design/#IClone-clone) 方法即可完成实体以及附属组件的克隆。 + ```typescript const cloneEntity = entity.clone(); ``` ## 脚本的克隆 -脚本的本质也是组件,所以当我们调用实体的 [clone()](/apis/design/#IClone-clone) 函数时,引擎不仅会对引擎内置组件进行克隆,还会对自定义脚本进行克隆。引擎内置组件的克隆规则官方已经完成定制,同样我们也将脚本的克隆能力和规则定制开放给了开发者。脚本字段默认的克隆方式为浅拷贝,例如我们对脚本的字段值进行修改后再克隆,克隆后的脚本将保持修改后的值,无需增加任何额外的编码。以下为自定义脚本的克隆案例: + +脚本的本质也是组件,所以当我们调用实体的 [clone()](/apis/design/#IClone-clone) 函数时,引擎不仅会对引擎内置组件进行克隆,还会对自定义脚本进行克隆。引擎内置组件的克隆规则官方已经完成定制,同样我们也将脚本的克隆能力和规则定制开放给了开发者。脚本字段会被自动克隆,每个字段值采用哪种克隆方式由值的类型决定(见下方默认克隆规则)。例如我们对脚本的字段值进行修改后再克隆,克隆后的脚本将保持修改后的值,无需增加任何额外的编码。以下为自定义脚本的克隆案例: + ```typescript // define a custom script -class CustomScript extends Script{ +class CustomScript extends Script { /** boolean type.*/ - a:boolean = false; - + a: boolean = false; + /** number type.*/ - b:number = 1; - + b: number = 1; + /** class type.*/ - c:Vector3 = new Vector3(0,0,0); + c: Vector3 = new Vector3(0, 0, 0); } // Init entity and script @@ -34,44 +37,98 @@ const entity = engine.createEntity(); const script = entity.addComponent(CustomScript); script.a = true; script.b = 2; -script.c.set(1,1,1); +script.c.set(1, 1, 1); // Clone logic const cloneEntity = entity.clone(); const cloneScript = cloneEntity.getComponent(CustomScript); console.log(cloneScript.a); // output is true. console.log(cloneScript.b); // output is 2. -console.log(cloneScript.c); // output is (1,1,1). +console.log(cloneScript.c); // output is (1,1,1),且是一份独立拷贝 —— Vector3 属于数学值类型,默认深克隆。 ``` + +### 默认克隆规则 + +当字段没有添加任何克隆装饰器时,引擎会根据字段值的类型决定克隆方式: + +| 值类型 | 默认克隆行为 | +| :-- | :-- | +| 基本类型(`number`、`string`、`boolean` 等) | 拷贝值。 | +| 资产(`Texture`、`Mesh`、`Material`、`Sprite`、`Font` 等 `ReferResource` 类型) | 共享引用 —— 克隆体与源使用同一份资产。 | +| `Entity` / `Component` 引用 | 自动重映射到克隆子树内对应的克隆对象;指向子树外的引用保持原引用不变。 | +| 数学值类型(`Vector2`、`Vector3`、`Vector4`、`Quaternion`、`Matrix`、`Color` 等)及其他值语义数据 | 深克隆 —— 克隆体获得完全独立的拷贝。 | +| 容器(`Array`、`Map`、`Set`、TypedArray、`DataView`、普通对象) | 深克隆 —— 创建全新的容器,每个成员(`Map` 连键一起)再按各自的类型语义继续克隆。 | +| 运行时容器(`UpdateFlagManager` 等内部瞬态状态) | 忽略 —— 克隆体保留自身构造时的值。 | +| 函数值 | 克隆体保留自身构造时的函数;没有则共享源的函数。 | +| 其他对象 | 共享引用(赋值)。 | + +一个类可以通过两种方式获得"默认深克隆"能力:继承 [DataObject](/apis/core/#DataObject)(数据类 —— 引擎逐字段克隆),或提供 `copyFrom` 方法(数学类这样的值类型 —— 引擎经由它拷贝)。两种方式都要求类型支持无参构造:当槽位上没有可复用的预置实例时,引擎会先 `new Type()` 裸构造一个再填充,无法无参构造的类型会直接报错。 + +因此在不加装饰器的情况下,普通数据对象和数组默认会得到独立的深拷贝,资产依旧共享,实体引用则自动重映射: + +```typescript +// define a custom script +class CustomScript extends Script { + /** Entity reference.*/ + target: Entity; + + /** Plain data object.*/ + config = { speed: 1, offsets: [0, 1, 2] }; + + /** Asset.*/ + texture: Texture2D; +} + +// Init entity and script +const entity = engine.createEntity(); +const child = entity.createChild("child"); +const script = entity.addComponent(CustomScript); +script.target = child; +script.texture = new Texture2D(engine, 128, 128); + +// Clone logic +const cloneEntity = entity.clone(); +const cloneScript = cloneEntity.getComponent(CustomScript); +console.log(cloneScript.target === cloneEntity.findByName("child")); // output is true,实体引用被重映射到克隆子树内对应的克隆对象。 +console.log(cloneScript.config === script.config); // output is false,普通数据对象被深克隆为独立拷贝。 +console.log(cloneScript.texture === script.texture); // output is true,资产在源与克隆体之间共享。 +``` + ### 克隆装饰器 -除了默认的克隆方式外,引擎还提供了“克隆装饰器“对脚本字段的克隆方式进行定制。引擎内置四种克隆装饰: + +除了默认的类型驱动规则外,引擎还提供了“克隆装饰器“对脚本字段的克隆方式进行定制。字段装饰器优先级最高 —— 会覆盖值类型的默认克隆行为,并在克隆走字段遍历的任何地方生效:组件顶层,以及任何本身被深克隆的对象内部(默认共享的值不会被逐字段遍历,其内部的装饰器也就不会被查询)。引擎内置三种克隆装饰: | 装饰器名称 | 装饰器释义 | -| :--- | :--- | -| [ignoreClone](/apis/core/#ignoreClone) | 克隆时对字段进行忽略。 | -| [assignmentClone](/apis/core/#assignmentClone) | ( 默认值,和不添加任何克隆装饰器等效) 克隆时对字段进行赋值。如果是基本类型则会拷贝值,如果是引用类型则会拷贝其引用地址。 | -| [shallowClone](/apis/core/#shallowClone) | 克隆时对字段进行浅克隆。克隆后会保持自身引用独立,并使用赋值的方式克隆其内部所有字段(如果内部字段是基本类型则会拷贝值,如果内部字段是引用类型则会拷贝其引用地址)。| -| [deepClone](/apis/core/#deepClone) | 克隆时对字段进行深克隆。克隆后会保持自身引用独立,并且其内部所有深层字段均保持完全独立。| +| :-- | :-- | +| [ignoreClone](/apis/core/#ignoreClone) | 克隆时对字段进行忽略,克隆体保留自身构造时的值。 | +| [assignmentClone](/apis/core/#assignmentClone) | 克隆时对字段进行赋值。如果是基本类型则会拷贝值,如果是引用类型,克隆体将与源共享同一个引用。 | +| [deepClone](/apis/core/#deepClone) | 克隆时深克隆字段的整棵子树。深意图会传播进成员;子树内的资产保持共享、实体引用重映射、运行时状态保留克隆体自己的。引擎绑定的对象无法被深克隆:对 `Entity` / `Component` 引用、资产、引擎运行时状态(如 `UpdateFlagManager`)或函数使用 `@deepClone` 会直接报错 —— 请移除该装饰器以走默认行为,或使用资产自身的克隆 API 复制资产。 | + + + `@shallowClone` 已被移除。它旧有的“拷贝容器本身、共享内部成员”语义已不复存在。请按意图迁移:希望字段独立就使用 + `@deepClone`,希望共享就使用 `@assignmentClone`。 + + +我们将上面的案例稍加修改,分别对 `CustomScript` 中的四个字段添加了不同的“克隆装饰器“。由于数组默认就会被深克隆,我们对字段 `c` 使用 `assignmentClone` 来强制共享,对字段 `d` 使用 `deepClone` 显式声明默认行为,并增加了额外的打印输出进行进一步讲解。 -我们将上面的案例稍加修改,分别对 `CustomScript` 中的四个字段添加了不同的“克隆装饰器“。由于 `shallowClone` 和 `deepCone`  较复杂,我们对字段 `c` 和 `d` 增加了额外的打印输出进行进一步讲解。 ```typescript // define a custom script -class CustomScript extends Script{ +class CustomScript extends Script { /** boolean type.*/ @ignoreClone - a:boolean = false; - + a: boolean = false; + /** number type.*/ @assignmentClone - b:number = 1; - + b: number = 1; + /** class type.*/ - @shallowClone - c:Vector3[] = [new Vector3(0,0,0)]; - + @assignmentClone + c: Vector3[] = [new Vector3(0, 0, 0)]; + /** class type.*/ @deepClone - d:Vector3[] = [new Vector3(0,0,0)]; + d: Vector3[] = [new Vector3(0, 0, 0)]; } // Init entity and script @@ -79,27 +136,31 @@ const entity = engine.createEntity(); const script = entity.addComponent(CustomScript); script.a = true; script.b = 2; -script.c[0].set(1,1,1); -script.d[0].set(1,1,1); +script.c[0].set(1, 1, 1); +script.d[0].set(1, 1, 1); // Clone logic const cloneEntity = entity.clone(); const cloneScript = cloneEntity.getComponent(CustomScript); -console.log(cloneScript.a); // output is false,ignoreClone will ignore the value. +console.log(cloneScript.a); // output is false,ignoreClone 使克隆体保留自身构造时的值。 console.log(cloneScript.b); // output is 2,assignmentClone is just assignment the origin value. -console.log(cloneScript.c[0]); // output is Vector3(1,1,1),shallowClone clone the array shell,but use the same element. +console.log(cloneScript.c[0]); // output is Vector3(1,1,1),assignmentClone 与源共享同一个数组实例。 console.log(cloneScript.d[0]); // output is Vector3(1,1,1),deepClone clone the array shell and also clone the element. -cloneScript.c[0].set(2,2,2); // change the field c[0] value to (2,2,2). -cloneScript.d[0].set(2,2,2); // change the field d[0] value to (2,2,2). +cloneScript.c[0].set(2, 2, 2); // change the field c[0] value to (2,2,2). +cloneScript.d[0].set(2, 2, 2); // change the field d[0] value to (2,2,2). -console.log(script.c[0]); // output is (2,2,2). bacause shallowClone let c[0] use the same reference with cloneScript's c[0]. +console.log(script.c[0]); // output is (2,2,2). bacause assignmentClone let c use the same array reference with cloneScript's c. console.log(script.d[0]); // output is (1,1,1). bacause deepClone let d[0] use the different reference with cloneScript's d[0]. ``` -- 注意: - - `shallowClone` 和 `deepClone` 通常用于 *Object*、*Array* 和 *Class* 类型。 - - `shallowClone` 克隆后会保持自身引用独立,并使用赋值的方式克隆其内部所有字段(如果内部字段是基本类型则会拷贝值,如果内部字段是引用类型则会拷贝其引用地址)。 - - `deepClone` 为深克隆,会对属性进行深度递归,至于属性的子属性如何克隆,取决于子属性的装饰器。 - - 如果克隆装饰器不能满足诉求,可以通过实现 [_cloneTo()](/apis/design/#IClone-cloneTo) 方法追加自定义克隆。 +- 注意: + + - 字段装饰器优先级最高,并且在克隆走字段遍历的任何地方对嵌套对象的字段同样生效。 + - `deepClone` 会深克隆整棵子树:深意图会传播进成员,没有深克隆默认的成员类实例同样被拷贝(因此必须支持无参构造)。子树内的资产保持共享,实体引用被重映射,运行时状态保留克隆体自己的,嵌套字段上的装饰器依然优先。 + - `deepClone` 无法深克隆引擎绑定的对象:对 `Entity` / `Component` 引用、资产、引擎运行时状态或函数使用 `@deepClone` 会直接报错,因为这个显式意图无法被满足。请移除该装饰器以回退到默认行为,或使用资产自身的克隆 API 得到真正的拷贝。 + - 如果克隆装饰器不能满足诉求,可以通过实现 [\_cloneTo()](/apis/design/#IClone-cloneTo) 方法追加自定义克隆。 + +## 克隆与资产引用计数 +克隆时,如果组件的顶层字段共享了一个引用计数资产(`ReferResource`,如 `Texture`、`Mesh`、`Material`),引擎会自动为克隆体增加一次引用计数,因此销毁源对象不会让资产在克隆体脚下被回收。内置组件会在销毁时释放自己持有的引用;被自定义脚本字段引用的资产则由克隆体一直保活 —— 不再需要时,调用资产自身的 `destroy()` 释放。 diff --git a/docs/zh/how-to-contribute.mdx b/docs/zh/how-to-contribute.mdx index 6a995b5d89..e55d4c23c8 100644 --- a/docs/zh/how-to-contribute.mdx +++ b/docs/zh/how-to-contribute.mdx @@ -121,7 +121,7 @@ class CustomScript extends Script { a:boolean = false; @assignmentClone b:number = 1; - @shallowClone + @deepClone c:Vector3[] = [new Vector3(0,0,0)]; } ``` @@ -135,7 +135,7 @@ class CustomScript extends Script{ a:boolean = false; @assignmentClone b:number = 1; - @shallowClone + @deepClone c:Vector3[] = [new Vector3(0,0,0)]; } ``` diff --git a/e2e/case/spine-spineboy.ts b/e2e/case/spine-spineboy.ts index 4ccfdc21bf..706be981e6 100644 --- a/e2e/case/spine-spineboy.ts +++ b/e2e/case/spine-spineboy.ts @@ -10,7 +10,6 @@ import { initScreenshot, updateForE2E } from "./.mockForE2E"; Logger.enable(); WebGLEngine.create({ canvas: "canvas" }).then((engine) => { - engine.canvas.resizeByClientSize(); const scene = engine.sceneManager.activeScene; const root = scene.createRootEntity(); diff --git a/e2e/case/spine-tint-black.ts b/e2e/case/spine-tint-black.ts index b2c6c91504..62d565ced0 100644 --- a/e2e/case/spine-tint-black.ts +++ b/e2e/case/spine-tint-black.ts @@ -10,7 +10,6 @@ import { initScreenshot, updateForE2E } from "./.mockForE2E"; Logger.enable(); WebGLEngine.create({ canvas: "canvas" }).then((engine) => { - engine.canvas.resizeByClientSize(); const scene = engine.sceneManager.activeScene; const root = scene.createRootEntity(); diff --git a/e2e/case/sprite-filled.ts b/e2e/case/sprite-filled.ts index dbe3415571..e32be407a3 100644 --- a/e2e/case/sprite-filled.ts +++ b/e2e/case/sprite-filled.ts @@ -17,7 +17,6 @@ import { initScreenshot, updateForE2E } from "./.mockForE2E"; // One baseline covering every fill mode (rows) across fill amounts 0/0.25/0.5/0.75/1 (columns). // The amount steps hit the topology boundaries (45° quad↔triangle, 90° quadrant edges). WebGLEngine.create({ canvas: "canvas" }).then((engine) => { - engine.canvas.resizeByClientSize(); const scene = engine.sceneManager.activeScene; const rootEntity = scene.createRootEntity(); diff --git a/packages/core/src/2d/sprite/MaskRenderable.ts b/packages/core/src/2d/sprite/MaskRenderable.ts index 441492ba85..11f1731494 100644 --- a/packages/core/src/2d/sprite/MaskRenderable.ts +++ b/packages/core/src/2d/sprite/MaskRenderable.ts @@ -2,7 +2,7 @@ import { BoundingBox, Vector2, Vector3 } from "@galacean/engine-math"; import { RenderElement } from "../../RenderPipeline/RenderElement"; import { VertexMergeBatcher } from "../../RenderPipeline/VertexMergeBatcher"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; -import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; import { ShaderProperty } from "../../shader/ShaderProperty"; import type { ISpriteRenderer } from "../assembler/ISpriteRenderer"; @@ -47,7 +47,6 @@ export function MaskRenderable( private static _maskTextureProperty = ShaderProperty.getByName("renderer_MaskTexture"); private static _alphaCutoffProperty = ShaderProperty.getByName("renderer_MaskAlphaCutoff"); - @assignmentClone private _influenceLayers: SpriteMaskLayer = SpriteMaskLayer.Everything; /** @internal */ @ignoreClone @@ -57,11 +56,8 @@ export function MaskRenderable( _maskIndex: number = -1; @ignoreClone private _sprite: Sprite = null; - @assignmentClone private _flipX: boolean = false; - @assignmentClone private _flipY: boolean = false; - @assignmentClone private _alphaCutoff: number = 0.5; /** diff --git a/packages/core/src/2d/sprite/SpriteMask.ts b/packages/core/src/2d/sprite/SpriteMask.ts index 771faead34..130a754896 100644 --- a/packages/core/src/2d/sprite/SpriteMask.ts +++ b/packages/core/src/2d/sprite/SpriteMask.ts @@ -4,7 +4,7 @@ import { PrimitiveChunkManager } from "../../RenderPipeline/PrimitiveChunkManage import { RenderContext } from "../../RenderPipeline/RenderContext"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; -import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderProperty } from "../../shader/ShaderProperty"; import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; import { MaskDirtyFlags, MaskRenderable } from "./MaskRenderable"; @@ -25,9 +25,7 @@ export class SpriteMask extends MaskRenderable(Renderer) { private _automaticWidth: number = 0; @ignoreClone private _automaticHeight: number = 0; - @assignmentClone private _customWidth: number = undefined; - @assignmentClone private _customHeight: number = undefined; /** diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts index 3e2bff4409..f01a4112c4 100644 --- a/packages/core/src/2d/sprite/SpriteRenderer.ts +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -6,7 +6,7 @@ import { RenderContext } from "../../RenderPipeline/RenderContext"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; import { RenderElement } from "../../RenderPipeline/RenderElement"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; -import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderProperty } from "../../shader/ShaderProperty"; import { ISpriteAssembler } from "../assembler/ISpriteAssembler"; import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; @@ -38,20 +38,13 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { private _drawMode: SpriteDrawMode; @ignoreClone private _assembler: ISpriteAssembler; - @assignmentClone private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; - @assignmentClone private _tiledAdaptiveThreshold: number = 0.5; - @assignmentClone private _filledMode: SpriteFilledMode = SpriteFilledMode.Radial360; - @assignmentClone private _filledAmount: number = 1; - @assignmentClone private _filledOrigin: SpriteFilledOrigin = SpriteFilledOrigin.Bottom; - @assignmentClone private _filledClockWise: boolean = true; - @deepClone private _color: Color = new Color(1, 1, 1, 1); @ignoreClone private _sprite: Sprite = null; @@ -60,13 +53,9 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { private _automaticWidth: number = 0; @ignoreClone private _automaticHeight: number = 0; - @assignmentClone private _customWidth: number = undefined; - @assignmentClone private _customHeight: number = undefined; - @assignmentClone private _flipX: boolean = false; - @assignmentClone private _flipY: boolean = false; /** diff --git a/packages/core/src/2d/text/TextRenderer.ts b/packages/core/src/2d/text/TextRenderer.ts index 7d2c26b2e0..47b4daa8ff 100644 --- a/packages/core/src/2d/text/TextRenderer.ts +++ b/packages/core/src/2d/text/TextRenderer.ts @@ -8,7 +8,7 @@ import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; import { VertexMergeBatcher } from "../../RenderPipeline/VertexMergeBatcher"; import { Renderer } from "../../Renderer"; import { TransformModifyFlags } from "../../Transform"; -import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; import { ShaderData, ShaderProperty } from "../../shader"; import { ShaderDataGroup } from "../../shader/enums/ShaderDataGroup"; @@ -40,42 +40,26 @@ export class TextRenderer extends Renderer implements ITextRenderer { @ignoreClone private _textChunks = Array(); /** @internal */ - @assignmentClone _subFont: SubFont = null; /** @internal */ @ignoreClone _dirtyFlag = DirtyFlag.Font; - @deepClone private _color = new Color(1, 1, 1, 1); - @assignmentClone private _text = ""; - @assignmentClone private _width = 0; - @assignmentClone private _height = 0; @ignoreClone private _localBounds = new BoundingBox(); - @assignmentClone private _font: Font = null; - @assignmentClone private _fontSize = 24; - @assignmentClone private _fontStyle = FontStyle.None; - @assignmentClone private _lineSpacing = 0; - @assignmentClone private _characterSpacing = 0; - @assignmentClone private _horizontalAlignment = TextHorizontalAlignment.Center; - @assignmentClone private _verticalAlignment = TextVerticalAlignment.Center; - @assignmentClone private _enableWrapping = false; - @assignmentClone private _overflowMode = OverflowMode.Overflow; - @deepClone private _outlineColor = new Color(0, 0, 0, 1); - @ignoreClone private _outlineWidth = 0; /** @@ -372,19 +356,6 @@ export class TextRenderer extends Renderer implements ITextRenderer { this._subFont && (this._subFont = null); } - /** - * @internal - */ - override _cloneTo(target: TextRenderer): void { - super._cloneTo(target); - target.font = this._font; - target._subFont = this._subFont; - target.outlineWidth = this._outlineWidth; - } - - /** - * @internal - */ _isContainDirtyFlag(type: number): boolean { return (this._dirtyFlag & type) != 0; } diff --git a/packages/core/src/Camera.ts b/packages/core/src/Camera.ts index 55d904f6c9..853ff61ad8 100644 --- a/packages/core/src/Camera.ts +++ b/packages/core/src/Camera.ts @@ -9,7 +9,7 @@ import { Transform } from "./Transform"; import { UpdateFlagManager } from "./UpdateFlagManager"; import { VirtualCamera } from "./VirtualCamera"; import { GLCapabilityType, Logger } from "./base"; -import { deepClone, ignoreClone } from "./clone/CloneManager"; +import { ignoreClone } from "./clone/CloneManager"; import { AntiAliasing } from "./enums/AntiAliasing"; import { CameraClearFlags } from "./enums/CameraClearFlags"; import { CameraModifyFlags } from "./enums/CameraModifyFlags"; @@ -116,13 +116,11 @@ export class Camera extends Component { @ignoreClone _globalShaderMacro: ShaderMacroCollection = new ShaderMacroCollection(); /** @internal */ - @deepClone _frustum: BoundingFrustum = new BoundingFrustum(); /** @internal */ @ignoreClone _renderPipeline: BasicRenderPipeline; /** @internal */ - @deepClone _virtualCamera: VirtualCamera = new VirtualCamera(); /** @internal */ _replacementShader: Shader = null; @@ -148,25 +146,16 @@ export class Camera extends Component { private _msaaSamples: MSAASamples; private _renderTarget: RenderTarget = null; - @ignoreClone private _updateFlagManager: UpdateFlagManager; - @ignoreClone private _frustumChangeFlag: BoolUpdateFlag; - @ignoreClone private _isViewMatrixDirty: BoolUpdateFlag; - @ignoreClone private _isInvViewProjDirty: BoolUpdateFlag; - @deepClone private _shaderData: ShaderData = new ShaderData(ShaderDataGroup.Camera); @ignoreClone private _depthBufferParams: Vector4 = new Vector4(); - @deepClone private _viewport: Vector4 = new Vector4(0, 0, 1, 1); - @deepClone private _pixelViewport: Rect = new Rect(0, 0, 0, 0); - @deepClone private _inverseProjectionMatrix: Matrix = new Matrix(); - @deepClone private _invViewProjMat: Matrix = new Matrix(); /** @@ -826,13 +815,6 @@ export class Camera extends Component { this._updateFlagManager?.removeListener(onChange); } - /** - * @internal - */ - _cloneTo(target: Camera): void { - this._renderTarget?._addReferCount(1); - } - /** * @internal * @inheritdoc diff --git a/packages/core/src/Component.ts b/packages/core/src/Component.ts index 2ebafbcd05..c3fd1733dd 100644 --- a/packages/core/src/Component.ts +++ b/packages/core/src/Component.ts @@ -1,7 +1,6 @@ import { IReferable } from "./asset/IReferable"; import { EngineObject } from "./base"; -import { assignmentClone, ignoreClone } from "./clone/CloneManager"; -import { CloneUtils } from "./clone/CloneUtils"; +import { ignoreClone } from "./clone/CloneManager"; import { Entity } from "./Entity"; import { ActiveChangeFlag } from "./enums/ActiveChangeFlag"; import { Scene } from "./Scene"; @@ -23,7 +22,6 @@ export class Component extends EngineObject { @ignoreClone private _phasedActive: boolean = false; - @assignmentClone private _enabled: boolean = true; /** @@ -154,13 +152,6 @@ export class Component extends EngineObject { } } - /** - * @internal - */ - _remap(srcRoot: Entity, targetRoot: Entity): T { - return CloneUtils.remapComponent(srcRoot, targetRoot, this) as unknown as T; - } - protected _addResourceReferCount(resource: IReferable, count: number): void { this._entity._isTemplate || resource._addReferCount(count); } diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index 13eeb54c75..6423056deb 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -10,7 +10,6 @@ import { Transform } from "./Transform"; import { UpdateFlagManager } from "./UpdateFlagManager"; import { ReferResource } from "./asset/ReferResource"; import { EngineObject, Logger } from "./base"; -import { CloneUtils } from "./clone/CloneUtils"; import { ComponentCloner } from "./clone/ComponentCloner"; import { ActiveChangeFlag } from "./enums/ActiveChangeFlag"; import { EntityModifyFlags } from "./enums/EntityModifyFlags"; @@ -425,8 +424,9 @@ export class Entity extends EngineObject { * @returns Cloned entity */ clone(): Entity { - const cloneEntity = this._createCloneEntity(); - this._parseCloneEntity(this, cloneEntity, this, cloneEntity, new Map()); + const cloneMap = new Map(); + const cloneEntity = this._createCloneEntity(cloneMap); + this._parseCloneEntity(this, cloneEntity, cloneMap); return cloneEntity; } @@ -438,13 +438,6 @@ export class Entity extends EngineObject { return this._updateFlagManager.createFlag(BoolUpdateFlag); } - /** - * @internal - */ - _remap(srcRoot: Entity, targetRoot: Entity): Entity { - return CloneUtils.remapEntity(srcRoot, targetRoot, this); - } - /** * @internal */ @@ -453,7 +446,7 @@ export class Entity extends EngineObject { this._templateResource = templateResource; } - private _createCloneEntity(): Entity { + private _createCloneEntity(cloneMap: Map): Entity { const componentConstructors = Entity._tempComponentConstructors; const components = this._components; for (let i = 0, n = components.length; i < n; i++) { @@ -461,6 +454,11 @@ export class Entity extends EngineObject { } const cloneEntity = new Entity(this.engine, this.name, ...componentConstructors); componentConstructors.length = 0; + cloneMap.set(this, cloneEntity); + const targetComponents = cloneEntity._components; + for (let i = 0, n = components.length; i < n; i++) { + cloneMap.set(components[i], targetComponents[i]); + } const templateResource = this._templateResource; if (templateResource) { cloneEntity._templateResource = templateResource; @@ -471,27 +469,21 @@ export class Entity extends EngineObject { cloneEntity._isActive = this._isActive; const srcChildren = this._children; for (let i = 0, n = srcChildren.length; i < n; i++) { - cloneEntity.addChild(srcChildren[i]._createCloneEntity()); + cloneEntity.addChild(srcChildren[i]._createCloneEntity(cloneMap)); } return cloneEntity; } - private _parseCloneEntity( - src: Entity, - target: Entity, - srcRoot: Entity, - targetRoot: Entity, - deepInstanceMap: Map - ): void { + private _parseCloneEntity(src: Entity, target: Entity, cloneMap: Map): void { const srcChildren = src._children; const targetChildren = target._children; for (let i = 0, n = srcChildren.length; i < n; i++) { - this._parseCloneEntity(srcChildren[i], targetChildren[i], srcRoot, targetRoot, deepInstanceMap); + this._parseCloneEntity(srcChildren[i], targetChildren[i], cloneMap); } const components = src._components; for (let i = 0, n = components.length; i < n; i++) { - ComponentCloner.cloneComponent(components[i], target._components[i], srcRoot, targetRoot, deepInstanceMap); + ComponentCloner.cloneComponent(components[i], target._components[i], cloneMap); } } diff --git a/packages/core/src/Renderer.ts b/packages/core/src/Renderer.ts index 5af1e71750..64a84953cd 100644 --- a/packages/core/src/Renderer.ts +++ b/packages/core/src/Renderer.ts @@ -7,7 +7,7 @@ import { Entity } from "./Entity"; import { RenderContext } from "./RenderPipeline/RenderContext"; import { RenderElement } from "./RenderPipeline/RenderElement"; import { Transform, TransformModifyFlags } from "./Transform"; -import { assignmentClone, deepClone, ignoreClone } from "./clone/CloneManager"; +import { ignoreClone } from "./clone/CloneManager"; import { SpriteMaskLayer } from "./enums/SpriteMaskLayer"; import { Material } from "./material"; import { ShaderMacro, ShaderProperty } from "./shader"; @@ -47,10 +47,7 @@ export class Renderer extends Component { _globalShaderMacro: ShaderMacroCollection = new ShaderMacroCollection(); @ignoreClone _renderFrameCount: number; - /** @internal */ - @assignmentClone _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; - @assignmentClone _maskLayer: SpriteMaskLayer = SpriteMaskLayer.Layer0; @ignoreClone @@ -65,7 +62,6 @@ export class Renderer extends Component { protected _bounds: BoundingBox = new BoundingBox(); protected _transformEntity: Entity; - @deepClone private _shaderData: ShaderData = new ShaderData(ShaderDataGroup.Renderer); @ignoreClone private _mvMatrix: Matrix = new Matrix(); @@ -75,9 +71,7 @@ export class Renderer extends Component { private _normalMatrix: Matrix = new Matrix(); @ignoreClone private _materialsInstanced: boolean[] = []; - @assignmentClone private _priority: number = 0; - @assignmentClone private _receiveShadows: boolean = true; /** diff --git a/packages/core/src/Signal.ts b/packages/core/src/Signal.ts index e337e03d9d..6d34f69eb5 100644 --- a/packages/core/src/Signal.ts +++ b/packages/core/src/Signal.ts @@ -1,14 +1,15 @@ +import { DataObject } from "./base/DataObject"; +import { ignoreClone } from "./clone/CloneManager"; import { Component } from "./Component"; import { Entity } from "./Entity"; -import { CloneUtils } from "./clone/CloneUtils"; -import { ignoreClone } from "./clone/CloneManager"; import { SafeLoopArray } from "./utils/SafeLoopArray"; /** * Signal is a typed event mechanism for Galacean Engine. * @typeParam T - Tuple type of the signal arguments */ -export class Signal { +export class Signal extends DataObject { + // Rebuilt by `_cloneTo`; must survive even a propagated @deepClone. @ignoreClone private _listeners: SafeLoopArray> = new SafeLoopArray>(); @@ -126,35 +127,30 @@ export class Signal { /** * @internal - * Clone listeners to target signal, remapping entity/component references. */ - _cloneTo(target: Signal, srcRoot: Entity, targetRoot: Entity): void { + _cloneTo(target: Signal, cloneMap: Map): void { const listeners = this._listeners.getLoopArray(); for (let i = 0, n = listeners.length; i < n; i++) { const listener = listeners[i]; if (listener.destroyed || !listener.methodName) continue; - const clonedTarget = CloneUtils.remapComponent(srcRoot, targetRoot, listener.target); - if (clonedTarget) { - const clonedArgs = this._cloneArguments(listener.arguments, srcRoot, targetRoot); - if (listener.once) { - target.once(clonedTarget, listener.methodName, ...clonedArgs); - } else { - target.on(clonedTarget, listener.methodName, ...clonedArgs); - } + const clonedTarget = (cloneMap.get(listener.target) ?? listener.target); + const clonedArgs = this._cloneArguments(listener.arguments, cloneMap); + if (listener.once) { + target.once(clonedTarget, listener.methodName, ...clonedArgs); + } else { + target.on(clonedTarget, listener.methodName, ...clonedArgs); } } } - private _cloneArguments(args: any[], srcRoot: Entity, targetRoot: Entity): any[] { + private _cloneArguments(args: any[], cloneMap: Map): any[] { if (!args || args.length === 0) return []; const len = args.length; const clonedArgs = new Array(len); for (let i = 0; i < len; i++) { const arg = args[i]; - if (arg instanceof Entity) { - clonedArgs[i] = CloneUtils.remapEntity(srcRoot, targetRoot, arg); - } else if (arg instanceof Component) { - clonedArgs[i] = CloneUtils.remapComponent(srcRoot, targetRoot, arg); + if (arg instanceof Entity || arg instanceof Component) { + clonedArgs[i] = cloneMap.get(arg) ?? arg; } else { clonedArgs[i] = arg; } diff --git a/packages/core/src/Transform.ts b/packages/core/src/Transform.ts index 748bb68915..d409cb69c9 100644 --- a/packages/core/src/Transform.ts +++ b/packages/core/src/Transform.ts @@ -2,7 +2,7 @@ import { MathUtil, Matrix, Matrix3x3, Quaternion, Vector3 } from "@galacean/engi import { BoolUpdateFlag } from "./BoolUpdateFlag"; import { Component } from "./Component"; import { Entity } from "./Entity"; -import { assignmentClone, ignoreClone } from "./clone/CloneManager"; +import { ignoreClone } from "./clone/CloneManager"; /** * Used to implement transformation related functions. @@ -26,7 +26,6 @@ export class Transform extends Component { private _rotationQuaternion: Quaternion = new Quaternion(); @ignoreClone private _scale: Vector3 = new Vector3(1, 1, 1); - @assignmentClone private _localUniformScaling: boolean = true; @ignoreClone private _worldPosition: Vector3 = new Vector3(); diff --git a/packages/core/src/UpdateFlag.ts b/packages/core/src/UpdateFlag.ts index d926b47b8c..207a9cf1a6 100644 --- a/packages/core/src/UpdateFlag.ts +++ b/packages/core/src/UpdateFlag.ts @@ -13,7 +13,7 @@ export abstract class UpdateFlag { * @param bit - Bit * @param param - Parameter */ - abstract dispatch(bit?: number, param?: Object): void; + abstract dispatch(bit?: number, param?: unknown): void; /** * Clear. diff --git a/packages/core/src/UpdateFlagManager.ts b/packages/core/src/UpdateFlagManager.ts index 0e1e98ec90..f93c10700a 100644 --- a/packages/core/src/UpdateFlagManager.ts +++ b/packages/core/src/UpdateFlagManager.ts @@ -10,7 +10,7 @@ export class UpdateFlagManager { _updateFlags: UpdateFlag[] = []; - private _listeners: ((type?: number, param?: Object) => void)[] = []; + private _listeners: ((type?: number, param?: unknown) => void)[] = []; /** * Create a UpdateFlag. @@ -46,7 +46,7 @@ export class UpdateFlagManager { * Add a listener. * @param listener - The listener */ - addListener(listener: (type?: number, param?: Object) => void): void { + addListener(listener: (type?: number, param?: unknown) => void): void { this._listeners.push(listener); } @@ -54,7 +54,7 @@ export class UpdateFlagManager { * Remove a listener. * @param listener - The listener */ - removeListener(listener: (type?: number, param?: Object) => void): void { + removeListener(listener: (type?: number, param?: unknown) => void): void { Utils.removeFromArray(this._listeners, listener); } @@ -63,7 +63,7 @@ export class UpdateFlagManager { * @param type - Event type, usually in the form of enumeration * @param param - Event param */ - dispatch(type?: number, param?: Object): void { + dispatch(type?: number, param?: unknown): void { this.version++; const updateFlags = this._updateFlags; diff --git a/packages/core/src/VirtualCamera.ts b/packages/core/src/VirtualCamera.ts index 4c8598888e..2fc88b3c8f 100644 --- a/packages/core/src/VirtualCamera.ts +++ b/packages/core/src/VirtualCamera.ts @@ -1,10 +1,11 @@ +import { DataObject } from "./base/DataObject"; import { Matrix, Vector3 } from "@galacean/engine-math"; import { ignoreClone } from "./clone/CloneManager"; /** * @internal */ -export class VirtualCamera { +export class VirtualCamera extends DataObject { isOrthographic: boolean = false; nearClipPlane: number = 0.1; farClipPlane: number = 100; diff --git a/packages/core/src/animation/AnimationClip.ts b/packages/core/src/animation/AnimationClip.ts index ef51647577..9238ce782c 100644 --- a/packages/core/src/animation/AnimationClip.ts +++ b/packages/core/src/animation/AnimationClip.ts @@ -55,7 +55,7 @@ export class AnimationClip extends EngineObject { * @param time - The time when the event be triggered * @param parameter - The parameter that is stored in the event and will be sent to the function */ - addEvent(functionName: string, time: number, parameter: Object): void; + addEvent(functionName: string, time: number, parameter: object): void; /** * Adds an animation event to the clip. @@ -63,7 +63,7 @@ export class AnimationClip extends EngineObject { */ addEvent(event: AnimationEvent): void; - addEvent(param: AnimationEvent | string, time?: number, parameter?: Object): void { + addEvent(param: AnimationEvent | string, time?: number, parameter?: object): void { let newEvent: AnimationEvent; if (typeof param === "string") { const event = new AnimationEvent(); diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index f58589955c..451384da8d 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -5,7 +5,7 @@ import { Entity } from "../Entity"; import { Renderer } from "../Renderer"; import { Script } from "../Script"; import { Logger } from "../base/Logger"; -import { assignmentClone, ignoreClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; import { AnimatorController } from "./AnimatorController"; import { AnimatorControllerLayer } from "./AnimatorControllerLayer"; import { AnimatorControllerParameter, AnimatorControllerParameterValue } from "./AnimatorControllerParameter"; @@ -37,10 +37,8 @@ export class Animator extends Component { /** Culling mode of this Animator. */ cullingMode: AnimatorCullingMode = AnimatorCullingMode.None; /** The playback speed of the Animator, 1.0 is normal playback speed. */ - @assignmentClone speed = 1.0; /** Whether the Animator sends AnimationEvent callbacks. */ - @assignmentClone fireEvents = true; /** @internal */ @@ -48,9 +46,7 @@ export class Animator extends Component { /** @internal */ _onUpdateIndex = -1; - @assignmentClone protected _animatorController: AnimatorController; - @ignoreClone protected _controllerUpdateFlag: BoolUpdateFlag; @ignoreClone protected _updateMark = 0; @@ -353,7 +349,6 @@ export class Animator extends Component { _cloneTo(target: Animator): void { const animatorController = target._animatorController; if (animatorController) { - target._addResourceReferCount(animatorController, 1); target._controllerUpdateFlag = animatorController._registerChangeFlag(); } } @@ -442,7 +437,7 @@ export class Animator extends Component { layerIndex: number ): void { const { entity, _curveOwnerPool: curveOwnerPool } = this; - let { mask } = this._animatorController.layers[layerIndex]; + const { mask } = this._animatorController.layers[layerIndex]; const { curveLayerOwner } = animatorStateData; const { _curveBindings: curves } = animatorState.clip; diff --git a/packages/core/src/animation/AnimatorController.ts b/packages/core/src/animation/AnimatorController.ts index 1f8e91a01c..cda119f6d0 100644 --- a/packages/core/src/animation/AnimatorController.ts +++ b/packages/core/src/animation/AnimatorController.ts @@ -85,7 +85,7 @@ export class AnimatorController extends ReferResource { */ clearParameters(): void { this._parameters.length = 0; - for (let name in this._parametersMap) { + for (const name in this._parametersMap) { delete this._parametersMap[name]; } } @@ -140,7 +140,7 @@ export class AnimatorController extends ReferResource { } layers.length = 0; - for (let name in this._layersMap) { + for (const name in this._layersMap) { delete this._layersMap[name]; } this._updateFlagManager.dispatch(); diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index b149e5609e..c0f5efd980 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -1,4 +1,4 @@ -import { assignmentClone, ignoreClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; import { Component } from "../Component"; import { Entity } from "../Entity"; import { AudioClip } from "./AudioClip"; @@ -16,7 +16,6 @@ export class AudioSource extends Component { @ignoreClone private _pendingPlay = false; - @assignmentClone private _clip: AudioClip; @ignoreClone private _gainNode: GainNode; @@ -28,13 +27,9 @@ export class AudioSource extends Component { @ignoreClone private _playTime = -1; - @assignmentClone private _volume = 1; - @assignmentClone private _lastVolume = 1; - @assignmentClone private _playbackRate = 1; - @assignmentClone private _loop = false; /** @@ -220,14 +215,6 @@ export class AudioSource extends Component { } } - /** - * @internal - */ - _cloneTo(target: AudioSource): void { - target._clip?._addReferCount(1); - // _volume is field-cloned; its gain node is applied lazily on first play - } - /** * @internal */ diff --git a/packages/core/src/base/DataObject.ts b/packages/core/src/base/DataObject.ts new file mode 100644 index 0000000000..01d2343692 --- /dev/null +++ b/packages/core/src/base/DataObject.ts @@ -0,0 +1,6 @@ +/** + * Base class of data objects: wherever an instance is held — a component field, an array, a map — + * cloning produces an independent deep copy instead of a shared reference. A subclass must be + * constructible without arguments; a preset-less copy is created bare, then populated. + */ +export abstract class DataObject {} diff --git a/packages/core/src/base/index.ts b/packages/core/src/base/index.ts index 74cd4ac148..2870931b65 100644 --- a/packages/core/src/base/index.ts +++ b/packages/core/src/base/index.ts @@ -1,6 +1,7 @@ export { EventDispatcher } from "./EventDispatcher"; export { Logger } from "./Logger"; export { Time } from "./Time"; +export { DataObject } from "./DataObject"; export { EngineObject } from "./EngineObject"; export * from "./Constant"; diff --git a/packages/core/src/clone/CloneManager.ts b/packages/core/src/clone/CloneManager.ts index e74b6cea57..0165be9914 100644 --- a/packages/core/src/clone/CloneManager.ts +++ b/packages/core/src/clone/CloneManager.ts @@ -1,289 +1,50 @@ -import { Entity } from "../Entity"; -import { ReferResource } from "../asset/ReferResource"; -import { TypedArray } from "../base/Constant"; -import { ICustomClone } from "./ComponentCloner"; import { CloneMode } from "./enums/CloneMode"; /** - * Property decorator, ignore the property when cloning. + * Property decorator — deep clone this field's whole subtree, overriding the value type's default + * clone mode (field-level decorators have the highest priority). The deep intent carries into the + * field's members; engine-bound members keep their defaults (assets share, entity refs remap). + * A decorator is an explicit intent: if the decorated value itself can't be deep cloned (an + * entity reference, an asset, or a function), cloning throws rather than silently falling back. */ -export function ignoreClone(target: object, propertyKey: string): void { - CloneManager.registerCloneMode(target, propertyKey, CloneMode.Ignore); +export function deepClone(target: object, propertyKey: string): void { + CloneManager._registerFieldMode(target, propertyKey, CloneMode.Deep); } /** - * Property decorator, assign value to the property when cloning. - * - * @remarks - * If it's a primitive type, the value will be copied. - * If it's a class type, the reference will be copied. + * Property decorator — assign (share the reference) this field, overriding the value type's default clone mode. */ export function assignmentClone(target: object, propertyKey: string): void { - CloneManager.registerCloneMode(target, propertyKey, CloneMode.Assignment); -} - -/** - * Property decorator, shallow clone the property when cloning. - * After cloning, it will keep its own reference independent, and use the method of assignment to clone all its internal properties. - * if the internal property is a primitive type, the value will be copied, if the internal property is a reference type, its reference address will be copied.。 - * - * @remarks - * Applicable to Object, Array, TypedArray and Class types. - */ -export function shallowClone(target: object, propertyKey: string): void { - CloneManager.registerCloneMode(target, propertyKey, CloneMode.Shallow); + CloneManager._registerFieldMode(target, propertyKey, CloneMode.Assignment); } /** - * Property decorator, deep clone the property when cloning. - * After cloning, it will maintain its own reference independence, and all its internal deep properties will remain completely independent. - * - * @remarks - * Applicable to Object, Array, TypedArray and Class types. - * If Class is encountered during the deep cloning process, the custom cloning function of the object will be called first. - * Custom cloning requires the object to implement the IClone interface. + * Property decorator — ignore this field when cloning; keep the clone's own constructor-built value. */ -export function deepClone(target: object, propertyKey: string): void { - CloneManager.registerCloneMode(target, propertyKey, CloneMode.Deep); +export function ignoreClone(target: object, propertyKey: string): void { + CloneManager._registerFieldMode(target, propertyKey, CloneMode.Ignore); } /** * @internal - * Clone manager. + * Field-level clone mode registry. Must import no engine class, directly or transitively: every + * class carrying a clone decorator imports this module while still being defined, and pulling a + * class in here would reorder module evaluation and break `extends` chains. Cloning itself lives + * in `CloneUtil`. */ export class CloneManager { - /** @internal */ - static _subCloneModeMap = new Map(); - /** @internal */ - static _cloneModeMap = new Map(); - - private static _objectType = Object.getPrototypeOf(Object); - - /** - * Register clone mode. - * @param target - Clone target - * @param propertyKey - Clone property name - * @param mode - Clone mode - */ - static registerCloneMode(target: object, propertyKey: string, mode: CloneMode): void { - let targetMap = CloneManager._subCloneModeMap.get(target.constructor); - if (!targetMap) { - targetMap = Object.create(null); - CloneManager._subCloneModeMap.set(target.constructor, targetMap); - } - targetMap[propertyKey] = mode; - } - - /** - * Get the clone mode according to the prototype chain. - */ - static getCloneMode(type: Function): object { - let cloneModes = CloneManager._cloneModeMap.get(type); - if (!cloneModes) { - cloneModes = Object.create(null); - CloneManager._cloneModeMap.set(type, cloneModes); - const objectType = CloneManager._objectType; - const cloneModeMap = CloneManager._subCloneModeMap; - while (type !== objectType) { - const subCloneModes = cloneModeMap.get(type); - if (subCloneModes) { - Object.assign(cloneModes, subCloneModes); - } - type = Object.getPrototypeOf(type); - } - } - return cloneModes; - } - - static cloneProperty( - source: object, - target: object, - k: string | number, - cloneMode: CloneMode, - srcRoot: Entity, - targetRoot: Entity, - deepInstanceMap: Map - ): void { - const sourceProperty = source[k]; - - // 1. Remappable references (Entity/Component) are always remapped, highest priority - if (sourceProperty instanceof Object && (sourceProperty)._remap) { - target[k] = (sourceProperty)._remap(srcRoot, targetRoot); - return; - } - - // 2. Explicit ignore - if (cloneMode === CloneMode.Ignore) return; - - // 3. Primitives / null / undefined - direct assign - if (!(sourceProperty instanceof Object)) { - target[k] = sourceProperty; - return; - } - - // 4. Determine effective clone mode - let effectiveCloneMode: CloneMode = cloneMode; - if (effectiveCloneMode === undefined) { - // Undecorated: infer from runtime type - effectiveCloneMode = CloneManager._inferCloneMode(sourceProperty, target[k]); - } else if (effectiveCloneMode !== CloneMode.Assignment) { - // Decorated Shallow/Deep: upgrade to Deep if target already has independent same-type instance. - // Assignment is never upgraded — it means the user explicitly wants a reference copy. - const targetProperty = target[k]; - if ( - targetProperty && - targetProperty !== sourceProperty && - targetProperty.constructor === sourceProperty.constructor - ) { - effectiveCloneMode = CloneMode.Deep; - } - } - - // 5. Assignment - direct reference copy - if (effectiveCloneMode === CloneMode.Assignment) { - target[k] = sourceProperty; - return; - } - - // 6. Shallow/Deep clone for complex types - const type = sourceProperty.constructor; - switch (type) { - case Uint8Array: - case Uint16Array: - case Uint32Array: - case Int8Array: - case Int16Array: - case Int32Array: - case Float32Array: - case Float64Array: - const targetPropertyT = target[k]; - if (targetPropertyT == null || targetPropertyT.length !== (sourceProperty).length) { - target[k] = (sourceProperty).slice(); - } else { - targetPropertyT.set(sourceProperty); - } - break; - case Map: - let targetPropertyM = >target[k]; - if (targetPropertyM == null) { - target[k] = targetPropertyM = new Map(); - } else { - targetPropertyM.clear(); - } - (>sourceProperty).forEach((value, key) => { - if (key instanceof Object && (key)._remap) { - key = (key)._remap(srcRoot, targetRoot); - } - if (value instanceof Object && (value)._remap) { - value = (value)._remap(srcRoot, targetRoot); - } - targetPropertyM.set(key, value); - }); - break; - case Set: - let targetPropertyS = >target[k]; - if (targetPropertyS == null) { - target[k] = targetPropertyS = new Set(); - } else { - targetPropertyS.clear(); - } - (>sourceProperty).forEach((value) => { - if (value instanceof Object && (value)._remap) { - value = (value)._remap(srcRoot, targetRoot); - } - targetPropertyS.add(value); - }); - break; - case Array: - let targetPropertyA = >target[k]; - const length = (>sourceProperty).length; - if (targetPropertyA == null) { - target[k] = targetPropertyA = new Array(length); - } else { - targetPropertyA.length = length; - } - for (let i = 0; i < length; i++) { - CloneManager.cloneProperty( - >sourceProperty, - targetPropertyA, - i, - cloneMode, // Pass original mode: decorated → children inherit, undecorated → children infer independently - srcRoot, - targetRoot, - deepInstanceMap - ); - } - break; - default: - // Check if we've already visited this source object (cycle detection) - if (deepInstanceMap.has(sourceProperty)) { - target[k] = deepInstanceMap.get(sourceProperty); - return; - } - - let targetPropertyD = target[k]; - if (!targetPropertyD) { - targetPropertyD = new sourceProperty.constructor(); - target[k] = targetPropertyD; - } - deepInstanceMap.set(sourceProperty, targetPropertyD); - - if ((sourceProperty).copyFrom) { - (targetPropertyD).copyFrom(sourceProperty); - } else { - const cloneModes = CloneManager.getCloneMode(sourceProperty.constructor); - for (const k in sourceProperty) { - CloneManager.cloneProperty( - sourceProperty, - targetPropertyD, - k, - cloneModes[k], - srcRoot, - targetRoot, - deepInstanceMap - ); - } - (sourceProperty)._cloneTo?.(targetPropertyD, srcRoot, targetRoot); - } - break; - } - } - /** - * Infer the appropriate clone mode for an undecorated property based on its runtime type. - * This enables user custom scripts to get correct clone behavior without decorators. + * @internal */ - private static _inferCloneMode(sourceProperty: object, targetProperty: any): CloneMode { - // If target already has an independent instance of the same type, - // deep clone to preserve isolation (e.g., constructor-created objects) - if ( - targetProperty && - targetProperty !== sourceProperty && - targetProperty.constructor === sourceProperty.constructor - ) { - return CloneMode.Deep; - } - - // Arrays need recursive processing (may contain Entity/Component refs) - if (Array.isArray(sourceProperty)) return CloneMode.Deep; - - // TypedArrays - copy data - if (ArrayBuffer.isView(sourceProperty)) return CloneMode.Deep; - - // Maps and Sets - create independent copies - if (sourceProperty instanceof Map || sourceProperty instanceof Set) return CloneMode.Deep; - - // Value types with copyFrom (math types like Vector3, Color, etc.) - if ((sourceProperty).copyFrom) return CloneMode.Deep; - - // Engine resources are reference-counted and shared. All other user/value - // classes are cloned so prefab instances do not share mutable state. - return sourceProperty instanceof ReferResource ? CloneMode.Assignment : CloneMode.Deep; - } - - static deepCloneObject(source: object, target: object, deepInstanceMap: Map): void { - for (const k in source) { - CloneManager.cloneProperty(source, target, k, CloneMode.Deep, null, null, deepInstanceMap); - } + static _registerFieldMode(target: any, propertyKey: string, mode: CloneMode): void { + // Each class gets its own `_fieldModes`, prototypally chained to its parent's, so property + // lookup resolves inheritance: a subclass re-decorating a field shadows the ancestor's. + if (!Object.prototype.hasOwnProperty.call(target, "_fieldModes")) { + Object.defineProperty(target, "_fieldModes", { + value: Object.create(target._fieldModes ?? null), + configurable: true + }); + } + target._fieldModes[propertyKey] = mode; } } diff --git a/packages/core/src/clone/CloneUtil.ts b/packages/core/src/clone/CloneUtil.ts new file mode 100644 index 0000000000..cd06d6b451 --- /dev/null +++ b/packages/core/src/clone/CloneUtil.ts @@ -0,0 +1,277 @@ +import { IReferable } from "../asset/IReferable"; +import { ReferResource } from "../asset/ReferResource"; +import { TypedArray } from "../base/Constant"; +import { DataObject } from "../base/DataObject"; +import { Logger } from "../base/Logger"; +import { Component } from "../Component"; +import { Entity } from "../Entity"; +import { UpdateFlag } from "../UpdateFlag"; +import { UpdateFlagManager } from "../UpdateFlagManager"; +import { DisorderedArray } from "../utils/DisorderedArray"; +import { SafeLoopArray } from "../utils/SafeLoopArray"; +import { ICustomClone } from "./ComponentCloner"; +import { CloneMode } from "./enums/CloneMode"; + +/** + * @internal + * Split from `CloneManager`, which must stay free of engine imports. + */ +export class CloneUtil { + /** + * @internal + */ + static _deepCloneObject(source: any, target: object, cloneMap: Map, forceDeepClone = false): void { + const fieldModes = source._fieldModes; + const keys = Object.keys(source); + for (let i = 0, n = keys.length; i < n; i++) { + const k = keys[i]; + const fieldMode = fieldModes?.[k]; + if (fieldMode === CloneMode.Ignore) continue; + target[k] = CloneUtil._cloneValue(source[k], target[k], cloneMap, fieldMode, forceDeepClone); + } + } + + /** + * @internal + */ + static _cloneValue( + source: any, + preset: any, + cloneMap: Map, + fieldMode?: CloneMode, + forceDeepClone = false + ): any { + if (fieldMode === CloneMode.Assignment) return source; + if (fieldMode === CloneMode.Deep) { + CloneUtil._assertDeepCloneable(source); + forceDeepClone = true; + } + return CloneUtil._cloneByDefault(source, preset, cloneMap, forceDeepClone); + } + + /** + * @internal + */ + static _cloneByDefault(source: any, preset: any, cloneMap: Map, forceDeepClone = false): any { + if (typeof source === "function") return forceDeepClone ? source : typeof preset === "function" ? preset : source; + if (source === null || typeof source !== "object") return source; + if (source instanceof Entity || source instanceof Component) return cloneMap.get(source) ?? source; + if (source instanceof ReferResource) return source; + if (source instanceof UpdateFlagManager || source instanceof UpdateFlag) return preset; + if (ArrayBuffer.isView(source)) return CloneUtil._deepCloneArrayBuffer(source, preset, cloneMap); + if (Array.isArray(source)) return CloneUtil._deepCloneArray(source, preset, cloneMap, forceDeepClone); + if (source instanceof Map) return CloneUtil._deepCloneMap(source, preset, cloneMap, forceDeepClone); + if (source instanceof Set) return CloneUtil._deepCloneSet(source, preset, cloneMap, forceDeepClone); + if (source instanceof DisorderedArray || source instanceof SafeLoopArray) { + if (!forceDeepClone) return preset; + const existing = cloneMap.get(source); + if (existing) return existing; + const dst = CloneUtil._createCloneTarget(source, preset, cloneMap); + CloneUtil._deepCloneObject(source, dst, cloneMap, true); + return dst; + } + + const ctor = (source).constructor; + if (ctor && ctor !== Object && typeof (source).copyFrom === "function") { + const existing = cloneMap.get(source); + if (existing) return existing; + const dst = CloneUtil._createCloneTarget(source, preset, cloneMap); + (dst).copyFrom(source); + (source)._cloneTo?.(dst, cloneMap); + return dst; + } + + if (source instanceof DataObject || ctor === Object || ctor === undefined || forceDeepClone) { + const existing = cloneMap.get(source); + if (existing) return existing; + const dst = CloneUtil._createCloneTarget(source, preset, cloneMap); + CloneUtil._deepCloneObject(source, dst, cloneMap, forceDeepClone); + (source)._cloneTo?.(dst, cloneMap); + return dst; + } + return source; + } + + /** + * @internal + */ + static _assertDeepCloneable(source: any): void { + if (typeof source === "function") { + throw new Error( + `CloneUtil: @deepClone cannot deep clone a function — code is not a cloneable graph. ` + + `Remove @deepClone to keep the clone's own binding.` + ); + } + if (source instanceof Entity || source instanceof Component) { + throw new Error( + `CloneUtil: @deepClone cannot deep clone "${source.constructor.name}" — Entity / Component ` + + `references are engine-bound. Remove @deepClone to remap the reference by default.` + ); + } + if (source instanceof ReferResource) { + throw new Error( + `CloneUtil: @deepClone cannot deep clone "${source.constructor.name}" — assets are engine-bound ` + + `and shared by reference. Remove @deepClone to share it, or copy it via the asset's own clone() API.` + ); + } + if (source instanceof UpdateFlagManager || source instanceof UpdateFlag) { + throw new Error( + `CloneUtil: @deepClone cannot deep clone "${source.constructor.name}" — a flag and its manager hold ` + + `each other, and a field copy resolves neither side, leaving the pair inconsistent. Remove ` + + `@deepClone to keep the clone's own.` + ); + } + } + + /** + * @internal + */ + static _createCloneTarget(source: any, preset: any, cloneMap: Map): any { + const ctor = (source).constructor; + const reusable = preset && preset !== source && preset.constructor === ctor ? preset : null; + let dst: any; + if (reusable) { + dst = reusable; + } else { + if (ctor) { + try { + dst = new ctor(); + } catch (e) { + throw new Error( + `CloneUtil: failed to bare-construct "${ctor.name}" — a type cloned deep must support ` + + `argument-less construction (the gate creates preset-less instances bare, then populates fields). ` + + `Cause: ${e}` + ); + } + } else { + dst = Object.create(null); + } + } + cloneMap.set(source, dst); + return dst; + } + + /** + * @internal + */ + static _transferSlotOwnership(cloned: any, source: any, preset: any): void { + if (cloned === preset) return; + if (preset instanceof ReferResource) { + const presetRefCount = (<{ refCount?: number }>preset).refCount; + presetRefCount !== undefined && + presetRefCount <= 0 && + Logger.error( + `CloneUtil: the clone's preset ${preset.constructor.name} holds no owned reference; ` + + `a constructor presetting a ref-counted resource must acquire it (assign via its setter or an explicit +1).` + ); + (preset)._addReferCount(-1); + } + if (cloned === source && cloned instanceof ReferResource) { + (cloned)._addReferCount(1); + } + } + + /** + * @internal + */ + static _deepCloneArrayBuffer(source: ArrayBufferView, preset: any, cloneMap: Map): ArrayBufferView { + const existing = cloneMap.get(source); + if (existing) return existing; + + let dst: ArrayBufferView; + if (source instanceof DataView) { + if (preset instanceof DataView && preset !== source && preset.byteLength === source.byteLength) { + new Uint8Array(preset.buffer, preset.byteOffset, preset.byteLength).set( + new Uint8Array(source.buffer, source.byteOffset, source.byteLength) + ); + dst = preset; + } else { + dst = new DataView(source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength)); + } + } else { + const src = source; + if ( + preset && + preset !== source && + preset.constructor === src.constructor && + (preset).length === src.length + ) { + (preset).set(src); + dst = preset; + } else { + dst = src.slice(); + } + } + cloneMap.set(source, dst); + return dst; + } + + /** + * @internal + */ + static _deepCloneArray(source: any[], preset: any, cloneMap: Map, forceDeepClone = false): any[] { + const existing = cloneMap.get(source); + if (existing) return existing; + + const dst = + preset !== source && + Array.isArray(preset) && + preset.constructor === source.constructor && + preset.length === source.length + ? preset + : new Array(source.length); + cloneMap.set(source, dst); + for (let i = 0, n = source.length; i < n; i++) { + dst[i] = CloneUtil._cloneByDefault(source[i], undefined, cloneMap, forceDeepClone); + } + return dst; + } + + /** + * @internal + */ + static _deepCloneMap( + source: Map, + preset: any, + cloneMap: Map, + forceDeepClone = false + ): Map { + const existing = cloneMap.get(source); + if (existing) return >existing; + + let dst: Map; + if (preset instanceof Map && preset !== source && preset.constructor === source.constructor) { + preset.clear(); + dst = preset; + } else { + dst = new Map(); + } + cloneMap.set(source, dst); + for (const entry of source) { + dst.set( + CloneUtil._cloneByDefault(entry[0], undefined, cloneMap, forceDeepClone), + CloneUtil._cloneByDefault(entry[1], undefined, cloneMap, forceDeepClone) + ); + } + return dst; + } + + /** + * @internal + */ + static _deepCloneSet(source: Set, preset: any, cloneMap: Map, forceDeepClone = false): Set { + const existing = cloneMap.get(source); + if (existing) return >existing; + + let dst: Set; + if (preset instanceof Set && preset !== source && preset.constructor === source.constructor) { + preset.clear(); + dst = preset; + } else { + dst = new Set(); + } + cloneMap.set(source, dst); + for (const v of source) dst.add(CloneUtil._cloneByDefault(v, undefined, cloneMap, forceDeepClone)); + return dst; + } +} diff --git a/packages/core/src/clone/CloneUtils.ts b/packages/core/src/clone/CloneUtils.ts deleted file mode 100644 index bded5ed474..0000000000 --- a/packages/core/src/clone/CloneUtils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component } from "../Component"; -import { Entity } from "../Entity"; - -/** - * @internal - * Utility functions for remapping Entity/Component references during cloning. - */ -export class CloneUtils { - private static _tempRemapPath: number[] = []; - - static remapEntity(srcRoot: Entity, targetRoot: Entity, entity: Entity): Entity { - const path = CloneUtils._tempRemapPath; - if (!CloneUtils._getEntityHierarchyPath(srcRoot, entity, path)) return entity; - return CloneUtils._getEntityByHierarchyPath(targetRoot, path); - } - - static remapComponent(srcRoot: Entity, targetRoot: Entity, component: T): T { - const path = CloneUtils._tempRemapPath; - const srcEntity = component.entity; - if (!CloneUtils._getEntityHierarchyPath(srcRoot, srcEntity, path)) return component; - return CloneUtils._getEntityByHierarchyPath(targetRoot, path)._components[ - srcEntity._components.indexOf(component) - ] as T; - } - - private static _getEntityHierarchyPath(rootEntity: Entity, searchEntity: Entity, inversePath: number[]): boolean { - inversePath.length = 0; - while (searchEntity !== rootEntity) { - const parent = searchEntity.parent; - if (!parent) { - return false; - } - inversePath.push(searchEntity.siblingIndex); - searchEntity = parent; - } - return true; - } - - private static _getEntityByHierarchyPath(rootEntity: Entity, inversePath: number[]): Entity { - let entity = rootEntity; - for (let i = inversePath.length - 1; i >= 0; i--) { - entity = entity.children[inversePath[i]]; - } - return entity; - } -} diff --git a/packages/core/src/clone/ComponentCloner.ts b/packages/core/src/clone/ComponentCloner.ts index f4e1b11651..b871e4c799 100644 --- a/packages/core/src/clone/ComponentCloner.ts +++ b/packages/core/src/clone/ComponentCloner.ts @@ -1,42 +1,42 @@ import { Component } from "../Component"; -import { Entity } from "../Entity"; -import { CloneManager } from "./CloneManager"; +import { CloneUtil } from "./CloneUtil"; +import { CloneMode } from "./enums/CloneMode"; /** - * Custom clone interface. + * Clone protocol read by the clone system; every member is optional. */ export interface ICustomClone { /** * @internal + * Post-clone hook; `cloneMap` maps every source object in the cloned subtree to its clone. */ - _remap?(srcRoot: Entity, targetRoot: Entity): Object; - /** - * @internal - */ - _cloneTo?(target: ICustomClone, srcRoot?: Entity, targetRoot?: Entity): void; + _cloneTo?(target: ICustomClone, cloneMap?: Map): void; /** * @internal + * Value-type marker — the gate copies via this instead of walking fields. */ copyFrom?(source: ICustomClone): void; } export class ComponentCloner { /** - * Clone component. + * Clone component (opt-out: all fields cloned except @ignoreClone), then run its `_cloneTo` hook. * @param source - Clone source * @param target - Clone target + * @param cloneMap - Identity map of the cloned subtree (source object → clone) */ - static cloneComponent( - source: Component, - target: Component, - srcRoot: Entity, - targetRoot: Entity, - deepInstanceMap: Map - ): void { - const cloneModes = CloneManager.getCloneMode(source.constructor); - for (let k in source) { - CloneManager.cloneProperty(source, target, k, cloneModes[k], srcRoot, targetRoot, deepInstanceMap); + static cloneComponent(source: Component, target: Component, cloneMap: Map): void { + const fieldModes = (source)._fieldModes; + const keys = Object.keys(source); + for (let i = 0, n = keys.length; i < n; i++) { + const k = keys[i]; + const fieldMode = fieldModes?.[k]; + if (fieldMode === CloneMode.Ignore) continue; + const sourceValue = source[k]; + const preset = target[k]; + const cloned = (target[k] = CloneUtil._cloneValue(sourceValue, preset, cloneMap, fieldMode)); + CloneUtil._transferSlotOwnership(cloned, sourceValue, preset); } - ((source as unknown))._cloneTo?.(target, srcRoot, targetRoot); + ((source as unknown))._cloneTo?.(target, cloneMap); } } diff --git a/packages/core/src/clone/enums/CloneMode.ts b/packages/core/src/clone/enums/CloneMode.ts index 77158ce3ce..b4e41ad31a 100644 --- a/packages/core/src/clone/enums/CloneMode.ts +++ b/packages/core/src/clone/enums/CloneMode.ts @@ -1,13 +1,15 @@ /** - * Clone mode. + * How a field is cloned when a clone decorator overrides the built-in default for its type. */ export enum CloneMode { - /** Ignore clone. */ + /** Skip — keep the clone's own constructor-built value (for runtime / transient state). */ Ignore, - /** Assignment clone. */ + /** Share the reference; a counted resource shared at a component's top-level slot is kept alive by the clone. */ Assignment, - /** Shallow clone. */ - Shallow, - /** Deep clone. */ + /** + * Deep clone the whole subtree (fresh containers/instances, the intent carries into members); + * engine-bound members keep their defaults — assets stay shared, entity refs remap, runtime + * state keeps the clone's own. + */ Deep } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index be483eccbd..5e83b9259b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -69,8 +69,7 @@ export * from "./trail/index"; export * from "./env-probe/index"; export * from "./shader/index"; export * from "./Layer"; -export * from "./clone/CloneManager"; -export { CloneUtils } from "./clone/CloneUtils"; +export { deepClone, assignmentClone, ignoreClone } from "./clone/CloneManager"; export * from "./renderingHardwareInterface/index"; export * from "./physics/index"; export * from "./Utils"; diff --git a/packages/core/src/lighting/Light.ts b/packages/core/src/lighting/Light.ts index 5406de6243..319a489d95 100644 --- a/packages/core/src/lighting/Light.ts +++ b/packages/core/src/lighting/Light.ts @@ -1,7 +1,7 @@ import { Color, MathUtil, Matrix } from "@galacean/engine-math"; import { Component } from "../Component"; import { Layer } from "../Layer"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; import { ShadowType } from "../shadow"; /** @@ -32,7 +32,6 @@ export abstract class Light extends Component { _lightIndex = -1; private _shadowStrength = 1.0; - @deepClone private _color = new Color(1, 1, 1, 1); @ignoreClone private _viewMat: Matrix; diff --git a/packages/core/src/mesh/Skin.ts b/packages/core/src/mesh/Skin.ts index 18489805c5..673632acf4 100644 --- a/packages/core/src/mesh/Skin.ts +++ b/packages/core/src/mesh/Skin.ts @@ -2,27 +2,23 @@ import { Matrix } from "@galacean/engine-math"; import { Entity } from "../Entity"; import { UpdateFlagManager } from "../UpdateFlagManager"; import { Utils } from "../Utils"; -import { EngineObject } from "../base/EngineObject"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { DataObject } from "../base/DataObject"; +import { ignoreClone } from "../clone/CloneManager"; import { SkinnedMeshRenderer } from "./SkinnedMeshRenderer"; /** * Skin used for skinned mesh renderer. */ -export class Skin extends EngineObject { +export class Skin extends DataObject { /** Inverse bind matrices. */ - @deepClone inverseBindMatrices = new Array(); /** @internal */ - @deepClone _skinMatrices: Float32Array; /** @internal */ - @ignoreClone _updatedManager = new UpdateFlagManager(); private _rootBone: Entity; - @deepClone private _bones = new Array(); @ignoreClone private _updateMark = -1; @@ -65,7 +61,7 @@ export class Skin extends EngineObject { } constructor(public name: string) { - super(null); + super(); } /** diff --git a/packages/core/src/mesh/SkinnedMeshRenderer.ts b/packages/core/src/mesh/SkinnedMeshRenderer.ts index 4d7f726184..7059cb20f3 100644 --- a/packages/core/src/mesh/SkinnedMeshRenderer.ts +++ b/packages/core/src/mesh/SkinnedMeshRenderer.ts @@ -4,7 +4,7 @@ import { RenderContext } from "../RenderPipeline/RenderContext"; import { RenderElement } from "../RenderPipeline/RenderElement"; import { RendererUpdateFlags } from "../Renderer"; import { Logger } from "../base/Logger"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; import { ShaderProperty } from "../shader"; import { Texture2D } from "../texture/Texture2D"; import { TextureFilterMode } from "../texture/enums/TextureFilterMode"; @@ -29,12 +29,10 @@ export class SkinnedMeshRenderer extends MeshRenderer { @ignoreClone _condensedBlendShapeWeights: Float32Array; - @deepClone private _localBounds: BoundingBox = new BoundingBox(); @ignoreClone private _jointDataCreateCache: Vector2 = new Vector2(-1, -1); - @ignoreClone private _blendShapeWeights: Float32Array; @ignoreClone private _maxVertexUniformVectors: number; @@ -42,7 +40,6 @@ export class SkinnedMeshRenderer extends MeshRenderer { @ignoreClone private _jointTexture: Texture2D; - @deepClone private _skin: Skin; /** @@ -146,12 +143,12 @@ export class SkinnedMeshRenderer extends MeshRenderer { */ override _cloneTo(target: SkinnedMeshRenderer): void { super._cloneTo(target); - + if (this._jointTexture) { + target.shaderData.setTexture(SkinnedMeshRenderer._jointSamplerProperty, null); + } if (this.skin) { target._applySkin(null, target.skin); } - - this._blendShapeWeights && (target._blendShapeWeights = this._blendShapeWeights.slice()); } protected override _update(context: RenderContext): void { @@ -256,7 +253,7 @@ export class SkinnedMeshRenderer extends MeshRenderer { } @ignoreClone - private _onSkinUpdated(type: SkinUpdateFlag, value: Object): void { + private _onSkinUpdated(type: SkinUpdateFlag, value: number | Entity): void { switch (type) { case SkinUpdateFlag.BoneCountChanged: const shaderData = this.shaderData; diff --git a/packages/core/src/particle/ParticleGenerator.ts b/packages/core/src/particle/ParticleGenerator.ts index e96070f56c..1a279f6e84 100644 --- a/packages/core/src/particle/ParticleGenerator.ts +++ b/packages/core/src/particle/ParticleGenerator.ts @@ -1,6 +1,7 @@ +import { DataObject } from "../base/DataObject"; import { BoundingBox, Color, MathUtil, Matrix, Quaternion, Vector2, Vector3 } from "@galacean/engine-math"; import { Transform } from "../Transform"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; import { Primitive } from "../graphic/Primitive"; import { SubMesh } from "../graphic/SubMesh"; import { SubPrimitive } from "../graphic/SubPrimitive"; @@ -42,7 +43,7 @@ import { SubEmittersModule } from "./modules/SubEmittersModule"; /** * Particle Generator. */ -export class ParticleGenerator { +export class ParticleGenerator extends DataObject { private static _tempVector20 = new Vector2(); private static _tempVector21 = new Vector2(); private static _tempVector22 = new Vector2(); @@ -63,40 +64,28 @@ export class ParticleGenerator { useAutoRandomSeed = true; /** Main module. */ - @deepClone readonly main: MainModule; /** Emission module. */ - @deepClone readonly emission = new EmissionModule(this); /** Velocity over lifetime module. */ - @deepClone readonly velocityOverLifetime: VelocityOverLifetimeModule; /** Force over lifetime module. */ - @deepClone readonly forceOverLifetime: ForceOverLifetimeModule; /** Limit velocity over lifetime module. */ - @deepClone readonly limitVelocityOverLifetime: LimitVelocityOverLifetimeModule; /** Size over lifetime module. */ - @deepClone readonly sizeOverLifetime: SizeOverLifetimeModule; /** Rotation over lifetime module. */ - @deepClone readonly rotationOverLifetime = new RotationOverLifetimeModule(this); /** Color over lifetime module. */ - @deepClone readonly colorOverLifetime = new ColorOverLifetimeModule(this); /** Texture sheet animation module. */ - @deepClone readonly textureSheetAnimation = new TextureSheetAnimationModule(this); /** Noise module. */ - @deepClone readonly noise: NoiseModule; /** Sub emitters module. */ - @deepClone readonly subEmitters: SubEmittersModule; /** Custom data module. */ - @deepClone readonly customData: CustomDataModule; /** @internal */ @@ -210,6 +199,7 @@ export class ParticleGenerator { * @internal */ constructor(renderer: ParticleRenderer) { + super(); this._renderer = renderer; const subPrimitive = new SubPrimitive(); subPrimitive.start = 0; diff --git a/packages/core/src/particle/ParticleRenderer.ts b/packages/core/src/particle/ParticleRenderer.ts index 47324c6e75..e73d8365f4 100644 --- a/packages/core/src/particle/ParticleRenderer.ts +++ b/packages/core/src/particle/ParticleRenderer.ts @@ -5,7 +5,7 @@ import { Renderer, RendererUpdateFlags } from "../Renderer"; import { TransformModifyFlags } from "../Transform"; import { GLCapabilityType } from "../base/Constant"; import { Logger } from "../base/Logger"; -import { deepClone, ignoreClone, shallowClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; import { ModelMesh } from "../mesh/ModelMesh"; import { ShaderMacro } from "../shader/ShaderMacro"; import { ShaderProperty } from "../shader/ShaderProperty"; @@ -30,14 +30,12 @@ export class ParticleRenderer extends Renderer { private static readonly _currentTime = ShaderProperty.getByName("renderer_CurrentTime"); /** Particle generator. */ - @deepClone readonly generator: ParticleGenerator; /** Specifies how much particles stretch depending on their velocity. */ velocityScale = 0; /** How much are the particles stretched in their direction of motion, defined as the length of the particle compared to its width. */ lengthScale = 2; /** The pivot of particle. */ - @shallowClone pivot = new Vector3(); /** @internal */ diff --git a/packages/core/src/particle/modules/Burst.ts b/packages/core/src/particle/modules/Burst.ts index a444381dc2..b885ca12c4 100644 --- a/packages/core/src/particle/modules/Burst.ts +++ b/packages/core/src/particle/modules/Burst.ts @@ -1,12 +1,11 @@ -import { deepClone } from "../../clone/CloneManager"; +import { DataObject } from "../../base/DataObject"; import { ParticleCompositeCurve } from "./ParticleCompositeCurve"; /** * A burst is a particle emission event, where a number of particles are all emitted at the same time */ -export class Burst { +export class Burst extends DataObject { public time: number; - @deepClone public count: ParticleCompositeCurve; private _cycles: number; @@ -49,6 +48,7 @@ export class Burst { */ constructor(time: number, count: ParticleCompositeCurve, cycles: number, repeatInterval: number); constructor(time: number, count: ParticleCompositeCurve, cycles?: number, repeatInterval?: number) { + super(); this.time = time; this.count = count; this._cycles = Math.max(cycles ?? 1, 1); diff --git a/packages/core/src/particle/modules/ColorOverLifetimeModule.ts b/packages/core/src/particle/modules/ColorOverLifetimeModule.ts index c920fb3fb4..53a9da55b1 100644 --- a/packages/core/src/particle/modules/ColorOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/ColorOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Color, Rand, Vector4 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderMacro } from "../../shader/ShaderMacro"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -23,7 +23,6 @@ export class ColorOverLifetimeModule extends ParticleGeneratorModule { static readonly _gradientKeysCount = ShaderProperty.getByName("renderer_COLGradientKeysMaxTime"); /** Color gradient over lifetime. */ - @deepClone color = new ParticleCompositeGradient( new ParticleGradient( [new GradientColorKey(0.0, new Color(1, 1, 1)), new GradientColorKey(1.0, new Color(1, 1, 1))], diff --git a/packages/core/src/particle/modules/CustomDataModule.ts b/packages/core/src/particle/modules/CustomDataModule.ts index 4ad2fa6fc9..3b9ecb91ea 100644 --- a/packages/core/src/particle/modules/CustomDataModule.ts +++ b/packages/core/src/particle/modules/CustomDataModule.ts @@ -1,5 +1,4 @@ import { Color, Vector4 } from "@galacean/engine-math"; -import { CloneManager, ignoreClone } from "../../clone/CloneManager"; import { Logger } from "../../base/Logger"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -53,14 +52,10 @@ export class CustomDataModule extends ParticleGeneratorModule { private static readonly _zeroColor = new Color(0, 0, 0, 0); private static readonly _zeroVector4 = new Vector4(0, 0, 0, 0); - @ignoreClone private _curves: Map = new Map(); - @ignoreClone private _gradients: Map = new Map(); - @ignoreClone private _curveStreams: CurveStream[] = []; - @ignoreClone private _gradientStreams: GradientStream[] = []; /** @@ -186,24 +181,6 @@ export class CustomDataModule extends ParticleGeneratorModule { this._gradients.delete(name); } - /** - * @internal - */ - _cloneTo(target: CustomDataModule): void { - // Shared across both loops so cross-entry sub-object references stay shared in the clone. - const deepInstanceMap = new Map(); - for (const [name, curve] of this._curves) { - const clonedCurve = new ParticleCompositeCurve(0); - CloneManager.deepCloneObject(curve, clonedCurve, deepInstanceMap); - target.addCurve(name, clonedCurve); - } - for (const [name, gradient] of this._gradients) { - const clonedGradient = new ParticleCompositeGradient(new Color()); - CloneManager.deepCloneObject(gradient, clonedGradient, deepInstanceMap); - target.addGradient(name, clonedGradient); - } - } - /** * @internal */ diff --git a/packages/core/src/particle/modules/EmissionModule.ts b/packages/core/src/particle/modules/EmissionModule.ts index a2d303a7d3..9e0cad5086 100644 --- a/packages/core/src/particle/modules/EmissionModule.ts +++ b/packages/core/src/particle/modules/EmissionModule.ts @@ -1,5 +1,5 @@ import { MathUtil, Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro } from "../../shader"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds"; @@ -19,13 +19,10 @@ export class EmissionModule extends ParticleGeneratorModule { private static _tempEmitPosition = new Vector3(); /** The rate of particle emission. */ - @deepClone rateOverTime: ParticleCompositeCurve = new ParticleCompositeCurve(10); /** The rate at which the emitter spawns new particles over distance. */ - @deepClone rateOverDistance: ParticleCompositeCurve = new ParticleCompositeCurve(0); - @deepClone _shape: BaseShape; /** @internal */ @ignoreClone @@ -46,7 +43,6 @@ export class EmissionModule extends ParticleGeneratorModule { @ignoreClone private _hasLastEmitPosition = false; - @deepClone private _bursts: Burst[] = []; private _currentBurstIndex = 0; @@ -172,7 +168,11 @@ export class EmissionModule extends ParticleGeneratorModule { * @internal */ _destroy(): void { - this._shape?._unRegisterOnValueChanged(this._generator._renderer._onGeneratorParamsChanged); + const shape = this._shape; + if (shape) { + shape._unRegisterOnValueChanged(this._generator._renderer._onGeneratorParamsChanged); + shape._destroy(); + } } private _emitByRateOverTime(playTime: number): void { diff --git a/packages/core/src/particle/modules/ForceOverLifetimeModule.ts b/packages/core/src/particle/modules/ForceOverLifetimeModule.ts index f96ce211c7..ce7c533c0f 100644 --- a/packages/core/src/particle/modules/ForceOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/ForceOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds"; @@ -39,11 +39,8 @@ export class ForceOverLifetimeModule extends ParticleGeneratorModule { @ignoreClone private _randomModeMacro: ShaderMacro; - @deepClone private _forceX: ParticleCompositeCurve; - @deepClone private _forceY: ParticleCompositeCurve; - @deepClone private _forceZ: ParticleCompositeCurve; private _space = ParticleSimulationSpace.Local; diff --git a/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts b/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts index 9d6a91bc54..5019273b03 100644 --- a/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector2, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro } from "../../shader"; import { ShaderProperty } from "../../shader/ShaderProperty"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; @@ -70,14 +70,10 @@ export class LimitVelocityOverLifetimeModule extends ParticleGeneratorModule { private _dragVelocityMacro: ShaderMacro; private _separateAxes = false; - @deepClone private _speedX: ParticleCompositeCurve; - @deepClone private _speedY: ParticleCompositeCurve; - @deepClone private _speedZ: ParticleCompositeCurve; private _dampen: number = 0; - @deepClone private _drag: ParticleCompositeCurve; private _multiplyDragByParticleSize = false; private _multiplyDragByParticleVelocity = false; diff --git a/packages/core/src/particle/modules/MainModule.ts b/packages/core/src/particle/modules/MainModule.ts index d17c156d9a..c736dd046a 100644 --- a/packages/core/src/particle/modules/MainModule.ts +++ b/packages/core/src/particle/modules/MainModule.ts @@ -1,6 +1,7 @@ +import { DataObject } from "../../base/DataObject"; import { Color, Rand, Vector3, Vector4 } from "@galacean/engine-math"; import { TransformModifyFlags } from "../../Transform"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ICustomClone } from "../../clone/ComponentCloner"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -11,7 +12,7 @@ import { ParticleSimulationSpace } from "../enums/ParticleSimulationSpace"; import { ParticleCompositeCurve } from "./ParticleCompositeCurve"; import { ParticleCompositeGradient } from "./ParticleCompositeGradient"; -export class MainModule implements ICustomClone { +export class MainModule extends DataObject implements ICustomClone { private _tempVector40 = new Vector4(); private static _vector3One = new Vector3(1, 1, 1); @@ -30,25 +31,20 @@ export class MainModule implements ICustomClone { isLoop = true; /** Start delay in seconds. */ - @deepClone startDelay = new ParticleCompositeCurve(0); /** A flag to enable 3D particle rotation, when disabled, only `startRotationZ` is used. */ startRotation3D = false; /** The initial rotation of particles around the x-axis when emitted, in degrees. */ - @deepClone startRotationX = new ParticleCompositeCurve(0); /** The initial rotation of particles around the y-axis when emitted, in degrees. */ - @deepClone startRotationY = new ParticleCompositeCurve(0); /** The initial rotation of particles around the z-axis when emitted, in degrees. */ - @deepClone startRotationZ = new ParticleCompositeCurve(0); /** Makes some particles spin in the opposite direction. */ flipRotation = 0; /** The mode of start color */ - @deepClone startColor = new ParticleCompositeGradient(new Color(1, 1, 1, 1)); /** A scale that this Particle Generator applies to gravity, defined by Physics.gravity. */ /** Override the default playback speed of the Particle Generator. */ @@ -59,7 +55,6 @@ export class MainModule implements ICustomClone { playOnEnabled = true; /** @internal */ - @ignoreClone _maxParticleBuffer = 1000; /** @internal */ @ignoreClone @@ -83,18 +78,12 @@ export class MainModule implements ICustomClone { @ignoreClone readonly _gravityModifierRand = new Rand(0, ParticleRandomSubSeeds.GravityModifier); - @deepClone private _startLifetime: ParticleCompositeCurve; - @deepClone private _startSpeed: ParticleCompositeCurve; private _startSize3D = false; - @deepClone private _startSizeX: ParticleCompositeCurve; - @deepClone private _startSizeY: ParticleCompositeCurve; - @deepClone private _startSizeZ: ParticleCompositeCurve; - @deepClone private _gravityModifier: ParticleCompositeCurve; private _simulationSpace = ParticleSimulationSpace.Local; @ignoreClone @@ -250,6 +239,7 @@ export class MainModule implements ICustomClone { * @internal */ constructor(generator: ParticleGenerator) { + super(); this._generator = generator; this.startLifetime = new ParticleCompositeCurve(5); @@ -332,8 +322,6 @@ export class MainModule implements ICustomClone { * @internal */ _cloneTo(target: MainModule): void { - target.maxParticles = this.maxParticles; - if (target._simulationSpace === ParticleSimulationSpace.World) { target._generator._generateTransformedBounds(); } diff --git a/packages/core/src/particle/modules/NoiseModule.ts b/packages/core/src/particle/modules/NoiseModule.ts index 153a40b5b9..46d2a7b5ca 100644 --- a/packages/core/src/particle/modules/NoiseModule.ts +++ b/packages/core/src/particle/modules/NoiseModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector3, Vector4 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader"; import { ParticleGenerator } from "../ParticleGenerator"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; @@ -47,11 +47,8 @@ export class NoiseModule extends ParticleGeneratorModule { @ignoreClone private _strengthMinConst = new Vector3(); - @deepClone private _strengthX: ParticleCompositeCurve; - @deepClone private _strengthY: ParticleCompositeCurve; - @deepClone private _strengthZ: ParticleCompositeCurve; private _scrollSpeed = 0; private _separateAxes = false; diff --git a/packages/core/src/particle/modules/ParticleCompositeCurve.ts b/packages/core/src/particle/modules/ParticleCompositeCurve.ts index 00de9f0291..0e761f6ff0 100644 --- a/packages/core/src/particle/modules/ParticleCompositeCurve.ts +++ b/packages/core/src/particle/modules/ParticleCompositeCurve.ts @@ -1,5 +1,6 @@ +import { DataObject } from "../../base/DataObject"; import { Vector2 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { UpdateFlagManager } from "../../UpdateFlagManager"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; import { CurveKey, ParticleCurve } from "./ParticleCurve"; @@ -7,17 +8,14 @@ import { CurveKey, ParticleCurve } from "./ParticleCurve"; /** * Particle composite curve. */ -export class ParticleCompositeCurve { +export class ParticleCompositeCurve extends DataObject { private static _minMaxRange = new Vector2(); - @ignoreClone private _updateManager = new UpdateFlagManager(); private _mode = ParticleCurveMode.Constant; private _constantMin = 0; private _constantMax = 0; - @deepClone private _curveMin: ParticleCurve; - @deepClone private _curveMax: ParticleCurve; @ignoreClone private _updateDispatch: () => void; @@ -142,6 +140,7 @@ export class ParticleCompositeCurve { constructor(curveMin: ParticleCurve, curveMax: ParticleCurve); constructor(constantOrCurve: number | ParticleCurve, constantMaxOrCurveMax?: number | ParticleCurve) { + super(); this._updateDispatch = this._updateManager.dispatch.bind(this._updateManager); if (typeof constantOrCurve === "number") { if (constantMaxOrCurveMax) { diff --git a/packages/core/src/particle/modules/ParticleCompositeGradient.ts b/packages/core/src/particle/modules/ParticleCompositeGradient.ts index e2777bb86b..d2deb3789a 100644 --- a/packages/core/src/particle/modules/ParticleCompositeGradient.ts +++ b/packages/core/src/particle/modules/ParticleCompositeGradient.ts @@ -1,27 +1,23 @@ +import { DataObject } from "../../base/DataObject"; import { Color } from "@galacean/engine-math"; -import { deepClone } from "../../clone/CloneManager"; import { ParticleGradientMode } from "../enums/ParticleGradientMode"; import { ParticleGradient } from "./ParticleGradient"; /** * Particle composite gradient. */ -export class ParticleCompositeGradient { +export class ParticleCompositeGradient extends DataObject { private static _tempColor = new Color(); /** The gradient mode. */ mode: ParticleGradientMode = ParticleGradientMode.Constant; /* The min constant color used by the gradient if mode is set to `TwoConstants`. */ - @deepClone constantMin: Color = new Color(); /* The max constant color used by the gradient if mode is set to `TwoConstants`. */ - @deepClone constantMax: Color = new Color(); /** The min gradient used by the gradient if mode is set to `Gradient`. */ - @deepClone gradientMin: ParticleGradient = new ParticleGradient(); /** The max gradient used by the gradient if mode is set to `Gradient`. */ - @deepClone gradientMax: ParticleGradient = new ParticleGradient(); /** @@ -46,6 +42,11 @@ export class ParticleCompositeGradient { this.gradientMax = value; } + /** + * Create a particle gradient in constant mode with the default color. + */ + constructor(); + /** * Create a particle gradient that generates a constant color. * @param constant - The constant color @@ -72,7 +73,9 @@ export class ParticleCompositeGradient { */ constructor(gradientMin: ParticleGradient, gradientMax: ParticleGradient); - constructor(constantOrGradient: Color | ParticleGradient, constantMaxOrGradientMax?: Color | ParticleGradient) { + constructor(constantOrGradient?: Color | ParticleGradient, constantMaxOrGradientMax?: Color | ParticleGradient) { + super(); + if (!constantOrGradient) return; if (constantOrGradient.constructor === Color) { if (constantMaxOrGradientMax) { this.constantMin.copyFrom(constantOrGradient); diff --git a/packages/core/src/particle/modules/ParticleCurve.ts b/packages/core/src/particle/modules/ParticleCurve.ts index fdba01cb62..a84de05a57 100644 --- a/packages/core/src/particle/modules/ParticleCurve.ts +++ b/packages/core/src/particle/modules/ParticleCurve.ts @@ -1,13 +1,12 @@ +import { DataObject } from "../../base/DataObject"; import { UpdateFlagManager } from "../../UpdateFlagManager"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; /** * Particle curve. */ -export class ParticleCurve { - @ignoreClone +export class ParticleCurve extends DataObject { private _updateManager = new UpdateFlagManager(); - @deepClone private _keys = new Array(); @ignoreClone private _typeArray: Float32Array; @@ -27,6 +26,7 @@ export class ParticleCurve { * @param keys - The keys of the curve */ constructor(...keys: CurveKey[]) { + super(); this._updateDispatch = this._updateManager.dispatch.bind(this._updateManager); for (let i = 0, n = keys.length; i < n; i++) { @@ -195,8 +195,7 @@ export class ParticleCurve { /** * The key of the curve. */ -export class CurveKey { - @ignoreClone +export class CurveKey extends DataObject { private _updateManager = new UpdateFlagManager(); private _time: number; private _value: number; @@ -233,6 +232,7 @@ export class CurveKey { * Create a new key. */ constructor(time: number, value: number) { + super(); this._time = time; this._value = value; } diff --git a/packages/core/src/particle/modules/ParticleGeneratorModule.ts b/packages/core/src/particle/modules/ParticleGeneratorModule.ts index 87f0e7ebfa..4353b4bf8d 100644 --- a/packages/core/src/particle/modules/ParticleGeneratorModule.ts +++ b/packages/core/src/particle/modules/ParticleGeneratorModule.ts @@ -1,3 +1,4 @@ +import { DataObject } from "../../base/DataObject"; import { ignoreClone } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro } from "../../shader"; import { ParticleGenerator } from "../ParticleGenerator"; @@ -6,7 +7,7 @@ import { ParticleCompositeCurve } from "./ParticleCompositeCurve"; /** * Particle generator module. */ -export abstract class ParticleGeneratorModule { +export abstract class ParticleGeneratorModule extends DataObject { /** @internal */ @ignoreClone _generator: ParticleGenerator; @@ -28,6 +29,7 @@ export abstract class ParticleGeneratorModule { * @internal */ constructor(generator: ParticleGenerator) { + super(); this._generator = generator; } diff --git a/packages/core/src/particle/modules/ParticleGradient.ts b/packages/core/src/particle/modules/ParticleGradient.ts index 732f879f5c..6daf6dba6b 100644 --- a/packages/core/src/particle/modules/ParticleGradient.ts +++ b/packages/core/src/particle/modules/ParticleGradient.ts @@ -1,13 +1,12 @@ +import { DataObject } from "../../base/DataObject"; import { Color } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; /** * Particle gradient. */ -export class ParticleGradient { - @deepClone +export class ParticleGradient extends DataObject { private _colorKeys: GradientColorKey[] = []; - @deepClone private _alphaKeys: GradientAlphaKey[] = []; @ignoreClone private _colorTypeArray: Float32Array; @@ -37,6 +36,7 @@ export class ParticleGradient { * @param alphaKeys - The alpha keys of the gradient */ constructor(colorKeys: GradientColorKey[] = null, alphaKeys: GradientAlphaKey[] = null) { + super(); if (colorKeys) { for (let i = 0, n = colorKeys.length; i < n; i++) { const key = colorKeys[i]; @@ -286,7 +286,7 @@ export class ParticleGradient { /** * The color key of the particle gradient. */ -export class GradientColorKey { +export class GradientColorKey extends DataObject { /** @internal */ _onValueChanged: () => void = null; @@ -324,6 +324,7 @@ export class GradientColorKey { * @param color - The alpha component of the gradient colorKey */ constructor(time: number, color: Color) { + super(); this._time = time; color && this._color.copyFrom(color); // @ts-ignore @@ -334,7 +335,7 @@ export class GradientColorKey { /** * The alpha key of the particle gradient. */ -export class GradientAlphaKey { +export class GradientAlphaKey extends DataObject { /** @internal */ _onValueChanged: () => void = null; @@ -371,6 +372,7 @@ export class GradientAlphaKey { * @param alpha - The alpha component of the gradient alpha key */ constructor(time: number, alpha: number) { + super(); this._time = time; this._alpha = alpha; } diff --git a/packages/core/src/particle/modules/RotationOverLifetimeModule.ts b/packages/core/src/particle/modules/RotationOverLifetimeModule.ts index 557f5b983b..b9753b6b6e 100644 --- a/packages/core/src/particle/modules/RotationOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/RotationOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderMacro } from "../../shader/ShaderMacro"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -29,13 +29,10 @@ export class RotationOverLifetimeModule extends ParticleGeneratorModule { /** Specifies whether the rotation is separate on each axis, when disabled, only `rotationZ` is used. */ separateAxes: boolean = false; /** Rotation over lifetime for x axis, in degrees. */ - @deepClone rotationX = new ParticleCompositeCurve(0); /** Rotation over lifetime for y axis, in degrees. */ - @deepClone rotationY = new ParticleCompositeCurve(0); /** Rotation over lifetime for z axis, in degrees. */ - @deepClone rotationZ = new ParticleCompositeCurve(45); /** @internal */ diff --git a/packages/core/src/particle/modules/SizeOverLifetimeModule.ts b/packages/core/src/particle/modules/SizeOverLifetimeModule.ts index b8c9c6fb18..0242d2b5ec 100644 --- a/packages/core/src/particle/modules/SizeOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/SizeOverLifetimeModule.ts @@ -1,4 +1,4 @@ -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderMacro } from "../../shader/ShaderMacro"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -24,11 +24,8 @@ export class SizeOverLifetimeModule extends ParticleGeneratorModule { static readonly _maxCurveZProperty = ShaderProperty.getByName("renderer_SOLMaxCurveZ"); private _separateAxes = false; - @deepClone private _sizeX: ParticleCompositeCurve; - @deepClone private _sizeY: ParticleCompositeCurve; - @deepClone private _sizeZ: ParticleCompositeCurve; @ignoreClone diff --git a/packages/core/src/particle/modules/SubEmitter.ts b/packages/core/src/particle/modules/SubEmitter.ts index c9c740207e..a05a0a98c5 100644 --- a/packages/core/src/particle/modules/SubEmitter.ts +++ b/packages/core/src/particle/modules/SubEmitter.ts @@ -1,3 +1,4 @@ +import { DataObject } from "../../base/DataObject"; import { ignoreClone } from "../../clone/CloneManager"; import { ParticleRenderer } from "../ParticleRenderer"; import { ParticleSubEmitterInheritProperty } from "../enums/ParticleSubEmitterInheritProperty"; @@ -8,7 +9,7 @@ import type { SubEmittersModule } from "./SubEmittersModule"; * One slot in `SubEmittersModule.subEmitters`. Configures which sub-emitter * fires, on which parent event, with what inheritance, probability, and count. */ -export class SubEmitter { +export class SubEmitter extends DataObject { /** Bitmask of properties inherited from the parent particle. */ inheritProperties: ParticleSubEmitterInheritProperty = ParticleSubEmitterInheritProperty.None; @@ -19,7 +20,6 @@ export class SubEmitter { emitCount: number = 1; /** @internal */ - @ignoreClone _module: SubEmittersModule = null; private _emitter: ParticleRenderer = null; diff --git a/packages/core/src/particle/modules/SubEmittersModule.ts b/packages/core/src/particle/modules/SubEmittersModule.ts index 90d19e5feb..de22030aaa 100644 --- a/packages/core/src/particle/modules/SubEmittersModule.ts +++ b/packages/core/src/particle/modules/SubEmittersModule.ts @@ -1,5 +1,5 @@ import { Color, Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds"; import { ParticleSubEmitterInheritProperty } from "../enums/ParticleSubEmitterInheritProperty"; import { ParticleSubEmitterType } from "../enums/ParticleSubEmitterType"; @@ -44,7 +44,6 @@ export class SubEmittersModule extends ParticleGeneratorModule { return found; } - @deepClone private _subEmitters: SubEmitter[] = []; /** @@ -168,17 +167,6 @@ export class SubEmittersModule extends ParticleGeneratorModule { return false; } - /** - * @internal - */ - _cloneTo(target: SubEmittersModule): void { - // _module is @ignoreClone, so re-link each cloned slot back to its new module - const subEmitters = target._subEmitters; - for (let i = 0, n = subEmitters.length; i < n; i++) { - subEmitters[i]._module = target; - } - } - /** * @internal */ diff --git a/packages/core/src/particle/modules/TextureSheetAnimationModule.ts b/packages/core/src/particle/modules/TextureSheetAnimationModule.ts index c75601c339..089b9191e2 100644 --- a/packages/core/src/particle/modules/TextureSheetAnimationModule.ts +++ b/packages/core/src/particle/modules/TextureSheetAnimationModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector2, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone, shallowClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderMacro } from "../../shader/ShaderMacro"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -24,7 +24,6 @@ export class TextureSheetAnimationModule extends ParticleGeneratorModule { private static readonly _tillingParamsProperty = ShaderProperty.getByName("renderer_TSATillingParams"); /** Frame over time curve of the texture sheet. */ - @deepClone readonly frameOverTime = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0), new CurveKey(1, 1))); /** Texture sheet animation type. */ type = TextureSheetAnimationType.WholeSheet; @@ -32,13 +31,11 @@ export class TextureSheetAnimationModule extends ParticleGeneratorModule { cycleCount = 1; /** @internal */ - @shallowClone _tillingInfo = new Vector3(1, 1, 1); // x:subU, y:subV, z:tileCount /** @internal */ @ignoreClone _frameOverTimeRand = new Rand(0, ParticleRandomSubSeeds.TextureSheetAnimation); - @deepClone private _tiling = new Vector2(1, 1); @ignoreClone private _frameCurveMacro: ShaderMacro; diff --git a/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts b/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts index 67cb113ba5..8e6abe3336 100644 --- a/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ShaderMacro } from "../../shader"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -74,21 +74,13 @@ export class VelocityOverLifetimeModule extends ParticleGeneratorModule { @ignoreClone private _radialRandomModeMacro: ShaderMacro; - @deepClone private _velocityX: ParticleCompositeCurve; - @deepClone private _velocityY: ParticleCompositeCurve; - @deepClone private _velocityZ: ParticleCompositeCurve; - @deepClone private _orbitalX: ParticleCompositeCurve; - @deepClone private _orbitalY: ParticleCompositeCurve; - @deepClone private _orbitalZ: ParticleCompositeCurve; - @deepClone private _radial: ParticleCompositeCurve; - @deepClone private _offset = new Vector3(); private _space = ParticleSimulationSpace.Local; diff --git a/packages/core/src/particle/modules/shape/BaseShape.ts b/packages/core/src/particle/modules/shape/BaseShape.ts index 9e00891b59..c9184b66ee 100644 --- a/packages/core/src/particle/modules/shape/BaseShape.ts +++ b/packages/core/src/particle/modules/shape/BaseShape.ts @@ -1,12 +1,13 @@ import { BoundingBox, MathUtil, Matrix, Quaternion, Rand, Vector2, Vector3 } from "@galacean/engine-math"; -import { ParticleShapeType } from "./enums/ParticleShapeType"; import { UpdateFlagManager } from "../../../UpdateFlagManager"; -import { deepClone, ignoreClone } from "../../../clone/CloneManager"; +import { DataObject } from "../../../base/DataObject"; +import { ignoreClone } from "../../../clone/CloneManager"; +import { ParticleShapeType } from "./enums/ParticleShapeType"; /** * Base class for all particle shapes. */ -export abstract class BaseShape { +export abstract class BaseShape extends DataObject { /** @internal */ static _tempVector20 = new Vector2(); /** @internal */ @@ -18,18 +19,13 @@ export abstract class BaseShape { private static _tempQuaternion = new Quaternion(); /** The type of shape to emit particles from. */ abstract readonly shapeType: ParticleShapeType; - - @ignoreClone protected _updateManager = new UpdateFlagManager(); private _enabled = true; private _randomDirectionAmount = 0; - @deepClone private _position = new Vector3(0, 0, 0); - @deepClone private _rotation = new Vector3(0, 0, 0); - @deepClone private _scale = new Vector3(1, 1, 1); @ignoreClone private _matrix = new Matrix(); @@ -106,6 +102,7 @@ export abstract class BaseShape { } constructor() { + super(); // @ts-ignore this._position._onValueChanged = this._onTransformChanged; // @ts-ignore @@ -128,6 +125,11 @@ export abstract class BaseShape { this._updateManager.removeListener(listener); } + /** + * @internal + */ + _destroy(): void {} + /** * @internal */ diff --git a/packages/core/src/particle/modules/shape/BoxShape.ts b/packages/core/src/particle/modules/shape/BoxShape.ts index 7ad762e2e8..e1a0796979 100644 --- a/packages/core/src/particle/modules/shape/BoxShape.ts +++ b/packages/core/src/particle/modules/shape/BoxShape.ts @@ -1,5 +1,4 @@ import { Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone } from "../../../clone/CloneManager"; import { BaseShape } from "./BaseShape"; import { ShapeUtils } from "./ShapeUtils"; import { ParticleShapeType } from "./enums/ParticleShapeType"; @@ -10,7 +9,6 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; export class BoxShape extends BaseShape { readonly shapeType = ParticleShapeType.Box; - @deepClone private _size = new Vector3(1, 1, 1); /** diff --git a/packages/core/src/particle/modules/shape/MeshShape.ts b/packages/core/src/particle/modules/shape/MeshShape.ts index e425003704..2f4f6d6e82 100644 --- a/packages/core/src/particle/modules/shape/MeshShape.ts +++ b/packages/core/src/particle/modules/shape/MeshShape.ts @@ -1,7 +1,6 @@ import { Rand, Vector3, Vector4 } from "@galacean/engine-math"; import { TypedArray } from "../../../base"; import { ignoreClone } from "../../../clone/CloneManager"; -import { Entity } from "../../../Entity"; import { VertexElement } from "../../../graphic"; import { MeshModifyFlags } from "../../../graphic/Mesh"; import { ModelMesh, VertexAttribute } from "../../../mesh"; @@ -51,6 +50,13 @@ export class MeshShape extends BaseShape { } } + /** + * @internal + */ + override _destroy(): void { + this.mesh = null; + } + /** * @internal */ diff --git a/packages/core/src/physics/CharacterController.ts b/packages/core/src/physics/CharacterController.ts index ea6961b70f..a77811dedc 100644 --- a/packages/core/src/physics/CharacterController.ts +++ b/packages/core/src/physics/CharacterController.ts @@ -5,7 +5,7 @@ import { Entity } from "../Entity"; import { Collider } from "./Collider"; import { ControllerNonWalkableMode } from "./enums/ControllerNonWalkableMode"; import { ColliderShape } from "./shape"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; /** * The character controllers. @@ -13,7 +13,6 @@ import { deepClone, ignoreClone } from "../clone/CloneManager"; export class CharacterController extends Collider { private _stepOffset = 0.5; private _nonWalkableMode: ControllerNonWalkableMode = ControllerNonWalkableMode.PreventClimbing; - @deepClone private _upDirection = new Vector3(0, 1, 0); private _slopeLimit = 45; diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index ce7f56f2f7..502c8fb035 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -1,7 +1,7 @@ import { ICollider, IStaticCollider } from "@galacean/engine-design"; import { Quaternion, Vector3 } from "@galacean/engine-math"; import { BoolUpdateFlag } from "../BoolUpdateFlag"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; import { ICustomClone } from "../clone/ComponentCloner"; import { Component } from "../Component"; import { DependentMode, dependentComponents } from "../ComponentsDependencies"; @@ -23,9 +23,7 @@ export class Collider extends Component implements ICustomClone { /** @internal */ @ignoreClone _nativeCollider: ICollider; - @ignoreClone protected _updateFlag: BoolUpdateFlag; - @deepClone protected _shapes: ColliderShape[] = []; protected _collisionLayerIndex: number = 0; @@ -240,16 +238,14 @@ export class Collider extends Component implements ICustomClone { _attachNativeShape(shape: ColliderShape): void { if (shape._nativeShape && !shape._isShapeAttached) { shape._nativeShape.setWorldScale(this.entity.transform.lossyWorldScale); - this._nativeCollider.addShape(shape._nativeShape); - shape._isShapeAttached = true; + shape._attachToCollider(); } } /** @internal */ _detachNativeShape(shape: ColliderShape): void { if (shape._nativeShape && shape._isShapeAttached) { - this._nativeCollider.removeShape(shape._nativeShape); - shape._isShapeAttached = false; + shape._detachFromCollider(); } } diff --git a/packages/core/src/physics/joint/HingeJoint.ts b/packages/core/src/physics/joint/HingeJoint.ts index acffbae4b6..1212343e2f 100644 --- a/packages/core/src/physics/joint/HingeJoint.ts +++ b/packages/core/src/physics/joint/HingeJoint.ts @@ -6,20 +6,17 @@ import { HingeJointFlag } from "../enums/HingeJointFlag"; import { Joint } from "./Joint"; import { JointLimits } from "./JointLimits"; import { JointMotor } from "./JointMotor"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { Entity } from "../../Entity"; /** * A joint which behaves in a similar way to a hinge or axle. */ export class HingeJoint extends Joint { - @deepClone private _axis = new Vector3(1, 0, 0); private _hingeFlags = HingeJointFlag.None; private _useSpring = false; - @deepClone private _jointMotor: JointMotor; - @deepClone private _limits: JointLimits; private _angle = 0; private _velocity = 0; diff --git a/packages/core/src/physics/joint/Joint.ts b/packages/core/src/physics/joint/Joint.ts index 8d9f8af178..1f721d93a8 100644 --- a/packages/core/src/physics/joint/Joint.ts +++ b/packages/core/src/physics/joint/Joint.ts @@ -1,10 +1,11 @@ +import { DataObject } from "../../base/DataObject"; import { IJoint } from "@galacean/engine-design"; import { Matrix, Quaternion, Vector3 } from "@galacean/engine-math"; import { Component } from "../../Component"; import { DependentMode, dependentComponents } from "../../ComponentsDependencies"; import { Entity } from "../../Entity"; import { TransformModifyFlags } from "../../Transform"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { Collider } from "../Collider"; import { DynamicCollider } from "../DynamicCollider"; @@ -18,9 +19,7 @@ export abstract class Joint extends Component { private static _tempQuat = new Quaternion(); private static _tempMatrix = new Matrix(); - @deepClone protected _colliderInfo = new JointColliderInfo(); - @deepClone protected _connectedColliderInfo = new JointColliderInfo(); @ignoreClone protected _nativeJoint: IJoint; @@ -321,11 +320,9 @@ enum AnchorOwner { /** * @internal */ -class JointColliderInfo { +class JointColliderInfo extends DataObject { collider: Collider = null; - @deepClone anchor = new Vector3(); - @deepClone actualAnchor = new Vector3(); massScale: number = 1; inertiaScale: number = 1; diff --git a/packages/core/src/physics/joint/JointLimits.ts b/packages/core/src/physics/joint/JointLimits.ts index 3d7ff811be..4369abb48d 100644 --- a/packages/core/src/physics/joint/JointLimits.ts +++ b/packages/core/src/physics/joint/JointLimits.ts @@ -1,11 +1,10 @@ -import { deepClone } from "../../clone/CloneManager"; +import { DataObject } from "../../base/DataObject"; import { UpdateFlagManager } from "../../UpdateFlagManager"; /** * JointLimits is used to limit the joints angle. */ -export class JointLimits { - @deepClone +export class JointLimits extends DataObject { /** @internal */ _updateFlagManager = new UpdateFlagManager(); diff --git a/packages/core/src/physics/joint/JointMotor.ts b/packages/core/src/physics/joint/JointMotor.ts index 0f381c1a6c..73024929fa 100644 --- a/packages/core/src/physics/joint/JointMotor.ts +++ b/packages/core/src/physics/joint/JointMotor.ts @@ -1,11 +1,10 @@ -import { deepClone } from "../../clone/CloneManager"; +import { DataObject } from "../../base/DataObject"; import { UpdateFlagManager } from "../../UpdateFlagManager"; /** * The JointMotor is used to motorize a joint. */ -export class JointMotor { - @deepClone +export class JointMotor extends DataObject { /** @internal */ _updateFlagManager = new UpdateFlagManager(); diff --git a/packages/core/src/physics/shape/BoxColliderShape.ts b/packages/core/src/physics/shape/BoxColliderShape.ts index 12c32a0a4b..2d9383aee1 100644 --- a/packages/core/src/physics/shape/BoxColliderShape.ts +++ b/packages/core/src/physics/shape/BoxColliderShape.ts @@ -2,13 +2,12 @@ import { ColliderShape } from "./ColliderShape"; import { IBoxColliderShape } from "@galacean/engine-design"; import { Vector3 } from "@galacean/engine-math"; import { Engine } from "../../Engine"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; /** * Physical collider shape for box. */ export class BoxColliderShape extends ColliderShape { - @deepClone private _size: Vector3 = new Vector3(1, 1, 1); /** diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index ac12ae9d19..bc569565d9 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -1,8 +1,9 @@ +import { DataObject } from "../../base/DataObject"; import { IColliderShape } from "@galacean/engine-design"; import { PhysicsMaterial } from "../PhysicsMaterial"; import { Vector3 } from "@galacean/engine-math"; import { Collider } from "../Collider"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { ignoreClone } from "../../clone/CloneManager"; import { ICustomClone } from "../../clone/ComponentCloner"; import { Engine } from "../../Engine"; import { ColliderShapeChangeFlag } from "../enums/ColliderShapeChangeFlag"; @@ -10,14 +11,14 @@ import { ColliderShapeChangeFlag } from "../enums/ColliderShapeChangeFlag"; /** * Abstract class for collider shapes. */ -export abstract class ColliderShape implements ICustomClone { +export abstract class ColliderShape extends DataObject implements ICustomClone { private static _idGenerator: number = 0; /** @internal */ _collider: Collider; /** @internal */ @ignoreClone - _isShapeAttached = false; + _isShapeAttached: boolean = false; /** @internal */ @ignoreClone _nativeShape: IColliderShape; @@ -26,9 +27,7 @@ export abstract class ColliderShape implements ICustomClone { protected _id: number; protected _material: PhysicsMaterial; private _isTrigger: boolean = false; - @deepClone private _rotation: Vector3 = new Vector3(); - @deepClone private _position: Vector3 = new Vector3(); private _contactOffset: number | undefined; @@ -127,6 +126,7 @@ export abstract class ColliderShape implements ICustomClone { } protected constructor() { + super(); this._material = new PhysicsMaterial(); this._id = ColliderShape._idGenerator++; @@ -182,6 +182,22 @@ export abstract class ColliderShape implements ICustomClone { delete Engine._physicalObjectsMap[this._id]; } + /** + * @internal + */ + _attachToCollider(): void { + this._collider._nativeCollider.addShape(this._nativeShape); + this._isShapeAttached = true; + } + + /** + * @internal + */ + _detachFromCollider(): void { + this._collider._nativeCollider.removeShape(this._nativeShape); + this._isShapeAttached = false; + } + protected _syncNative(): void { if (!this._nativeShape) return; this._nativeShape.setPosition(this._position); diff --git a/packages/core/src/physics/shape/MeshColliderShape.ts b/packages/core/src/physics/shape/MeshColliderShape.ts index 253e188736..9f4f72d8ee 100644 --- a/packages/core/src/physics/shape/MeshColliderShape.ts +++ b/packages/core/src/physics/shape/MeshColliderShape.ts @@ -2,6 +2,7 @@ import { IMeshColliderShape } from "@galacean/engine-design"; import { Engine } from "../../Engine"; import { ModelMesh } from "../../mesh/ModelMesh"; import { Vector3 } from "@galacean/engine-math"; +import { ignoreClone } from "../../clone/CloneManager"; import { DynamicCollider } from "../DynamicCollider"; import { MeshColliderShapeCookingFlag } from "../enums/MeshColliderShapeCookingFlag"; import { ColliderShape } from "./ColliderShape"; @@ -10,9 +11,12 @@ import { ColliderShape } from "./ColliderShape"; * Collider shape based on mesh geometry, supporting both convex hull and triangle mesh modes. */ export class MeshColliderShape extends ColliderShape { + @ignoreClone private _mesh: ModelMesh = null; private _isConvex = false; + @ignoreClone private _positions: Vector3[] = null; + @ignoreClone private _indices: Uint8Array | Uint16Array | Uint32Array | null = null; private _cookingFlags = MeshColliderShapeCookingFlag.Cleaning | MeshColliderShapeCookingFlag.VertexWelding; @@ -125,6 +129,14 @@ export class MeshColliderShape extends ColliderShape { return super.getClosestPoint(point, outClosestPoint); } + /** + * @internal + */ + override _cloneTo(target: MeshColliderShape): void { + target.mesh = this._mesh; + super._cloneTo(target); + } + /** * @internal */ @@ -228,18 +240,4 @@ export class MeshColliderShape extends ColliderShape { } return true; } - - /** - * @internal - * After CloneManager deep-copies `_positions` / `_indices` / `_mesh` and remaps `_collider`, - * the cloned shape still has no native PhysX shape because `_nativeShape` is `@ignoreClone`. - * Cook a fresh native shape now using the already-cloned vertex/index buffers. - */ - override _cloneTo(target: MeshColliderShape): void { - target._mesh?._addReferCount(1); - super._cloneTo(target); - if (this._nativeShape) { - target._createNativeShape(false); - } - } } diff --git a/packages/core/src/postProcess/PostProcess.ts b/packages/core/src/postProcess/PostProcess.ts index 66521a02f1..0a926f49ee 100644 --- a/packages/core/src/postProcess/PostProcess.ts +++ b/packages/core/src/postProcess/PostProcess.ts @@ -1,5 +1,4 @@ import { Logger } from "../base"; -import { deepClone } from "../clone/CloneManager"; import { Component } from "../Component"; import { Layer } from "../Layer"; import { PostProcessEffect } from "./PostProcessEffect"; @@ -19,7 +18,6 @@ export class PostProcess extends Component { blendDistance = 0; /** @internal */ - @deepClone _effects: PostProcessEffect[] = []; private _priority = 0; diff --git a/packages/core/src/postProcess/PostProcessEffect.ts b/packages/core/src/postProcess/PostProcessEffect.ts index a2a29d382a..d976861d04 100644 --- a/packages/core/src/postProcess/PostProcessEffect.ts +++ b/packages/core/src/postProcess/PostProcessEffect.ts @@ -1,9 +1,10 @@ +import { DataObject } from "../base/DataObject"; import { PostProcessEffectParameter } from "./PostProcessEffectParameter"; /** * The base class for post process effect. */ -export class PostProcessEffect { +export class PostProcessEffect extends DataObject { private _enabled = true; private _parameters: PostProcessEffectParameter[] = []; private _parameterInitialized = false; @@ -56,7 +57,7 @@ export class PostProcessEffect { private _getParameters(): PostProcessEffectParameter[] { if (!this._parameterInitialized) { this._parameterInitialized = true; - for (let key in this) { + for (const key in this) { const value = this[key]; if (value instanceof PostProcessEffectParameter) { this._parameters.push(value); diff --git a/packages/core/src/postProcess/PostProcessEffectParameter.ts b/packages/core/src/postProcess/PostProcessEffectParameter.ts index 899860ba76..15e16ed442 100644 --- a/packages/core/src/postProcess/PostProcessEffectParameter.ts +++ b/packages/core/src/postProcess/PostProcessEffectParameter.ts @@ -1,3 +1,4 @@ +import { DataObject } from "../base/DataObject"; import { Color, MathUtil, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; import { Texture } from "../texture"; @@ -6,7 +7,7 @@ import { Texture } from "../texture"; * @remarks * The parameter will be mixed to a final value and be used in post process manager. */ -export abstract class PostProcessEffectParameter { +export abstract class PostProcessEffectParameter extends DataObject { /** * Whether the parameter is enabled. */ @@ -27,6 +28,7 @@ export abstract class PostProcessEffectParameter { } constructor(value: T, needLerp = false) { + super(); this._needLerp = needLerp; this._value = value; } diff --git a/packages/core/src/shader/ShaderData.ts b/packages/core/src/shader/ShaderData.ts index 038406fb80..e338cfdd21 100644 --- a/packages/core/src/shader/ShaderData.ts +++ b/packages/core/src/shader/ShaderData.ts @@ -1,7 +1,9 @@ +import { DataObject } from "../base/DataObject"; import { IClone } from "@galacean/engine-design"; import { Color, Matrix, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; import { IReferable } from "../asset/IReferable"; -import { CloneManager, ignoreClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; +import { CloneUtil } from "../clone/CloneUtil"; import { Texture } from "../texture/Texture"; import { ShaderMacro } from "./ShaderMacro"; import { ShaderMacroCollection } from "./ShaderMacroCollection"; @@ -12,7 +14,7 @@ import { ShaderPropertyType } from "./enums/ShaderPropertyType"; /** * Shader data collection,Correspondence includes shader properties data and macros data. */ -export class ShaderData implements IReferable, IClone { +export class ShaderData extends DataObject implements IReferable, IClone { /** @internal */ @ignoreClone _group: ShaderDataGroup; @@ -34,6 +36,7 @@ export class ShaderData implements IReferable, IClone { * @internal */ constructor(group: ShaderDataGroup) { + super(); this._group = group; } @@ -608,7 +611,7 @@ export class ShaderData implements IReferable, IClone { } cloneTo(target: ShaderData): void { - CloneManager.deepCloneObject(this._macroCollection, target._macroCollection, new Map()); + CloneUtil._deepCloneObject(this._macroCollection, target._macroCollection, new Map()); const targetMacroMap = target._macroMap; for (const key in targetMacroMap) { delete targetMacroMap[key]; @@ -628,7 +631,9 @@ export class ShaderData implements IReferable, IClone { targetPropertyValueMap[k] = property; referCount > 0 && property._addReferCount(referCount); } else if (property instanceof Array || property instanceof Float32Array || property instanceof Int32Array) { - targetPropertyValueMap[k] = property.slice(); + const cloned = property.slice(); + targetPropertyValueMap[k] = cloned; + referCount > 0 && this._addTexturesReferCount(cloned, referCount); } else { const targetProperty = targetPropertyValueMap[k]; if (targetProperty) { @@ -643,6 +648,13 @@ export class ShaderData implements IReferable, IClone { } } + /** + * @internal + */ + _cloneTo(target: ShaderData): void { + this.cloneTo(target); + } + /** * @internal */ @@ -713,8 +725,17 @@ export class ShaderData implements IReferable, IClone { for (const k in properties) { const property = properties[k]; // @todo: Separate array to speed performance. - if (property && property instanceof Texture) { - property._addReferCount(value); + property && this._addTexturesReferCount(property, value); + } + } + + private _addTexturesReferCount(property: ShaderPropertyValueType, count: number): void { + if (property instanceof Texture) { + property._addReferCount(count); + } else if (property instanceof Array) { + for (let i = 0, n = property.length; i < n; i++) { + const element = property[i]; + element instanceof Texture && element._addReferCount(count); } } } diff --git a/packages/core/src/shader/state/BlendState.ts b/packages/core/src/shader/state/BlendState.ts index 870526d0ab..443cd1acbe 100644 --- a/packages/core/src/shader/state/BlendState.ts +++ b/packages/core/src/shader/state/BlendState.ts @@ -1,8 +1,8 @@ +import { DataObject } from "../../base/DataObject"; import { IHardwareRenderer } from "@galacean/engine-design"; import { Color } from "@galacean/engine-math"; import { RenderStateElementMap } from "../../BasicResources"; import { GLCapabilityType } from "../../base/Constant"; -import { deepClone } from "../../clone/CloneManager"; import { ShaderData } from "../ShaderData"; import { ShaderProperty } from "../ShaderProperty"; import { BlendFactor } from "../enums/BlendFactor"; @@ -15,7 +15,7 @@ import { RenderTargetBlendState } from "./RenderTargetBlendState"; /** * Blend state. */ -export class BlendState { +export class BlendState extends DataObject { private static _getGLBlendFactor(rhi: IHardwareRenderer, blendFactor: BlendFactor): number { const gl = rhi.gl; @@ -73,10 +73,8 @@ export class BlendState { } /** The blend state of the render target. */ - @deepClone readonly targetBlendState: RenderTargetBlendState = new RenderTargetBlendState(); /** Constant blend color. */ - @deepClone readonly blendColor: Color = new Color(0, 0, 0, 0); /** Whether to use (Alpha-to-Coverage) technology. */ alphaToCoverage: boolean = false; diff --git a/packages/core/src/shader/state/DepthState.ts b/packages/core/src/shader/state/DepthState.ts index 9757e22fda..15b221efb9 100644 --- a/packages/core/src/shader/state/DepthState.ts +++ b/packages/core/src/shader/state/DepthState.ts @@ -1,3 +1,4 @@ +import { DataObject } from "../../base/DataObject"; import { IHardwareRenderer } from "@galacean/engine-design"; import { RenderStateElementMap } from "../../BasicResources"; import { ShaderData } from "../ShaderData"; @@ -9,7 +10,7 @@ import { RenderState } from "./RenderState"; /** * Depth state. */ -export class DepthState { +export class DepthState extends DataObject { private static _getGLCompareFunction(rhi: IHardwareRenderer, compareFunction: CompareFunction): number { const gl = rhi.gl; diff --git a/packages/core/src/shader/state/RasterState.ts b/packages/core/src/shader/state/RasterState.ts index 660abff343..ddebc3afa8 100644 --- a/packages/core/src/shader/state/RasterState.ts +++ b/packages/core/src/shader/state/RasterState.ts @@ -1,3 +1,4 @@ +import { DataObject } from "../../base/DataObject"; import { IHardwareRenderer } from "@galacean/engine-design"; import { RenderStateElementMap } from "../../BasicResources"; import { ShaderData } from "../ShaderData"; @@ -9,7 +10,7 @@ import { RenderState } from "./RenderState"; /** * Raster state. */ -export class RasterState { +export class RasterState extends DataObject { /** Specifies whether or not front- and/or back-facing polygons can be culled. */ cullMode: CullMode = CullMode.Back; /** The multiplier by which an implementation-specific value is multiplied with to create a constant depth offset. */ diff --git a/packages/core/src/shader/state/RenderState.ts b/packages/core/src/shader/state/RenderState.ts index 502bfd7d83..9f67e4b37a 100644 --- a/packages/core/src/shader/state/RenderState.ts +++ b/packages/core/src/shader/state/RenderState.ts @@ -1,7 +1,7 @@ +import { DataObject } from "../../base/DataObject"; import { ShaderData, ShaderProperty } from ".."; import { RenderStateElementMap } from "../../BasicResources"; import { Engine } from "../../Engine"; -import { deepClone } from "../../clone/CloneManager"; import { RenderQueueType } from "../enums/RenderQueueType"; import { RenderStateElementKey } from "../enums/RenderStateElementKey"; import { BlendState } from "./BlendState"; @@ -12,18 +12,14 @@ import { StencilState } from "./StencilState"; /** * Render state. */ -export class RenderState { +export class RenderState extends DataObject { /** Blend state. */ - @deepClone readonly blendState: BlendState = new BlendState(); /** Depth state. */ - @deepClone readonly depthState: DepthState = new DepthState(); /** Stencil state. */ - @deepClone readonly stencilState: StencilState = new StencilState(); /** Raster state. */ - @deepClone readonly rasterState: RasterState = new RasterState(); /** Render queue type. */ diff --git a/packages/core/src/shader/state/RenderTargetBlendState.ts b/packages/core/src/shader/state/RenderTargetBlendState.ts index 88e3d3f62e..cdb32b6c17 100644 --- a/packages/core/src/shader/state/RenderTargetBlendState.ts +++ b/packages/core/src/shader/state/RenderTargetBlendState.ts @@ -1,3 +1,4 @@ +import { DataObject } from "../../base/DataObject"; import { BlendOperation } from "../enums/BlendOperation"; import { BlendFactor } from "../enums/BlendFactor"; import { ColorWriteMask } from "../enums/ColorWriteMask"; @@ -5,7 +6,7 @@ import { ColorWriteMask } from "../enums/ColorWriteMask"; /** * The blend state of the render target. */ -export class RenderTargetBlendState { +export class RenderTargetBlendState extends DataObject { /** Whether to enable blend. */ enabled: boolean = false; /** color (RGB) blend operation. */ diff --git a/packages/core/src/shader/state/StencilState.ts b/packages/core/src/shader/state/StencilState.ts index a479c2fcde..9a77ec38cf 100644 --- a/packages/core/src/shader/state/StencilState.ts +++ b/packages/core/src/shader/state/StencilState.ts @@ -1,3 +1,4 @@ +import { DataObject } from "../../base/DataObject"; import { IHardwareRenderer } from "@galacean/engine-design"; import { RenderStateElementMap } from "../../BasicResources"; import { ShaderData } from "../ShaderData"; @@ -10,7 +11,7 @@ import { RenderState } from "./RenderState"; /** * Stencil state. */ -export class StencilState { +export class StencilState extends DataObject { private static _getGLCompareFunction(rhi: IHardwareRenderer, compareFunction: CompareFunction): number { const gl = rhi.gl; diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 6485d658ca..a1fc73e28a 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -2,7 +2,7 @@ import { BoundingBox, Color, Vector2, Vector3, Vector4 } from "@galacean/engine- import { Entity } from "../Entity"; import { RenderContext } from "../RenderPipeline/RenderContext"; import { Renderer, RendererUpdateFlags } from "../Renderer"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { ignoreClone } from "../clone/CloneManager"; import { Buffer } from "../graphic/Buffer"; import { Primitive } from "../graphic/Primitive"; import { SubPrimitive } from "../graphic/SubPrimitive"; @@ -47,20 +47,16 @@ export class TrailRenderer extends Renderer { minVertexDistance = 0.1; /** The curve describing the trail width from start to end. */ - @deepClone widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 1)); /** The gradient describing the trail color from start to end. */ - @deepClone colorGradient = new ParticleGradient( [new GradientColorKey(0, new Color(1, 1, 1, 1)), new GradientColorKey(1, new Color(1, 1, 1, 1))], [new GradientAlphaKey(0, 1), new GradientAlphaKey(1, 1)] ); // Shader parameters - @deepClone private _trailParams = new Vector4(TrailTextureMode.Stretch, 1.0, 1.0, 0); // x: textureMode, y: textureScaleX, z: textureScaleY - @deepClone private _textureScale = new Vector2(1.0, 1.0); @ignoreClone private _distanceParams = new Vector2(); // x: headDistance, y: tailDistance diff --git a/packages/math/src/Ray.ts b/packages/math/src/Ray.ts index 80ba2e2f64..075a55bf6a 100644 --- a/packages/math/src/Ray.ts +++ b/packages/math/src/Ray.ts @@ -1,13 +1,15 @@ import { BoundingBox } from "./BoundingBox"; import { BoundingSphere } from "./BoundingSphere"; import { CollisionUtil } from "./CollisionUtil"; +import { IClone } from "./IClone"; +import { ICopy } from "./ICopy"; import { Plane } from "./Plane"; import { Vector3 } from "./Vector3"; /** * Represents a ray with an origin and a direction in 3D space. */ -export class Ray { +export class Ray implements IClone, ICopy { /** The origin of the ray. */ readonly origin: Vector3 = new Vector3(); /** The normalized direction of the ray. */ @@ -60,4 +62,25 @@ export class Ray { Vector3.scale(this.direction, distance, out); return out.add(this.origin); } + + /** + * Creates a clone of this ray. + * @returns A clone of this ray + */ + clone(): Ray { + const out = new Ray(); + out.copyFrom(this); + return out; + } + + /** + * Copy this ray from the specified ray. + * @param source - The specified ray + * @returns This ray + */ + copyFrom(source: Ray): Ray { + this.origin.copyFrom(source.origin); + this.direction.copyFrom(source.direction); + return this; + } } diff --git a/packages/spine/src/renderer/SpineAnimationRenderer.ts b/packages/spine/src/renderer/SpineAnimationRenderer.ts index 2be54a129c..f0005ab510 100644 --- a/packages/spine/src/renderer/SpineAnimationRenderer.ts +++ b/packages/spine/src/renderer/SpineAnimationRenderer.ts @@ -1,11 +1,10 @@ import type { AnimationState, Skeleton } from "@esotericsoftware/spine-core"; import { - assignmentClone, BoundingBox, Buffer, BufferBindFlag, BufferUsage, - deepClone, + DataObject, Entity, ignoreClone, IndexBufferBinding, @@ -42,7 +41,6 @@ export class SpineAnimationRenderer extends Renderer implements ISpineRenderTarg /** * The spacing between z layers in world units. */ - @assignmentClone zSpacing = 0.001; /** @@ -51,10 +49,8 @@ export class SpineAnimationRenderer extends Renderer implements ISpineRenderTarg * @remarks If this option is enabled, the Spine editor must export textures with "Premultiply Alpha" checked. */ - @assignmentClone premultipliedAlpha = false; - @assignmentClone private _tintBlack = false; /** @@ -77,7 +73,6 @@ export class SpineAnimationRenderer extends Renderer implements ISpineRenderTarg * Default state for spine animation. * Contains the default animation name to be played, whether this animation should loop, the default skin name. */ - @deepClone readonly defaultConfig: SpineAnimationDefaultConfig = new SpineAnimationDefaultConfig(); /** @internal */ @@ -399,7 +394,7 @@ export enum RendererUpdateFlags { * Contains the default animation name to be played, whether this animation should loop, * the default skin name, and the default scale of the skeleton. */ -export class SpineAnimationDefaultConfig { +export class SpineAnimationDefaultConfig extends DataObject { /** * Creates an instance of default config */ @@ -418,5 +413,7 @@ export class SpineAnimationDefaultConfig { * The name of the default skin @defaultValue `default` */ public skinName: string = "default" - ) {} + ) { + super(); + } } diff --git a/packages/ui/src/component/UICanvas.ts b/packages/ui/src/component/UICanvas.ts index 48e733151a..b83e698608 100644 --- a/packages/ui/src/component/UICanvas.ts +++ b/packages/ui/src/component/UICanvas.ts @@ -14,8 +14,6 @@ import { RenderElement, Vector2, Vector3, - assignmentClone, - deepClone, dependentComponents, ignoreClone } from "@galacean/engine"; @@ -79,28 +77,21 @@ export class UICanvas extends Component implements IElement { @ignoreClone _realRenderMode: number = CanvasRealRenderMode.None; /** @internal */ - @ignoreClone _disorderedElements: DisorderedArray = new DisorderedArray(); @ignoreClone private _renderMode = CanvasRenderMode.WorldSpace; private _camera: Camera; private _cameraObserver: Camera; - @assignmentClone private _resolutionAdaptationMode = ResolutionAdaptationMode.HeightAdaptation; - @assignmentClone private _sortOrder: number = 0; - @assignmentClone private _distance: number = 10; - @deepClone private _referenceResolution: Vector2 = new Vector2(800, 600); - @assignmentClone private _referenceResolutionPerUnit: number = 100; @ignoreClone private _hierarchyVersion: number = -1; @ignoreClone private _center: Vector3 = new Vector3(); - @ignoreClone private _centerDirtyFlag: BoolUpdateFlag; /** diff --git a/packages/ui/src/component/UIGroup.ts b/packages/ui/src/component/UIGroup.ts index b876e05ec4..99b59f3d12 100644 --- a/packages/ui/src/component/UIGroup.ts +++ b/packages/ui/src/component/UIGroup.ts @@ -1,4 +1,4 @@ -import { Component, DisorderedArray, Entity, EntityModifyFlags, assignmentClone, ignoreClone } from "@galacean/engine"; +import { Component, DisorderedArray, Entity, EntityModifyFlags, ignoreClone } from "@galacean/engine"; import { Utils } from "../Utils"; import { IGroupAble } from "../interface/IGroupAble"; import { EntityUIModifyFlags, UICanvas } from "./UICanvas"; @@ -15,7 +15,6 @@ export class UIGroup extends Component implements IGroupAble { /** @internal */ _rootCanvas: UICanvas; /** @internal */ - @ignoreClone _disorderedElements: DisorderedArray = new DisorderedArray(); /** @internal */ @@ -25,11 +24,8 @@ export class UIGroup extends Component implements IGroupAble { @ignoreClone _globalInteractive = true; - @assignmentClone private _alpha = 1; - @assignmentClone private _interactive = true; - @assignmentClone private _ignoreParentGroup = false; /** @internal */ diff --git a/packages/ui/src/component/UIRenderer.ts b/packages/ui/src/component/UIRenderer.ts index 527ad3bd23..e090b503aa 100644 --- a/packages/ui/src/component/UIRenderer.ts +++ b/packages/ui/src/component/UIRenderer.ts @@ -15,8 +15,6 @@ import { SpriteMaskLayer, Vector3, Vector4, - assignmentClone, - deepClone, dependentComponents, ignoreClone, Vector2 @@ -58,7 +56,6 @@ export class UIRenderer extends Renderer implements IGraphics { * Custom boundary for raycast detection. * @remarks this is based on `this.entity.transform`. */ - @deepClone raycastPadding: Vector4 = new Vector4(0, 0, 0, 0); /** @internal */ _rootCanvas: UICanvas; @@ -101,9 +98,7 @@ export class UIRenderer extends Renderer implements IGraphics { @ignoreClone _rectMaskHardClip: boolean = false; - @assignmentClone private _raycastEnabled: boolean = false; - @deepClone protected _color: Color = new Color(1, 1, 1, 1); /** diff --git a/packages/ui/src/component/UITransform.ts b/packages/ui/src/component/UITransform.ts index 9144373472..366ef3aaa2 100644 --- a/packages/ui/src/component/UITransform.ts +++ b/packages/ui/src/component/UITransform.ts @@ -1,13 +1,4 @@ -import { - Entity, - MathUtil, - Rect, - Transform, - TransformModifyFlags, - Vector2, - deepClone, - ignoreClone -} from "@galacean/engine"; +import { Entity, MathUtil, Rect, Transform, TransformModifyFlags, Vector2, ignoreClone } from "@galacean/engine"; import { HorizontalAlignmentMode } from "../enums/HorizontalAlignmentMode"; import { VerticalAlignmentMode } from "../enums/VerticalAlignmentMode"; @@ -19,7 +10,6 @@ export class UITransform extends Transform { private _size = new Vector2(100, 100); @ignoreClone private _pivot = new Vector2(0.5, 0.5); - @deepClone private _rect = new Rect(-50, -50, 100, 100); private _alignLeft = 0; diff --git a/packages/ui/src/component/advanced/Button.ts b/packages/ui/src/component/advanced/Button.ts index 73bf817562..c407c7a9cf 100644 --- a/packages/ui/src/component/advanced/Button.ts +++ b/packages/ui/src/component/advanced/Button.ts @@ -1,9 +1,8 @@ -import { deepClone, PointerEventData, Signal } from "@galacean/engine"; +import { PointerEventData, Signal } from "@galacean/engine"; import { UIInteractive } from "../interactive/UIInteractive"; export class Button extends UIInteractive { /** Signal emitted when the button is clicked. */ - @deepClone readonly onClick = new Signal<[PointerEventData]>(); override onPointerClick(event: PointerEventData): void { diff --git a/packages/ui/src/component/advanced/Image.ts b/packages/ui/src/component/advanced/Image.ts index 5485cdae20..a58bc81916 100644 --- a/packages/ui/src/component/advanced/Image.ts +++ b/packages/ui/src/component/advanced/Image.ts @@ -15,7 +15,6 @@ import { SpriteModifyFlags, SpriteTileMode, TiledSpriteAssembler, - assignmentClone, ignoreClone } from "@galacean/engine"; import { CanvasRenderMode } from "../../enums/CanvasRenderMode"; @@ -33,17 +32,11 @@ export class Image extends UIRenderer implements ISpriteRenderer { private _drawMode: SpriteDrawMode; @ignoreClone private _assembler: ISpriteAssembler; - @assignmentClone private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; - @assignmentClone private _tiledAdaptiveThreshold: number = 0.5; - @assignmentClone private _filledMode: SpriteFilledMode = SpriteFilledMode.Radial360; - @assignmentClone private _filledAmount: number = 1; - @assignmentClone private _filledOrigin: SpriteFilledOrigin = SpriteFilledOrigin.Bottom; - @assignmentClone private _filledClockWise: boolean = true; /** diff --git a/packages/ui/src/component/advanced/RectMask2D.ts b/packages/ui/src/component/advanced/RectMask2D.ts index 9cba135e84..44e591873e 100644 --- a/packages/ui/src/component/advanced/RectMask2D.ts +++ b/packages/ui/src/component/advanced/RectMask2D.ts @@ -1,14 +1,4 @@ -import { - Component, - DependentMode, - Entity, - Vector2, - Vector3, - Vector4, - assignmentClone, - deepClone, - dependentComponents -} from "@galacean/engine"; +import { Component, DependentMode, Entity, Vector2, Vector3, Vector4, dependentComponents } from "@galacean/engine"; import { UICanvas } from "../UICanvas"; import { UITransform } from "../UITransform"; @@ -23,9 +13,7 @@ export class RectMask2D extends Component { private static _tempCorner2: Vector3 = new Vector3(); private static _tempCorner3: Vector3 = new Vector3(); - @deepClone private _softness: Vector2 = new Vector2(0, 0); - @assignmentClone private _alphaClip: boolean = false; /** diff --git a/packages/ui/src/component/advanced/Text.ts b/packages/ui/src/component/advanced/Text.ts index 329902f9e7..d2ae7bec78 100644 --- a/packages/ui/src/component/advanced/Text.ts +++ b/packages/ui/src/component/advanced/Text.ts @@ -19,8 +19,6 @@ import { Texture2D, Vector3, VertexMergeBatcher, - assignmentClone, - deepClone, ignoreClone } from "@galacean/engine"; import { RootCanvasModifyFlags } from "../UICanvas"; @@ -42,31 +40,19 @@ export class Text extends UIRenderer implements ITextRenderer { private _textChunks = Array(); @ignoreClone private _subFont: SubFont = null; - @assignmentClone private _text: string = ""; @ignoreClone private _localBounds: BoundingBox = new BoundingBox(); - @assignmentClone private _font: Font = null; - @assignmentClone private _fontSize: number = 24; - @assignmentClone private _fontStyle: FontStyle = FontStyle.None; - @assignmentClone private _lineSpacing: number = 0; - @assignmentClone private _characterSpacing: number = 0; - @assignmentClone private _horizontalAlignment: TextHorizontalAlignment = TextHorizontalAlignment.Center; - @assignmentClone private _verticalAlignment: TextVerticalAlignment = TextVerticalAlignment.Center; - @assignmentClone private _enableWrapping: boolean = false; - @assignmentClone private _overflowMode: OverflowMode = OverflowMode.Overflow; - @deepClone private _outlineColor: Color = new Color(0, 0, 0, 1); - @ignoreClone private _outlineWidth: number = 0; /** @@ -249,8 +235,14 @@ export class Text extends UIRenderer implements ITextRenderer { if (this._isTextNoVisible()) { if (this._isContainDirtyFlag(RendererUpdateFlags.WorldVolume)) { const localBounds = this._localBounds; - localBounds.min.set(0, 0, 0); - localBounds.max.set(0, 0, 0); + if (this._getRootCanvas()) { + localBounds.min.set(0, 0, 0); + localBounds.max.set(0, 0, 0); + } else { + const { size, pivot } = this.entity.transform; + localBounds.min.set(-size.x * pivot.x, -size.y * pivot.y, 0); + localBounds.max.set(size.x * (1 - pivot.x), size.y * (1 - pivot.y), 0); + } this._updateBounds(this._bounds); this._setDirtyFlagFalse(RendererUpdateFlags.WorldVolume); } @@ -297,15 +289,6 @@ export class Text extends UIRenderer implements ITextRenderer { this._subFont && (this._subFont = null); } - // @ts-ignore - override _cloneTo(target: Text): void { - // @ts-ignore - super._cloneTo(target); - target.font = this._font; - target._subFont = this._subFont; - target.outlineWidth = this._outlineWidth; - } - /** * @internal */ diff --git a/packages/ui/src/component/interactive/UIInteractive.ts b/packages/ui/src/component/interactive/UIInteractive.ts index 6811819585..43a7a36e47 100644 --- a/packages/ui/src/component/interactive/UIInteractive.ts +++ b/packages/ui/src/component/interactive/UIInteractive.ts @@ -1,12 +1,4 @@ -import { - CloneUtils, - Entity, - EntityModifyFlags, - Script, - assignmentClone, - deepClone, - ignoreClone -} from "@galacean/engine"; +import { Entity, EntityModifyFlags, Script, ignoreClone } from "@galacean/engine"; import { UIGroup } from "../.."; import { Utils } from "../../Utils"; import { IGroupAble } from "../../interface/IGroupAble"; @@ -48,9 +40,7 @@ export class UIInteractive extends Script implements IGroupAble { @ignoreClone _globalInteractiveDirty: boolean = false; - @deepClone protected _transitions: Transition[] = []; - @assignmentClone protected _interactive: boolean = true; @ignoreClone protected _state: InteractiveState = InteractiveState.Normal; diff --git a/packages/ui/src/component/interactive/transition/SpriteTransition.ts b/packages/ui/src/component/interactive/transition/SpriteTransition.ts index 4a20c1bc13..f90441236d 100644 --- a/packages/ui/src/component/interactive/transition/SpriteTransition.ts +++ b/packages/ui/src/component/interactive/transition/SpriteTransition.ts @@ -6,35 +6,6 @@ import { Transition } from "./Transition"; * Sprite transition. */ export class SpriteTransition extends Transition { - /** - * @internal - */ - override destroy(): void { - super.destroy(); - if (this._normal) { - // @ts-ignore - this._normal._addReferCount(-1); - this._normal = null; - } - if (this._hover) { - // @ts-ignore - this._hover._addReferCount(-1); - this._hover = null; - } - if (this._pressed) { - // @ts-ignore - this._pressed._addReferCount(-1); - this._pressed = null; - } - if (this._disabled) { - // @ts-ignore - this._disabled._addReferCount(-1); - this._disabled = null; - } - this._initialValue = this._currentValue = this._finalValue = null; - this._target = null; - } - protected _getTargetValueCopy(): Sprite { return this._target?.sprite; } diff --git a/packages/ui/src/component/interactive/transition/Transition.ts b/packages/ui/src/component/interactive/transition/Transition.ts index 5dd461e615..1b76932098 100644 --- a/packages/ui/src/component/interactive/transition/Transition.ts +++ b/packages/ui/src/component/interactive/transition/Transition.ts @@ -1,4 +1,4 @@ -import { Color, ReferResource, Sprite } from "@galacean/engine"; +import { Color, DataObject, ReferResource, Sprite, ignoreClone } from "@galacean/engine"; import { UIRenderer } from "../../UIRenderer"; import { InteractiveState, UIInteractive } from "../UIInteractive"; @@ -8,7 +8,7 @@ import { InteractiveState, UIInteractive } from "../UIInteractive"; export abstract class Transition< T extends TransitionValueType = TransitionValueType, K extends UIRenderer = UIRenderer -> { +> extends DataObject { /** @internal */ _interactive: UIInteractive; @@ -18,10 +18,15 @@ export abstract class Transition< protected _hover: T; protected _disabled: T; protected _duration: number = 0; + @ignoreClone protected _countDown: number = 0; + @ignoreClone protected _initialValue: T; + @ignoreClone protected _finalValue: T; + @ignoreClone protected _currentValue: T; + @ignoreClone protected _finalState: InteractiveState = InteractiveState.Normal; /** @@ -119,9 +124,19 @@ export abstract class Transition< destroy(): void { this._interactive?.removeTransition(this); + this._addStateValuesReferCount(-1); + this._normal = this._pressed = this._hover = this._disabled = null; + this._initialValue = this._currentValue = this._finalValue = null; this._target = null; } + /** + * @internal + */ + _cloneTo(target: Transition): void { + target._addStateValuesReferCount(1); + } + /** * @internal */ @@ -170,6 +185,18 @@ export abstract class Transition< this._target?.enabled && this._applyValue(this._currentValue); } + private _addStateValuesReferCount(count: number): void { + const { _normal, _pressed, _hover, _disabled } = this; + // @ts-ignore + _normal instanceof ReferResource && _normal._addReferCount(count); + // @ts-ignore + _pressed instanceof ReferResource && _pressed._addReferCount(count); + // @ts-ignore + _hover instanceof ReferResource && _hover._addReferCount(count); + // @ts-ignore + _disabled instanceof ReferResource && _disabled._addReferCount(count); + } + private _getValueByState(state: InteractiveState): T { switch (state) { case InteractiveState.Normal: diff --git a/tests/src/core/CloneManager.test.ts b/tests/src/core/CloneManager.test.ts new file mode 100644 index 0000000000..3d3d6a6363 --- /dev/null +++ b/tests/src/core/CloneManager.test.ts @@ -0,0 +1,2243 @@ +import { + BoolUpdateFlag, + Burst, + DataObject, + DisorderedArray, + Entity, + Logger, + MeshRenderer, + ParticleCompositeCurve, + ParticleCompositeGradient, + ParticleRenderer, + Script, + Signal, + Texture2D, + assignmentClone, + deepClone, + ignoreClone +} from "@galacean/engine-core"; +import * as EngineCore from "@galacean/engine-core"; +import * as EngineMath from "@galacean/engine-math"; +import * as EngineUI from "@galacean/engine-ui"; +import { Color, Ray, Vector3 } from "@galacean/engine-math"; +import { WebGLEngine } from "@galacean/engine"; +import { describe, expect, it, vi } from "vitest"; + +class TestScript extends Script { + targetEntity: Entity; + targetRenderer: MeshRenderer; + externalEntity: Entity; + externalRenderer: MeshRenderer; + deepChild: Entity; + selfRef: Entity; + speed: number; + name2: string; + flag: boolean; + data: object; +} + +/** Script with multiple entity/component refs pointing to different nodes */ +class MultiRefScript extends Script { + entityA: Entity; + entityB: Entity; + rendererA: MeshRenderer; + rendererB: MeshRenderer; +} + +/** Script where the same entity is referenced by multiple properties */ +class DuplicateRefScript extends Script { + ref1: Entity; + ref2: Entity; +} + +/** Script with null/undefined entity/component refs */ +class NullRefScript extends Script { + nullEntity: Entity = null; + undefinedEntity: Entity; + nullRenderer: MeshRenderer = null; + someNumber: number = 0; +} + +/** Script referencing a sibling entity (not parent/child, but sibling under clone root) */ +class SiblingRefScript extends Script { + sibling: Entity; + siblingRenderer: MeshRenderer; +} + +/** Script with a mix of decorated and undecorated entity refs */ +class DecoratedRefScript extends Script { + // Undecorated — Entity type default (Remap) applies + autoRemapEntity: Entity; + + // @assignmentClone — field decorator wins over the type default: shares the source reference + @assignmentClone + assignedEntity: Entity; + + // @ignoreClone — field decorator wins over the type default: keeps the clone's own value + @ignoreClone + ignoredEntity: Entity; +} + +/** Script with a @deepClone array of entities */ +class ArrayRefScript extends Script { + @deepClone + entities: Entity[] = []; +} + +/** Script with Component self-reference */ +class SelfComponentRefScript extends Script { + selfScript: SelfComponentRefScript; +} + +/** Script referencing another Script on a different entity */ +class CrossScriptRefScript extends Script { + otherScript: TestScript; +} + +/** Script with a nested plain object containing entity refs */ +class NestedObjectScript extends Script { + @deepClone + config: { target: Entity; label: string } = { target: null, label: "" }; +} + +/** Script for testing multiple same-type components on one entity */ +class CounterScript extends Script { + value: number = 0; + partner: CounterScript; + targetEntity: Entity; +} + +/** Script that references a CounterScript */ +class CounterRefScript extends Script { + counter: CounterScript; +} + +/** Handler script used for Signal structured binding tests */ +class ClickHandler extends Script { + callCount = 0; + lastPrefix: string = ""; + + handleClick(): void { + this.callCount++; + } + + handleClickWithPrefix(arg: number, prefix: string): void { + this.callCount++; + this.lastPrefix = prefix; + } +} + +/** Script with a Signal property */ +class SignalScript extends Script { + @deepClone + readonly onFire = new Signal<[number]>(); +} + +/** Script with function-valued fields, standalone and inside containers */ +class HandlerScript extends Script { + onTick: () => void; + handlers: Array<() => void> = []; + handlerSet: Set<() => void> = new Set(); + config: { onDone: (() => void) | null; x: number } = { onDone: null, x: 0 }; +} + +/** Data object counting how often the gate runs its post-clone hook */ +class HookCountPayload extends DataObject { + static runs = 0; + value = 0; + _cloneTo(): void { + HookCountPayload.runs++; + } +} + +/** Script referencing one payload from two slots */ +class SharedPayloadScript extends Script { + slotA: HookCountPayload = null; + slotB: HookCountPayload = null; +} + +/** Script opting a plain runtime container into a deep copy */ +class DeepContainerScript extends Script { + @deepClone + entries: DisorderedArray<{ id: number }> = new DisorderedArray<{ id: number }>(); +} + +/** Script asking for a deep copy the paired flag registry cannot honor */ +class DeepFlagManagerScript extends Script { + @deepClone + flagManager: any = null; +} + +/** Script whose constructor establishes its own bound handler */ +class BoundHandlerScript extends Script { + tickCount = 0; + boundTick = this._tick.bind(this); + + private _tick(): void { + this.tickCount++; + } +} + +/** Base script decorating two Entity fields, one to be overridden by a subclass, one left inherited. */ +class BaseOverrideScript extends Script { + @assignmentClone + reDecorated: Entity; + @ignoreClone + inherited: Entity = null; +} + +/** Subclass re-decorates `reDecorated` (Assignment → Ignore) but never mentions `inherited`. */ +class SubOverrideScript extends BaseOverrideScript { + @ignoreClone + reDecorated: Entity; +} + +/** Script holding binary data views */ +class BinaryScript extends Script { + view: DataView; + bytes: Float32Array; +} + +/** + * Script holding a shared ReferResource, honoring the slot-ownership contract the same way + * engine components do: acquire on assignment, release on destroy. The clone gate adds +1 for + * the cloned backing slot, which the same onDestroy release balances. + */ +class ResourceRefScript extends Script { + private _texture: Texture2D; + + get texture(): Texture2D { + return this._texture; + } + + set texture(value: Texture2D) { + if (this._texture !== value) { + (this._texture as any)?._addReferCount(-1); + (value as any)?._addReferCount(1); + this._texture = value; + } + } + + override onDestroy(): void { + this.texture = null; + } +} + +/** Script whose constructor presets an owned counted resource into a clonable slot. */ +class PresetTextureScript extends Script { + static created: Texture2D[] = []; + + texture: Texture2D; + + constructor(entity: Entity) { + super(entity); + const texture = new Texture2D(entity.engine, 1, 1); + (texture as any)._addReferCount(1); + PresetTextureScript.created.push(texture); + this.texture = texture; + } +} + +/** Script with two fields aliasing one typed array (identity must survive the clone). */ +class AliasedBinaryScript extends Script { + a: Float32Array; + b: Float32Array; +} + +/** Unregistered user value type without any counting API — must share, never count. */ +class SharedConfig { + value = 1; +} + +/** Script holding a user Assignment-registered object. */ +class SharedConfigScript extends Script { + config: SharedConfig = null; +} + +/** Script whose constructor presets a counted resource WITHOUT acquiring it (contract violation). */ +class UnownedPresetScript extends Script { + static created: Texture2D[] = []; + + tex: Texture2D; + + constructor(entity: Entity) { + super(entity); + const texture = new Texture2D(entity.engine, 1, 1); + UnownedPresetScript.created.push(texture); + this.tex = texture; + } +} + +/** User DataObject whose constructor dereferences a required argument (contract violation). */ +class ParamDeepConfig extends DataObject { + target: string; + + constructor(source: { id: string }) { + super(); + this.target = source.id; + } +} + +/** Script holding plain data whose payload happens to carry a `copyFrom` key. */ +class CopyFromDataScript extends Script { + config: any = null; +} + +/** Script whose binary fields alias class-level shared default tables (preset === source). */ +class SharedDefaultTableScript extends Script { + static DEFAULT_WEIGHTS = new Float32Array([1, 2, 3]); + static DEFAULT_VIEW = new DataView(new ArrayBuffer(4)); + weights: Float32Array = SharedDefaultTableScript.DEFAULT_WEIGHTS; + view: DataView = SharedDefaultTableScript.DEFAULT_VIEW; +} + +/** Script whose Map / Set / Array fields carry constructor-built entries (the clone's preset). */ +class PresetContainerScript extends Script { + map = new Map([["preset", 1]]); + set = new Set(["preset"]); + list: number[] = [1, 2, 3]; +} + +/** Script whose Array / Map / Set fields alias class-level shared defaults (preset === source). */ +class SharedDefaultContainerScript extends Script { + static DEFAULT_LIST = [1, 2, 3]; + static DEFAULT_MAP = new Map([["a", 1]]); + static DEFAULT_SET = new Set(["a"]); + list: number[] = SharedDefaultContainerScript.DEFAULT_LIST; + map: Map = SharedDefaultContainerScript.DEFAULT_MAP; + set: Set = SharedDefaultContainerScript.DEFAULT_SET; +} + +/** Plain class with no deep-clone default (not DataObject / math / container). */ +class PlainConfig { + count = 0; + nested = { x: 1 }; +} + +/** Script sharing one container through an @assignmentClone field */ +class AssignedContainerScript extends Script { + @assignmentClone + shared: number[] = []; +} + +/** Script pointing @deepClone at a function value */ +class DeepFnScript extends Script { + @deepClone + onTick: () => void = () => {}; +} + +/** Script whose @deepClone fields hold members with no deep default of their own */ +class DeepSubtreeScript extends Script { + @deepClone + configs: PlainConfig[] = []; + @deepClone + bag: any = null; + @deepClone + mixed: any[] = null; +} + +/** Script whose ctor-built binary values stay observable through @ignoreClone aliases */ +class BinaryPresetScript extends Script { + weights = new Float32Array(3); + view = new DataView(new ArrayBuffer(4)); + shortWeights = new Float32Array(2); + @ignoreClone + ownWeights = this.weights; + @ignoreClone + ownView = this.view; + @ignoreClone + ownShort = this.shortWeights; +} + +/** Non-component class carrying its own @ignoreClone, to be reached through a field walk. */ +class Bag { + kept = 1; + @ignoreClone + runtime = 1; +} + +/** Script forcing the walk into Bag with @deepClone. */ +class BagHolderScript extends Script { + @deepClone + bag: Bag = null; +} + +/** Script @deepClone-ing a class that has no deep-clone default — must force a deep copy. */ +class ForcedDeepScript extends Script { + @deepClone + config: PlainConfig = null; +} + +/** Two fields on one script pointing at the same default-less instance, one @deepClone'd. */ +class MixedIntentScript extends Script { + @deepClone + deep: PlainConfig = null; + shared: PlainConfig = null; +} + +/** Same pair, declared the other way round — field order must not change either outcome. */ +class MixedIntentReversedScript extends Script { + shared: PlainConfig = null; + @deepClone + deep: PlainConfig = null; +} + +/** Script holding the same class without @deepClone — the default path shares it. */ +class SharedPlainScript extends Script { + config: PlainConfig = null; +} + +/** Script misusing @deepClone on an Entity ref — cloning it must throw. */ +class DeepEntityRefScript extends Script { + @deepClone + target: Entity; +} + +/** Script misusing @deepClone on an engine-bound asset — cloning it must throw. */ +class DeepAssetRefScript extends Script { + @deepClone + texture: Texture2D; +} + +/** Script with an @assignmentClone function field preset by the constructor */ +class AssignedHandlerScript extends Script { + @assignmentClone + handler: () => void = this._noop.bind(this); + + private _noop(): void {} +} + +/** Script with an @ignoreClone function field preset by the constructor */ +class IgnoredHandlerScript extends Script { + @ignoreClone + handler: () => void = this._noop.bind(this); + + private _noop(): void {} +} + +describe("Clone remap", async () => { + const engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + const scene = engine.sceneManager.activeScene; + engine.run(); + + describe("Basic Entity/Component remap", () => { + it("script undecorated Entity ref should remap to cloned entity", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(TestScript); + script.targetEntity = child; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + const clonedChild = clonedParent.children[0]; + + expect(clonedScript.targetEntity).not.eq(child); + expect(clonedScript.targetEntity).eq(clonedChild); + expect(clonedScript.targetEntity.name).eq("child"); + + rootEntity.destroy(); + }); + + it("script undecorated Component ref should remap to cloned component", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const meshRenderer = child.addComponent(MeshRenderer); + const script = parent.addComponent(TestScript); + script.targetRenderer = meshRenderer; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + const clonedChild = clonedParent.children[0]; + const clonedMeshRenderer = clonedChild.getComponent(MeshRenderer); + + expect(clonedScript.targetRenderer).not.eq(meshRenderer); + expect(clonedScript.targetRenderer).eq(clonedMeshRenderer); + + rootEntity.destroy(); + }); + + it("script ref to entity outside hierarchy should keep original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(TestScript); + script.externalEntity = external; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + + expect(clonedScript.externalEntity).eq(external); + + rootEntity.destroy(); + }); + + it("script ref to component outside hierarchy should keep original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const externalMR = external.addComponent(MeshRenderer); + const script = parent.addComponent(TestScript); + script.externalRenderer = externalMR; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + + expect(clonedScript.externalRenderer).eq(externalMR); + + rootEntity.destroy(); + }); + + it("deep hierarchy entity ref should remap correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const grandchild = child.createChild("grandchild"); + const script = parent.addComponent(TestScript); + script.deepChild = grandchild; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + const clonedGrandchild = clonedParent.children[0].children[0]; + + expect(clonedScript.deepChild).not.eq(grandchild); + expect(clonedScript.deepChild).eq(clonedGrandchild); + expect(clonedScript.deepChild.name).eq("grandchild"); + + rootEntity.destroy(); + }); + + it("script ref to self entity (clone root) should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(TestScript); + script.selfRef = parent; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + + expect(clonedScript.selfRef).not.eq(parent); + expect(clonedScript.selfRef).eq(clonedParent); + + rootEntity.destroy(); + }); + + it("primitive props copied by value; plain object deep cloned", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(TestScript); + const obj = { x: 1 }; + script.speed = 42; + script.name2 = "test"; + script.flag = true; + script.data = obj; + + const clonedParent = parent.clone(); + const clonedScript = clonedParent.getComponent(TestScript); + + expect(clonedScript.speed).eq(42); + expect(clonedScript.name2).eq("test"); + expect(clonedScript.flag).eq(true); + // plain object is deep cloned into an independent copy, not shared + expect(clonedScript.data).not.eq(obj); + expect(clonedScript.data.x).eq(1); + + rootEntity.destroy(); + }); + }); + + describe("Multiple and duplicate refs", () => { + it("a shared object clones once, hook included", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(SharedPayloadScript); + const shared = new HookCountPayload(); + shared.value = 42; + script.slotA = shared; + script.slotB = shared; + + HookCountPayload.runs = 0; + const cloned = parent.clone(); + const cs = cloned.getComponent(SharedPayloadScript); + + expect(cs.slotA).not.eq(shared); + expect(cs.slotA).eq(cs.slotB); + expect(cs.slotA.value).eq(42); + // A second slot resolving to the same clone must not re-run the post-clone hook: hooks + // acquire references and register listeners, so a replay silently doubles both. + expect(HookCountPayload.runs).eq(1); + + rootEntity.destroy(); + }); + + it("multiple entity/component refs on same script all remap independently", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const mrA = childA.addComponent(MeshRenderer); + const mrB = childB.addComponent(MeshRenderer); + const script = parent.addComponent(MultiRefScript); + script.entityA = childA; + script.entityB = childB; + script.rendererA = mrA; + script.rendererB = mrB; + + const cloned = parent.clone(); + const cs = cloned.getComponent(MultiRefScript); + + expect(cs.entityA).not.eq(childA); + expect(cs.entityB).not.eq(childB); + expect(cs.entityA.name).eq("childA"); + expect(cs.entityB.name).eq("childB"); + expect(cs.entityA).eq(cloned.children[0]); + expect(cs.entityB).eq(cloned.children[1]); + expect(cs.rendererA).eq(cloned.children[0].getComponent(MeshRenderer)); + expect(cs.rendererB).eq(cloned.children[1].getComponent(MeshRenderer)); + + rootEntity.destroy(); + }); + + it("two properties referencing the same entity both remap to the same cloned entity", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DuplicateRefScript); + script.ref1 = child; + script.ref2 = child; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DuplicateRefScript); + + expect(cs.ref1).not.eq(child); + expect(cs.ref1).eq(cs.ref2); + expect(cs.ref1).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + }); + + describe("Null and undefined refs", () => { + it("null entity/component refs should not crash and remain null", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(NullRefScript); + + const cloned = parent.clone(); + const cs = cloned.getComponent(NullRefScript); + + expect(cs.nullEntity).eq(null); + expect(cs.nullRenderer).eq(null); + expect(cs.someNumber).eq(0); + + rootEntity.destroy(); + }); + }); + + describe("Sibling entity refs", () => { + it("ref to sibling entity under clone root should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const mrB = childB.addComponent(MeshRenderer); + const script = childA.addComponent(SiblingRefScript); + script.sibling = childB; + script.siblingRenderer = mrB; + + const cloned = parent.clone(); + const clonedChildA = cloned.children[0]; + const clonedChildB = cloned.children[1]; + const cs = clonedChildA.getComponent(SiblingRefScript); + + expect(cs.sibling).not.eq(childB); + expect(cs.sibling).eq(clonedChildB); + expect(cs.siblingRenderer).not.eq(mrB); + expect(cs.siblingRenderer).eq(clonedChildB.getComponent(MeshRenderer)); + + rootEntity.destroy(); + }); + }); + + describe("Field decorators take priority over Entity/Component remap", () => { + it("@assignmentClone entity ref shares the source reference (decorator wins)", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DecoratedRefScript); + script.assignedEntity = child; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DecoratedRefScript); + + expect(cs.assignedEntity).eq(child); + + rootEntity.destroy(); + }); + + it("@ignoreClone entity ref keeps the clone's own value (decorator wins)", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DecoratedRefScript); + script.ignoredEntity = child; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DecoratedRefScript); + + expect(cs.ignoredEntity).eq(undefined); + + rootEntity.destroy(); + }); + + it("undecorated entity ref remaps correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DecoratedRefScript); + script.autoRemapEntity = child; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DecoratedRefScript); + + expect(cs.autoRemapEntity).not.eq(child); + expect(cs.autoRemapEntity).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + + it("@ignoreClone entity ref outside hierarchy is ignored the same way", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(DecoratedRefScript); + script.ignoredEntity = external; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DecoratedRefScript); + + expect(cs.ignoredEntity).eq(undefined); + + rootEntity.destroy(); + }); + + it("@deepClone on an Entity ref throws — the explicit intent can't be honored, so it's surfaced", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DeepEntityRefScript); + script.target = child; + + // A decorator is the developer's explicit intent; @deepClone on an engine-bound Entity is a + // mistake — thrown, never silently remapped (an undecorated Entity ref remaps by default). + expect(() => parent.clone()).toThrowError(/@deepClone cannot deep clone/); + + rootEntity.destroy(); + }); + + it("@deepClone on an asset ref throws — assets are engine-bound and shared by reference", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(DeepAssetRefScript); + const texture = new Texture2D(engine, 4, 4); + script.texture = texture; + + expect(() => parent.clone()).toThrowError(/@deepClone cannot deep clone/); + + rootEntity.destroy(); + texture.destroy(true); + }); + + it("@deepClone forces a deep copy of a class that has no deep-clone default", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(ForcedDeepScript); + script.config = new PlainConfig(); + script.config.count = 7; + script.config.nested.x = 42; + + const cloned = parent.clone(); + const cs = cloned.getComponent(ForcedDeepScript); + + // @deepClone is an explicit request: the value is field-walked into an independent copy, + // even though PlainConfig is not DataObject / math / container (the default would share it). + expect(cs.config).not.eq(script.config); + expect(cs.config).instanceOf(PlainConfig); + expect(cs.config.count).eq(7); + expect(cs.config.nested).not.eq(script.config.nested); + expect(cs.config.nested.x).eq(42); + + rootEntity.destroy(); + }); + + it("the same class without @deepClone is shared by the default path", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(SharedPlainScript); + script.config = new PlainConfig(); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SharedPlainScript); + + // No deep-clone default and no decorator — the clone shares the source instance. + expect(cs.config).eq(script.config); + + rootEntity.destroy(); + }); + + it("each field keeps its own intent when both point at one instance (@deepClone declared first)", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(MixedIntentScript); + const config = new PlainConfig(); + script.deep = config; + script.shared = config; + + const cloned = parent.clone(); + const cs = cloned.getComponent(MixedIntentScript); + + // The two fields ask for opposite things about the same instance, so each is honored on its + // own: the decorated one gets an independent copy, the undecorated one keeps sharing. + expect(cs.deep).not.eq(config); + expect(cs.shared).eq(config); + + rootEntity.destroy(); + }); + + it("each field keeps its own intent when both point at one instance (@deepClone declared last)", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(MixedIntentReversedScript); + const config = new PlainConfig(); + script.deep = config; + script.shared = config; + + const cloned = parent.clone(); + const cs = cloned.getComponent(MixedIntentReversedScript); + + // Same expectations as above: declaration order must not change the outcome. + expect(cs.deep).not.eq(config); + expect(cs.shared).eq(config); + + rootEntity.destroy(); + }); + + it("@assignmentClone function field shares the source function (decorator wins over reuse)", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(AssignedHandlerScript); + const custom = () => {}; + script.handler = custom; + + const cloned = parent.clone(); + const cs = cloned.getComponent(AssignedHandlerScript); + + expect(cs.handler).eq(custom); + + rootEntity.destroy(); + }); + + it("@ignoreClone function field keeps the clone's own constructor-built value (never the source)", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(IgnoredHandlerScript); + const custom = () => {}; + script.handler = custom; + + const cloned = parent.clone(); + const cs = cloned.getComponent(IgnoredHandlerScript); + + // Ignore means the field is untouched by the gate — the clone's constructor already bound + // its own handler to its own `this`, so it must be neither the overridden source value nor + // the source instance's original bound handler. + expect(cs.handler).not.eq(custom); + expect(cs.handler).not.eq(script.handler); + expect(typeof cs.handler).eq("function"); + + rootEntity.destroy(); + }); + }); + + describe("Subclass field re-decoration shadows the base class's", () => { + it("the subclass's own decorator wins on a re-decorated field; an untouched field still inherits the base's", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const sibling = parent.createChild("sibling"); + const script = parent.addComponent(SubOverrideScript); + script.reDecorated = sibling; + script.inherited = sibling; + + const cloned = parent.clone(); + const cs = cloned.getComponent(SubOverrideScript); + + // Base decorates `reDecorated` @assignmentClone (share); the subclass re-decorates it + // @ignoreClone — the subclass's own decoration must win, not the base's. + expect(cs.reDecorated).eq(undefined); + // `inherited` is never touched by the subclass — the base's @ignoreClone must still apply. + expect(cs.inherited).eq(null); + + rootEntity.destroy(); + }); + + it("does not leak the subclass's re-decoration back onto the base class", () => { + // A subclass re-decorating a field must leave the base class untouched. + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const sibling = parent.createChild("sibling"); + const baseScript = parent.addComponent(BaseOverrideScript); + baseScript.reDecorated = sibling; + + const cloned = parent.clone(); + const cs = cloned.getComponent(BaseOverrideScript); + + // The base class's own @assignmentClone on `reDecorated` must still share the source. + expect(cs.reDecorated).eq(sibling); + + rootEntity.destroy(); + }); + }); + + describe("@deepClone array of entities", () => { + it("deep cloned entity array should remap internal refs", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const script = parent.addComponent(ArrayRefScript); + script.entities = [childA, childB]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(ArrayRefScript); + + expect(cs.entities).not.eq(script.entities); + expect(cs.entities.length).eq(2); + expect(cs.entities[0]).not.eq(childA); + expect(cs.entities[1]).not.eq(childB); + expect(cs.entities[0]).eq(cloned.children[0]); + expect(cs.entities[1]).eq(cloned.children[1]); + + rootEntity.destroy(); + }); + + it("deep cloned entity array with external ref keeps original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(ArrayRefScript); + script.entities = [child, external]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(ArrayRefScript); + + expect(cs.entities[0]).eq(cloned.children[0]); + expect(cs.entities[1]).eq(external); + + rootEntity.destroy(); + }); + + it("deep cloned empty entity array stays empty", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(ArrayRefScript); + script.entities = []; + + const cloned = parent.clone(); + const cs = cloned.getComponent(ArrayRefScript); + + expect(cs.entities).not.eq(script.entities); + expect(cs.entities.length).eq(0); + + rootEntity.destroy(); + }); + }); + + describe("Component self and cross references", () => { + it("script referencing itself should remap to cloned script", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(SelfComponentRefScript); + script.selfScript = script; + + const cloned = parent.clone(); + const cs = cloned.getComponent(SelfComponentRefScript); + + expect(cs.selfScript).not.eq(script); + expect(cs.selfScript).eq(cs); + + rootEntity.destroy(); + }); + + it("script referencing another script on child entity should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const childScript = child.addComponent(TestScript); + const script = parent.addComponent(CrossScriptRefScript); + script.otherScript = childScript; + + const cloned = parent.clone(); + const cs = cloned.getComponent(CrossScriptRefScript); + const clonedChildScript = cloned.children[0].getComponent(TestScript); + + expect(cs.otherScript).not.eq(childScript); + expect(cs.otherScript).eq(clonedChildScript); + + rootEntity.destroy(); + }); + + it("script referencing external script should keep original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const externalScript = external.addComponent(TestScript); + const script = parent.addComponent(CrossScriptRefScript); + script.otherScript = externalScript; + + const cloned = parent.clone(); + const cs = cloned.getComponent(CrossScriptRefScript); + + expect(cs.otherScript).eq(externalScript); + + rootEntity.destroy(); + }); + }); + + describe("Nested @deepClone object with entity refs", () => { + it("entity ref inside deep cloned plain object should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(NestedObjectScript); + script.config = { target: child, label: "hello" }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(NestedObjectScript); + + expect(cs.config).not.eq(script.config); + expect(cs.config.label).eq("hello"); + expect(cs.config.target).not.eq(child); + expect(cs.config.target).eq(cloned.children[0]); + + rootEntity.destroy(); + }); + + it("entity ref inside deep cloned object pointing outside keeps original", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(NestedObjectScript); + script.config = { target: external, label: "ext" }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(NestedObjectScript); + + expect(cs.config.target).eq(external); + expect(cs.config.label).eq("ext"); + + rootEntity.destroy(); + }); + }); + + describe("Signal clone with structured bindings", () => { + it("@deepClone Signal should not copy closure listeners", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(SignalScript); + let called = false; + script.onFire.on(() => { + called = true; + }); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + + expect(cs.onFire).not.eq(script.onFire); + cs.onFire.invoke(1); + expect(called).eq(false); + + rootEntity.destroy(); + }); + + it("@deepClone Signal should remap structured binding target to cloned hierarchy", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const handlerEntity = parent.createChild("handler"); + const handler = handlerEntity.addComponent(ClickHandler); + const script = parent.addComponent(SignalScript); + script.onFire.on(handler, "handleClick"); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); + + cs.onFire.invoke(1); + expect(clonedHandler.callCount).eq(1); + expect(handler.callCount).eq(0); + + rootEntity.destroy(); + }); + + it("@deepClone Signal should keep external structured binding target", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const external = rootEntity.createChild("external"); + const externalHandler = external.addComponent(ClickHandler); + const script = parent.addComponent(SignalScript); + script.onFire.on(externalHandler, "handleClick"); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + + cs.onFire.invoke(1); + expect(externalHandler.callCount).eq(1); + + rootEntity.destroy(); + }); + + it("@deepClone Signal should remap structured binding with pre-resolved args", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const handlerEntity = parent.createChild("handler"); + const handler = handlerEntity.addComponent(ClickHandler); + const script = parent.addComponent(SignalScript); + script.onFire.on(handler, "handleClickWithPrefix", "myPrefix"); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); + + cs.onFire.invoke(1); + expect(clonedHandler.callCount).eq(1); + expect(clonedHandler.lastPrefix).eq("myPrefix"); + expect(handler.callCount).eq(0); + + rootEntity.destroy(); + }); + + it("@deepClone Signal shares non-entity object args deterministically", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const handlerEntity = parent.createChild("handler"); + const handler = handlerEntity.addComponent(ClickHandler); + const script = parent.addComponent(SignalScript); + const payload = { hp: 5 }; + script.onFire.on(handler, "handleClickWithPrefix", payload); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); + + cs.onFire.invoke(1); + // Non-entity object args are shared with the source, independent of field-walk order. + expect(clonedHandler.lastPrefix).eq(payload); + + rootEntity.destroy(); + }); + + it("@deepClone Signal should preserve once flag on structured binding", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const handlerEntity = parent.createChild("handler"); + const handler = handlerEntity.addComponent(ClickHandler); + const script = parent.addComponent(SignalScript); + script.onFire.once(handler, "handleClick"); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SignalScript); + const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); + + cs.onFire.invoke(1); + expect(clonedHandler.callCount).eq(1); + cs.onFire.invoke(2); + expect(clonedHandler.callCount).eq(1); // once: removed after first call + + rootEntity.destroy(); + }); + }); + + describe("Clone hierarchy integrity", () => { + it("clone preserves children count and names", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + parent.createChild("a"); + parent.createChild("b"); + parent.createChild("c"); + + const cloned = parent.clone(); + expect(cloned.children.length).eq(3); + expect(cloned.children[0].name).eq("a"); + expect(cloned.children[1].name).eq("b"); + expect(cloned.children[2].name).eq("c"); + + rootEntity.destroy(); + }); + + it("clone of deeply nested hierarchy preserves structure", () => { + const rootEntity = scene.createRootEntity("root"); + const a = rootEntity.createChild("a"); + const b = a.createChild("b"); + const c = b.createChild("c"); + const d = c.createChild("d"); + + const cloned = a.clone(); + expect(cloned.children[0].name).eq("b"); + expect(cloned.children[0].children[0].name).eq("c"); + expect(cloned.children[0].children[0].children[0].name).eq("d"); + + rootEntity.destroy(); + }); + + it("script on child entity with ref to parent should remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = child.addComponent(TestScript); + script.targetEntity = parent; + + const cloned = parent.clone(); + const clonedChild = cloned.children[0]; + const cs = clonedChild.getComponent(TestScript); + + expect(cs.targetEntity).not.eq(parent); + expect(cs.targetEntity).eq(cloned); + + rootEntity.destroy(); + }); + + it("multiple scripts on different entities with cross-refs all remap correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + + const scriptA = childA.addComponent(TestScript); + scriptA.targetEntity = childB; + + const scriptB = childB.addComponent(TestScript); + scriptB.targetEntity = childA; + + const cloned = parent.clone(); + const clonedA = cloned.children[0]; + const clonedB = cloned.children[1]; + const csA = clonedA.getComponent(TestScript); + const csB = clonedB.getComponent(TestScript); + + expect(csA.targetEntity).eq(clonedB); + expect(csB.targetEntity).eq(clonedA); + + rootEntity.destroy(); + }); + }); + + describe("Function fields", () => { + it("@deepClone on a function throws", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + parent.addComponent(DeepFnScript); + + expect(() => parent.clone()).toThrowError(/@deepClone cannot deep clone a function/); + + rootEntity.destroy(); + }); + + it("a function inside a @deepClone subtree is shared, not thrown on", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(DeepSubtreeScript); + const fn = () => {}; + script.bag = { onDone: fn }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DeepSubtreeScript); + + expect(cs.bag.onDone).eq(fn); + + rootEntity.destroy(); + }); + + it("plain function field is shared, not lost", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(HandlerScript); + const fn = () => {}; + script.onTick = fn; + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript); + + expect(cs.onTick).eq(fn); + + rootEntity.destroy(); + }); + + it("functions inside arrays / sets / plain objects survive cloning", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(HandlerScript); + const fn = () => {}; + script.handlers = [fn]; + script.handlerSet = new Set([fn]); + script.config = { onDone: fn, x: 1 }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript); + + expect(cs.handlers).not.eq(script.handlers); + expect(cs.handlers.length).eq(1); + expect(cs.handlers[0]).eq(fn); + expect(cs.handlerSet).not.eq(script.handlerSet); + expect(cs.handlerSet.has(fn)).eq(true); + expect(cs.config).not.eq(script.config); + expect(cs.config.onDone).eq(fn); + expect(cs.config.x).eq(1); + + rootEntity.destroy(); + }); + + it("constructor-bound function field keeps the clone's own binding", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(BoundHandlerScript); + + const cloned = parent.clone(); + const cs = cloned.getComponent(BoundHandlerScript); + + expect(cs.boundTick).not.eq(script.boundTick); + cs.boundTick(); + expect(cs.tickCount).eq(1); + expect(script.tickCount).eq(0); + + rootEntity.destroy(); + }); + }); + + describe("Runtime-container type defaults (Ignore)", () => { + it("an undecorated DisorderedArray slot keeps the clone's own instance", async () => { + const { DisorderedArray } = await import("@galacean/engine-core"); + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(HandlerScript); + const runtimeList = new DisorderedArray(); + runtimeList.add(1); + (script as any).runtimeList = runtimeList; + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript) as any; + + // Type-level Ignore: the slot is neither shared nor deep-cloned — the clone keeps its + // own (absent) value instead of aliasing the source's runtime container. + expect(cs.runtimeList).not.eq(runtimeList); + expect(cs.runtimeList).eq(undefined); + expect(runtimeList.length).eq(1); + + rootEntity.destroy(); + }); + + it("undecorated SafeLoopArray and UpdateFlag slots keep the clone's own value", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(HandlerScript); + const loopList = (new Signal())._listeners; + loopList.push({ once: false }); + (script as any).loopList = loopList; + (script as any).flag = new BoolUpdateFlag(); + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript) as any; + + expect(cs.loopList).eq(undefined); + expect(cs.flag).eq(undefined); + expect(loopList.length).eq(1); + + rootEntity.destroy(); + }); + + it("@deepClone overrides the default on a plain container", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(DeepContainerScript); + script.entries.add({ id: 1 }); + + const cloned = parent.clone(); + const cs = cloned.getComponent(DeepContainerScript); + + expect(cs.entries).instanceOf(DisorderedArray); + expect(cs.entries).not.eq(script.entries); + expect(cs.entries.length).eq(1); + expect(cs.entries._elements[0]).not.eq(script.entries._elements[0]); + expect(cs.entries._elements[0].id).eq(1); + + rootEntity.destroy(); + }); + + it("@deepClone on a paired flag registry throws", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(DeepFlagManagerScript); + script.flagManager = (parent as any)._updateFlagManager; + + expect(() => parent.clone()).toThrowError(/@deepClone cannot deep clone "UpdateFlagManager"/); + + rootEntity.destroy(); + }); + }); + + describe("Null-prototype containers", () => { + it("Object.create(null) fields deep-clone as data containers, not shared references", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(HandlerScript); + const bag = Object.create(null); + bag.hp = 5; + bag.target = child; + (script as any).bag = bag; + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript) as any; + + expect(cs.bag).not.eq(bag); + expect(Object.getPrototypeOf(cs.bag)).eq(null); + expect(cs.bag.hp).eq(5); + // Entity refs nested in the bag remap like in any other container. + expect(cs.bag.target).eq(cloned.children[0]); + cs.bag.hp = 9; + expect(bag.hp).eq(5); + + rootEntity.destroy(); + }); + }); + + describe("Field-walk decorator awareness", () => { + it("respects @ignoreClone on a walked non-component class", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(BagHolderScript); + script.bag = new Bag(); + script.bag.kept = 42; + script.bag.runtime = 42; + + const cloned = parent.clone(); + const cs = cloned.getComponent(BagHolderScript); + + // @deepClone forces the field walk into Bag, which must still honor Bag's own @ignoreClone: + // `kept` is copied, `runtime` keeps the fresh instance's constructor value. + expect(cs.bag).not.eq(script.bag); + expect(cs.bag.kept).eq(42); + expect(cs.bag.runtime).eq(1); + + rootEntity.destroy(); + }); + }); + + describe("Container member semantics", () => { + it("@assignmentClone on a container shares the instance with the source", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(AssignedContainerScript); + script.shared.push(1, 2); + + const cloned = parent.clone(); + const cs = cloned.getComponent(AssignedContainerScript); + + expect(cs.shared).eq(script.shared); + cs.shared.push(3); + expect(script.shared.length).eq(3); + + rootEntity.destroy(); + }); + + it("Map keys are cloned along with values", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(HandlerScript); + const key = { id: 7 }; + (script as any).byObject = new Map([[key, 1]]); + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript) as any; + + // The clone's key is a fresh object: lookups by the source key miss by design. + expect(cs.byObject.size).eq(1); + expect(cs.byObject.has(key)).eq(false); + const clonedKey = [...cs.byObject.keys()][0]; + expect(clonedKey).not.eq(key); + expect(clonedKey.id).eq(7); + expect(cs.byObject.get(clonedKey)).eq(1); + + rootEntity.destroy(); + }); + + it("entity Map keys remap to the cloned subtree", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(HandlerScript); + (script as any).byEntity = new Map([[child, 5]]); + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript) as any; + + expect(cs.byEntity.get(cloned.children[0])).eq(5); + expect(cs.byEntity.has(child)).eq(false); + + rootEntity.destroy(); + }); + + it("a function held as a Map value is shared", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(HandlerScript); + const fn = () => 1; + (script as any).handlerMap = new Map([["k", fn]]); + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript) as any; + + expect(cs.handlerMap.get("k")).eq(fn); + + rootEntity.destroy(); + }); + }); + + describe("@deepClone subtree propagation", () => { + it("plain-class members of a @deepClone container are copied, not shared", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(DeepSubtreeScript); + const config = new PlainConfig(); + config.count = 5; + script.configs.push(config); + + const cloned = parent.clone(); + const cs = cloned.getComponent(DeepSubtreeScript); + + expect(cs.configs[0]).not.eq(config); + expect(cs.configs[0].count).eq(5); + expect(cs.configs[0].nested).not.eq(config.nested); + cs.configs[0].count = 1; + expect(config.count).eq(5); + + rootEntity.destroy(); + }); + + it("the force carries through nested plain objects", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(DeepSubtreeScript); + const config = new PlainConfig(); + script.bag = { cfg: config }; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DeepSubtreeScript); + + expect(cs.bag.cfg).not.eq(config); + expect(cs.bag.cfg.nested).not.eq(config.nested); + + rootEntity.destroy(); + }); + + it("engine-bound members inside a @deepClone subtree keep their defaults", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const script = parent.addComponent(DeepSubtreeScript); + const texture = new Texture2D(engine, 1, 1); + script.mixed = [child, texture]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(DeepSubtreeScript); + + expect(cs.mixed[0]).eq(cloned.children[0]); + expect(cs.mixed[1]).eq(texture); + + rootEntity.destroy(); + }); + }); + + describe("copyFrom value types via entity.clone", () => { + it("a Ray field deep-clones through the gate", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(HandlerScript); + (script as any).ray = new Ray(new Vector3(1, 2, 3), new Vector3(0, 1, 0)); + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript) as any; + + expect(cs.ray).instanceOf(Ray); + expect(cs.ray).not.eq((script as any).ray); + expect(cs.ray.origin.x).eq(1); + cs.ray.origin.x = 9; + expect((script as any).ray.origin.x).eq(1); + + rootEntity.destroy(); + }); + }); + + describe("Aliasing topology", () => { + it("one instance referenced three times clones into one instance referenced three times", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(HandlerScript); + const vec = new Vector3(1, 2, 3); + (script as any).points = [vec, vec, vec]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript) as any; + + // One NEW instance, shared by all three slots — the reference topology is preserved. + expect(cs.points[0]).not.eq(vec); + expect(cs.points[0]).eq(cs.points[1]); + expect(cs.points[1]).eq(cs.points[2]); + expect(cs.points[0].x).eq(1); + + // Mutating through one slot is visible through the others, matching the source's behavior. + cs.points[0].x = 9; + expect(cs.points[2].x).eq(9); + expect(vec.x).eq(1); + + rootEntity.destroy(); + }); + + it("a self-referencing plain object clones into a self-referencing clone", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(HandlerScript); + const node: any = { value: 1 }; + node.self = node; + const ring: any[] = [node]; + ring.push(ring); + (script as any).node = node; + (script as any).ring = ring; + + const cloned = parent.clone(); + const cs = cloned.getComponent(HandlerScript) as any; + + expect(cs.node).not.eq(node); + expect(cs.node.self).eq(cs.node); + expect(cs.ring).not.eq(ring); + expect(cs.ring[1]).eq(cs.ring); + expect(cs.ring[0]).eq(cs.node); + + rootEntity.destroy(); + }); + }); + + describe("Binary data fields", () => { + it("matching binary presets are written into, not replaced", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(BinaryPresetScript); + script.weights = new Float32Array([9, 8, 7]); + const view = new DataView(new ArrayBuffer(4)); + view.setUint8(0, 42); + script.view = view; + + const cloned = parent.clone(); + const cs = cloned.getComponent(BinaryPresetScript); + + // Same length / byteLength: the clone's ctor-built instance is reused as the target. + expect(cs.weights).eq(cs.ownWeights); + expect(Array.from(cs.weights)).deep.eq([9, 8, 7]); + expect(cs.view).eq(cs.ownView); + expect(cs.view.getUint8(0)).eq(42); + + rootEntity.destroy(); + }); + + it("a length-mismatched binary preset is replaced by a fresh copy", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(BinaryPresetScript); + script.shortWeights = new Float32Array([1, 2, 3]); + + const cloned = parent.clone(); + const cs = cloned.getComponent(BinaryPresetScript); + + expect(cs.shortWeights).not.eq(cs.ownShort); + expect(Array.from(cs.shortWeights)).deep.eq([1, 2, 3]); + + rootEntity.destroy(); + }); + + it("DataView field clones by bytes without crashing", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(BinaryScript); + const buffer = new ArrayBuffer(8); + const view = new DataView(buffer); + view.setFloat32(0, 3.5); + view.setUint16(4, 42); + script.view = view; + + const cloned = parent.clone(); + const cs = cloned.getComponent(BinaryScript); + + expect(cs.view).not.eq(view); + expect(cs.view.buffer).not.eq(buffer); + expect(cs.view.getFloat32(0)).eq(3.5); + expect(cs.view.getUint16(4)).eq(42); + cs.view.setUint16(4, 7); + expect(view.getUint16(4)).eq(42); + + rootEntity.destroy(); + }); + + it("typed array field clones into an independent copy", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(BinaryScript); + script.bytes = new Float32Array([1, 2, 3]); + + const cloned = parent.clone(); + const cs = cloned.getComponent(BinaryScript); + + expect(cs.bytes).not.eq(script.bytes); + expect(Array.from(cs.bytes)).deep.eq([1, 2, 3]); + cs.bytes[0] = 9; + expect(script.bytes[0]).eq(1); + + rootEntity.destroy(); + }); + + it("aliased typed arrays keep identity through the clone", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(AliasedBinaryScript); + const shared = new Float32Array([1, 2, 3]); + script.a = shared; + script.b = shared; + + const cloned = parent.clone(); + const cs = cloned.getComponent(AliasedBinaryScript); + + expect(cs.a).not.eq(shared); + expect(cs.a).eq(cs.b); + expect(Array.from(cs.a)).deep.eq([1, 2, 3]); + + rootEntity.destroy(); + }); + + it("typed-array preset aliasing the source value still yields a fresh copy", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(SharedDefaultTableScript); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SharedDefaultTableScript); + + expect(cs.weights).not.eq(SharedDefaultTableScript.DEFAULT_WEIGHTS); + expect(Array.from(cs.weights)).deep.eq([1, 2, 3]); + cs.weights[0] = 9; + expect(SharedDefaultTableScript.DEFAULT_WEIGHTS[0]).eq(1); + expect(script.weights[0]).eq(1); + expect(cs.view).not.eq(SharedDefaultTableScript.DEFAULT_VIEW); + + rootEntity.destroy(); + }); + + it("array / map / set presets aliasing the source value are never written into", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + parent.addComponent(SharedDefaultContainerScript); + + const cloned = parent.clone(); + const cs = cloned.getComponent(SharedDefaultContainerScript); + + // The clone's preset IS the source value (a class-level shared default), so it can't be + // filled in place — that would write through into the source. + expect(cs.list).not.eq(SharedDefaultContainerScript.DEFAULT_LIST); + expect(cs.map).not.eq(SharedDefaultContainerScript.DEFAULT_MAP); + expect(cs.set).not.eq(SharedDefaultContainerScript.DEFAULT_SET); + expect(cs.list).deep.eq([1, 2, 3]); + expect(SharedDefaultContainerScript.DEFAULT_LIST).deep.eq([1, 2, 3]); + expect(SharedDefaultContainerScript.DEFAULT_MAP.size).eq(1); + expect(SharedDefaultContainerScript.DEFAULT_SET.size).eq(1); + + rootEntity.destroy(); + }); + + it("a reused map / set preset drops its own constructor-built entries", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(PresetContainerScript); + // Source diverges from what the clone's constructor will build. + script.map = new Map([["source", 9]]); + script.set = new Set(["source"]); + script.list = [7, 8, 9]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(PresetContainerScript); + + // The clone's own preset entries ("preset") must not survive next to the source's. + expect([...cs.map.keys()]).deep.eq(["source"]); + expect([...cs.set]).deep.eq(["source"]); + expect(cs.list).deep.eq([7, 8, 9]); + // ...and the source is untouched. + expect([...script.map.keys()]).deep.eq(["source"]); + + rootEntity.destroy(); + }); + }); + + describe("Plain data carrying copyFrom-shaped keys", () => { + it("plain object with a string copyFrom key deep-clones without crashing", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(CopyFromDataScript); + script.config = { copyFrom: "nodeA", other: 1 }; + + const cloned = parent.clone(); + const cc = cloned.getComponent(CopyFromDataScript).config; + expect(cc).not.eq(script.config); + expect(cc.copyFrom).eq("nodeA"); + expect(cc.other).eq(1); + + rootEntity.destroy(); + }); + + it("plain object with a function copyFrom key shares the function and clones the rest", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(CopyFromDataScript); + const fn = () => 42; + script.config = { copyFrom: fn, n: 2 }; + + const cloned = parent.clone(); + const cc = cloned.getComponent(CopyFromDataScript).config; + expect(cc).not.eq(script.config); + expect(cc.copyFrom).eq(fn); + expect(cc.n).eq(2); + + rootEntity.destroy(); + }); + + it("null-prototype object with a copyFrom key deep-clones as null-prototype", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(CopyFromDataScript); + const data = Object.create(null); + data.copyFrom = "x"; + data.v = 2; + script.config = data; + + const cloned = parent.clone(); + const cc = cloned.getComponent(CopyFromDataScript).config; + expect(cc).not.eq(data); + expect(Object.getPrototypeOf(cc)).eq(null); + expect(cc.copyFrom).eq("x"); + expect(cc.v).eq(2); + + rootEntity.destroy(); + }); + }); + + describe("Parameter-constructed Deep values as container elements", () => { + it("clones gradients and curves held in arrays / maps without a reusable preset", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(CopyFromDataScript); + const gradient = new ParticleCompositeGradient(new Color(1, 0, 0, 1)); + const curve = new ParticleCompositeCurve(0.5); + script.config = { gradients: [gradient], curves: new Map([["a", curve]]) }; + + const cloned = parent.clone(); + const cc = cloned.getComponent(CopyFromDataScript).config; + expect(cc.gradients[0]).not.eq(gradient); + expect(cc.gradients[0].mode).eq(gradient.mode); + expect(cc.gradients[0].constant.r).eq(1); + expect(cc.curves.get("a")).not.eq(curve); + expect(cc.curves.get("a").constant).eq(0.5); + + rootEntity.destroy(); + }); + + it("a Deep type that cannot construct bare fails with the contract named", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(CopyFromDataScript); + script.config = { items: [new ParamDeepConfig({ id: "a" })] }; + + expect(() => parent.clone()).toThrowError(/bare-construct "ParamDeepConfig"/); + + rootEntity.destroy(); + }); + + it("a host-bound instance in a container remaps when its engine slot clones first", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + // Renderer added first: its generator/module tree enters the identity map before the + // script's fields walk, so the container reference dedups onto the cloned module. + const renderer = parent.addComponent(ParticleRenderer); + const script = parent.addComponent(CopyFromDataScript); + script.config = { modules: [renderer.generator.main] }; + + const cloned = parent.clone(); + const clonedModule = cloned.getComponent(CopyFromDataScript).config.modules[0]; + expect(clonedModule).eq(cloned.getComponent(ParticleRenderer).generator.main); + expect(clonedModule).not.eq(renderer.generator.main); + + rootEntity.destroy(); + }); + + it("a host-bound instance in a container fails with the named error when no host precedes", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + // Script added first: the container walks before any engine slot registers the module, + // so the gate must bare-construct a host-bound structure — rejected with the contract + // named (sharing must be declared via @assignmentClone). + const script = parent.addComponent(CopyFromDataScript); + const renderer = parent.addComponent(ParticleRenderer); + script.config = { modules: [renderer.generator.main] }; + + expect(() => parent.clone()).toThrowError(/bare-construct "MainModule"/); + + rootEntity.destroy(); + }); + + it("every exported Deep-registered type constructs bare (gate contract)", () => { + // The gate creates container elements and preset-less slots with `new Type()` and then + // populates every field, so a Deep-registered type MUST construct without arguments. + // Exemptions are engine-bound structural types the gate only ever clones against a + // same-type constructor preset (`reusable`) — each entry states why it cannot be bare. + const exempt = new Set([ + // Physics shapes construct a PhysicsMaterial and need an initialized physics backend — + // bare-constructible in a real runtime, just not in this physics-less test engine. + "ColliderShape", + "BoxColliderShape", + "SphereColliderShape", + "CapsuleColliderShape", + "PlaneColliderShape", + "MeshColliderShape", + // Host-bound structural types wired to their host at construction — in their engine + // slots the gate clones them against the component's same-type constructor preset; a + // preset-less occurrence (e.g. a user container) fails with the named bare-construction + // error by design (share explicitly via @assignmentClone instead). + "ParticleGenerator", + "MainModule", + "VelocityOverLifetimeModule", + "SizeOverLifetimeModule", + "LimitVelocityOverLifetimeModule", + "NoiseModule" + ]); + const failures: string[] = []; + const packages: [string, Record][] = [ + ["core", EngineCore], + ["math", EngineMath], + ["ui", EngineUI] + ]; + for (const [pkg, ns] of packages) { + for (const [name, exported] of Object.entries(ns)) { + if (typeof exported !== "function" || !exported.prototype) continue; + // math value types dispatch by their callable copyFrom; core/ui Deep types are the + // DataObject family + const isDeep = + pkg === "math" + ? typeof exported.prototype.copyFrom === "function" + : exported.prototype instanceof DataObject; + if (!isDeep) continue; + if (exempt.has(name)) continue; + try { + new exported(); + } catch (e) { + failures.push(`${pkg}/${name}: ${(e as Error).message}`); + } + } + } + expect(failures).deep.eq([]); + }); + + it("orbital velocity fields deep-clone through the type default", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const renderer = parent.addComponent(ParticleRenderer); + const vol = renderer.generator.velocityOverLifetime; + vol.orbitalX = new ParticleCompositeCurve(1, 2); + vol.centerOffset.set(3, 4, 5); + + const cloned = parent.clone(); + const clonedVol = cloned.getComponent(ParticleRenderer).generator.velocityOverLifetime; + expect(clonedVol.orbitalX).not.eq(vol.orbitalX); + expect(clonedVol.orbitalX.constantMin).eq(1); + expect(clonedVol.orbitalX.constantMax).eq(2); + expect(clonedVol.centerOffset).not.eq(vol.centerOffset); + expect(clonedVol.centerOffset.z).eq(5); + + rootEntity.destroy(); + }); + + it("clones the emission bursts array through the engine path", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const renderer = parent.addComponent(ParticleRenderer); + renderer.generator.emission.addBurst(new Burst(0.5, new ParticleCompositeCurve(30))); + + const cloned = parent.clone(); + const clonedBursts = cloned.getComponent(ParticleRenderer).generator.emission.bursts; + expect(clonedBursts.length).eq(1); + expect(clonedBursts[0]).not.eq(renderer.generator.emission.bursts[0]); + expect(clonedBursts[0].time).eq(0.5); + expect(clonedBursts[0].count.constant).eq(30); + + rootEntity.destroy(); + }); + }); + + describe("Script-held ReferResource", () => { + it("cloned slot owns one reference; the script's own contract releases it", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(ResourceRefScript); + const texture = new Texture2D(engine, 4, 4); + script.texture = texture; + expect(texture.refCount).eq(1); + + const cloned = parent.clone(); + const cs = cloned.getComponent(ResourceRefScript); + + // Shared by reference; the cloned slot owns one count — same contract as engine components. + expect(cs.texture).eq(texture); + expect(texture.refCount).eq(2); + + // Releasing on destroy is the script class's responsibility (onDestroy → setter -1). + cloned.destroy(); + expect(texture.refCount).eq(1); + + parent.destroy(); + expect(texture.refCount).eq(0); + + rootEntity.destroy(); + texture.destroy(); + }); + + it("a replaced owned preset releases its count even when the source slot is empty", () => { + PresetTextureScript.created.length = 0; + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(PresetTextureScript); + // The source empties the slot, releasing its own preset ownership first. + (script.texture as any)._addReferCount(-1); + script.texture = null; + + const cloned = parent.clone(); + expect(PresetTextureScript.created.length).eq(2); + const [sourcePreset, clonePreset] = PresetTextureScript.created; + + // The clone's constructor preset was displaced by the empty slot — its owned count returns. + expect(cloned.getComponent(PresetTextureScript).texture).eq(null); + expect(clonePreset.refCount).eq(0); + expect(sourcePreset.refCount).eq(0); + + rootEntity.destroy(); + }); + + it("an unowned counted preset triggers the contract diagnostic and still releases", () => { + UnownedPresetScript.created.length = 0; + const errorSpy = vi.spyOn(Logger, "error"); + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(UnownedPresetScript); + script.tex = null; + + const cloned = parent.clone(); + expect(cloned.getComponent(UnownedPresetScript).tex).eq(null); + const diagnostics = errorSpy.mock.calls.filter((c) => String(c[0]).includes("holds no owned reference")); + expect(diagnostics.length).eq(1); + // Pins the current semantics: the unconditional -1 drives the unowned preset negative. + expect(UnownedPresetScript.created[1].refCount).eq(-1); + + errorSpy.mockRestore(); + rootEntity.destroy(); + }); + + it("a replaced owned preset releases its count when displaced by a deep-cloned value", () => { + PresetTextureScript.created.length = 0; + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(PresetTextureScript); + (script.texture as any)._addReferCount(-1); + // The source slot holds a container: the gate deep-clones it, displacing the clone's preset. + (script as any).texture = [1, 2, 3]; + + const cloned = parent.clone(); + const [, clonePreset] = PresetTextureScript.created; + expect(cloned.getComponent(PresetTextureScript).texture as any).deep.eq([1, 2, 3]); + expect(clonePreset.refCount).eq(0); + + rootEntity.destroy(); + }); + + it("a user type registered Assignment without counting API shares safely", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const script = parent.addComponent(SharedConfigScript); + script.config = new SharedConfig(); + + const cloned = parent.clone(); + expect(cloned.getComponent(SharedConfigScript).config).eq(script.config); + + rootEntity.destroy(); + }); + }); + + describe("Single entity with multiple same-type components", () => { + it("clone preserves multiple same-type components with correct state", () => { + const rootEntity = scene.createRootEntity("root"); + const entity = rootEntity.createChild("entity"); + const script1 = entity.addComponent(CounterScript); + const script2 = entity.addComponent(CounterScript); + script1.value = 10; + script2.value = 20; + + const cloned = entity.clone(); + const clonedScripts = cloned.getComponents(CounterScript, []); + + expect(clonedScripts.length).eq(2); + expect(clonedScripts[0].value).eq(10); + expect(clonedScripts[1].value).eq(20); + expect(clonedScripts[0]).not.eq(script1); + expect(clonedScripts[1]).not.eq(script2); + + rootEntity.destroy(); + }); + + it("ref to second component of same type should remap correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const counter1 = child.addComponent(CounterScript); + const counter2 = child.addComponent(CounterScript); + counter1.value = 1; + counter2.value = 2; + + const refScript = parent.addComponent(CounterRefScript); + refScript.counter = counter2; + + const cloned = parent.clone(); + const clonedRef = cloned.getComponent(CounterRefScript); + const clonedCounters = cloned.children[0].getComponents(CounterScript, []); + + expect(clonedRef.counter).not.eq(counter2); + expect(clonedRef.counter).eq(clonedCounters[1]); + expect(clonedRef.counter.value).eq(2); + + rootEntity.destroy(); + }); + + it("ref to first component of same type should remap correctly", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const counter1 = child.addComponent(CounterScript); + const counter2 = child.addComponent(CounterScript); + counter1.value = 100; + counter2.value = 200; + + const refScript = parent.addComponent(CounterRefScript); + refScript.counter = counter1; + + const cloned = parent.clone(); + const clonedRef = cloned.getComponent(CounterRefScript); + const clonedCounters = cloned.children[0].getComponents(CounterScript, []); + + expect(clonedRef.counter).not.eq(counter1); + expect(clonedRef.counter).eq(clonedCounters[0]); + expect(clonedRef.counter.value).eq(100); + + rootEntity.destroy(); + }); + + it("cross-references between multiple same-type components on same entity", () => { + const rootEntity = scene.createRootEntity("root"); + const entity = rootEntity.createChild("entity"); + const script1 = entity.addComponent(CounterScript); + const script2 = entity.addComponent(CounterScript); + script1.value = 1; + script2.value = 2; + script1.partner = script2; + script2.partner = script1; + + const cloned = entity.clone(); + const clonedScripts = cloned.getComponents(CounterScript, []); + + expect(clonedScripts[0].partner).eq(clonedScripts[1]); + expect(clonedScripts[1].partner).eq(clonedScripts[0]); + expect(clonedScripts[0].partner).not.eq(script2); + expect(clonedScripts[1].partner).not.eq(script1); + + rootEntity.destroy(); + }); + + it("self-reference among multiple same-type components remaps to correct clone", () => { + const rootEntity = scene.createRootEntity("root"); + const entity = rootEntity.createChild("entity"); + const script1 = entity.addComponent(CounterScript); + const script2 = entity.addComponent(CounterScript); + script1.partner = script1; + script2.partner = script2; + + const cloned = entity.clone(); + const clonedScripts = cloned.getComponents(CounterScript, []); + + expect(clonedScripts[0].partner).eq(clonedScripts[0]); + expect(clonedScripts[1].partner).eq(clonedScripts[1]); + + rootEntity.destroy(); + }); + + it("multiple same-type components with entity refs all remap independently", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const script1 = parent.addComponent(CounterScript); + const script2 = parent.addComponent(CounterScript); + script1.targetEntity = childA; + script2.targetEntity = childB; + + const cloned = parent.clone(); + const clonedScripts = cloned.getComponents(CounterScript, []); + + expect(clonedScripts[0].targetEntity).eq(cloned.children[0]); + expect(clonedScripts[1].targetEntity).eq(cloned.children[1]); + expect(clonedScripts[0].targetEntity.name).eq("childA"); + expect(clonedScripts[1].targetEntity.name).eq("childB"); + + rootEntity.destroy(); + }); + + it("@deepClone array referencing specific component among same-type siblings", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const counter1 = child.addComponent(CounterScript); + const counter2 = child.addComponent(CounterScript); + const counter3 = child.addComponent(CounterScript); + counter1.value = 1; + counter2.value = 2; + counter3.value = 3; + + const arrayScript = parent.addComponent(ArrayRefScript); + // Note: ArrayRefScript uses Entity[], but we test component indexing + // via direct component references instead + const refScript1 = parent.addComponent(CounterRefScript); + const refScript2 = parent.addComponent(CounterRefScript); + refScript1.counter = counter1; + refScript2.counter = counter3; + + const cloned = parent.clone(); + const clonedRefs = cloned.getComponents(CounterRefScript, []); + const clonedCounters = cloned.children[0].getComponents(CounterScript, []); + + expect(clonedRefs[0].counter).eq(clonedCounters[0]); + expect(clonedRefs[0].counter.value).eq(1); + expect(clonedRefs[1].counter).eq(clonedCounters[2]); + expect(clonedRefs[1].counter.value).eq(3); + + rootEntity.destroy(); + }); + }); +}); diff --git a/tests/src/core/CloneTextureRefCount.test.ts b/tests/src/core/CloneTextureRefCount.test.ts new file mode 100644 index 0000000000..f4eafdd53d --- /dev/null +++ b/tests/src/core/CloneTextureRefCount.test.ts @@ -0,0 +1,120 @@ +import { + Animator, + AnimatorController, + Camera, + Entity, + Font, + MeshRenderer, + RenderTarget, + SkinnedMeshRenderer, + TextRenderer, + Texture2D +} from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine"; +import { describe, beforeAll, expect, it } from "vitest"; + +describe("Clone resource refCount", async function () { + let engine: WebGLEngine; + let rootEntity: Entity; + + beforeAll(async function () { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + rootEntity = engine.sceneManager.activeScene.createRootEntity(); + engine.run(); + }); + + it("renderer-private joint texture is not propagated into clones", () => { + const entity = rootEntity.createChild("skinnedSrc"); + const smr = entity.addComponent(SkinnedMeshRenderer); + // Simulate the state _update builds when the bone count exceeds the uniform budget. + const jointTexture = new Texture2D(engine, 4, 4); + jointTexture.isGCIgnored = true; + // @ts-ignore + smr._jointTexture = jointTexture; + smr.shaderData.setTexture("renderer_JointSampler", jointTexture); + expect(jointTexture.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + const clonedSmr = clone.getComponent(SkinnedMeshRenderer); + // The clone must not hold the source's private texture; it rebuilds its own on first update. + expect(clonedSmr.shaderData.getTexture("renderer_JointSampler") ?? null).eq(null); + expect(jointTexture.refCount).eq(1); + + // With no stray reference, the source's one-shot destroy() in _onDestroy succeeds (refCount is zero). + entity.destroy(); + expect(jointTexture.refCount).eq(0); + + clone.destroy(); + }); + + it("clone shares texture with balanced refCount (no leak)", () => { + const texture = new Texture2D(engine, 1, 1); + const entity = rootEntity.createChild("src"); + entity.addComponent(MeshRenderer).shaderData.setTexture("u_tex", texture); + expect(texture.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + // The clone owns an independent shaderData that references the shared texture → +1. + expect(texture.refCount).eq(2); + + clone.destroy(); + // Destroying the clone releases that reference → back to baseline, no leak. + expect(texture.refCount).eq(1); + + entity.destroy(); + }); + + it("camera renderTarget refCount stays balanced across clone/destroy", () => { + const rt = new RenderTarget(engine, 4, 4, new Texture2D(engine, 4, 4)); + const entity = rootEntity.createChild("cameraSrc"); + entity.addComponent(Camera).renderTarget = rt; + expect(rt.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + // The clone holds exactly one additional reference (acquired by the clone gate on the shared `_renderTarget` slot). + expect(rt.refCount).eq(2); + + clone.destroy(); + expect(rt.refCount).eq(1); + + entity.destroy(); + expect(rt.refCount).eq(0); + }); + + it("text renderer font refCount stays balanced across clone/destroy", () => { + const font = Font.createFromOS(engine, "Arial-CloneTest"); + const entity = rootEntity.createChild("textSrc"); + entity.addComponent(TextRenderer).font = font; + const baseline = font.refCount; + + const clone = entity.clone(); + rootEntity.addChild(clone); + // The clone holds exactly one additional reference (acquired by the clone gate on the shared `_font` slot). + expect(font.refCount).eq(baseline + 1); + + clone.destroy(); + expect(font.refCount).eq(baseline); + + entity.destroy(); + }); + + it("animator controller refCount stays balanced across clone/destroy", () => { + const controller = new AnimatorController(engine); + const entity = rootEntity.createChild("animatorSrc"); + entity.addComponent(Animator).animatorController = controller; + const baseline = controller.refCount; + + const clone = entity.clone(); + rootEntity.addChild(clone); + // The clone holds one additional reference (clone gate on the shared `_animatorController` slot; _cloneTo only re-registers the change flag). + expect(controller.refCount).eq(baseline + 1); + + clone.destroy(); + expect(controller.refCount).eq(baseline); + + entity.destroy(); + }); +}); diff --git a/tests/src/core/CloneUtils.test.ts b/tests/src/core/CloneUtils.test.ts deleted file mode 100644 index 841399d21c..0000000000 --- a/tests/src/core/CloneUtils.test.ts +++ /dev/null @@ -1,1030 +0,0 @@ -import { Entity, MeshRenderer, Script, Signal, assignmentClone, deepClone, ignoreClone } from "@galacean/engine-core"; -import { WebGLEngine } from "@galacean/engine"; -import { describe, expect, it } from "vitest"; - -class TestScript extends Script { - targetEntity: Entity; - targetRenderer: MeshRenderer; - externalEntity: Entity; - externalRenderer: MeshRenderer; - deepChild: Entity; - selfRef: Entity; - speed: number; - name2: string; - flag: boolean; - data: object; -} - -/** Script with multiple entity/component refs pointing to different nodes */ -class MultiRefScript extends Script { - entityA: Entity; - entityB: Entity; - rendererA: MeshRenderer; - rendererB: MeshRenderer; -} - -/** Script where the same entity is referenced by multiple properties */ -class DuplicateRefScript extends Script { - ref1: Entity; - ref2: Entity; -} - -/** Script with null/undefined entity/component refs */ -class NullRefScript extends Script { - nullEntity: Entity = null; - undefinedEntity: Entity; - nullRenderer: MeshRenderer = null; - someNumber: number = 0; -} - -/** Script referencing a sibling entity (not parent/child, but sibling under clone root) */ -class SiblingRefScript extends Script { - sibling: Entity; - siblingRenderer: MeshRenderer; -} - -/** Script with a mix of decorated and undecorated entity refs */ -class DecoratedRefScript extends Script { - // Undecorated — should auto-remap via _remap - autoRemapEntity: Entity; - - // @assignmentClone — should still auto-remap since _remap takes priority - @assignmentClone - assignedEntity: Entity; - - // @ignoreClone — should still auto-remap since _remap takes priority - @ignoreClone - ignoredEntity: Entity; -} - -/** Script with a @deepClone array of entities */ -class ArrayRefScript extends Script { - @deepClone - entities: Entity[] = []; -} - -/** Script with Component self-reference */ -class SelfComponentRefScript extends Script { - selfScript: SelfComponentRefScript; -} - -/** Script referencing another Script on a different entity */ -class CrossScriptRefScript extends Script { - otherScript: TestScript; -} - -/** Script with a nested plain object containing entity refs */ -class NestedObjectScript extends Script { - @deepClone - config: { target: Entity; label: string } = { target: null, label: "" }; -} - -/** Script with UNDECORATED array of entities (no @deepClone) */ -class UndecoratedArrayScript extends Script { - entities: Entity[] = []; -} - -/** Script with UNDECORATED nested object containing entity refs */ -class UndecoratedObjectScript extends Script { - config: { target: Entity; label: string } = { target: null, label: "" }; -} - -/** Script with UNDECORATED nested array of arrays containing entities */ -class NestedArrayScript extends Script { - groups: Entity[][] = []; -} - -/** Script with UNDECORATED Map containing entity values */ -class MapRefScript extends Script { - entityMap: Map = new Map(); -} - -/** Script for testing multiple same-type components on one entity */ -class CounterScript extends Script { - value: number = 0; - partner: CounterScript; - targetEntity: Entity; -} - -/** Script that references a CounterScript */ -class CounterRefScript extends Script { - counter: CounterScript; -} - -/** Handler script used for Signal structured binding tests */ -class ClickHandler extends Script { - callCount = 0; - lastPrefix: string = ""; - - handleClick(): void { - this.callCount++; - } - - handleClickWithPrefix(arg: number, prefix: string): void { - this.callCount++; - this.lastPrefix = prefix; - } -} - -/** Script with a Signal property */ -class SignalScript extends Script { - @deepClone - readonly onFire = new Signal<[number]>(); -} - -describe("Clone remap", async () => { - const engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); - const scene = engine.sceneManager.activeScene; - engine.run(); - - describe("Basic Entity/Component remap", () => { - it("script undecorated Entity ref should remap to cloned entity", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const script = parent.addComponent(TestScript); - script.targetEntity = child; - - const clonedParent = parent.clone(); - const clonedScript = clonedParent.getComponent(TestScript); - const clonedChild = clonedParent.children[0]; - - expect(clonedScript.targetEntity).not.eq(child); - expect(clonedScript.targetEntity).eq(clonedChild); - expect(clonedScript.targetEntity.name).eq("child"); - - rootEntity.destroy(); - }); - - it("script undecorated Component ref should remap to cloned component", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const meshRenderer = child.addComponent(MeshRenderer); - const script = parent.addComponent(TestScript); - script.targetRenderer = meshRenderer; - - const clonedParent = parent.clone(); - const clonedScript = clonedParent.getComponent(TestScript); - const clonedChild = clonedParent.children[0]; - const clonedMeshRenderer = clonedChild.getComponent(MeshRenderer); - - expect(clonedScript.targetRenderer).not.eq(meshRenderer); - expect(clonedScript.targetRenderer).eq(clonedMeshRenderer); - - rootEntity.destroy(); - }); - - it("script ref to entity outside hierarchy should keep original", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const external = rootEntity.createChild("external"); - const script = parent.addComponent(TestScript); - script.externalEntity = external; - - const clonedParent = parent.clone(); - const clonedScript = clonedParent.getComponent(TestScript); - - expect(clonedScript.externalEntity).eq(external); - - rootEntity.destroy(); - }); - - it("script ref to component outside hierarchy should keep original", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const external = rootEntity.createChild("external"); - const externalMR = external.addComponent(MeshRenderer); - const script = parent.addComponent(TestScript); - script.externalRenderer = externalMR; - - const clonedParent = parent.clone(); - const clonedScript = clonedParent.getComponent(TestScript); - - expect(clonedScript.externalRenderer).eq(externalMR); - - rootEntity.destroy(); - }); - - it("deep hierarchy entity ref should remap correctly", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const grandchild = child.createChild("grandchild"); - const script = parent.addComponent(TestScript); - script.deepChild = grandchild; - - const clonedParent = parent.clone(); - const clonedScript = clonedParent.getComponent(TestScript); - const clonedGrandchild = clonedParent.children[0].children[0]; - - expect(clonedScript.deepChild).not.eq(grandchild); - expect(clonedScript.deepChild).eq(clonedGrandchild); - expect(clonedScript.deepChild.name).eq("grandchild"); - - rootEntity.destroy(); - }); - - it("script ref to self entity (clone root) should remap", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const script = parent.addComponent(TestScript); - script.selfRef = parent; - - const clonedParent = parent.clone(); - const clonedScript = clonedParent.getComponent(TestScript); - - expect(clonedScript.selfRef).not.eq(parent); - expect(clonedScript.selfRef).eq(clonedParent); - - rootEntity.destroy(); - }); - - it("primitive and plain object props should not be affected", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const script = parent.addComponent(TestScript); - const obj = { x: 1 }; - script.speed = 42; - script.name2 = "test"; - script.flag = true; - script.data = obj; - - const clonedParent = parent.clone(); - const clonedScript = clonedParent.getComponent(TestScript); - - expect(clonedScript.speed).eq(42); - expect(clonedScript.name2).eq("test"); - expect(clonedScript.flag).eq(true); - // Plain objects are now deep cloned (independent copy) for undecorated properties - expect(clonedScript.data).not.eq(obj); - expect((clonedScript.data).x).eq(1); - - rootEntity.destroy(); - }); - }); - - describe("Multiple and duplicate refs", () => { - it("multiple entity/component refs on same script all remap independently", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const childA = parent.createChild("childA"); - const childB = parent.createChild("childB"); - const mrA = childA.addComponent(MeshRenderer); - const mrB = childB.addComponent(MeshRenderer); - const script = parent.addComponent(MultiRefScript); - script.entityA = childA; - script.entityB = childB; - script.rendererA = mrA; - script.rendererB = mrB; - - const cloned = parent.clone(); - const cs = cloned.getComponent(MultiRefScript); - - expect(cs.entityA).not.eq(childA); - expect(cs.entityB).not.eq(childB); - expect(cs.entityA.name).eq("childA"); - expect(cs.entityB.name).eq("childB"); - expect(cs.entityA).eq(cloned.children[0]); - expect(cs.entityB).eq(cloned.children[1]); - expect(cs.rendererA).eq(cloned.children[0].getComponent(MeshRenderer)); - expect(cs.rendererB).eq(cloned.children[1].getComponent(MeshRenderer)); - - rootEntity.destroy(); - }); - - it("two properties referencing the same entity both remap to the same cloned entity", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const script = parent.addComponent(DuplicateRefScript); - script.ref1 = child; - script.ref2 = child; - - const cloned = parent.clone(); - const cs = cloned.getComponent(DuplicateRefScript); - - expect(cs.ref1).not.eq(child); - expect(cs.ref1).eq(cs.ref2); - expect(cs.ref1).eq(cloned.children[0]); - - rootEntity.destroy(); - }); - }); - - describe("Null and undefined refs", () => { - it("null entity/component refs should not crash and remain null", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const script = parent.addComponent(NullRefScript); - - const cloned = parent.clone(); - const cs = cloned.getComponent(NullRefScript); - - expect(cs.nullEntity).eq(null); - expect(cs.nullRenderer).eq(null); - expect(cs.someNumber).eq(0); - - rootEntity.destroy(); - }); - }); - - describe("Sibling entity refs", () => { - it("ref to sibling entity under clone root should remap", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const childA = parent.createChild("childA"); - const childB = parent.createChild("childB"); - const mrB = childB.addComponent(MeshRenderer); - const script = childA.addComponent(SiblingRefScript); - script.sibling = childB; - script.siblingRenderer = mrB; - - const cloned = parent.clone(); - const clonedChildA = cloned.children[0]; - const clonedChildB = cloned.children[1]; - const cs = clonedChildA.getComponent(SiblingRefScript); - - expect(cs.sibling).not.eq(childB); - expect(cs.sibling).eq(clonedChildB); - expect(cs.siblingRenderer).not.eq(mrB); - expect(cs.siblingRenderer).eq(clonedChildB.getComponent(MeshRenderer)); - - rootEntity.destroy(); - }); - }); - - describe("Clone decorator interaction with _remap", () => { - it("@assignmentClone entity ref still gets remapped via _remap priority", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const script = parent.addComponent(DecoratedRefScript); - script.assignedEntity = child; - - const cloned = parent.clone(); - const cs = cloned.getComponent(DecoratedRefScript); - - expect(cs.assignedEntity).not.eq(child); - expect(cs.assignedEntity).eq(cloned.children[0]); - - rootEntity.destroy(); - }); - - it("@ignoreClone entity ref still gets remapped via _remap priority", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const script = parent.addComponent(DecoratedRefScript); - script.ignoredEntity = child; - - const cloned = parent.clone(); - const cs = cloned.getComponent(DecoratedRefScript); - - expect(cs.ignoredEntity).not.eq(child); - expect(cs.ignoredEntity).eq(cloned.children[0]); - - rootEntity.destroy(); - }); - - it("undecorated entity ref remaps correctly", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const script = parent.addComponent(DecoratedRefScript); - script.autoRemapEntity = child; - - const cloned = parent.clone(); - const cs = cloned.getComponent(DecoratedRefScript); - - expect(cs.autoRemapEntity).not.eq(child); - expect(cs.autoRemapEntity).eq(cloned.children[0]); - - rootEntity.destroy(); - }); - - it("@ignoreClone entity ref outside hierarchy stays original", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const external = rootEntity.createChild("external"); - const script = parent.addComponent(DecoratedRefScript); - script.ignoredEntity = external; - - const cloned = parent.clone(); - const cs = cloned.getComponent(DecoratedRefScript); - - expect(cs.ignoredEntity).eq(external); - - rootEntity.destroy(); - }); - }); - - describe("@deepClone array of entities", () => { - it("deep cloned entity array should remap internal refs", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const childA = parent.createChild("childA"); - const childB = parent.createChild("childB"); - const script = parent.addComponent(ArrayRefScript); - script.entities = [childA, childB]; - - const cloned = parent.clone(); - const cs = cloned.getComponent(ArrayRefScript); - - expect(cs.entities).not.eq(script.entities); - expect(cs.entities.length).eq(2); - expect(cs.entities[0]).not.eq(childA); - expect(cs.entities[1]).not.eq(childB); - expect(cs.entities[0]).eq(cloned.children[0]); - expect(cs.entities[1]).eq(cloned.children[1]); - - rootEntity.destroy(); - }); - - it("deep cloned entity array with external ref keeps original", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const external = rootEntity.createChild("external"); - const script = parent.addComponent(ArrayRefScript); - script.entities = [child, external]; - - const cloned = parent.clone(); - const cs = cloned.getComponent(ArrayRefScript); - - expect(cs.entities[0]).eq(cloned.children[0]); - expect(cs.entities[1]).eq(external); - - rootEntity.destroy(); - }); - - it("deep cloned empty entity array stays empty", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const script = parent.addComponent(ArrayRefScript); - script.entities = []; - - const cloned = parent.clone(); - const cs = cloned.getComponent(ArrayRefScript); - - expect(cs.entities).not.eq(script.entities); - expect(cs.entities.length).eq(0); - - rootEntity.destroy(); - }); - }); - - describe("Component self and cross references", () => { - it("script referencing itself should remap to cloned script", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const script = parent.addComponent(SelfComponentRefScript); - script.selfScript = script; - - const cloned = parent.clone(); - const cs = cloned.getComponent(SelfComponentRefScript); - - expect(cs.selfScript).not.eq(script); - expect(cs.selfScript).eq(cs); - - rootEntity.destroy(); - }); - - it("script referencing another script on child entity should remap", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const childScript = child.addComponent(TestScript); - const script = parent.addComponent(CrossScriptRefScript); - script.otherScript = childScript; - - const cloned = parent.clone(); - const cs = cloned.getComponent(CrossScriptRefScript); - const clonedChildScript = cloned.children[0].getComponent(TestScript); - - expect(cs.otherScript).not.eq(childScript); - expect(cs.otherScript).eq(clonedChildScript); - - rootEntity.destroy(); - }); - - it("script referencing external script should keep original", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const external = rootEntity.createChild("external"); - const externalScript = external.addComponent(TestScript); - const script = parent.addComponent(CrossScriptRefScript); - script.otherScript = externalScript; - - const cloned = parent.clone(); - const cs = cloned.getComponent(CrossScriptRefScript); - - expect(cs.otherScript).eq(externalScript); - - rootEntity.destroy(); - }); - }); - - describe("Nested @deepClone object with entity refs", () => { - it("entity ref inside deep cloned plain object should remap", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const script = parent.addComponent(NestedObjectScript); - script.config = { target: child, label: "hello" }; - - const cloned = parent.clone(); - const cs = cloned.getComponent(NestedObjectScript); - - expect(cs.config).not.eq(script.config); - expect(cs.config.label).eq("hello"); - expect(cs.config.target).not.eq(child); - expect(cs.config.target).eq(cloned.children[0]); - - rootEntity.destroy(); - }); - - it("entity ref inside deep cloned object pointing outside keeps original", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const external = rootEntity.createChild("external"); - const script = parent.addComponent(NestedObjectScript); - script.config = { target: external, label: "ext" }; - - const cloned = parent.clone(); - const cs = cloned.getComponent(NestedObjectScript); - - expect(cs.config.target).eq(external); - expect(cs.config.label).eq("ext"); - - rootEntity.destroy(); - }); - }); - - describe("Signal clone with structured bindings", () => { - it("@deepClone Signal should not copy closure listeners", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const script = parent.addComponent(SignalScript); - let called = false; - script.onFire.on(() => { - called = true; - }); - - const cloned = parent.clone(); - const cs = cloned.getComponent(SignalScript); - - expect(cs.onFire).not.eq(script.onFire); - cs.onFire.invoke(1); - expect(called).eq(false); - - rootEntity.destroy(); - }); - - it("@deepClone Signal should remap structured binding target to cloned hierarchy", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const handlerEntity = parent.createChild("handler"); - const handler = handlerEntity.addComponent(ClickHandler); - const script = parent.addComponent(SignalScript); - script.onFire.on(handler, "handleClick"); - - const cloned = parent.clone(); - const cs = cloned.getComponent(SignalScript); - const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); - - cs.onFire.invoke(1); - expect(clonedHandler.callCount).eq(1); - expect(handler.callCount).eq(0); - - rootEntity.destroy(); - }); - - it("@deepClone Signal should keep external structured binding target", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const external = rootEntity.createChild("external"); - const externalHandler = external.addComponent(ClickHandler); - const script = parent.addComponent(SignalScript); - script.onFire.on(externalHandler, "handleClick"); - - const cloned = parent.clone(); - const cs = cloned.getComponent(SignalScript); - - cs.onFire.invoke(1); - expect(externalHandler.callCount).eq(1); - - rootEntity.destroy(); - }); - - it("@deepClone Signal should remap structured binding with pre-resolved args", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const handlerEntity = parent.createChild("handler"); - const handler = handlerEntity.addComponent(ClickHandler); - const script = parent.addComponent(SignalScript); - script.onFire.on(handler, "handleClickWithPrefix", "myPrefix"); - - const cloned = parent.clone(); - const cs = cloned.getComponent(SignalScript); - const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); - - cs.onFire.invoke(1); - expect(clonedHandler.callCount).eq(1); - expect(clonedHandler.lastPrefix).eq("myPrefix"); - expect(handler.callCount).eq(0); - - rootEntity.destroy(); - }); - - it("@deepClone Signal should preserve once flag on structured binding", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const handlerEntity = parent.createChild("handler"); - const handler = handlerEntity.addComponent(ClickHandler); - const script = parent.addComponent(SignalScript); - script.onFire.once(handler, "handleClick"); - - const cloned = parent.clone(); - const cs = cloned.getComponent(SignalScript); - const clonedHandler = cloned.findByName("handler").getComponent(ClickHandler); - - cs.onFire.invoke(1); - expect(clonedHandler.callCount).eq(1); - cs.onFire.invoke(2); - expect(clonedHandler.callCount).eq(1); // once: removed after first call - - rootEntity.destroy(); - }); - }); - - describe("Clone hierarchy integrity", () => { - it("clone preserves children count and names", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - parent.createChild("a"); - parent.createChild("b"); - parent.createChild("c"); - - const cloned = parent.clone(); - expect(cloned.children.length).eq(3); - expect(cloned.children[0].name).eq("a"); - expect(cloned.children[1].name).eq("b"); - expect(cloned.children[2].name).eq("c"); - - rootEntity.destroy(); - }); - - it("clone of deeply nested hierarchy preserves structure", () => { - const rootEntity = scene.createRootEntity("root"); - const a = rootEntity.createChild("a"); - const b = a.createChild("b"); - const c = b.createChild("c"); - const d = c.createChild("d"); - - const cloned = a.clone(); - expect(cloned.children[0].name).eq("b"); - expect(cloned.children[0].children[0].name).eq("c"); - expect(cloned.children[0].children[0].children[0].name).eq("d"); - - rootEntity.destroy(); - }); - - it("script on child entity with ref to parent should remap", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const script = child.addComponent(TestScript); - script.targetEntity = parent; - - const cloned = parent.clone(); - const clonedChild = cloned.children[0]; - const cs = clonedChild.getComponent(TestScript); - - expect(cs.targetEntity).not.eq(parent); - expect(cs.targetEntity).eq(cloned); - - rootEntity.destroy(); - }); - - it("multiple scripts on different entities with cross-refs all remap correctly", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const childA = parent.createChild("childA"); - const childB = parent.createChild("childB"); - - const scriptA = childA.addComponent(TestScript); - scriptA.targetEntity = childB; - - const scriptB = childB.addComponent(TestScript); - scriptB.targetEntity = childA; - - const cloned = parent.clone(); - const clonedA = cloned.children[0]; - const clonedB = cloned.children[1]; - const csA = clonedA.getComponent(TestScript); - const csB = clonedB.getComponent(TestScript); - - expect(csA.targetEntity).eq(clonedB); - expect(csB.targetEntity).eq(clonedA); - - rootEntity.destroy(); - }); - }); - - describe("Single entity with multiple same-type components", () => { - it("clone preserves multiple same-type components with correct state", () => { - const rootEntity = scene.createRootEntity("root"); - const entity = rootEntity.createChild("entity"); - const script1 = entity.addComponent(CounterScript); - const script2 = entity.addComponent(CounterScript); - script1.value = 10; - script2.value = 20; - - const cloned = entity.clone(); - const clonedScripts = cloned.getComponents(CounterScript, []); - - expect(clonedScripts.length).eq(2); - expect(clonedScripts[0].value).eq(10); - expect(clonedScripts[1].value).eq(20); - expect(clonedScripts[0]).not.eq(script1); - expect(clonedScripts[1]).not.eq(script2); - - rootEntity.destroy(); - }); - - it("ref to second component of same type should remap correctly", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const counter1 = child.addComponent(CounterScript); - const counter2 = child.addComponent(CounterScript); - counter1.value = 1; - counter2.value = 2; - - const refScript = parent.addComponent(CounterRefScript); - refScript.counter = counter2; - - const cloned = parent.clone(); - const clonedRef = cloned.getComponent(CounterRefScript); - const clonedCounters = cloned.children[0].getComponents(CounterScript, []); - - expect(clonedRef.counter).not.eq(counter2); - expect(clonedRef.counter).eq(clonedCounters[1]); - expect(clonedRef.counter.value).eq(2); - - rootEntity.destroy(); - }); - - it("ref to first component of same type should remap correctly", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const counter1 = child.addComponent(CounterScript); - const counter2 = child.addComponent(CounterScript); - counter1.value = 100; - counter2.value = 200; - - const refScript = parent.addComponent(CounterRefScript); - refScript.counter = counter1; - - const cloned = parent.clone(); - const clonedRef = cloned.getComponent(CounterRefScript); - const clonedCounters = cloned.children[0].getComponents(CounterScript, []); - - expect(clonedRef.counter).not.eq(counter1); - expect(clonedRef.counter).eq(clonedCounters[0]); - expect(clonedRef.counter.value).eq(100); - - rootEntity.destroy(); - }); - - it("cross-references between multiple same-type components on same entity", () => { - const rootEntity = scene.createRootEntity("root"); - const entity = rootEntity.createChild("entity"); - const script1 = entity.addComponent(CounterScript); - const script2 = entity.addComponent(CounterScript); - script1.value = 1; - script2.value = 2; - script1.partner = script2; - script2.partner = script1; - - const cloned = entity.clone(); - const clonedScripts = cloned.getComponents(CounterScript, []); - - expect(clonedScripts[0].partner).eq(clonedScripts[1]); - expect(clonedScripts[1].partner).eq(clonedScripts[0]); - expect(clonedScripts[0].partner).not.eq(script2); - expect(clonedScripts[1].partner).not.eq(script1); - - rootEntity.destroy(); - }); - - it("self-reference among multiple same-type components remaps to correct clone", () => { - const rootEntity = scene.createRootEntity("root"); - const entity = rootEntity.createChild("entity"); - const script1 = entity.addComponent(CounterScript); - const script2 = entity.addComponent(CounterScript); - script1.partner = script1; - script2.partner = script2; - - const cloned = entity.clone(); - const clonedScripts = cloned.getComponents(CounterScript, []); - - expect(clonedScripts[0].partner).eq(clonedScripts[0]); - expect(clonedScripts[1].partner).eq(clonedScripts[1]); - - rootEntity.destroy(); - }); - - it("multiple same-type components with entity refs all remap independently", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const childA = parent.createChild("childA"); - const childB = parent.createChild("childB"); - const script1 = parent.addComponent(CounterScript); - const script2 = parent.addComponent(CounterScript); - script1.targetEntity = childA; - script2.targetEntity = childB; - - const cloned = parent.clone(); - const clonedScripts = cloned.getComponents(CounterScript, []); - - expect(clonedScripts[0].targetEntity).eq(cloned.children[0]); - expect(clonedScripts[1].targetEntity).eq(cloned.children[1]); - expect(clonedScripts[0].targetEntity.name).eq("childA"); - expect(clonedScripts[1].targetEntity.name).eq("childB"); - - rootEntity.destroy(); - }); - - it("@deepClone array referencing specific component among same-type siblings", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const counter1 = child.addComponent(CounterScript); - const counter2 = child.addComponent(CounterScript); - const counter3 = child.addComponent(CounterScript); - counter1.value = 1; - counter2.value = 2; - counter3.value = 3; - - const arrayScript = parent.addComponent(ArrayRefScript); - // Note: ArrayRefScript uses Entity[], but we test component indexing - // via direct component references instead - const refScript1 = parent.addComponent(CounterRefScript); - const refScript2 = parent.addComponent(CounterRefScript); - refScript1.counter = counter1; - refScript2.counter = counter3; - - const cloned = parent.clone(); - const clonedRefs = cloned.getComponents(CounterRefScript, []); - const clonedCounters = cloned.children[0].getComponents(CounterScript, []); - - expect(clonedRefs[0].counter).eq(clonedCounters[0]); - expect(clonedRefs[0].counter.value).eq(1); - expect(clonedRefs[1].counter).eq(clonedCounters[2]); - expect(clonedRefs[1].counter.value).eq(3); - - rootEntity.destroy(); - }); - }); - - describe("Undecorated array auto clone + remap (type inference)", () => { - it("undecorated entity array should create new array and remap elements", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const childA = parent.createChild("childA"); - const childB = parent.createChild("childB"); - const script = parent.addComponent(UndecoratedArrayScript); - script.entities = [childA, childB]; - - const cloned = parent.clone(); - const cs = cloned.getComponent(UndecoratedArrayScript); - - expect(cs.entities).not.eq(script.entities); - expect(cs.entities.length).eq(2); - expect(cs.entities[0]).not.eq(childA); - expect(cs.entities[1]).not.eq(childB); - expect(cs.entities[0]).eq(cloned.children[0]); - expect(cs.entities[1]).eq(cloned.children[1]); - - rootEntity.destroy(); - }); - - it("undecorated entity array with external ref keeps original", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const external = rootEntity.createChild("external"); - const script = parent.addComponent(UndecoratedArrayScript); - script.entities = [child, external]; - - const cloned = parent.clone(); - const cs = cloned.getComponent(UndecoratedArrayScript); - - expect(cs.entities[0]).eq(cloned.children[0]); - expect(cs.entities[1]).eq(external); - - rootEntity.destroy(); - }); - - it("undecorated empty array stays empty with independent reference", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const script = parent.addComponent(UndecoratedArrayScript); - script.entities = []; - - const cloned = parent.clone(); - const cs = cloned.getComponent(UndecoratedArrayScript); - - expect(cs.entities).not.eq(script.entities); - expect(cs.entities.length).eq(0); - - rootEntity.destroy(); - }); - }); - - describe("Undecorated nested object auto clone + remap (type inference)", () => { - it("undecorated object with entity ref should deep clone and remap", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const script = parent.addComponent(UndecoratedObjectScript); - script.config = { target: child, label: "hello" }; - - const cloned = parent.clone(); - const cs = cloned.getComponent(UndecoratedObjectScript); - - expect(cs.config).not.eq(script.config); - expect(cs.config.label).eq("hello"); - expect(cs.config.target).not.eq(child); - expect(cs.config.target).eq(cloned.children[0]); - - rootEntity.destroy(); - }); - - it("undecorated object with external entity ref keeps original", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const external = rootEntity.createChild("external"); - const script = parent.addComponent(UndecoratedObjectScript); - script.config = { target: external, label: "ext" }; - - const cloned = parent.clone(); - const cs = cloned.getComponent(UndecoratedObjectScript); - - expect(cs.config.target).eq(external); - expect(cs.config.label).eq("ext"); - - rootEntity.destroy(); - }); - }); - - describe("Nested array of arrays with entity refs (type inference)", () => { - it("undecorated nested entity arrays should recursively clone and remap", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const childA = parent.createChild("childA"); - const childB = parent.createChild("childB"); - const childC = parent.createChild("childC"); - const script = parent.addComponent(NestedArrayScript); - script.groups = [[childA, childB], [childC]]; - - const cloned = parent.clone(); - const cs = cloned.getComponent(NestedArrayScript); - - expect(cs.groups).not.eq(script.groups); - expect(cs.groups.length).eq(2); - expect(cs.groups[0]).not.eq(script.groups[0]); - expect(cs.groups[1]).not.eq(script.groups[1]); - expect(cs.groups[0][0]).eq(cloned.children[0]); - expect(cs.groups[0][1]).eq(cloned.children[1]); - expect(cs.groups[1][0]).eq(cloned.children[2]); - - rootEntity.destroy(); - }); - }); - - describe("Map with entity values (type inference)", () => { - it("undecorated Map should create new Map and remap entity values", () => { - const rootEntity = scene.createRootEntity("root"); - const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const external = rootEntity.createChild("external"); - const script = parent.addComponent(MapRefScript); - script.entityMap.set("internal", child); - script.entityMap.set("external", external); - - const cloned = parent.clone(); - const cs = cloned.getComponent(MapRefScript); - - expect(cs.entityMap).not.eq(script.entityMap); - expect(cs.entityMap.size).eq(2); - expect(cs.entityMap.get("internal")).eq(cloned.children[0]); - expect(cs.entityMap.get("external")).eq(external); - - rootEntity.destroy(); - }); - }); -}); diff --git a/tests/src/core/RefCountContract.test.ts b/tests/src/core/RefCountContract.test.ts new file mode 100644 index 0000000000..392aeb1520 --- /dev/null +++ b/tests/src/core/RefCountContract.test.ts @@ -0,0 +1,266 @@ +import { + Animator, + AnimatorController, + AudioClip, + AudioSource, + Camera, + Entity, + MeshRenderer, + MeshShape, + ModelMesh, + ParticleRenderer, + RenderTarget, + Sprite, + SpriteMask, + SpriteRenderer, + Texture2D +} from "@galacean/engine-core"; +import { Vector3 } from "@galacean/engine-math"; +import { WebGLEngine } from "@galacean/engine"; +import { beforeAll, describe, expect, it } from "vitest"; + +/** + * Slot-ownership contract: a component top-level field sharing a ref-counted resource owns one + * reference — acquired by the setter (user path) or the clone gate (clone path), transferred by + * setter churn (-old/+new), and released by the component's destroy path. + * Every suite below asserts the full lifecycle: baseline → clone +1 → churn → destroy release. + */ +describe("RefCount slot-ownership contract", () => { + let engine: WebGLEngine; + let rootEntity: Entity; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + rootEntity = engine.sceneManager.activeScene.createRootEntity(); + engine.run(); + }); + + describe("Camera.renderTarget", () => { + it("clone acquires, setter churn transfers, destroy releases", () => { + const rtA = new RenderTarget(engine, 4, 4, new Texture2D(engine, 4, 4)); + const rtB = new RenderTarget(engine, 4, 4, new Texture2D(engine, 4, 4)); + const entity = rootEntity.createChild("cameraSlot"); + entity.addComponent(Camera).renderTarget = rtA; + expect(rtA.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + expect(rtA.refCount).eq(2); + + clone.getComponent(Camera).renderTarget = rtB; + expect(rtA.refCount).eq(1); + expect(rtB.refCount).eq(1); + + clone.destroy(); + expect(rtB.refCount).eq(0); + expect(rtA.refCount).eq(1); + + entity.destroy(); + expect(rtA.refCount).eq(0); + }); + }); + + describe("Animator.animatorController", () => { + it("clone acquires, setter churn transfers, destroy releases", () => { + const controllerA = new AnimatorController(engine); + const controllerB = new AnimatorController(engine); + const entity = rootEntity.createChild("animatorSlot"); + entity.addComponent(Animator).animatorController = controllerA; + expect(controllerA.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + expect(controllerA.refCount).eq(2); + + clone.getComponent(Animator).animatorController = controllerB; + expect(controllerA.refCount).eq(1); + expect(controllerB.refCount).eq(1); + + clone.destroy(); + expect(controllerB.refCount).eq(0); + expect(controllerA.refCount).eq(1); + + entity.destroy(); + expect(controllerA.refCount).eq(0); + }); + }); + + describe("AudioSource.clip", () => { + it("clone acquires, setter churn transfers, destroy releases", () => { + const clipA = new AudioClip(engine, "clipA"); + const clipB = new AudioClip(engine, "clipB"); + const entity = rootEntity.createChild("audioSlot"); + entity.addComponent(AudioSource).clip = clipA; + expect(clipA.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + expect(clipA.refCount).eq(2); + + clone.getComponent(AudioSource).clip = clipB; + expect(clipA.refCount).eq(1); + expect(clipB.refCount).eq(1); + + clone.destroy(); + expect(clipB.refCount).eq(0); + expect(clipA.refCount).eq(1); + + entity.destroy(); + expect(clipA.refCount).eq(0); + }); + }); + + describe("SpriteRenderer.sprite", () => { + it("clone acquires, setter churn transfers, destroy releases", () => { + const spriteA = new Sprite(engine, new Texture2D(engine, 1, 1)); + const spriteB = new Sprite(engine, new Texture2D(engine, 1, 1)); + const entity = rootEntity.createChild("spriteRendererSlot"); + entity.addComponent(SpriteRenderer).sprite = spriteA; + expect(spriteA.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + expect(spriteA.refCount).eq(2); + + clone.getComponent(SpriteRenderer).sprite = spriteB; + expect(spriteA.refCount).eq(1); + expect(spriteB.refCount).eq(1); + + clone.destroy(); + expect(spriteB.refCount).eq(0); + expect(spriteA.refCount).eq(1); + + entity.destroy(); + expect(spriteA.refCount).eq(0); + }); + }); + + describe("SpriteMask.sprite", () => { + it("clone acquires, setter churn transfers, destroy releases", () => { + const spriteA = new Sprite(engine, new Texture2D(engine, 1, 1)); + const spriteB = new Sprite(engine, new Texture2D(engine, 1, 1)); + const entity = rootEntity.createChild("spriteMaskSlot"); + entity.addComponent(SpriteMask).sprite = spriteA; + expect(spriteA.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + expect(spriteA.refCount).eq(2); + + clone.getComponent(SpriteMask).sprite = spriteB; + expect(spriteA.refCount).eq(1); + expect(spriteB.refCount).eq(1); + + clone.destroy(); + expect(spriteB.refCount).eq(0); + expect(spriteA.refCount).eq(1); + + entity.destroy(); + expect(spriteA.refCount).eq(0); + }); + }); + + describe("MeshRenderer.mesh (setter-mode slot)", () => { + it("clone acquires via the setter in _cloneTo, churn transfers, destroy releases", () => { + const meshA = new ModelMesh(engine); + const meshB = new ModelMesh(engine); + const entity = rootEntity.createChild("meshRendererSlot"); + entity.addComponent(MeshRenderer).mesh = meshA; + expect(meshA.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + expect(meshA.refCount).eq(2); + + clone.getComponent(MeshRenderer).mesh = meshB; + expect(meshA.refCount).eq(1); + expect(meshB.refCount).eq(1); + + clone.destroy(); + expect(meshB.refCount).eq(0); + expect(meshA.refCount).eq(1); + + entity.destroy(); + expect(meshA.refCount).eq(0); + }); + }); + + describe("Particle MeshShape.mesh", () => { + function createShapeMesh(): ModelMesh { + const mesh = new ModelMesh(engine); + mesh.setPositions([new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(0, 1, 0)]); + mesh.setNormals([new Vector3(0, 0, 1), new Vector3(0, 0, 1), new Vector3(0, 0, 1)]); + mesh.uploadData(false); + return mesh; + } + + it("setter churn transfers the count", () => { + const meshA = createShapeMesh(); + const meshB = createShapeMesh(); + const shape = new MeshShape(); + shape.mesh = meshA; + expect(meshA.refCount).eq(1); + + shape.mesh = meshB; + expect(meshA.refCount).eq(0); + expect(meshB.refCount).eq(1); + + shape.mesh = null; + expect(meshB.refCount).eq(0); + }); + + it("destroying the hosting renderer releases the shape's mesh", () => { + const mesh = createShapeMesh(); + const shape = new MeshShape(); + shape.mesh = mesh; + const entity = rootEntity.createChild("particleShapeSlot"); + entity.addComponent(ParticleRenderer).generator.emission.shape = shape; + expect(mesh.refCount).eq(1); + + entity.destroy(); + expect(mesh.refCount).eq(0); + }); + + it("clone acquires via the shape's _cloneTo, destroy releases", () => { + const mesh = createShapeMesh(); + const shape = new MeshShape(); + shape.mesh = mesh; + const entity = rootEntity.createChild("particleShapeClone"); + entity.addComponent(ParticleRenderer).generator.emission.shape = shape; + expect(mesh.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + expect(mesh.refCount).eq(2); + + clone.destroy(); + expect(mesh.refCount).eq(1); + + entity.destroy(); + expect(mesh.refCount).eq(0); + }); + }); + + describe("Template-marked source", () => { + it("each clone counts the template resource; the suppressed template itself never does", () => { + const template = engine.createEntity("template"); + const templateResource = new Texture2D(engine, 1, 1); + (template as any)._markAsTemplate(templateResource); + expect(templateResource.refCount).eq(0); + + const instA = template.clone(); + rootEntity.addChild(instA); + expect(templateResource.refCount).eq(1); + + const instB = template.clone(); + expect(templateResource.refCount).eq(2); + + instA.destroy(); + instB.destroy(); + expect(templateResource.refCount).eq(0); + + template.destroy(); + expect(templateResource.refCount).eq(0); + }); + }); +}); diff --git a/tests/src/core/ShaderDataRefCount.test.ts b/tests/src/core/ShaderDataRefCount.test.ts new file mode 100644 index 0000000000..1a7560e3d2 --- /dev/null +++ b/tests/src/core/ShaderDataRefCount.test.ts @@ -0,0 +1,183 @@ +import { BlinnPhongMaterial, Entity, MeshRenderer, Texture2D } from "@galacean/engine-core"; +import { WebGLEngine } from "@galacean/engine"; +import { beforeAll, describe, expect, it } from "vitest"; + +/** + * ShaderData is the cascade hub of ref counting: a texture set on a ShaderData owns + * `host.refCount` references, and every change of the host's count propagates to its textures + * (`ShaderData._addReferCount`), as does `Material._addReferCount` to shaderData + shader. + */ +describe("ShaderData / Material refCount cascade", () => { + let engine: WebGLEngine; + let rootEntity: Entity; + + beforeAll(async () => { + engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + rootEntity = engine.sceneManager.activeScene.createRootEntity(); + engine.run(); + }); + + it("setTexture swap / clear transfers counts by the host's refCount", () => { + const material = new BlinnPhongMaterial(engine); + const entity = rootEntity.createChild("swapHost"); + entity.addComponent(MeshRenderer).setMaterial(material); + expect(material.refCount).eq(1); + + const texA = new Texture2D(engine, 1, 1); + const texB = new Texture2D(engine, 1, 1); + material.shaderData.setTexture("u_custom", texA); + expect(texA.refCount).eq(1); + + material.shaderData.setTexture("u_custom", texB); + expect(texA.refCount).eq(0); + expect(texB.refCount).eq(1); + + material.shaderData.setTexture("u_custom", null); + expect(texB.refCount).eq(0); + + entity.destroy(); + expect(material.refCount).eq(0); + }); + + it("a texture set on a doubly-referenced material owns two counts; dropping one reference cascades", () => { + const material = new BlinnPhongMaterial(engine); + const other = new BlinnPhongMaterial(engine); + const entityA = rootEntity.createChild("hostA"); + const entityB = rootEntity.createChild("hostB"); + entityA.addComponent(MeshRenderer).setMaterial(material); + entityB.addComponent(MeshRenderer).setMaterial(material); + expect(material.refCount).eq(2); + + const tex = new Texture2D(engine, 1, 1); + material.shaderData.setTexture("u_custom", tex); + expect(tex.refCount).eq(2); + + // Dropping one material reference cascades -1 through shaderData to its textures. + entityB.getComponent(MeshRenderer).setMaterial(other); + expect(material.refCount).eq(1); + expect(tex.refCount).eq(1); + + entityA.destroy(); + expect(material.refCount).eq(0); + expect(tex.refCount).eq(0); + entityB.destroy(); + expect(other.refCount).eq(0); + }); + + it("cloning a renderer keeps the shared material and its textures balanced", () => { + const material = new BlinnPhongMaterial(engine); + const tex = new Texture2D(engine, 1, 1); + const entity = rootEntity.createChild("cloneHost"); + entity.addComponent(MeshRenderer).setMaterial(material); + material.shaderData.setTexture("u_custom", tex); + expect(material.refCount).eq(1); + expect(tex.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + // The clone shares the material (+1), which cascades +1 to its textures. + expect(material.refCount).eq(2); + expect(tex.refCount).eq(2); + + clone.destroy(); + expect(material.refCount).eq(1); + expect(tex.refCount).eq(1); + + entity.destroy(); + expect(material.refCount).eq(0); + expect(tex.refCount).eq(0); + }); + + it("instance materials release on destroy and stay balanced across clone", () => { + const material = new BlinnPhongMaterial(engine); + const entity = rootEntity.createChild("instanceHost"); + const renderer = entity.addComponent(MeshRenderer); + renderer.setMaterial(material); + + const instanced = renderer.getInstanceMaterial(); + expect(instanced).not.eq(material); + // Instancing replaces the slot: the original material is released, the instance is owned. + expect(material.refCount).eq(0); + expect(instanced.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + expect(instanced.refCount).eq(2); + + clone.destroy(); + expect(instanced.refCount).eq(1); + + entity.destroy(); + expect(instanced.refCount).eq(0); + }); + + it("texture-array entries cascade with the host's refCount like single textures", () => { + const material = new BlinnPhongMaterial(engine); + const entityA = rootEntity.createChild("texArrHostA"); + entityA.addComponent(MeshRenderer).setMaterial(material); + expect(material.refCount).eq(1); + + const texA = new Texture2D(engine, 1, 1); + const texB = new Texture2D(engine, 1, 1); + material.shaderData.setTextureArray("u_texArr", [texA, texB]); + expect(texA.refCount).eq(1); + expect(texB.refCount).eq(1); + + // A second owner of the material cascades +1 through the array entries. + const entityB = rootEntity.createChild("texArrHostB"); + entityB.addComponent(MeshRenderer).setMaterial(material); + expect(texA.refCount).eq(2); + expect(texB.refCount).eq(2); + + entityB.destroy(); + expect(texA.refCount).eq(1); + + entityA.destroy(); + expect(texA.refCount).eq(0); + expect(texB.refCount).eq(0); + }); + + it("cloning a shaderData counts the shared entries of a texture array", () => { + const texA = new Texture2D(engine, 1, 1); + const texB = new Texture2D(engine, 1, 1); + const entity = rootEntity.createChild("texArrCloneSrc"); + entity.addComponent(MeshRenderer).shaderData.setTextureArray("u_rendererTexArr", [texA, texB]); + expect(texA.refCount).eq(1); + + const clone = entity.clone(); + rootEntity.addChild(clone); + // The cloned array is a fresh container sharing the same counted entries. + expect(texA.refCount).eq(2); + expect(texB.refCount).eq(2); + + clone.destroy(); + expect(texA.refCount).eq(1); + + entity.destroy(); + expect(texA.refCount).eq(0); + expect(texB.refCount).eq(0); + }); + + it("cloneTo into a referenced, populated target keeps the displaced entry's count", () => { + const matA = new BlinnPhongMaterial(engine); + const matB = new BlinnPhongMaterial(engine); + const host = rootEntity.createChild("cloneToPopulated"); + host.addComponent(MeshRenderer).setMaterial(matB); + const texA = new Texture2D(engine, 1, 1); + const texB = new Texture2D(engine, 1, 1); + matA.shaderData.setTexture("u_custom", texA); + matB.shaderData.setTexture("u_custom", texB); + expect(texB.refCount).eq(1); + + matA.shaderData.cloneTo(matB.shaderData); + + // Pins current semantics: the incoming entry gains the target's count, while the displaced + // entry keeps the count it held — cloneTo does not release what it overwrites. + expect(texA.refCount).eq(1); + expect(texB.refCount).eq(1); + + host.destroy(); + expect(texA.refCount).eq(0); + expect(texB.refCount).eq(1); + }); +}); diff --git a/tests/src/core/Signal.test.ts b/tests/src/core/Signal.test.ts index 82a4fef4f4..c3c255ab08 100644 --- a/tests/src/core/Signal.test.ts +++ b/tests/src/core/Signal.test.ts @@ -1,4 +1,4 @@ -import { Script, Signal } from "@galacean/engine-core"; +import { Entity, Script, Signal } from "@galacean/engine-core"; import { WebGLEngine } from "@galacean/engine"; import { describe, expect, it, vi } from "vitest"; @@ -332,6 +332,17 @@ describe("Signal", async () => { // ---- Clone ---- + /** Build the identity map (source entity/component -> clone) the engine passes to `_cloneTo`. */ + function buildCloneMap(src: Entity, target: Entity, map = new Map()): Map { + map.set(src, target); + // @ts-ignore + const srcComponents = src._components, + targetComponents = target._components; + for (let i = 0; i < srcComponents.length; i++) map.set(srcComponents[i], targetComponents[i]); + for (let i = 0; i < src.children.length; i++) buildCloneMap(src.children[i], target.children[i], map); + return map; + } + it("clone: closure-based listeners are not cloned", () => { const signal = new Signal<[number]>(); const targetSignal = new Signal<[number]>(); @@ -341,7 +352,7 @@ describe("Signal", async () => { const srcRoot = root.createChild("clSrc1"); const targetRoot = srcRoot.clone(); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo(targetSignal, buildCloneMap(srcRoot, targetRoot)); // Closure listeners should NOT be copied to clone targetSignal.invoke(42); @@ -362,7 +373,7 @@ describe("Signal", async () => { const targetRoot = srcRoot.clone(); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo(targetSignal, buildCloneMap(srcRoot, targetRoot)); const clonedHandler = targetRoot.findByName("handler").getComponent(TestHandler); targetSignal.invoke(); @@ -385,7 +396,7 @@ describe("Signal", async () => { const targetRoot = srcRoot.clone(); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo(targetSignal, buildCloneMap(srcRoot, targetRoot)); targetSignal.invoke(); expect(externalHandler.callCount).toBe(1); @@ -408,7 +419,7 @@ describe("Signal", async () => { const targetRoot = srcRoot.clone(); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo(targetSignal, buildCloneMap(srcRoot, targetRoot)); const clonedHandler = targetRoot.findByName("handler").getComponent(TestHandler); targetSignal.invoke(); @@ -431,7 +442,7 @@ describe("Signal", async () => { const targetRoot = srcRoot.clone(); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo(targetSignal, buildCloneMap(srcRoot, targetRoot)); const clonedHandler = targetRoot.findByName("handler").getComponent(TestHandler); targetSignal.invoke(); diff --git a/tests/src/core/SkinnedMeshRenderer.test.ts b/tests/src/core/SkinnedMeshRenderer.test.ts index 6c29b24331..658e6abed7 100644 --- a/tests/src/core/SkinnedMeshRenderer.test.ts +++ b/tests/src/core/SkinnedMeshRenderer.test.ts @@ -128,4 +128,57 @@ describe("SkinnedMeshRenderer", async () => { skinnedMeshRenderer.blendShapeWeights ); }); + + it("clone skin", () => { + const entity = rootEntity.createChild("SkinCloneRoot"); + const bone0 = entity.createChild("Bone0"); + const bone1 = entity.createChild("Bone1"); + + const modelMesh = new ModelMesh(engine); + modelMesh.setPositions([new Vector3(0, 0, 0), new Vector3(0, 1, 0), new Vector3(1, 1, 0)]); + + const skinnedMeshRenderer = entity.addComponent(SkinnedMeshRenderer); + skinnedMeshRenderer.mesh = modelMesh; + + const skin = new Skin("CloneSkin"); + skin.rootBone = bone0; + skin.bones = [bone0, bone1]; + skin.inverseBindMatrices = [ + new Matrix(), + new Matrix(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 1, 2, 3, 1) + ]; + skinnedMeshRenderer.skin = skin; + + const cloneEntity = entity.clone(); + const cloneSkin = cloneEntity.getComponent(SkinnedMeshRenderer).skin; + + // The skin itself is deep cloned. + expect(cloneSkin).to.be.not.equal(skin); + + // Entity references are remapped into the cloned subtree, not shared with the source. + const cloneBone0 = cloneEntity.findByName("Bone0"); + const cloneBone1 = cloneEntity.findByName("Bone1"); + expect(cloneSkin.rootBone).to.be.equal(cloneBone0); + expect(cloneSkin.rootBone).to.be.not.equal(bone0); + expect(cloneSkin.bones.length).to.be.equal(2); + expect(cloneSkin.bones[0]).to.be.equal(cloneBone0); + expect(cloneSkin.bones[1]).to.be.equal(cloneBone1); + expect(cloneSkin.bones[0]).to.be.not.equal(bone0); + expect(cloneSkin.bones[1]).to.be.not.equal(bone1); + + // Inverse bind matrices are independent deep copies with equal values. + expect(cloneSkin.inverseBindMatrices).to.be.not.equal(skin.inverseBindMatrices); + expect(cloneSkin.inverseBindMatrices.length).to.be.equal(2); + expect(cloneSkin.inverseBindMatrices[0]).to.be.not.equal(skin.inverseBindMatrices[0]); + expect(cloneSkin.inverseBindMatrices[1]).to.be.not.equal(skin.inverseBindMatrices[1]); + expect(cloneSkin.inverseBindMatrices[0].elements).to.be.deep.equal(skin.inverseBindMatrices[0].elements); + expect(cloneSkin.inverseBindMatrices[1].elements).to.be.deep.equal(skin.inverseBindMatrices[1].elements); + + // The clone keeps its own update flag manager. + // @ts-ignore + expect(cloneSkin._updatedManager).to.be.not.equal(skin._updatedManager); + + entity.destroy(); + cloneEntity.destroy(); + }); }); diff --git a/tests/src/core/Trail.test.ts b/tests/src/core/Trail.test.ts index 7d1e8be7e8..6dd55f0f3a 100644 --- a/tests/src/core/Trail.test.ts +++ b/tests/src/core/Trail.test.ts @@ -165,6 +165,62 @@ describe("Trail", async () => { expect(trailRenderer.destroyed).to.eq(true); }); + it("clone", () => { + const rootEntity = scene.getRootEntity(); + const trailEntity = rootEntity.createChild("trailCloneSrc"); + const trailRenderer = trailEntity.addComponent(TrailRenderer); + + trailRenderer.widthCurve = new ParticleCurve(new CurveKey(0, 0.5), new CurveKey(0.6, 2), new CurveKey(1, 0)); + trailRenderer.colorGradient = new ParticleGradient( + [new GradientColorKey(0, new Color(1, 0, 0, 1)), new GradientColorKey(1, new Color(0, 0, 1, 1))], + [new GradientAlphaKey(0, 0.8), new GradientAlphaKey(1, 0.2)] + ); + trailRenderer.textureScale = new Vector2(2.0, 0.5); + + const cloneEntity = trailEntity.clone(); + const cloneTrail = cloneEntity.getComponent(TrailRenderer); + + // widthCurve is an independent deep copy with equal keys. + expect(cloneTrail.widthCurve).not.to.eq(trailRenderer.widthCurve); + expect(cloneTrail.widthCurve.keys.length).to.eq(3); + for (let i = 0; i < 3; i++) { + expect(cloneTrail.widthCurve.keys[i]).not.to.eq(trailRenderer.widthCurve.keys[i]); + expect(cloneTrail.widthCurve.keys[i].time).to.eq(trailRenderer.widthCurve.keys[i].time); + expect(cloneTrail.widthCurve.keys[i].value).to.eq(trailRenderer.widthCurve.keys[i].value); + } + cloneTrail.widthCurve.keys[0].value = 9; + expect(trailRenderer.widthCurve.keys[0].value).to.eq(0.5); + + // colorGradient is an independent deep copy with equal color/alpha keys. + expect(cloneTrail.colorGradient).not.to.eq(trailRenderer.colorGradient); + expect(cloneTrail.colorGradient.colorKeys.length).to.eq(2); + expect(cloneTrail.colorGradient.alphaKeys.length).to.eq(2); + for (let i = 0; i < 2; i++) { + expect(cloneTrail.colorGradient.colorKeys[i]).not.to.eq(trailRenderer.colorGradient.colorKeys[i]); + expect(cloneTrail.colorGradient.colorKeys[i].color).not.to.eq(trailRenderer.colorGradient.colorKeys[i].color); + expect( + Color.equals(cloneTrail.colorGradient.colorKeys[i].color, trailRenderer.colorGradient.colorKeys[i].color) + ).to.eq(true); + expect(cloneTrail.colorGradient.colorKeys[i].time).to.eq(trailRenderer.colorGradient.colorKeys[i].time); + expect(cloneTrail.colorGradient.alphaKeys[i]).not.to.eq(trailRenderer.colorGradient.alphaKeys[i]); + expect(cloneTrail.colorGradient.alphaKeys[i].alpha).to.eq(trailRenderer.colorGradient.alphaKeys[i].alpha); + expect(cloneTrail.colorGradient.alphaKeys[i].time).to.eq(trailRenderer.colorGradient.alphaKeys[i].time); + } + cloneTrail.colorGradient.alphaKeys[0].alpha = 0.1; + expect(trailRenderer.colorGradient.alphaKeys[0].alpha).to.eq(0.8); + + // textureScale is an independent copy with equal values. + expect(cloneTrail.textureScale).not.to.eq(trailRenderer.textureScale); + expect(cloneTrail.textureScale.x).to.eq(2.0); + expect(cloneTrail.textureScale.y).to.eq(0.5); + cloneTrail.textureScale.set(3.0, 3.0); + expect(trailRenderer.textureScale.x).to.eq(2.0); + expect(trailRenderer.textureScale.y).to.eq(0.5); + + trailEntity.destroy(); + cloneEntity.destroy(); + }); + it("bounds", () => { const rootEntity = scene.getRootEntity(); const trailEntity = rootEntity.createChild("trail"); diff --git a/tests/src/core/particle/CustomData.test.ts b/tests/src/core/particle/CustomData.test.ts index 540f3b0461..2cd348297f 100644 --- a/tests/src/core/particle/CustomData.test.ts +++ b/tests/src/core/particle/CustomData.test.ts @@ -306,11 +306,9 @@ describe("CustomDataModule", function () { }); it("clones deep — entries detached, internal caches rebuilt", function () { - // Bug guard: CloneManager can't recurse into Map entries, so the default - // field-by-field clone would leave `cloned.curves === source.curves` - // (mutation aliasing) and an empty `_curveStreams` (silent no-op - // _updateShaderData). The module's `_cloneTo` hook deep-clones each - // entry and rebuilds the internal caches via addCurve / addGradient. + // The maps and the stream caches are all cloned by the gate: fresh Maps holding deep-cloned + // entries, and stream objects whose `curve` / `gradient` resolve through the identity map to + // those same clones (asserted below), so `_updateShaderData` uploads what the maps hold. const scene = engine.sceneManager.activeScene; const sourceEntity = scene.createRootEntity("source-particle"); const sourceRenderer = sourceEntity.addComponent(ParticleRenderer); @@ -334,13 +332,44 @@ describe("CustomDataModule", function () { expect(clonedCustomData.gradients.get("Tint")).to.not.eq(sourceCustomData.gradients.get("Tint")); expect(clonedCustomData.gradients.get("Tint")!.constantMax.r).to.be.closeTo(1, 1e-6); - // Internal caches are rebuilt — _updateShaderData would now upload uniforms. - //@ts-ignore - inspecting private internal cache - const clonedCurveStreams = (clonedCustomData as any)._curveStreams as { name: string }[]; - //@ts-ignore - const clonedGradientStreams = (clonedCustomData as any)._gradientStreams as { name: string }[]; - expect(clonedCurveStreams.map((s) => s.name)).to.deep.eq(["Intensity"]); - expect(clonedGradientStreams.map((s) => s.name)).to.deep.eq(["Tint"]); + // Internal caches are rebuilt — _updateShaderData would now upload uniforms. Every stream + // field is checked against the source's, each with the comparison its role demands. + const expectStreamsEquivalent = (key: string, entryKey: string, mapKey: string, names: string[]) => { + const sourceStreams = (sourceCustomData as any)[key]; + const clonedStreams = (clonedCustomData as any)[key]; + expect(clonedStreams.map((s: any) => s.name)).to.deep.eq(names); + expect(clonedStreams.length).to.eq(sourceStreams.length); + + for (let i = 0; i < sourceStreams.length; i++) { + const sourceStream = sourceStreams[i]; + const clonedStream = clonedStreams[i]; + expect(clonedStream.name).to.eq(sourceStream.name); + expect(clonedStream.lastMode).to.eq(sourceStream.lastMode); + + // A ShaderProperty is registered globally by name, so the clone must hold the very same + // instance. Identity, not deep equality — a structurally identical copy would pass the + // latter while breaking uniform lookup. + const propKeys = Object.keys(sourceStream).filter((k) => k.startsWith("prop")); + expect(propKeys.length).to.be.greaterThan(0); + for (const propKey of propKeys) { + expect(clonedStream[propKey]).to.eq(sourceStream[propKey]); + } + + // The keyframe-count cache is per-instance scratch: same values, own vector. + if (sourceStream.keysCountCache) { + expect(Array.from(clonedStream.keysCountCache)).to.deep.eq(Array.from(sourceStream.keysCountCache)); + expect(clonedStream.keysCountCache).to.not.eq(sourceStream.keysCountCache); + } + + // The stream points at the very object its own map holds (the identity map collapses both + // references onto one clone), and that clone is distinct from the source's entry. + expect(clonedStream[entryKey]).to.eq((clonedCustomData as any)[mapKey].get(clonedStream.name)); + expect(clonedStream[entryKey]).to.not.eq(sourceStream[entryKey]); + } + }; + + expectStreamsEquivalent("_curveStreams", "curve", "_curves", ["Intensity"]); + expectStreamsEquivalent("_gradientStreams", "gradient", "_gradients", ["Tint"]); // Mutation isolation: bumping the clone does not bleed back into the source. clonedCustomData.curves.get("Intensity")!.constantMax = 0.1; diff --git a/tests/src/core/particle/ParticleStopResume.test.ts b/tests/src/core/particle/ParticleStopResume.test.ts deleted file mode 100644 index 0e0db6ac05..0000000000 --- a/tests/src/core/particle/ParticleStopResume.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - Burst, - Camera, - ParticleCompositeCurve, - ParticleRenderer, - ParticleStopMode, - Scene -} from "@galacean/engine-core"; -import { WebGLEngine } from "@galacean/engine-rhi-webgl"; -import { beforeAll, describe, expect, it } from "vitest"; - -describe("ParticleGenerator stop/resume timeline", () => { - let engine: WebGLEngine; - let scene: Scene; - - beforeAll(async () => { - engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); - scene = engine.sceneManager.activeScene; - const root = scene.createRootEntity("root"); - const camera = root.createChild("Camera"); - camera.addComponent(Camera); - camera.transform.setPosition(0, 0, 10); - }); - - /** - * Drive `generator._update(dt)` directly so we control the timeline. - * `ParticleRenderer._update` would call this with `engine.time.deltaTime`, - * which is wall-clock and not reproducible. - */ - function tick(generator: any, frames: number, dt: number): void { - for (let i = 0; i < frames; i++) { - generator._update(dt); - } - } - - it("rate-over-time: stop -> idle -> play emits a catch-up batch in one frame", () => { - const entity = scene.createRootEntity("rate"); - const renderer = entity.addComponent(ParticleRenderer); - const generator = renderer.generator; - generator.useAutoRandomSeed = false; - generator.main.duration = 1; - generator.main.startLifetime.constant = 1; - generator.main.maxParticles = 10000; - generator.emission.rateOverTime.constant = 100; - - let totalEmitted = 0; - const origEmit = (generator as any)._emit.bind(generator); - (generator as any)._emit = (playTime: number, count: number) => { - totalEmitted += count; - origEmit(playTime, count); - }; - - generator.play(); - // Run 0.5s at 60fps -> ~50 particles - tick(generator, 30, 1 / 60); - const emittedDuringPlay = totalEmitted; - const playTimeAfterPlay = generator._playTime; - - generator.stop(true, ParticleStopMode.StopEmitting); - const playTimeAtStop = generator._playTime; - const emittedAtStop = totalEmitted; - - // Idle 4.5s while stopped -> _emit must not run, but _playTime drifts - tick(generator, 270, 1 / 60); - const playTimeAfterIdle = generator._playTime; - const emittedDuringIdle = totalEmitted - emittedAtStop; - - generator.play(); - // Single frame after resume - tick(generator, 1, 1 / 60); - const emittedFirstFrameAfterResume = totalEmitted - emittedAtStop; - - entity.destroy(); - - // eslint-disable-next-line no-console - console.log("[bug-repro/rate]", { - emittedDuringPlay, - playTimeAfterPlay, - playTimeAtStop, - playTimeAfterIdle, - emittedDuringIdle, - emittedFirstFrameAfterResume - }); - - expect(emittedDuringIdle).toBe(0); - // Buggy behavior: emits a large catch-up batch (~ idleSeconds * rate). - expect(emittedFirstFrameAfterResume).toBeGreaterThan(100); - expect(playTimeAfterIdle - playTimeAtStop).toBeGreaterThan(4); - }); - - it("burst: stop -> idle -> play replays multiple cycles of bursts", () => { - const entity = scene.createRootEntity("burst"); - const renderer = entity.addComponent(ParticleRenderer); - const generator = renderer.generator; - generator.useAutoRandomSeed = false; - generator.main.duration = 1; - generator.main.isLoop = true; - generator.main.startLifetime.constant = 1; - generator.main.maxParticles = 10000; - generator.emission.rateOverTime.constant = 0; - generator.emission.addBurst(new Burst(0, new ParticleCompositeCurve(10))); - - let totalEmitted = 0; - const origEmit = (generator as any)._emit.bind(generator); - (generator as any)._emit = (playTime: number, count: number) => { - totalEmitted += count; - origEmit(playTime, count); - }; - - generator.play(); - // 1 full cycle -> burst at t=0 fires once (10 particles at frame 0) - tick(generator, 60, 1 / 60); - const emittedAfterOneSecond = totalEmitted; - - generator.stop(true, ParticleStopMode.StopEmitting); - const playTimeAtStop = generator._playTime; - const emittedAtStop = totalEmitted; - - // Idle 4 cycles - tick(generator, 240, 1 / 60); - const playTimeAfterIdle = generator._playTime; - - generator.play(); - tick(generator, 1, 1 / 60); // first frame after resume - const emittedFirstFrameAfterResume = totalEmitted - emittedAtStop; - - entity.destroy(); - - // eslint-disable-next-line no-console - console.log("[bug-repro/burst]", { - emittedAfterOneSecond, - playTimeAtStop, - playTimeAfterIdle, - emittedFirstFrameAfterResume - }); - - // After fix this should be 0 (next burst at t=0 of the new cycle hasn't reached yet). - // Buggy behavior: replays the burst once because `_currentBurstIndex` is 0 and - // `_emitBySubBurst(lastPlayTime, playTime, ...)` sees burst.time === startTime. - expect(emittedFirstFrameAfterResume).toBe(10); - }); -}); diff --git a/tests/src/core/physics/ColliderShape.test.ts b/tests/src/core/physics/ColliderShape.test.ts index 008756681d..5b42fc15cc 100644 --- a/tests/src/core/physics/ColliderShape.test.ts +++ b/tests/src/core/physics/ColliderShape.test.ts @@ -356,4 +356,38 @@ describe("ColliderShape PhysX", () => { expect((newCollider3.shapes[0] as CapsuleColliderShape).radius).to.eq(2); expect((newCollider3.shapes[0] as CapsuleColliderShape).height).to.eq(3); }); + + it("cloned shapes are independent instances owned by the cloned collider", () => { + const boxShape = new BoxColliderShape(); + boxShape.size = new Vector3(2, 3, 4); + boxShape.position = new Vector3(1, 2, 3); + dynamicCollider.addShape(boxShape); + + const srcEntity = dynamicCollider.entity; + const clonedEntity = srcEntity.clone(); + srcEntity.parent.addChild(clonedEntity); + const clonedCollider = clonedEntity.getComponent(DynamicCollider); + const clonedShape = clonedCollider.shapes[0] as BoxColliderShape; + + // Instance independence — not the source's shape shared by reference. + expect(clonedShape).not.to.eq(boxShape); + expect(clonedShape.id).not.to.eq(boxShape.id); + // @ts-ignore + expect(clonedShape._nativeShape).not.to.eq(boxShape._nativeShape); + // Ownership: each shape belongs to its own collider. + expect(clonedShape.collider).to.eq(clonedCollider); + expect(boxShape.collider).to.eq(dynamicCollider); + // Values copied. + expect(clonedShape.size).to.deep.include({ x: 2, y: 3, z: 4 }); + expect(clonedShape.position).to.deep.include({ x: 1, y: 2, z: 3 }); + // Mutating the clone must not affect the source. + clonedShape.size = new Vector3(9, 9, 9); + expect(boxShape.size).to.deep.include({ x: 2, y: 3, z: 4 }); + + // Destroying the clone must not destroy the source's shape / native handle. + clonedEntity.destroy(); + expect(boxShape.collider).to.eq(dynamicCollider); + // @ts-ignore + expect(boxShape._nativeShape).not.to.eq(null); + }); }); diff --git a/tests/src/core/physics/MeshColliderShape.test.ts b/tests/src/core/physics/MeshColliderShape.test.ts index 77fbb2dd1a..759ef22d8a 100644 --- a/tests/src/core/physics/MeshColliderShape.test.ts +++ b/tests/src/core/physics/MeshColliderShape.test.ts @@ -1053,4 +1053,36 @@ describe("MeshColliderShape PhysX", () => { entity.destroy(); }); }); + + describe("mesh refCount (slot-ownership contract)", () => { + it("clone acquires via the setter, churn transfers, destroy releases", () => { + const meshA = createModelMesh(engine, [-1, 0, -1, 1, 0, -1, 0, 0, 1], [0, 1, 2]); + const meshB = createModelMesh(engine, [-2, 0, -2, 2, 0, -2, 0, 0, 2], [0, 1, 2]); + const entity = root.createChild("meshRefSlot"); + const collider = entity.addComponent(StaticCollider); + const shape = new MeshColliderShape(); + shape.mesh = meshA; + collider.addShape(shape); + expect(meshA.refCount).toBe(1); + + const clone = entity.clone(); + root.addChild(clone); + expect(meshA.refCount).toBe(2); + + const clonedShape = clone.getComponent(StaticCollider).shapes[0] as MeshColliderShape; + expect(clonedShape).not.toBe(shape); + // The rebuilt native shape must be attached exactly once (setter attaches; _syncNative skips). + expect((clone.getComponent(StaticCollider) as any)._nativeCollider._shapes.length).toBe(1); + clonedShape.mesh = meshB; + expect(meshA.refCount).toBe(1); + expect(meshB.refCount).toBe(1); + + clone.destroy(); + expect(meshB.refCount).toBe(0); + expect(meshA.refCount).toBe(1); + + entity.destroy(); + expect(meshA.refCount).toBe(0); + }); + }); }); diff --git a/tests/src/core/postProcess/PostProcess.test.ts b/tests/src/core/postProcess/PostProcess.test.ts index 1d0178b549..cc6caba195 100644 --- a/tests/src/core/postProcess/PostProcess.test.ts +++ b/tests/src/core/postProcess/PostProcess.test.ts @@ -173,6 +173,57 @@ describe("PostProcess", () => { expect(pp._effects.length).to.equal(0); }); + it("Post Process clone", () => { + const pp = postEntity.addComponent(PostProcess); + const bloomEffect = pp.addEffect(BloomEffect); + const dirtTexture = new Texture2D(engine, 1, 1); + + bloomEffect.intensity.value = 1.5; + bloomEffect.threshold.value = 0.9; + bloomEffect.tint.value = new Color(0.5, 0.25, 0.1, 1); + bloomEffect.highQualityFiltering.value = true; + bloomEffect.dirtTexture.value = dirtTexture; + bloomEffect.enabled = false; + + const refCount = dirtTexture.refCount; + + const cloneEntity = postEntity.clone(); + const clonePP = cloneEntity.getComponent(PostProcess); + const cloneBloom = clonePP.getEffect(BloomEffect); + + // Effects, effect and parameters are all fresh instances. + expect(clonePP).to.not.equal(pp); + // @ts-ignore + expect(clonePP._effects).to.not.equal(pp._effects); + // @ts-ignore + expect(clonePP._effects.length).to.equal(1); + expect(cloneBloom).to.instanceOf(BloomEffect); + expect(cloneBloom).to.not.equal(bloomEffect); + expect(cloneBloom.intensity).to.not.equal(bloomEffect.intensity); + expect(cloneBloom.tint).to.not.equal(bloomEffect.tint); + + // Values are preserved. + expect(cloneBloom.intensity.value).to.equal(1.5); + expect(cloneBloom.threshold.value).to.equal(0.9); + expect(cloneBloom.highQualityFiltering.value).to.true; + expect(cloneBloom.enabled).to.false; + expect(cloneBloom.tint.value).to.include(new Color(0.5, 0.25, 0.1, 1)); + + // Values are independent: mutating the clone leaves the source untouched. + expect(cloneBloom.tint.value).to.not.equal(bloomEffect.tint.value); + cloneBloom.tint.value.r = 0.9; + expect(bloomEffect.tint.value.r).to.equal(0.5); + cloneBloom.intensity.value = 3; + expect(bloomEffect.intensity.value).to.equal(1.5); + + // Texture parameter shares the same texture reference without touching its refCount. + expect(cloneBloom.dirtTexture).to.not.equal(bloomEffect.dirtTexture); + expect(cloneBloom.dirtTexture.value).to.equal(dirtTexture); + expect(dirtTexture.refCount).to.equal(refCount); + + cloneEntity.destroy(); + }); + it("Post Process", () => { const ppManager = scene.postProcessManager; diff --git a/tests/src/math/Ray.test.ts b/tests/src/math/Ray.test.ts index 2a7eb2b5e4..21be16603a 100644 --- a/tests/src/math/Ray.test.ts +++ b/tests/src/math/Ray.test.ts @@ -31,4 +31,24 @@ describe("Ray test", () => { expect(Vector3.equals(out, new Vector3(0, 10, 0))).to.eq(true); }); + + it("ray-clone", () => { + const ray = new Ray(new Vector3(1, 2, 3), new Vector3(0, 0, 1)); + const out = ray.clone(); + + expect(out).not.to.eq(ray); + expect(out.origin).not.to.eq(ray.origin); + expect(Vector3.equals(out.origin, ray.origin)).to.eq(true); + expect(Vector3.equals(out.direction, ray.direction)).to.eq(true); + }); + + it("ray-copyFrom", () => { + const ray = new Ray(new Vector3(1, 2, 3), new Vector3(0, 0, 1)); + const out = new Ray(); + const result = out.copyFrom(ray); + + expect(result).to.eq(out); + expect(Vector3.equals(out.origin, ray.origin)).to.eq(true); + expect(Vector3.equals(out.direction, ray.direction)).to.eq(true); + }); }); diff --git a/tests/src/ui/Image.test.ts b/tests/src/ui/Image.test.ts index eaa6bd026a..1f409fe447 100644 --- a/tests/src/ui/Image.test.ts +++ b/tests/src/ui/Image.test.ts @@ -68,4 +68,27 @@ describe("Image", async () => { expect(cloneImage.raycastPadding.z).to.eq(1); expect(cloneImage.raycastPadding.w).to.eq(1); }); + + it("sprite refCount: clone acquires, setter churn transfers, destroy releases", () => { + const entity = canvasEntity.createChild("imageRefSlot"); + const spriteA = new Sprite(engine, new Texture2D(engine, 1, 1)); + const spriteB = new Sprite(engine, new Texture2D(engine, 1, 1)); + entity.addComponent(Image).sprite = spriteA; + expect(spriteA.refCount).to.eq(1); + + const clone = entity.clone(); + canvasEntity.addChild(clone); + expect(spriteA.refCount).to.eq(2); + + clone.getComponent(Image).sprite = spriteB; + expect(spriteA.refCount).to.eq(1); + expect(spriteB.refCount).to.eq(1); + + clone.destroy(); + expect(spriteB.refCount).to.eq(0); + expect(spriteA.refCount).to.eq(1); + + entity.destroy(); + expect(spriteA.refCount).to.eq(0); + }); }); diff --git a/tests/src/ui/UIInteractive.test.ts b/tests/src/ui/UIInteractive.test.ts index 5780463ec0..d1cfd264e9 100644 --- a/tests/src/ui/UIInteractive.test.ts +++ b/tests/src/ui/UIInteractive.test.ts @@ -1,4 +1,4 @@ -import { Camera, PointerEventData, Script, SpriteDrawMode } from "@galacean/engine-core"; +import { Camera, PointerEventData, Script, Sprite, SpriteDrawMode, Texture2D } from "@galacean/engine-core"; import { Color, Vector3 } from "@galacean/engine-math"; import { WebGLEngine } from "@galacean/engine"; import { @@ -6,6 +6,7 @@ import { ColorTransition, Image, ScaleTransition, + SpriteTransition, Text, UICanvas, UIGroup, @@ -251,4 +252,98 @@ describe("Button", async () => { testEntity.destroy(); }); + + it("cloned interactive gets independent deep-cloned transitions with remapped targets", () => { + const testEntity = canvasEntity.createChild("transitionClone"); + const testImage = testEntity.addComponent(Image); + (testEntity.transform).size.set(100, 40); + const testButton = testEntity.addComponent(Button); + + const transition = new ColorTransition(); + transition.target = testImage; + transition.normal = new Color(1, 0, 0, 1); + transition.hover = new Color(0, 1, 0, 1); + testButton.addTransition(transition); + + const cloneEntity = testEntity.clone(); + canvasEntity.addChild(cloneEntity); + const cloneButton = cloneEntity.getComponent(Button); + const cloneImage = cloneEntity.getComponent(Image); + + expect(cloneButton.transitions.length).to.eq(1); + const cloneTransition = cloneButton.transitions[0] as ColorTransition; + // Independent instance, wired to the clone's own components. + expect(cloneTransition).not.to.eq(transition); + expect(cloneTransition.target).to.eq(cloneImage); + // @ts-ignore + expect(cloneTransition._interactive).to.eq(cloneButton); + // State values deep cloned. + expect(cloneTransition.normal).not.to.eq(transition.normal); + expect(cloneTransition.normal.r).to.eq(1); + expect(cloneTransition.hover.g).to.eq(1); + + // Destroying the clone must not strip the source button's transitions. + cloneEntity.destroy(); + expect(testButton.transitions.length).to.eq(1); + expect(transition.target).to.eq(testImage); + + testEntity.destroy(); + }); + + it("cloned sprite transition keeps shared sprites' refCount balanced", () => { + const testEntity = canvasEntity.createChild("spriteTransitionClone"); + const testImage = testEntity.addComponent(Image); + const testButton = testEntity.addComponent(Button); + + const sprite = new Sprite(engine, new Texture2D(engine, 1, 1)); + const transition = new SpriteTransition(); + transition.target = testImage; + transition.normal = sprite; + testButton.addTransition(transition); + const baseline = sprite.refCount; + + const cloneEntity = testEntity.clone(); + canvasEntity.addChild(cloneEntity); + // +1 by the cloned transition (acquired in _cloneTo), +1 by the cloned Image + // (transition._applyValue assigns the sprite through the Image.sprite setter on activation). + expect(sprite.refCount).to.eq(baseline + 2); + + cloneEntity.destroy(); + expect(sprite.refCount).to.eq(baseline); + + testEntity.destroy(); + expect(sprite.refCount).to.eq(baseline - 2); + }); + + it("cloned sprite transition setter churn transfers sprite counts", () => { + const testEntity = canvasEntity.createChild("spriteTransitionChurn"); + const testImage = testEntity.addComponent(Image); + const testButton = testEntity.addComponent(Button); + + const spriteA = new Sprite(engine, new Texture2D(engine, 1, 1)); + const spriteB = new Sprite(engine, new Texture2D(engine, 1, 1)); + const transition = new SpriteTransition(); + transition.target = testImage; + transition.normal = spriteA; + testButton.addTransition(transition); + const baselineA = spriteA.refCount; + + const cloneEntity = testEntity.clone(); + canvasEntity.addChild(cloneEntity); + // +1 cloned transition (_cloneTo) + 1 cloned Image (applied through its sprite setter). + expect(spriteA.refCount).to.eq(baselineA + 2); + + const cloneTransition = cloneEntity.getComponent(Button).transitions[0] as SpriteTransition; + cloneTransition.normal = spriteB; + // Churn releases A from the cloned transition AND from the cloned Image (re-applied), B gains both. + expect(spriteA.refCount).to.eq(baselineA); + expect(spriteB.refCount).to.eq(2); + + cloneEntity.destroy(); + expect(spriteB.refCount).to.eq(0); + expect(spriteA.refCount).to.eq(baselineA); + + testEntity.destroy(); + expect(spriteA.refCount).to.eq(baselineA - 2); + }); }); From 2fc950865b8403b5101024d2be8d37d5cc1ce498 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Mon, 20 Jul 2026 01:05:36 +0800 Subject: [PATCH 18/23] chore: release v0.0.0-experimental-2.0-migrate.1 --- e2e/package.json | 2 +- examples/package.json | 2 +- package.json | 2 +- packages/core/package.json | 2 +- packages/design/package.json | 2 +- packages/galacean/package.json | 2 +- packages/loader/package.json | 2 +- packages/math/package.json | 2 +- packages/physics-physx/package.json | 2 +- packages/rhi-webgl/package.json | 2 +- packages/shader-compiler/package.json | 2 +- packages/shader/package.json | 2 +- packages/spine-core-3.8/package.json | 2 +- packages/spine-core-4.2/package.json | 2 +- packages/spine/package.json | 2 +- packages/ui/package.json | 2 +- packages/xr-webxr/package.json | 2 +- packages/xr/package.json | 2 +- tests/package.json | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/e2e/package.json b/e2e/package.json index c5a59ec567..7f22f893cc 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-e2e", "private": true, - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "license": "MIT", "scripts": { "case": "vite serve .dev --config .dev/vite.config.js", diff --git a/examples/package.json b/examples/package.json index 48b483c7e0..8505cc070f 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-examples", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "private": true, "license": "MIT", "main": "dist/main.js", diff --git a/package.json b/package.json index b96706d3e9..370aae69c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-root", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "packageManager": "pnpm@9.3.0", "private": true, "engines": { diff --git a/packages/core/package.json b/packages/core/package.json index 2082de1ce4..4bd6fa3663 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-core", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/design/package.json b/packages/design/package.json index ca24c7f69b..83f75ce6da 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-design", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/galacean/package.json b/packages/galacean/package.json index 127e4d0466..5280235e71 100644 --- a/packages/galacean/package.json +++ b/packages/galacean/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/loader/package.json b/packages/loader/package.json index 87532cb158..054f931fdf 100644 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-loader", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/math/package.json b/packages/math/package.json index 96855d8329..83206f6dfb 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-math", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-physx/package.json b/packages/physics-physx/package.json index 9ef5fe570e..b83f89ac45 100644 --- a/packages/physics-physx/package.json +++ b/packages/physics-physx/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-physics-physx", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/rhi-webgl/package.json b/packages/rhi-webgl/package.json index 9ae280e0f2..bc872eda7d 100644 --- a/packages/rhi-webgl/package.json +++ b/packages/rhi-webgl/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-rhi-webgl", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "repository": { "url": "https://github.com/galacean/engine.git" }, diff --git a/packages/shader-compiler/package.json b/packages/shader-compiler/package.json index 7c4600bc01..37ab1bb711 100644 --- a/packages/shader-compiler/package.json +++ b/packages/shader-compiler/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader-compiler", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/shader/package.json b/packages/shader/package.json index e38be5b235..ff8df290b8 100644 --- a/packages/shader/package.json +++ b/packages/shader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/spine-core-3.8/package.json b/packages/spine-core-3.8/package.json index 6a8d6e6c33..88ac79a746 100644 --- a/packages/spine-core-3.8/package.json +++ b/packages/spine-core-3.8/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-spine-core-3.8", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/spine-core-4.2/package.json b/packages/spine-core-4.2/package.json index 020c4d1474..516d5b36f3 100644 --- a/packages/spine-core-4.2/package.json +++ b/packages/spine-core-4.2/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-spine-core-4.2", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/spine/package.json b/packages/spine/package.json index 1c3c28bb1f..fc16fc9c80 100644 --- a/packages/spine/package.json +++ b/packages/spine/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-spine", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/ui/package.json b/packages/ui/package.json index 27aa07b7a3..c673057eeb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-ui", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr-webxr/package.json b/packages/xr-webxr/package.json index 71f021fda4..d721a81bf2 100644 --- a/packages/xr-webxr/package.json +++ b/packages/xr-webxr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr-webxr", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr/package.json b/packages/xr/package.json index be05c40951..9102ef084a 100644 --- a/packages/xr/package.json +++ b/packages/xr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr", - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/tests/package.json b/tests/package.json index 33779e4ba4..de02542243 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-tests", "private": true, - "version": "2.0.0-alpha.38", + "version": "0.0.0-experimental-2.0-migrate.1", "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", From 768ba05fbbb8ee7a5f61195eaf5fc00b381ca435 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Tue, 21 Jul 2026 21:00:13 +0800 Subject: [PATCH 19/23] fix(core): preserve hierarchy updates when clearing children --- packages/core/src/Entity.ts | 19 ++----------------- tests/src/core/Entity.test.ts | 30 +++++++++++++++++++++++++----- tests/src/ui/UICanvas.test.ts | 17 ++++++++++++++++- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index 6423056deb..c07c5b3a6d 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -399,24 +399,9 @@ export class Entity extends EngineObject { */ clearChildren(): void { const children = this._children; - for (let i = children.length - 1; i >= 0; i--) { - const child = children[i]; - child._parent = null; - child._siblingIndex = -1; - - let activeChangeFlag = ActiveChangeFlag.None; - child._isActiveInHierarchy && (activeChangeFlag |= ActiveChangeFlag.Hierarchy); - child._isActiveInScene && (activeChangeFlag |= ActiveChangeFlag.Scene); - activeChangeFlag && child._processInActive(activeChangeFlag); - - Entity._traverseSetOwnerScene(child, null); // Must after child._processInActive(). - - child._setParentChange(); + while (children.length > 0) { + children[children.length - 1]._setParent(null); } - children.length = 0; - // Dispatch a single `Child` modify event for the whole clear so subscribers - // (e.g. UICanvas) can invalidate their cached hierarchy state once. - this._dispatchModify(EntityModifyFlags.Child, this); } /** diff --git a/tests/src/core/Entity.test.ts b/tests/src/core/Entity.test.ts index f1e1dad3f1..67bec83495 100644 --- a/tests/src/core/Entity.test.ts +++ b/tests/src/core/Entity.test.ts @@ -1,5 +1,5 @@ import { Quaternion } from "@galacean/engine"; -import { DynamicCollider, Entity, EntityModifyFlags, Scene, Script } from "@galacean/engine-core"; +import { DynamicCollider, Entity, EntityModifyFlags, Logger, Scene, Script } from "@galacean/engine-core"; import { PhysXPhysics } from "@galacean/engine-physics-physx"; import { WebGLEngine } from "@galacean/engine"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -332,8 +332,18 @@ describe("Entity", async () => { const parentModifyCount = [0, 0, 0]; const childModifyCount = [0, 0, 0]; const child2ModifyCount = [0, 0, 0]; + const childCountsDuringChildModify: number[] = []; + const remainingChildrenConsistent: boolean[] = []; + const detachedChildCounts: number[] = []; // @ts-ignore - parent._registerModifyListener((flag: EntityModifyFlags) => ++parentModifyCount[flag]); + parent._registerModifyListener((flag: EntityModifyFlags) => { + ++parentModifyCount[flag]; + if (flag === EntityModifyFlags.Child) { + childCountsDuringChildModify.push(parent.children.length); + remainingChildrenConsistent.push(parent.children.every((child) => child.parent === parent)); + detachedChildCounts.push(Number(child.parent === null) + Number(child2.parent === null)); + } + }); // @ts-ignore child._registerModifyListener((flag: EntityModifyFlags) => ++childModifyCount[flag]); // @ts-ignore @@ -342,15 +352,22 @@ describe("Entity", async () => { parent.clearChildren(); expect(parent.children.length).eq(0); - // Parent should receive a single `Child` modify event for the whole clear so - // listeners (e.g. UICanvas) can invalidate their cached state. - expect(parentModifyCount[EntityModifyFlags.Child]).eq(1); + // Clearing children is equivalent to removing them from back to front. Each removal + // dispatches after the hierarchy is updated but before inactive listeners are cleaned up. + expect(parentModifyCount[EntityModifyFlags.Child]).eq(2); + expect(childCountsDuringChildModify).deep.eq([1, 0]); + expect(remainingChildrenConsistent).deep.eq([true, true]); + expect(detachedChildCounts).deep.eq([1, 2]); // Each detached child should receive a `Parent` modify event. expect(childModifyCount[EntityModifyFlags.Parent]).eq(1); expect(child2ModifyCount[EntityModifyFlags.Parent]).eq(1); // Sibling index must be reset so the entity is treated as lonely afterwards. expect(child.siblingIndex).eq(-1); expect(child2.siblingIndex).eq(-1); + + // Clearing an empty hierarchy should not dispatch another modify event. + parent.clearChildren(); + expect(parentModifyCount[EntityModifyFlags.Child]).eq(2); }); it("sibling index", () => { const root = scene.createRootEntity(); @@ -418,11 +435,14 @@ describe("Entity", async () => { // setting sibling index on a lonely entity (no parent, not in scene root) warns instead of throwing const entityX = new Entity(engine, "entityX"); + const warnSpy = vi.spyOn(Logger, "warn").mockImplementation(() => {}); var lonelyFn = function () { entityX.siblingIndex = 1; }; expect(lonelyFn).not.to.throw(); expect(entityX.siblingIndex).eq(-1); + expect(warnSpy).toHaveBeenCalledOnce(); + warnSpy.mockRestore(); }); it("isRoot", () => { diff --git a/tests/src/ui/UICanvas.test.ts b/tests/src/ui/UICanvas.test.ts index 5e534763c8..9889792a9f 100644 --- a/tests/src/ui/UICanvas.test.ts +++ b/tests/src/ui/UICanvas.test.ts @@ -1,7 +1,7 @@ import { Camera } from "@galacean/engine-core"; import { Vector2 } from "@galacean/engine-math"; import { WebGLEngine } from "@galacean/engine"; -import { CanvasRenderMode, ResolutionAdaptationMode, UICanvas, UITransform } from "@galacean/engine-ui"; +import { CanvasRenderMode, Image, ResolutionAdaptationMode, UICanvas, UITransform } from "@galacean/engine-ui"; import { describe, expect, it } from "vitest"; describe("UICanvas", async () => { @@ -98,6 +98,21 @@ describe("UICanvas", async () => { expect(rootCanvas._isRootCanvas).to.eq(true); }); + it("clearChildren invalidates the root canvas renderer cache", () => { + const container = canvasEntity.createChild("clear-container"); + const image = container.createChild("image").addComponent(Image); + + // Populate the ordered renderer cache before removing the subtree. + // @ts-ignore + expect(rootCanvas._getRenderers()).toContain(image); + + container.clearChildren(); + + // @ts-ignore + expect(rootCanvas._getRenderers()).not.toContain(image); + container.destroy(); + }); + // Pose it("Pose Fit", () => { const canvasTransform = canvasEntity.transform; From 9f6fe8ea00dd28f2d99de24373e5fb25ec86a68a Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Tue, 21 Jul 2026 21:11:37 +0800 Subject: [PATCH 20/23] chore: release v0.0.0-experimental-2.0-migrate.2 --- e2e/package.json | 2 +- examples/package.json | 2 +- package.json | 2 +- packages/core/package.json | 2 +- packages/design/package.json | 2 +- packages/galacean/package.json | 2 +- packages/loader/package.json | 2 +- packages/math/package.json | 2 +- packages/physics-physx/package.json | 2 +- packages/rhi-webgl/package.json | 2 +- packages/shader-compiler/package.json | 2 +- packages/shader/package.json | 2 +- packages/spine-core-3.8/package.json | 2 +- packages/spine-core-4.2/package.json | 2 +- packages/spine/package.json | 2 +- packages/ui/package.json | 2 +- packages/xr-webxr/package.json | 2 +- packages/xr/package.json | 2 +- tests/package.json | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/e2e/package.json b/e2e/package.json index 7f22f893cc..adf4447256 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-e2e", "private": true, - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "license": "MIT", "scripts": { "case": "vite serve .dev --config .dev/vite.config.js", diff --git a/examples/package.json b/examples/package.json index 8505cc070f..0d42bfdd87 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-examples", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "private": true, "license": "MIT", "main": "dist/main.js", diff --git a/package.json b/package.json index 370aae69c5..98a619601b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-root", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "packageManager": "pnpm@9.3.0", "private": true, "engines": { diff --git a/packages/core/package.json b/packages/core/package.json index 4bd6fa3663..7bddc1b55c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-core", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/design/package.json b/packages/design/package.json index 83f75ce6da..dc65edce3e 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-design", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/galacean/package.json b/packages/galacean/package.json index 5280235e71..fab0b14130 100644 --- a/packages/galacean/package.json +++ b/packages/galacean/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/loader/package.json b/packages/loader/package.json index 054f931fdf..656c59850f 100644 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-loader", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/math/package.json b/packages/math/package.json index 83206f6dfb..58a54c4df0 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-math", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-physx/package.json b/packages/physics-physx/package.json index b83f89ac45..bc93202c74 100644 --- a/packages/physics-physx/package.json +++ b/packages/physics-physx/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-physics-physx", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/rhi-webgl/package.json b/packages/rhi-webgl/package.json index bc872eda7d..4bbee42c57 100644 --- a/packages/rhi-webgl/package.json +++ b/packages/rhi-webgl/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-rhi-webgl", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "repository": { "url": "https://github.com/galacean/engine.git" }, diff --git a/packages/shader-compiler/package.json b/packages/shader-compiler/package.json index 37ab1bb711..eef0d5873f 100644 --- a/packages/shader-compiler/package.json +++ b/packages/shader-compiler/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader-compiler", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/shader/package.json b/packages/shader/package.json index ff8df290b8..086ff2d0b7 100644 --- a/packages/shader/package.json +++ b/packages/shader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/spine-core-3.8/package.json b/packages/spine-core-3.8/package.json index 88ac79a746..b579faad7b 100644 --- a/packages/spine-core-3.8/package.json +++ b/packages/spine-core-3.8/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-spine-core-3.8", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/spine-core-4.2/package.json b/packages/spine-core-4.2/package.json index 516d5b36f3..fab1e28696 100644 --- a/packages/spine-core-4.2/package.json +++ b/packages/spine-core-4.2/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-spine-core-4.2", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/spine/package.json b/packages/spine/package.json index fc16fc9c80..baaf9a4c01 100644 --- a/packages/spine/package.json +++ b/packages/spine/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-spine", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/ui/package.json b/packages/ui/package.json index c673057eeb..19cefa60c3 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-ui", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr-webxr/package.json b/packages/xr-webxr/package.json index d721a81bf2..4d1f9aa70a 100644 --- a/packages/xr-webxr/package.json +++ b/packages/xr-webxr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr-webxr", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr/package.json b/packages/xr/package.json index 9102ef084a..b441c2a51f 100644 --- a/packages/xr/package.json +++ b/packages/xr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr", - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/tests/package.json b/tests/package.json index de02542243..6d1c0a0151 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-tests", "private": true, - "version": "0.0.0-experimental-2.0-migrate.1", + "version": "0.0.0-experimental-2.0-migrate.2", "license": "MIT", "main": "dist/main.js", "module": "dist/module.js", From 23b6e9d53ea6bf3646855f4809d6879e2154fabe Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Thu, 23 Jul 2026 11:33:43 +0800 Subject: [PATCH 21/23] fix(core): preserve Transform replacement dependencies --- packages/core/src/ComponentsDependencies.ts | 8 +- packages/core/src/Entity.ts | 60 ++++++++--- tests/src/core/Transform.test.ts | 109 +++++++++++++++++++- tests/src/ui/UITransform.test.ts | 21 +++- 4 files changed, 176 insertions(+), 22 deletions(-) diff --git a/packages/core/src/ComponentsDependencies.ts b/packages/core/src/ComponentsDependencies.ts index 9af8569b12..b76dec5982 100644 --- a/packages/core/src/ComponentsDependencies.ts +++ b/packages/core/src/ComponentsDependencies.ts @@ -36,10 +36,12 @@ export class ComponentsDependencies { /** * @internal */ - static _removeCheck(entity: Entity, type: ComponentConstructor): void { + static _removeCheck(entity: Entity, type: ComponentConstructor, replace?: ComponentConstructor): void { const components = entity._components; const n = components.length; while (type !== Component) { + // The replacement still satisfies this type and all of its base types. + if (replace && (replace === type || replace.prototype instanceof type)) return; let count = 0; for (let i = 0; i < n; i++) { if (components[i] instanceof type && ++count > 1) return; @@ -64,7 +66,7 @@ export class ComponentsDependencies { dependentComponent: ComponentConstructor, map: Map ): void { - let components = map.get(targetInfo); + const components = map.get(targetInfo); if (!components) { map.set(targetInfo, [dependentComponent]); } else { @@ -77,7 +79,7 @@ export class ComponentsDependencies { */ static _addInvDependency(currentComponent: ComponentConstructor, dependentComponent: ComponentConstructor): void { const map = this._invDependenciesMap; - let components = map.get(currentComponent); + const components = map.get(currentComponent); if (!components) { map.set(currentComponent, [dependentComponent]); } else { diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index c07c5b3a6d..b7aa21798a 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -21,6 +21,11 @@ import { DisorderedArray } from "./utils/DisorderedArray"; export class Entity extends EngineObject { /** @internal */ static _tempComponentConstructors: ComponentConstructor[] = []; + + private static _isTransformType(type: ComponentConstructor): boolean { + return type === Transform || type.prototype instanceof Transform; + } + /** * @internal */ @@ -233,10 +238,22 @@ export class Entity extends EngineObject { constructor(engine: Engine, name?: string, ...components: ComponentConstructor[]) { super(engine); this.name = name ?? "Entity"; - for (let i = 0, n = components.length; i < n; i++) { - this.addComponent(components[i]); + let transformType: ComponentConstructor = Transform; + const n = components.length; + for (let i = n - 1; i >= 0; i--) { + const componentType = components[i]; + if (Entity._isTransformType(componentType)) { + transformType = componentType; + break; + } + } + this._transform = this.addComponent(transformType); + for (let i = 0; i < n; i++) { + const componentType = components[i]; + if (!Entity._isTransformType(componentType)) { + this.addComponent(componentType); + } } - !this._transform && this.addComponent(Transform); this._inverseWorldMatFlag = this.registerWorldChangeFlag(); } @@ -247,12 +264,16 @@ export class Entity extends EngineObject { * @returns The component which has been added */ addComponent(type: T, ...args: ComponentArguments): InstanceType { + const needReplaceTransform = Entity._isTransformType(type) && this._transform; + if (needReplaceTransform) + ComponentsDependencies._removeCheck(this, this._transform.constructor, type); ComponentsDependencies._addCheck(this, type); const component = new type(this, ...args) as InstanceType; - this._components.push(component); - - // @todo: temporary solution - if (component instanceof Transform) this._setTransform(component); + if (needReplaceTransform) { + this._replaceTransform(component); + } else { + this._components.push(component); + } component._setActive(true, ActiveChangeFlag.All); return component; } @@ -510,9 +531,13 @@ export class Entity extends EngineObject { * @internal */ _removeComponent(component: Component): void { - ComponentsDependencies._removeCheck(this, component.constructor as ComponentConstructor); const components = this._components; - components.splice(components.indexOf(component), 1); + const index = components.indexOf(component); + // A replaced Transform is detached from the component slot immediately but + // can still reach here later because object destruction may be deferred. + if (index < 0) return; + ComponentsDependencies._removeCheck(this, component.constructor as ComponentConstructor); + components.splice(index, 1); } /** @@ -743,15 +768,18 @@ export class Entity extends EngineObject { } } - private _setTransform(value: Transform): void { + private _replaceTransform(value: Transform): void { const previous = this._transform; - if (previous) { - value.position.copyFrom(previous.position); - value.rotationQuaternion.copyFrom(previous.rotationQuaternion); - value.scale.copyFrom(previous.scale); - previous.destroy(); - } + value.position.copyFrom(previous.position); + value.rotationQuaternion.copyFrom(previous.rotationQuaternion); + value.scale.copyFrom(previous.scale); + // Keep the unique Transform in the same component slot. Detach the old + // instance before destroy because destroy can be deferred during a frame. + const components = this._components; + const previousIndex = components.indexOf(previous); + components[previousIndex] = value; this._transform = value; + previous.destroy(); const children = this._children; for (let i = 0, n = children.length; i < n; i++) { children[i].transform?._parentChange(); diff --git a/tests/src/core/Transform.test.ts b/tests/src/core/Transform.test.ts index 038cb93eef..f96f29afda 100644 --- a/tests/src/core/Transform.test.ts +++ b/tests/src/core/Transform.test.ts @@ -1,4 +1,13 @@ -import { deepClone, Entity, Scene, Script, Transform } from "@galacean/engine-core"; +import { + deepClone, + dependentComponents, + DependentMode, + Entity, + MeshRenderer, + Scene, + Script, + Transform +} from "@galacean/engine-core"; import { Vector2, Vector3 } from "@galacean/engine-math"; import { WebGLEngine } from "@galacean/engine"; import { beforeAll, describe, expect, it } from "vitest"; @@ -112,10 +121,14 @@ describe("Transform test", function () { // Add component const preTransform0 = entity0.transform; + const meshRenderer = entity0.addComponent(MeshRenderer); + const transformIndex = entity0._components.indexOf(preTransform0); entity0.addComponent(SubClassOfTransform); expect(preTransform0.destroyed).to.equal(true); expect(entity0.transform instanceof Transform).to.equal(true); expect(entity0.transform instanceof SubClassOfTransform).to.equal(true); + expect(entity0._components[transformIndex]).to.equal(entity0.transform); + expect(entity0._components.indexOf(meshRenderer)).to.equal(1); expect(entity0.transform.position).to.deep.include({ x: 1, y: 2, z: 3 }); expect(entity0.transform.rotation.x).to.be.approximately(0, 1e-6); expect(entity0.transform.rotation.y).to.be.approximately(45, 1e-6); @@ -123,10 +136,14 @@ describe("Transform test", function () { expect(entity0.transform.scale).to.deep.include({ x: 1, y: 2, z: 3 }); const preTransform1 = entity1.transform; + const meshRenderer1 = entity1.addComponent(MeshRenderer); + const transformIndex1 = entity1._components.indexOf(preTransform1); entity1.addComponent(Transform); expect(preTransform1.destroyed).to.equal(true); expect(entity1.transform instanceof Transform).to.equal(true); expect(entity1.transform instanceof SubClassOfTransform).to.equal(false); + expect(entity1._components[transformIndex1]).to.equal(entity1.transform); + expect(entity1._components.indexOf(meshRenderer1)).to.equal(1); expect(entity1.transform.position).to.deep.include({ x: 4, y: 5, z: 6 }); expect(entity1.transform.rotation.x).to.be.approximately(0, 1e-6); expect(entity1.transform.rotation.y).to.be.approximately(90, 1e-6); @@ -134,6 +151,87 @@ describe("Transform test", function () { expect(entity1.transform.scale).to.deep.include({ x: 4, y: 5, z: 6 }); }); + it("creates the unique Transform before constructor components", () => { + const entityWithRenderer = new Entity(engine, "entity-with-renderer", MeshRenderer); + expect(entityWithRenderer._components[0]).to.equal(entityWithRenderer.transform); + expect(entityWithRenderer.getComponent(MeshRenderer)).not.to.equal(null); + + const entityWithLateTransform = new Entity( + engine, + "entity-with-late-transform", + MeshRenderer, + Transform, + SubClassOfTransform + ); + const transforms: Transform[] = []; + entityWithLateTransform.getComponents(Transform, transforms); + expect(transforms).to.deep.equal([entityWithLateTransform.transform]); + expect(entityWithLateTransform.transform).to.be.instanceOf(SubClassOfTransform); + expect(entityWithLateTransform._components[0]).to.equal(entityWithLateTransform.transform); + expect(entityWithLateTransform._components[1]).to.be.instanceOf(MeshRenderer); + + const clone = entityWithLateTransform.clone(); + expect(clone.transform).to.be.instanceOf(SubClassOfTransform); + expect(clone._components.map((component) => component.constructor)).to.deep.equal( + entityWithLateTransform._components.map((component) => component.constructor) + ); + }); + + it("keeps the Transform slot unique while destruction is deferred", () => { + const deferredEntity = new Entity(engine, "deferred-transform"); + const previous = deferredEntity.transform; + let replacement: SubClassOfTransform; + + engine._frameInProcess = true; + try { + replacement = deferredEntity.addComponent(SubClassOfTransform); + const transforms: Transform[] = []; + deferredEntity.getComponents(Transform, transforms); + expect(previous.pendingDestroy).to.equal(true); + expect(transforms).to.deep.equal([replacement]); + expect(deferredEntity._components[0]).to.equal(replacement); + } finally { + engine._frameInProcess = false; + previous.destroy(); + } + }); + + it("rolls back Transform replacement when a dependency prevents it", () => { + const dependentEntity = new Entity(engine, "dependent-transform", SubClassOfTransform); + const previous = dependentEntity.transform; + dependentEntity.addComponent(RequiresSubClassOfTransform); + + expect(() => dependentEntity.addComponent(Transform)).to.throw( + "Should remove RequiresSubClassOfTransform before remove SubClassOfTransform" + ); + const transforms: Transform[] = []; + dependentEntity.getComponents(Transform, transforms); + expect(dependentEntity.transform).to.equal(previous); + expect(transforms).to.deep.equal([previous]); + expect(dependentEntity._components[0]).to.equal(previous); + }); + + it("checks dependencies declared by a replacement Transform", () => { + const dependentEntity = new Entity(engine, "check-only-dependent-transform"); + const previous = dependentEntity.transform; + + expect(() => dependentEntity.addComponent(CheckOnlyDependentTransform)).to.throw( + "Should add MeshRenderer before adding CheckOnlyDependentTransform" + ); + expect(dependentEntity.transform).to.equal(previous); + expect(dependentEntity._components).to.deep.equal([previous]); + }); + + it("auto adds dependencies declared by a replacement Transform", () => { + const dependentEntity = new Entity(engine, "auto-add-dependent-transform"); + const replacement = dependentEntity.addComponent(AutoAddDependentTransform); + + expect(dependentEntity.transform).to.equal(replacement); + expect(dependentEntity._components[0]).to.equal(replacement); + expect(dependentEntity._components[1]).to.be.instanceOf(MeshRenderer); + expect(dependentEntity.getComponent(MeshRenderer)).not.to.equal(null); + }); + it("clone with worldMatrix listener should not produce stale parent cache after reparent", () => { class WorldMatrixListener extends Script { constructor(entity: Entity) { @@ -201,3 +299,12 @@ class SubClassOfTransform extends Transform { @deepClone size: Vector2 = new Vector2(); } + +@dependentComponents(SubClassOfTransform, DependentMode.CheckOnly) +class RequiresSubClassOfTransform extends Script {} + +@dependentComponents(MeshRenderer, DependentMode.CheckOnly) +class CheckOnlyDependentTransform extends Transform {} + +@dependentComponents(MeshRenderer, DependentMode.AutoAdd) +class AutoAddDependentTransform extends Transform {} diff --git a/tests/src/ui/UITransform.test.ts b/tests/src/ui/UITransform.test.ts index 534f0f3927..84ab5487d1 100644 --- a/tests/src/ui/UITransform.test.ts +++ b/tests/src/ui/UITransform.test.ts @@ -1,5 +1,5 @@ -import { WebGLEngine } from "@galacean/engine"; -import { HorizontalAlignmentMode, UICanvas, UITransform, VerticalAlignmentMode } from "@galacean/engine-ui"; +import { Entity, MeshRenderer, WebGLEngine } from "@galacean/engine"; +import { HorizontalAlignmentMode, Image, UICanvas, UITransform, VerticalAlignmentMode } from "@galacean/engine-ui"; import { describe, expect, it } from "vitest"; describe("UITransform", async () => { @@ -404,6 +404,23 @@ describe("UITransform", async () => { }); describe("clone", () => { + it("keeps component mapping after a renderer replaces Transform with UITransform", () => { + const original = new Entity(engine, "clone-transform-replacement"); + original.addComponent(MeshRenderer); + original.addComponent(Image); + + expect(original.transform).to.be.instanceOf(UITransform); + expect(original._components[0]).to.equal(original.transform); + + const cloned = original.clone(); + expect(cloned.transform).to.be.instanceOf(UITransform); + expect(cloned.getComponent(MeshRenderer)).not.to.equal(null); + expect(cloned.getComponent(Image)).not.to.equal(null); + expect(cloned._components.map((component) => component.constructor)).to.deep.equal( + original._components.map((component) => component.constructor) + ); + }); + it("clones basic properties correctly", () => { const parent = root.createChild("clone-parent"); parent.addComponent(UICanvas); From 6e86f4d74e4a0d968a8ef5aba5c98db0b4539622 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Thu, 23 Jul 2026 11:50:39 +0800 Subject: [PATCH 22/23] style(core): brace Transform replacement check --- packages/core/src/Entity.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index b7aa21798a..1bbabb92d7 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -265,8 +265,9 @@ export class Entity extends EngineObject { */ addComponent(type: T, ...args: ComponentArguments): InstanceType { const needReplaceTransform = Entity._isTransformType(type) && this._transform; - if (needReplaceTransform) + if (needReplaceTransform) { ComponentsDependencies._removeCheck(this, this._transform.constructor, type); + } ComponentsDependencies._addCheck(this, type); const component = new type(this, ...args) as InstanceType; if (needReplaceTransform) { From f041bdb7136f11f5857f312b17a622bde98a5c38 Mon Sep 17 00:00:00 2001 From: cptbtptpbcptdtptp Date: Thu, 23 Jul 2026 11:54:49 +0800 Subject: [PATCH 23/23] chore: release v0.0.0-experimental-2.0-migrate.3 --- e2e/package.json | 2 +- examples/package.json | 2 +- package.json | 2 +- packages/core/package.json | 2 +- packages/design/package.json | 2 +- packages/galacean/package.json | 2 +- packages/loader/package.json | 2 +- packages/math/package.json | 2 +- packages/physics-physx/package.json | 2 +- packages/rhi-webgl/package.json | 2 +- packages/shader-compiler/package.json | 2 +- packages/shader/package.json | 2 +- packages/spine-core-3.8/package.json | 2 +- packages/spine-core-4.2/package.json | 2 +- packages/spine/package.json | 2 +- packages/ui/package.json | 2 +- packages/xr-webxr/package.json | 2 +- packages/xr/package.json | 2 +- tests/package.json | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) diff --git a/e2e/package.json b/e2e/package.json index adf4447256..5e9ac7cd67 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-e2e", "private": true, - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "license": "MIT", "scripts": { "case": "vite serve .dev --config .dev/vite.config.js", diff --git a/examples/package.json b/examples/package.json index 0d42bfdd87..82f8e21566 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-examples", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "private": true, "license": "MIT", "main": "dist/main.js", diff --git a/package.json b/package.json index 98a619601b..9759985992 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-root", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "packageManager": "pnpm@9.3.0", "private": true, "engines": { diff --git a/packages/core/package.json b/packages/core/package.json index 7bddc1b55c..dcac8e9d09 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-core", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/design/package.json b/packages/design/package.json index dc65edce3e..647c14da4f 100644 --- a/packages/design/package.json +++ b/packages/design/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-design", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/galacean/package.json b/packages/galacean/package.json index fab0b14130..581e6ad043 100644 --- a/packages/galacean/package.json +++ b/packages/galacean/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/loader/package.json b/packages/loader/package.json index 656c59850f..bcec44a97a 100644 --- a/packages/loader/package.json +++ b/packages/loader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-loader", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/math/package.json b/packages/math/package.json index 58a54c4df0..5d56418130 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-math", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/physics-physx/package.json b/packages/physics-physx/package.json index bc93202c74..ddc062e737 100644 --- a/packages/physics-physx/package.json +++ b/packages/physics-physx/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-physics-physx", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/rhi-webgl/package.json b/packages/rhi-webgl/package.json index 4bbee42c57..b7fe25511d 100644 --- a/packages/rhi-webgl/package.json +++ b/packages/rhi-webgl/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-rhi-webgl", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "repository": { "url": "https://github.com/galacean/engine.git" }, diff --git a/packages/shader-compiler/package.json b/packages/shader-compiler/package.json index eef0d5873f..6a1fcb4584 100644 --- a/packages/shader-compiler/package.json +++ b/packages/shader-compiler/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader-compiler", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/shader/package.json b/packages/shader/package.json index 086ff2d0b7..4cb8f305c0 100644 --- a/packages/shader/package.json +++ b/packages/shader/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-shader", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/spine-core-3.8/package.json b/packages/spine-core-3.8/package.json index b579faad7b..412899d422 100644 --- a/packages/spine-core-3.8/package.json +++ b/packages/spine-core-3.8/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-spine-core-3.8", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/spine-core-4.2/package.json b/packages/spine-core-4.2/package.json index fab1e28696..3716a274f0 100644 --- a/packages/spine-core-4.2/package.json +++ b/packages/spine-core-4.2/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-spine-core-4.2", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/spine/package.json b/packages/spine/package.json index baaf9a4c01..8eaa828db8 100644 --- a/packages/spine/package.json +++ b/packages/spine/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-spine", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/ui/package.json b/packages/ui/package.json index 19cefa60c3..f7ef671633 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-ui", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr-webxr/package.json b/packages/xr-webxr/package.json index 4d1f9aa70a..0f4af9d9f9 100644 --- a/packages/xr-webxr/package.json +++ b/packages/xr-webxr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr-webxr", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/packages/xr/package.json b/packages/xr/package.json index b441c2a51f..6cafa4bbbf 100644 --- a/packages/xr/package.json +++ b/packages/xr/package.json @@ -1,6 +1,6 @@ { "name": "@galacean/engine-xr", - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" diff --git a/tests/package.json b/tests/package.json index 6d1c0a0151..123079cf1a 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,7 +1,7 @@ { "name": "@galacean/engine-tests", "private": true, - "version": "0.0.0-experimental-2.0-migrate.2", + "version": "0.0.0-experimental-2.0-migrate.3", "license": "MIT", "main": "dist/main.js", "module": "dist/module.js",