From 73a89738668b40488afcff6c210253569b2209f9 Mon Sep 17 00:00:00 2001 From: Will Eastcott Date: Thu, 11 Jun 2026 15:50:26 +0100 Subject: [PATCH 1/5] refactor(particlesystem): align ParticleSystemComponent with CameraComponent architecture Move all ~60 properties from ParticleSystemComponentData onto the component as private fields with real getters/setters, inlining the side effects that previously lived in set_* event handlers. The data class now holds only 'enabled', the system schema shrinks to ['enabled'], and the system applies incoming data through the public setters and clones via an explicit property list. Behavior is preserved, including the legacy mesh->meshAsset id remap, vec3/curve/curveset deserialization, the dual resetTime+resetMaterial side effect of the loop property, and skipping null values when cloning. Co-Authored-By: Claude Fable 5 --- .../components/particle-system/component.js | 1198 ++++++++++------- .../components/particle-system/data.js | 166 --- .../components/particle-system/system.js | 197 +-- .../particlesystem/component.test.mjs | 181 +++ 4 files changed, 940 insertions(+), 802 deletions(-) diff --git a/src/framework/components/particle-system/component.js b/src/framework/components/particle-system/component.js index 7a40ce03cb0..883781e3b39 100644 --- a/src/framework/components/particle-system/component.js +++ b/src/framework/components/particle-system/component.js @@ -1,4 +1,5 @@ -import { LAYERID_DEPTH } from '../../../scene/constants.js'; +import { Vec3 } from '../../../core/math/vec3.js'; +import { BLEND_NORMAL, EMITTERSHAPE_BOX, LAYERID_DEPTH, LAYERID_WORLD, PARTICLEORIENTATION_SCREEN } from '../../../scene/constants.js'; import { Mesh } from '../../../scene/mesh.js'; import { ParticleEmitter } from '../../../scene/particle-system/particle-emitter.js'; import { Asset } from '../../asset/asset.js'; @@ -7,85 +8,78 @@ import { Component } from '../component.js'; /** * @import { CurveSet } from '../../../core/math/curve-set.js' * @import { Curve } from '../../../core/math/curve.js' - * @import { Entity } from '../../entity.js' * @import { EventHandle } from '../../../core/event-handle.js' - * @import { ParticleSystemComponentSystem } from './system.js' * @import { Texture } from '../../../platform/graphics/texture.js' - * @import { Vec3 } from '../../../core/math/vec3.js' */ -// properties that do not need rebuilding the particle system -const SIMPLE_PROPERTIES = [ - 'emitterExtents', - 'emitterRadius', - 'emitterExtentsInner', - 'emitterRadiusInner', - 'loop', - 'initialVelocity', - 'animSpeed', - 'normalMap', - 'particleNormal' -]; +const ASSET_PROPERTIES = ['colorMapAsset', 'normalMapAsset', 'meshAsset', 'renderAsset']; -// properties that need rebuilding the particle system -const COMPLEX_PROPERTIES = [ +// properties that the component can be initialized with, in the order they are applied +const _properties = [ + 'autoPlay', 'numParticles', 'lifetime', 'rate', 'rate2', 'startAngle', 'startAngle2', + 'loop', + 'preWarm', 'lighting', 'halfLambert', 'intensity', - 'wrap', - 'wrapBounds', 'depthWrite', 'noFog', + 'depthSoftening', 'sort', + 'blendType', 'stretch', 'alignToMotion', - 'preWarm', 'emitterShape', - 'animTilesX', - 'animTilesY', - 'animStartFrame', - 'animNumFrames', - 'animNumAnimations', - 'animIndex', - 'randomizeAnimIndex', - 'animLoop', - 'colorMap', + 'emitterExtents', + 'emitterExtentsInner', + 'emitterRadius', + 'emitterRadiusInner', + 'initialVelocity', + 'wrap', + 'wrapBounds', 'localSpace', 'screenSpace', - 'orientation' -]; - -const GRAPH_PROPERTIES = [ + 'colorMapAsset', + 'normalMapAsset', + 'mesh', + 'meshAsset', + 'renderAsset', + 'orientation', + 'particleNormal', + 'localVelocityGraph', + 'localVelocityGraph2', + 'velocityGraph', + 'velocityGraph2', + 'rotationSpeedGraph', + 'rotationSpeedGraph2', + 'radialSpeedGraph', + 'radialSpeedGraph2', 'scaleGraph', 'scaleGraph2', - 'colorGraph', 'colorGraph2', - 'alphaGraph', 'alphaGraph2', - - 'velocityGraph', - 'velocityGraph2', - - 'localVelocityGraph', - 'localVelocityGraph2', - - 'rotationSpeedGraph', - 'rotationSpeedGraph2', - - 'radialSpeedGraph', - 'radialSpeedGraph2' + 'colorMap', + 'normalMap', + 'animTilesX', + 'animTilesY', + 'animStartFrame', + 'animNumFrames', + 'animNumAnimations', + 'animIndex', + 'randomizeAnimIndex', + 'animSpeed', + 'animLoop', + 'layers' ]; -const ASSET_PROPERTIES = ['colorMapAsset', 'normalMapAsset', 'meshAsset', 'renderAsset']; - let depthLayer; /** @@ -138,12 +132,24 @@ let depthLayer; * @category Graphics */ class ParticleSystemComponent extends Component { + /** + * The particle emitter that performs the simulation. Only set while the component is or has + * been enabled and the platform supports particle systems. + * + * @type {ParticleEmitter|null} + * @ignore + */ + emitter = null; + /** @private */ _requestedDepth = false; /** @private */ _drawOrder = 0; + /** @private */ + _paused = false; + /** * @type {EventHandle|null} * @private @@ -168,55 +174,263 @@ class ParticleSystemComponent extends Component { */ _evtSetMeshes = null; + /** @private */ + _autoPlay = true; + + /** @private */ + _numParticles = 1; + + /** @private */ + _lifetime = 50; + + /** @private */ + _rate = 1; + /** - * Create a new ParticleSystemComponent. - * - * @param {ParticleSystemComponentSystem} system - The ComponentSystem that created this Component. - * @param {Entity} entity - The Entity this Component is attached to. + * @type {number|null} + * @private */ - constructor(system, entity) { - super(system, entity); + _rate2 = null; - this.on('set_colorMapAsset', this.onSetColorMapAsset, this); - this.on('set_normalMapAsset', this.onSetNormalMapAsset, this); - this.on('set_meshAsset', this.onSetMeshAsset, this); - this.on('set_mesh', this.onSetMesh, this); - this.on('set_renderAsset', this.onSetRenderAsset, this); - this.on('set_loop', this.onSetLoop, this); - this.on('set_blendType', this.onSetBlendType, this); - this.on('set_depthSoftening', this.onSetDepthSoftening, this); - this.on('set_layers', this.onSetLayers, this); + /** @private */ + _startAngle = 0; - SIMPLE_PROPERTIES.forEach((prop) => { - this.on(`set_${prop}`, this.onSetSimpleProperty, this); - }); + /** + * @type {number|null} + * @private + */ + _startAngle2 = null; - COMPLEX_PROPERTIES.forEach((prop) => { - this.on(`set_${prop}`, this.onSetComplexProperty, this); - }); + /** @private */ + _loop = true; - GRAPH_PROPERTIES.forEach((prop) => { - this.on(`set_${prop}`, this.onSetGraphProperty, this); - }); - } + /** @private */ + _preWarm = false; + + /** @private */ + _lighting = false; + + /** @private */ + _halfLambert = false; + + /** @private */ + _intensity = 1; + + /** @private */ + _depthWrite = false; + + /** @private */ + _noFog = false; + + /** @private */ + _depthSoftening = 0; + + /** @private */ + _sort = 0; + + /** @private */ + _blendType = BLEND_NORMAL; + + /** @private */ + _stretch = 0.0; + + /** @private */ + _alignToMotion = false; + + /** @private */ + _emitterShape = EMITTERSHAPE_BOX; + + /** @private */ + _emitterExtents = new Vec3(); + + /** @private */ + _emitterExtentsInner = new Vec3(); + + /** @private */ + _emitterRadius = 0; + + /** @private */ + _emitterRadiusInner = 0; + + /** @private */ + _initialVelocity = 0; + + /** @private */ + _wrap = false; + + /** @private */ + _wrapBounds = new Vec3(); + + /** @private */ + _localSpace = false; + + /** @private */ + _screenSpace = false; /** - * Sets the enabled state of the component. - * - * @type {boolean} + * @type {number|null} + * @private */ - set enabled(arg) { - this._setValue('enabled', arg); - } + _colorMapAsset = null; /** - * Gets the enabled state of the component. - * - * @type {boolean} + * @type {number|null} + * @private */ - get enabled() { - return this.data.enabled; - } + _normalMapAsset = null; + + /** + * @type {Mesh|null} + * @private + */ + _mesh = null; + + /** + * @type {number|null} + * @private + */ + _meshAsset = null; + + /** + * @type {number|null} + * @private + */ + _renderAsset = null; + + /** @private */ + _orientation = PARTICLEORIENTATION_SCREEN; + + /** @private */ + _particleNormal = new Vec3(0, 1, 0); + + /** + * @type {CurveSet|null} + * @private + */ + _localVelocityGraph = null; + + /** + * @type {CurveSet|null} + * @private + */ + _localVelocityGraph2 = null; + + /** + * @type {CurveSet|null} + * @private + */ + _velocityGraph = null; + + /** + * @type {CurveSet|null} + * @private + */ + _velocityGraph2 = null; + + /** + * @type {Curve|null} + * @private + */ + _rotationSpeedGraph = null; + + /** + * @type {Curve|null} + * @private + */ + _rotationSpeedGraph2 = null; + + /** + * @type {Curve|null} + * @private + */ + _radialSpeedGraph = null; + + /** + * @type {Curve|null} + * @private + */ + _radialSpeedGraph2 = null; + + /** + * @type {Curve|null} + * @private + */ + _scaleGraph = null; + + /** + * @type {Curve|null} + * @private + */ + _scaleGraph2 = null; + + /** + * @type {CurveSet|null} + * @private + */ + _colorGraph = null; + + /** + * @type {CurveSet|null} + * @private + */ + _colorGraph2 = null; + + /** + * @type {Curve|null} + * @private + */ + _alphaGraph = null; + + /** + * @type {Curve|null} + * @private + */ + _alphaGraph2 = null; + + /** + * @type {Texture|null} + * @private + */ + _colorMap = null; + + /** + * @type {Texture|null} + * @private + */ + _normalMap = null; + + /** @private */ + _animTilesX = 1; + + /** @private */ + _animTilesY = 1; + + /** @private */ + _animStartFrame = 0; + + /** @private */ + _animNumFrames = 1; + + /** @private */ + _animNumAnimations = 1; + + /** @private */ + _animIndex = 0; + + /** @private */ + _randomizeAnimIndex = false; + + /** @private */ + _animSpeed = 1; + + /** @private */ + _animLoop = true; + + /** + * @type {number[]} + * @private + */ + _layers = [LAYERID_WORLD]; /** * Sets whether the particle system plays automatically on creation. If set to false, it is @@ -225,7 +439,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set autoPlay(arg) { - this._setValue('autoPlay', arg); + this._autoPlay = arg; } /** @@ -234,7 +448,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get autoPlay() { - return this.data.autoPlay; + return this._autoPlay; } /** @@ -243,7 +457,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set numParticles(arg) { - this._setValue('numParticles', arg); + this._setComplexProperty('numParticles', arg); } /** @@ -252,7 +466,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get numParticles() { - return this.data.numParticles; + return this._numParticles; } /** @@ -261,7 +475,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set lifetime(arg) { - this._setValue('lifetime', arg); + this._setComplexProperty('lifetime', arg); } /** @@ -270,7 +484,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get lifetime() { - return this.data.lifetime; + return this._lifetime; } /** @@ -279,7 +493,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set rate(arg) { - this._setValue('rate', arg); + this._setComplexProperty('rate', arg); } /** @@ -288,7 +502,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get rate() { - return this.data.rate; + return this._rate; } /** @@ -297,7 +511,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set rate2(arg) { - this._setValue('rate2', arg); + this._setComplexProperty('rate2', arg); } /** @@ -306,7 +520,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get rate2() { - return this.data.rate2; + return this._rate2; } /** @@ -315,7 +529,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set startAngle(arg) { - this._setValue('startAngle', arg); + this._setComplexProperty('startAngle', arg); } /** @@ -324,7 +538,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get startAngle() { - return this.data.startAngle; + return this._startAngle; } /** @@ -333,7 +547,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set startAngle2(arg) { - this._setValue('startAngle2', arg); + this._setComplexProperty('startAngle2', arg); } /** @@ -342,7 +556,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get startAngle2() { - return this.data.startAngle2; + return this._startAngle2; } /** @@ -351,7 +565,12 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set loop(arg) { - this._setValue('loop', arg); + this._loop = arg; + if (this.emitter) { + this.emitter.loop = arg; + this.emitter.resetTime(); + this.emitter.resetMaterial(); + } } /** @@ -360,7 +579,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get loop() { - return this.data.loop; + return this._loop; } /** @@ -370,7 +589,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set preWarm(arg) { - this._setValue('preWarm', arg); + this._setComplexProperty('preWarm', arg); } /** @@ -380,7 +599,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get preWarm() { - return this.data.preWarm; + return this._preWarm; } /** @@ -389,7 +608,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set lighting(arg) { - this._setValue('lighting', arg); + this._setComplexProperty('lighting', arg); } /** @@ -398,7 +617,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get lighting() { - return this.data.lighting; + return this._lighting; } /** @@ -409,7 +628,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set halfLambert(arg) { - this._setValue('halfLambert', arg); + this._setComplexProperty('halfLambert', arg); } /** @@ -418,7 +637,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get halfLambert() { - return this.data.halfLambert; + return this._halfLambert; } /** @@ -427,7 +646,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set intensity(arg) { - this._setValue('intensity', arg); + this._setComplexProperty('intensity', arg); } /** @@ -436,7 +655,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get intensity() { - return this.data.intensity; + return this._intensity; } /** @@ -447,7 +666,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set depthWrite(arg) { - this._setValue('depthWrite', arg); + this._setComplexProperty('depthWrite', arg); } /** @@ -456,7 +675,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get depthWrite() { - return this.data.depthWrite; + return this._depthWrite; } /** @@ -465,7 +684,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set noFog(arg) { - this._setValue('noFog', arg); + this._setComplexProperty('noFog', arg); } /** @@ -474,7 +693,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get noFog() { - return this.data.noFog; + return this._noFog; } /** @@ -486,7 +705,21 @@ class ParticleSystemComponent extends Component { * @type {number} */ set depthSoftening(arg) { - this._setValue('depthSoftening', arg); + const oldValue = this._depthSoftening; + if (oldValue !== arg) { + this._depthSoftening = arg; + if (arg) { + if (this.enabled && this.entity.enabled) this._requestDepth(); + } else { + if (this.enabled && this.entity.enabled) this._releaseDepth(); + } + if (this.emitter) { + this.emitter.depthSoftening = arg; + this.reset(); + this.emitter.resetMaterial(); + this.rebuild(); + } + } } /** @@ -495,7 +728,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get depthSoftening() { - return this.data.depthSoftening; + return this._depthSoftening; } /** @@ -510,7 +743,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set sort(arg) { - this._setValue('sort', arg); + this._setComplexProperty('sort', arg); } /** @@ -519,7 +752,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get sort() { - return this.data.sort; + return this._sort; } /** @@ -547,7 +780,13 @@ class ParticleSystemComponent extends Component { * @type {number} */ set blendType(arg) { - this._setValue('blendType', arg); + this._blendType = arg; + if (this.emitter) { + this.emitter.blendType = arg; + this.emitter.material.blendType = arg; + this.emitter.resetMaterial(); + this.rebuild(); + } } /** @@ -556,7 +795,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get blendType() { - return this.data.blendType; + return this._blendType; } /** @@ -567,7 +806,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set stretch(arg) { - this._setValue('stretch', arg); + this._setComplexProperty('stretch', arg); } /** @@ -576,7 +815,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get stretch() { - return this.data.stretch; + return this._stretch; } /** @@ -585,7 +824,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set alignToMotion(arg) { - this._setValue('alignToMotion', arg); + this._setComplexProperty('alignToMotion', arg); } /** @@ -594,7 +833,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get alignToMotion() { - return this.data.alignToMotion; + return this._alignToMotion; } /** @@ -609,7 +848,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set emitterShape(arg) { - this._setValue('emitterShape', arg); + this._setComplexProperty('emitterShape', arg); } /** @@ -618,7 +857,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get emitterShape() { - return this.data.emitterShape; + return this._emitterShape; } /** @@ -628,7 +867,7 @@ class ParticleSystemComponent extends Component { * @type {Vec3} */ set emitterExtents(arg) { - this._setValue('emitterExtents', arg); + this._setSimpleProperty('emitterExtents', arg); } /** @@ -638,7 +877,7 @@ class ParticleSystemComponent extends Component { * @type {Vec3} */ get emitterExtents() { - return this.data.emitterExtents; + return this._emitterExtents; } /** @@ -649,7 +888,7 @@ class ParticleSystemComponent extends Component { * @type {Vec3} */ set emitterExtentsInner(arg) { - this._setValue('emitterExtentsInner', arg); + this._setSimpleProperty('emitterExtentsInner', arg); } /** @@ -659,7 +898,7 @@ class ParticleSystemComponent extends Component { * @type {Vec3} */ get emitterExtentsInner() { - return this.data.emitterExtentsInner; + return this._emitterExtentsInner; } /** @@ -669,7 +908,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set emitterRadius(arg) { - this._setValue('emitterRadius', arg); + this._setSimpleProperty('emitterRadius', arg); } /** @@ -678,7 +917,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get emitterRadius() { - return this.data.emitterRadius; + return this._emitterRadius; } /** @@ -688,7 +927,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set emitterRadiusInner(arg) { - this._setValue('emitterRadiusInner', arg); + this._setSimpleProperty('emitterRadiusInner', arg); } /** @@ -697,7 +936,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get emitterRadiusInner() { - return this.data.emitterRadiusInner; + return this._emitterRadiusInner; } /** @@ -706,7 +945,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set initialVelocity(arg) { - this._setValue('initialVelocity', arg); + this._setSimpleProperty('initialVelocity', arg); } /** @@ -715,7 +954,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get initialVelocity() { - return this.data.initialVelocity; + return this._initialVelocity; } /** @@ -724,7 +963,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set wrap(arg) { - this._setValue('wrap', arg); + this._setComplexProperty('wrap', arg); } /** @@ -733,7 +972,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get wrap() { - return this.data.wrap; + return this._wrap; } /** @@ -744,7 +983,7 @@ class ParticleSystemComponent extends Component { * @type {Vec3} */ set wrapBounds(arg) { - this._setValue('wrapBounds', arg); + this._setComplexProperty('wrapBounds', arg); } /** @@ -753,7 +992,7 @@ class ParticleSystemComponent extends Component { * @type {Vec3} */ get wrapBounds() { - return this.data.wrapBounds; + return this._wrapBounds; } /** @@ -762,7 +1001,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set localSpace(arg) { - this._setValue('localSpace', arg); + this._setComplexProperty('localSpace', arg); } /** @@ -771,7 +1010,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get localSpace() { - return this.data.localSpace; + return this._localSpace; } /** @@ -784,7 +1023,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set screenSpace(arg) { - this._setValue('screenSpace', arg); + this._setComplexProperty('screenSpace', arg); } /** @@ -793,7 +1032,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get screenSpace() { - return this.data.screenSpace; + return this._screenSpace; } /** @@ -802,7 +1041,32 @@ class ParticleSystemComponent extends Component { * @type {Asset} */ set colorMapAsset(arg) { - this._setValue('colorMapAsset', arg); + const assets = this.system.app.assets; + + if (this._colorMapAsset) { + const asset = assets.get(this._colorMapAsset); + if (asset) { + this._unbindColorMapAsset(asset); + } + } + + if (arg instanceof Asset) { + arg = arg.id; + } + this._colorMapAsset = arg; + + if (arg) { + const asset = assets.get(arg); + if (asset) { + this._bindColorMapAsset(asset); + } else { + assets.once(`add:${arg}`, (asset) => { + this._bindColorMapAsset(asset); + }); + } + } else { + this.colorMap = null; + } } /** @@ -811,7 +1075,7 @@ class ParticleSystemComponent extends Component { * @type {Asset} */ get colorMapAsset() { - return this.data.colorMapAsset; + return this._colorMapAsset; } /** @@ -820,7 +1084,32 @@ class ParticleSystemComponent extends Component { * @type {Asset} */ set normalMapAsset(arg) { - this._setValue('normalMapAsset', arg); + const assets = this.system.app.assets; + + if (this._normalMapAsset) { + const asset = assets.get(this._normalMapAsset); + if (asset) { + this._unbindNormalMapAsset(asset); + } + } + + if (arg instanceof Asset) { + arg = arg.id; + } + this._normalMapAsset = arg; + + if (arg) { + const asset = assets.get(arg); + if (asset) { + this._bindNormalMapAsset(asset); + } else { + assets.once(`add:${arg}`, (asset) => { + this._bindNormalMapAsset(asset); + }); + } + } else { + this.normalMap = null; + } } /** @@ -829,7 +1118,7 @@ class ParticleSystemComponent extends Component { * @type {Asset} */ get normalMapAsset() { - return this.data.normalMapAsset; + return this._normalMapAsset; } /** @@ -839,7 +1128,13 @@ class ParticleSystemComponent extends Component { * @type {Mesh} */ set mesh(arg) { - this._setValue('mesh', arg); + // if the value being set is null, an asset or an asset id, then assume we are + // setting the mesh asset, which will in turn update the mesh + if (!arg || arg instanceof Asset || typeof arg === 'number') { + this.meshAsset = arg; + } else { + this._onMeshChanged(arg); + } } /** @@ -848,7 +1143,7 @@ class ParticleSystemComponent extends Component { * @type {Mesh} */ get mesh() { - return this.data.mesh; + return this._mesh; } /** @@ -857,7 +1152,28 @@ class ParticleSystemComponent extends Component { * @type {Asset} */ set meshAsset(arg) { - this._setValue('meshAsset', arg); + const assets = this.system.app.assets; + + if (this._meshAsset) { + const asset = assets.get(this._meshAsset); + if (asset) { + this._unbindMeshAsset(asset); + } + } + + if (arg instanceof Asset) { + arg = arg.id; + } + this._meshAsset = arg; + + if (arg) { + const asset = assets.get(arg); + if (asset) { + this._bindMeshAsset(asset); + } + } else { + this._onMeshChanged(null); + } } /** @@ -866,7 +1182,7 @@ class ParticleSystemComponent extends Component { * @type {Asset} */ get meshAsset() { - return this.data.meshAsset; + return this._meshAsset; } /** @@ -875,7 +1191,28 @@ class ParticleSystemComponent extends Component { * @type {Asset} */ set renderAsset(arg) { - this._setValue('renderAsset', arg); + const assets = this.system.app.assets; + + if (this._renderAsset) { + const asset = assets.get(this._renderAsset); + if (asset) { + this._unbindRenderAsset(asset); + } + } + + if (arg instanceof Asset) { + arg = arg.id; + } + this._renderAsset = arg; + + if (arg) { + const asset = assets.get(arg); + if (asset) { + this._bindRenderAsset(asset); + } + } else { + this._onRenderChanged(null); + } } /** @@ -884,7 +1221,7 @@ class ParticleSystemComponent extends Component { * @type {Asset} */ get renderAsset() { - return this.data.renderAsset; + return this._renderAsset; } /** @@ -899,7 +1236,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set orientation(arg) { - this._setValue('orientation', arg); + this._setComplexProperty('orientation', arg); } /** @@ -908,7 +1245,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get orientation() { - return this.data.orientation; + return this._orientation; } /** @@ -918,7 +1255,7 @@ class ParticleSystemComponent extends Component { * @type {Vec3} */ set particleNormal(arg) { - this._setValue('particleNormal', arg); + this._setSimpleProperty('particleNormal', arg); } /** @@ -927,7 +1264,7 @@ class ParticleSystemComponent extends Component { * @type {Vec3} */ get particleNormal() { - return this.data.particleNormal; + return this._particleNormal; } /** @@ -936,7 +1273,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ set localVelocityGraph(arg) { - this._setValue('localVelocityGraph', arg); + this._setGraphProperty('localVelocityGraph', arg); } /** @@ -945,7 +1282,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ get localVelocityGraph() { - return this.data.localVelocityGraph; + return this._localVelocityGraph; } /** @@ -955,7 +1292,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ set localVelocityGraph2(arg) { - this._setValue('localVelocityGraph2', arg); + this._setGraphProperty('localVelocityGraph2', arg); } /** @@ -964,7 +1301,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ get localVelocityGraph2() { - return this.data.localVelocityGraph2; + return this._localVelocityGraph2; } /** @@ -973,7 +1310,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ set velocityGraph(arg) { - this._setValue('velocityGraph', arg); + this._setGraphProperty('velocityGraph', arg); } /** @@ -982,7 +1319,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ get velocityGraph() { - return this.data.velocityGraph; + return this._velocityGraph; } /** @@ -992,7 +1329,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ set velocityGraph2(arg) { - this._setValue('velocityGraph2', arg); + this._setGraphProperty('velocityGraph2', arg); } /** @@ -1001,7 +1338,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ get velocityGraph2() { - return this.data.velocityGraph2; + return this._velocityGraph2; } /** @@ -1010,7 +1347,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ set rotationSpeedGraph(arg) { - this._setValue('rotationSpeedGraph', arg); + this._setGraphProperty('rotationSpeedGraph', arg); } /** @@ -1019,7 +1356,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ get rotationSpeedGraph() { - return this.data.rotationSpeedGraph; + return this._rotationSpeedGraph; } /** @@ -1029,7 +1366,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ set rotationSpeedGraph2(arg) { - this._setValue('rotationSpeedGraph2', arg); + this._setGraphProperty('rotationSpeedGraph2', arg); } /** @@ -1038,7 +1375,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ get rotationSpeedGraph2() { - return this.data.rotationSpeedGraph2; + return this._rotationSpeedGraph2; } /** @@ -1047,7 +1384,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ set radialSpeedGraph(arg) { - this._setValue('radialSpeedGraph', arg); + this._setGraphProperty('radialSpeedGraph', arg); } /** @@ -1056,7 +1393,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ get radialSpeedGraph() { - return this.data.radialSpeedGraph; + return this._radialSpeedGraph; } /** @@ -1067,7 +1404,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ set radialSpeedGraph2(arg) { - this._setValue('radialSpeedGraph2', arg); + this._setGraphProperty('radialSpeedGraph2', arg); } /** @@ -1076,7 +1413,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ get radialSpeedGraph2() { - return this.data.radialSpeedGraph2; + return this._radialSpeedGraph2; } /** @@ -1085,7 +1422,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ set scaleGraph(arg) { - this._setValue('scaleGraph', arg); + this._setGraphProperty('scaleGraph', arg); } /** @@ -1094,7 +1431,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ get scaleGraph() { - return this.data.scaleGraph; + return this._scaleGraph; } /** @@ -1104,7 +1441,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ set scaleGraph2(arg) { - this._setValue('scaleGraph2', arg); + this._setGraphProperty('scaleGraph2', arg); } /** @@ -1113,7 +1450,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ get scaleGraph2() { - return this.data.scaleGraph2; + return this._scaleGraph2; } /** @@ -1122,7 +1459,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ set colorGraph(arg) { - this._setValue('colorGraph', arg); + this._setGraphProperty('colorGraph', arg); } /** @@ -1131,7 +1468,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ get colorGraph() { - return this.data.colorGraph; + return this._colorGraph; } /** @@ -1141,7 +1478,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ set colorGraph2(arg) { - this._setValue('colorGraph2', arg); + this._setGraphProperty('colorGraph2', arg); } /** @@ -1150,7 +1487,7 @@ class ParticleSystemComponent extends Component { * @type {CurveSet} */ get colorGraph2() { - return this.data.colorGraph2; + return this._colorGraph2; } /** @@ -1159,7 +1496,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ set alphaGraph(arg) { - this._setValue('alphaGraph', arg); + this._setGraphProperty('alphaGraph', arg); } /** @@ -1168,7 +1505,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ get alphaGraph() { - return this.data.alphaGraph; + return this._alphaGraph; } /** @@ -1178,7 +1515,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ set alphaGraph2(arg) { - this._setValue('alphaGraph2', arg); + this._setGraphProperty('alphaGraph2', arg); } /** @@ -1187,7 +1524,7 @@ class ParticleSystemComponent extends Component { * @type {Curve} */ get alphaGraph2() { - return this.data.alphaGraph2; + return this._alphaGraph2; } /** @@ -1197,7 +1534,7 @@ class ParticleSystemComponent extends Component { * @type {Texture} */ set colorMap(arg) { - this._setValue('colorMap', arg); + this._setComplexProperty('colorMap', arg); } /** @@ -1206,7 +1543,7 @@ class ParticleSystemComponent extends Component { * @type {Texture} */ get colorMap() { - return this.data.colorMap; + return this._colorMap; } /** @@ -1216,7 +1553,7 @@ class ParticleSystemComponent extends Component { * @type {Texture} */ set normalMap(arg) { - this._setValue('normalMap', arg); + this._setSimpleProperty('normalMap', arg); } /** @@ -1225,7 +1562,7 @@ class ParticleSystemComponent extends Component { * @type {Texture} */ get normalMap() { - return this.data.normalMap; + return this._normalMap; } /** @@ -1234,7 +1571,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set animTilesX(arg) { - this._setValue('animTilesX', arg); + this._setComplexProperty('animTilesX', arg); } /** @@ -1243,7 +1580,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get animTilesX() { - return this.data.animTilesX; + return this._animTilesX; } /** @@ -1252,7 +1589,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set animTilesY(arg) { - this._setValue('animTilesY', arg); + this._setComplexProperty('animTilesY', arg); } /** @@ -1261,7 +1598,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get animTilesY() { - return this.data.animTilesY; + return this._animTilesY; } /** @@ -1271,7 +1608,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set animStartFrame(arg) { - this._setValue('animStartFrame', arg); + this._setComplexProperty('animStartFrame', arg); } /** @@ -1280,7 +1617,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get animStartFrame() { - return this.data.animStartFrame; + return this._animStartFrame; } /** @@ -1291,7 +1628,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set animNumFrames(arg) { - this._setValue('animNumFrames', arg); + this._setComplexProperty('animNumFrames', arg); } /** @@ -1300,7 +1637,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get animNumFrames() { - return this.data.animNumFrames; + return this._animNumFrames; } /** @@ -1311,7 +1648,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set animNumAnimations(arg) { - this._setValue('animNumAnimations', arg); + this._setComplexProperty('animNumAnimations', arg); } /** @@ -1320,7 +1657,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get animNumAnimations() { - return this.data.animNumAnimations; + return this._animNumAnimations; } /** @@ -1330,7 +1667,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set animIndex(arg) { - this._setValue('animIndex', arg); + this._setComplexProperty('animIndex', arg); } /** @@ -1339,7 +1676,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get animIndex() { - return this.data.animIndex; + return this._animIndex; } /** @@ -1349,7 +1686,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set randomizeAnimIndex(arg) { - this._setValue('randomizeAnimIndex', arg); + this._setComplexProperty('randomizeAnimIndex', arg); } /** @@ -1359,7 +1696,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get randomizeAnimIndex() { - return this.data.randomizeAnimIndex; + return this._randomizeAnimIndex; } /** @@ -1369,7 +1706,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ set animSpeed(arg) { - this._setValue('animSpeed', arg); + this._setSimpleProperty('animSpeed', arg); } /** @@ -1378,7 +1715,7 @@ class ParticleSystemComponent extends Component { * @type {number} */ get animSpeed() { - return this.data.animSpeed; + return this._animSpeed; } /** @@ -1387,7 +1724,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ set animLoop(arg) { - this._setValue('animLoop', arg); + this._setComplexProperty('animLoop', arg); } /** @@ -1396,7 +1733,7 @@ class ParticleSystemComponent extends Component { * @type {boolean} */ get animLoop() { - return this.data.animLoop; + return this._animLoop; } /** @@ -1406,7 +1743,21 @@ class ParticleSystemComponent extends Component { * @type {number[]} */ set layers(arg) { - this._setValue('layers', arg); + const oldLayers = this._layers; + this._layers = arg; + + if (!this.emitter) return; + for (let i = 0; i < oldLayers.length; i++) { + const layer = this.system.app.scene.layers.getLayerById(oldLayers[i]); + if (!layer) continue; + layer.removeMeshInstances([this.emitter.meshInstance]); + } + if (!this.enabled || !this.entity.enabled) return; + for (let i = 0; i < arg.length; i++) { + const layer = this.system.app.scene.layers.getLayerById(arg[i]); + if (!layer) continue; + layer.addMeshInstances([this.emitter.meshInstance]); + } } /** @@ -1415,7 +1766,7 @@ class ParticleSystemComponent extends Component { * @type {number[]} */ get layers() { - return this.data.layers; + return this._layers; } /** @@ -1441,18 +1792,58 @@ class ParticleSystemComponent extends Component { return this._drawOrder; } - /** @ignore */ - _setValue(name, value) { - const data = this.data; - const oldValue = data[name]; - data[name] = value; - this.fire('set', name, oldValue, value); + /** + * Sets a property that only requires the emitter material to be updated. + * + * @param {string} name - The name of the property to set. + * @param {*} arg - The new value of the property. + * @private + */ + _setSimpleProperty(name, arg) { + this[`_${name}`] = arg; + if (this.emitter) { + this.emitter[name] = arg; + this.emitter.resetMaterial(); + } + } + + /** + * Sets a property that requires the particle system to be rebuilt. + * + * @param {string} name - The name of the property to set. + * @param {*} arg - The new value of the property. + * @private + */ + _setComplexProperty(name, arg) { + this[`_${name}`] = arg; + if (this.emitter) { + this.emitter[name] = arg; + this.emitter.resetMaterial(); + this.rebuild(); + this.reset(); + } + } + + /** + * Sets a curve property that requires the emitter graphs to be rebuilt. + * + * @param {string} name - The name of the property to set. + * @param {*} arg - The new value of the property. + * @private + */ + _setGraphProperty(name, arg) { + this[`_${name}`] = arg; + if (this.emitter) { + this.emitter[name] = arg; + this.emitter.rebuildGraphs(); + this.emitter.resetMaterial(); + } } addMeshInstanceToLayers() { if (!this.emitter) return; - for (let i = 0; i < this.layers.length; i++) { - const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); + for (let i = 0; i < this._layers.length; i++) { + const layer = this.system.app.scene.layers.getLayerById(this._layers[i]); if (!layer) continue; layer.addMeshInstances([this.emitter.meshInstance]); this.emitter._layer = layer; @@ -1461,28 +1852,13 @@ class ParticleSystemComponent extends Component { removeMeshInstanceFromLayers() { if (!this.emitter) return; - for (let i = 0; i < this.layers.length; i++) { - const layer = this.system.app.scene.layers.getLayerById(this.layers[i]); + for (let i = 0; i < this._layers.length; i++) { + const layer = this.system.app.scene.layers.getLayerById(this._layers[i]); if (!layer) continue; layer.removeMeshInstances([this.emitter.meshInstance]); } } - onSetLayers(name, oldValue, newValue) { - if (!this.emitter) return; - for (let i = 0; i < oldValue.length; i++) { - const layer = this.system.app.scene.layers.getLayerById(oldValue[i]); - if (!layer) continue; - layer.removeMeshInstances([this.emitter.meshInstance]); - } - if (!this.enabled || !this.entity.enabled) return; - for (let i = 0; i < newValue.length; i++) { - const layer = this.system.app.scene.layers.getLayerById(newValue[i]); - if (!layer) continue; - layer.addMeshInstances([this.emitter.meshInstance]); - } - } - onLayersChanged(oldComp, newComp) { this.addMeshInstanceToLayers(); oldComp.off('add', this.onLayerAdded, this); @@ -1493,14 +1869,14 @@ class ParticleSystemComponent extends Component { onLayerAdded(layer) { if (!this.emitter) return; - const index = this.layers.indexOf(layer.id); + const index = this._layers.indexOf(layer.id); if (index < 0) return; layer.addMeshInstances([this.emitter.meshInstance]); } onLayerRemoved(layer) { if (!this.emitter) return; - const index = this.layers.indexOf(layer.id); + const index = this._layers.indexOf(layer.id); if (index < 0) return; layer.removeMeshInstances([this.emitter.meshInstance]); } @@ -1541,34 +1917,6 @@ class ParticleSystemComponent extends Component { _onColorMapAssetChange(asset) {} - onSetColorMapAsset(name, oldValue, newValue) { - const assets = this.system.app.assets; - if (oldValue) { - const asset = assets.get(oldValue); - if (asset) { - this._unbindColorMapAsset(asset); - } - } - - if (newValue) { - if (newValue instanceof Asset) { - this.data.colorMapAsset = newValue.id; - newValue = newValue.id; - } - - const asset = assets.get(newValue); - if (asset) { - this._bindColorMapAsset(asset); - } else { - assets.once(`add:${newValue}`, (asset) => { - this._bindColorMapAsset(asset); - }); - } - } else { - this.colorMap = null; - } - } - _bindNormalMapAsset(asset) { asset.on('load', this._onNormalMapAssetLoad, this); asset.on('unload', this._onNormalMapAssetUnload, this); @@ -1605,35 +1953,6 @@ class ParticleSystemComponent extends Component { _onNormalMapAssetChange(asset) {} - onSetNormalMapAsset(name, oldValue, newValue) { - const assets = this.system.app.assets; - - if (oldValue) { - const asset = assets.get(oldValue); - if (asset) { - this._unbindNormalMapAsset(asset); - } - } - - if (newValue) { - if (newValue instanceof Asset) { - this.data.normalMapAsset = newValue.id; - newValue = newValue.id; - } - - const asset = assets.get(newValue); - if (asset) { - this._bindNormalMapAsset(asset); - } else { - assets.once(`add:${newValue}`, (asset) => { - this._bindNormalMapAsset(asset); - }); - } - } else { - this.normalMap = null; - } - } - _bindMeshAsset(asset) { asset.on('load', this._onMeshAssetLoad, this); asset.on('unload', this._onMeshAssetUnload, this); @@ -1670,42 +1989,6 @@ class ParticleSystemComponent extends Component { _onMeshAssetChange(asset) {} - onSetMeshAsset(name, oldValue, newValue) { - const assets = this.system.app.assets; - - if (oldValue) { - const asset = assets.get(oldValue); - if (asset) { - this._unbindMeshAsset(asset); - } - } - - if (newValue) { - if (newValue instanceof Asset) { - this.data.meshAsset = newValue.id; - newValue = newValue.id; - } - - const asset = assets.get(newValue); - if (asset) { - this._bindMeshAsset(asset); - } - } else { - this._onMeshChanged(null); - } - } - - onSetMesh(name, oldValue, newValue) { - // hack this for now - // if the value being set is null, an asset or an asset id, then assume we are - // setting the mesh asset, which will in turn update the mesh - if (!newValue || newValue instanceof Asset || typeof newValue === 'number') { - this.meshAsset = newValue; - } else { - this._onMeshChanged(newValue); - } - } - _onMeshChanged(mesh) { if (mesh && !(mesh instanceof Mesh)) { // if mesh is a pc.Model, use the first meshInstance @@ -1716,7 +1999,7 @@ class ParticleSystemComponent extends Component { } } - this.data.mesh = mesh; + this._mesh = mesh; if (this.emitter) { this.emitter.mesh = mesh; @@ -1725,31 +2008,6 @@ class ParticleSystemComponent extends Component { } } - onSetRenderAsset(name, oldValue, newValue) { - const assets = this.system.app.assets; - - if (oldValue) { - const asset = assets.get(oldValue); - if (asset) { - this._unbindRenderAsset(asset); - } - } - - if (newValue) { - if (newValue instanceof Asset) { - this.data.renderAsset = newValue.id; - newValue = newValue.id; - } - - const asset = assets.get(newValue); - if (asset) { - this._bindRenderAsset(asset); - } - } else { - this._onRenderChanged(null); - } - } - _bindRenderAsset(asset) { asset.on('load', this._onRenderAssetLoad, this); asset.on('unload', this._onRenderAssetUnload, this); @@ -1803,22 +2061,6 @@ class ParticleSystemComponent extends Component { this._onMeshChanged(meshes && meshes[0]); } - onSetLoop(name, oldValue, newValue) { - if (this.emitter) { - this.emitter[name] = newValue; - this.emitter.resetTime(); - } - } - - onSetBlendType(name, oldValue, newValue) { - if (this.emitter) { - this.emitter[name] = newValue; - this.emitter.material.blendType = newValue; - this.emitter.resetMaterial(); - this.rebuild(); - } - } - _requestDepth() { if (this._requestedDepth) return; if (!depthLayer) depthLayer = this.system.app.scene.layers.getLayerById(LAYERID_DEPTH); @@ -1836,57 +2078,13 @@ class ParticleSystemComponent extends Component { } } - onSetDepthSoftening(name, oldValue, newValue) { - if (oldValue !== newValue) { - if (newValue) { - if (this.enabled && this.entity.enabled) this._requestDepth(); - if (this.emitter) this.emitter[name] = newValue; - } else { - if (this.enabled && this.entity.enabled) this._releaseDepth(); - if (this.emitter) this.emitter[name] = newValue; - } - if (this.emitter) { - this.reset(); - this.emitter.resetMaterial(); - this.rebuild(); - } - } - } - - onSetSimpleProperty(name, oldValue, newValue) { - if (this.emitter) { - this.emitter[name] = newValue; - this.emitter.resetMaterial(); - } - } - - onSetComplexProperty(name, oldValue, newValue) { - if (this.emitter) { - this.emitter[name] = newValue; - this.emitter.resetMaterial(); - this.rebuild(); - this.reset(); - } - } - - onSetGraphProperty(name, oldValue, newValue) { - if (this.emitter) { - this.emitter[name] = newValue; - this.emitter.rebuildGraphs(); - this.emitter.resetMaterial(); - } - } - onEnable() { const scene = this.system.app.scene; const layers = scene.layers; - // get data store once - const data = this.data; - // load any assets that haven't been loaded yet for (let i = 0, len = ASSET_PROPERTIES.length; i < len; i++) { - let asset = data[ASSET_PROPERTIES[i]]; + let asset = this[`_${ASSET_PROPERTIES[i]}`]; if (asset) { if (!(asset instanceof Asset)) { const id = parseInt(asset, 10); @@ -1909,90 +2107,82 @@ class ParticleSystemComponent extends Component { } if (!this.emitter) { - let mesh = data.mesh; - - // mesh might be an asset id of an asset - // that hasn't been loaded yet - if (!(mesh instanceof Mesh)) { - mesh = null; - } - this.emitter = new ParticleEmitter(this.system.app.graphicsDevice, { - numParticles: data.numParticles, - emitterExtents: data.emitterExtents, - emitterExtentsInner: data.emitterExtentsInner, - emitterRadius: data.emitterRadius, - emitterRadiusInner: data.emitterRadiusInner, - emitterShape: data.emitterShape, - initialVelocity: data.initialVelocity, - wrap: data.wrap, - localSpace: data.localSpace, - screenSpace: data.screenSpace, - wrapBounds: data.wrapBounds, - lifetime: data.lifetime, - rate: data.rate, - rate2: data.rate2, - - orientation: data.orientation, - particleNormal: data.particleNormal, - - animTilesX: data.animTilesX, - animTilesY: data.animTilesY, - animStartFrame: data.animStartFrame, - animNumFrames: data.animNumFrames, - animNumAnimations: data.animNumAnimations, - animIndex: data.animIndex, - randomizeAnimIndex: data.randomizeAnimIndex, - animSpeed: data.animSpeed, - animLoop: data.animLoop, - - startAngle: data.startAngle, - startAngle2: data.startAngle2, - - scaleGraph: data.scaleGraph, - scaleGraph2: data.scaleGraph2, - - colorGraph: data.colorGraph, - colorGraph2: data.colorGraph2, - - alphaGraph: data.alphaGraph, - alphaGraph2: data.alphaGraph2, - - localVelocityGraph: data.localVelocityGraph, - localVelocityGraph2: data.localVelocityGraph2, - - velocityGraph: data.velocityGraph, - velocityGraph2: data.velocityGraph2, - - rotationSpeedGraph: data.rotationSpeedGraph, - rotationSpeedGraph2: data.rotationSpeedGraph2, - - radialSpeedGraph: data.radialSpeedGraph, - radialSpeedGraph2: data.radialSpeedGraph2, - - colorMap: data.colorMap, - normalMap: data.normalMap, - loop: data.loop, - preWarm: data.preWarm, - sort: data.sort, - stretch: data.stretch, - alignToMotion: data.alignToMotion, - lighting: data.lighting, - halfLambert: data.halfLambert, - intensity: data.intensity, - depthSoftening: data.depthSoftening, + numParticles: this._numParticles, + emitterExtents: this._emitterExtents, + emitterExtentsInner: this._emitterExtentsInner, + emitterRadius: this._emitterRadius, + emitterRadiusInner: this._emitterRadiusInner, + emitterShape: this._emitterShape, + initialVelocity: this._initialVelocity, + wrap: this._wrap, + localSpace: this._localSpace, + screenSpace: this._screenSpace, + wrapBounds: this._wrapBounds, + lifetime: this._lifetime, + rate: this._rate, + rate2: this._rate2, + + orientation: this._orientation, + particleNormal: this._particleNormal, + + animTilesX: this._animTilesX, + animTilesY: this._animTilesY, + animStartFrame: this._animStartFrame, + animNumFrames: this._animNumFrames, + animNumAnimations: this._animNumAnimations, + animIndex: this._animIndex, + randomizeAnimIndex: this._randomizeAnimIndex, + animSpeed: this._animSpeed, + animLoop: this._animLoop, + + startAngle: this._startAngle, + startAngle2: this._startAngle2, + + scaleGraph: this._scaleGraph, + scaleGraph2: this._scaleGraph2, + + colorGraph: this._colorGraph, + colorGraph2: this._colorGraph2, + + alphaGraph: this._alphaGraph, + alphaGraph2: this._alphaGraph2, + + localVelocityGraph: this._localVelocityGraph, + localVelocityGraph2: this._localVelocityGraph2, + + velocityGraph: this._velocityGraph, + velocityGraph2: this._velocityGraph2, + + rotationSpeedGraph: this._rotationSpeedGraph, + rotationSpeedGraph2: this._rotationSpeedGraph2, + + radialSpeedGraph: this._radialSpeedGraph, + radialSpeedGraph2: this._radialSpeedGraph2, + + colorMap: this._colorMap, + normalMap: this._normalMap, + loop: this._loop, + preWarm: this._preWarm, + sort: this._sort, + stretch: this._stretch, + alignToMotion: this._alignToMotion, + lighting: this._lighting, + halfLambert: this._halfLambert, + intensity: this._intensity, + depthSoftening: this._depthSoftening, scene: this.system.app.scene, - mesh: mesh, - depthWrite: data.depthWrite, - noFog: data.noFog, + mesh: this._mesh, + depthWrite: this._depthWrite, + noFog: this._noFog, node: this.entity, - blendType: data.blendType + blendType: this._blendType }); this.emitter.meshInstance.node = this.entity; - this.emitter.drawOrder = this.drawOrder; + this.emitter.drawOrder = this._drawOrder; - if (!data.autoPlay) { + if (!this._autoPlay) { this.pause(); this.emitter.meshInstance.visible = false; } @@ -2009,7 +2199,7 @@ class ParticleSystemComponent extends Component { this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this); } - if (this.enabled && this.entity.enabled && data.depthSoftening) { + if (this.enabled && this.entity.enabled && this._depthSoftening) { this._requestDepth(); } } @@ -2030,7 +2220,7 @@ class ParticleSystemComponent extends Component { if (this.emitter) { this.removeMeshInstanceFromLayers(); - if (this.data.depthSoftening) this._releaseDepth(); + if (this._depthSoftening) this._releaseDepth(); // clear camera as it isn't updated while disabled and we don't want to hold // onto old reference @@ -2052,7 +2242,7 @@ class ParticleSystemComponent extends Component { for (let i = 0; i < ASSET_PROPERTIES.length; i++) { const prop = ASSET_PROPERTIES[i]; - if (this.data[prop]) { + if (this[`_${prop}`]) { this[prop] = null; } } @@ -2084,24 +2274,24 @@ class ParticleSystemComponent extends Component { * Freezes the simulation. */ pause() { - this.data.paused = true; + this._paused = true; } /** * Unfreezes the simulation. */ unpause() { - this.data.paused = false; + this._paused = false; } /** * Enables/unfreezes the simulation. */ play() { - this.data.paused = false; + this._paused = false; if (this.emitter) { this.emitter.meshInstance.visible = true; - this.emitter.loop = this.data.loop; + this.emitter.loop = this._loop; this.emitter.resetTime(); } } @@ -2112,7 +2302,7 @@ class ParticleSystemComponent extends Component { * @returns {boolean} True if the particle system is currently playing and false otherwise. */ isPlaying() { - if (this.data.paused) { + if (this._paused) { return false; } if (this.emitter && this.emitter.loop) { @@ -2152,4 +2342,4 @@ class ParticleSystemComponent extends Component { } } -export { ParticleSystemComponent }; +export { _properties, ParticleSystemComponent }; diff --git a/src/framework/components/particle-system/data.js b/src/framework/components/particle-system/data.js index d5275fc46af..528d64697ba 100644 --- a/src/framework/components/particle-system/data.js +++ b/src/framework/components/particle-system/data.js @@ -1,171 +1,5 @@ -import { Vec3 } from '../../../core/math/vec3.js'; -import { BLEND_NORMAL, EMITTERSHAPE_BOX, LAYERID_WORLD, PARTICLEMODE_GPU, PARTICLEORIENTATION_SCREEN } from '../../../scene/constants.js'; - -/** - * @import { Asset } from '../../../framework/asset/asset.js' - * @import { CurveSet } from '../../../core/math/curve-set.js' - * @import { Curve } from '../../../core/math/curve.js' - * @import { Mesh } from '../../../scene/mesh.js' - * @import { Texture } from '../../../platform/graphics/texture.js' - */ - class ParticleSystemComponentData { - numParticles = 1; // Amount of particles allocated (max particles = max GL texture width at this moment) - - rate = 1; // Emission rate - - /** @type {number|null} */ - rate2 = null; - - startAngle = 0; - - /** @type {number|null} */ - startAngle2 = null; - - lifetime = 50; // Particle lifetime - - emitterExtents = new Vec3(); // Spawn point divergence - - emitterExtentsInner = new Vec3(); - - emitterRadius = 0; - - emitterRadiusInner = 0; - - emitterShape = EMITTERSHAPE_BOX; - - initialVelocity = 0; - - wrap = false; - - wrapBounds = new Vec3(); - - localSpace = false; - - screenSpace = false; - - /** @type {Texture|null} */ - colorMap = null; - - /** @type {Asset|null} */ - colorMapAsset = null; - - /** @type {Texture|null} */ - normalMap = null; - - /** @type {Asset|null} */ - normalMapAsset = null; - - loop = true; - - preWarm = false; - - sort = 0; // Sorting mode: 0 = none, 1 = by distance, 2 = by life, 3 = by -life; Forces CPU mode if not 0 - - mode = PARTICLEMODE_GPU; - - scene = null; - - lighting = false; - - halfLambert = false; // Uses half-lambert lighting instead of Lambert - - intensity = 1; - - stretch = 0.0; - - alignToMotion = false; - - depthSoftening = 0; - - /** @type {Asset|null} */ - renderAsset = null; - - /** @type {Asset|null} */ - meshAsset = null; - - /** @type {Mesh|null} */ - mesh = null; // Mesh to be used as particle. Vertex buffer is supposed to hold vertex position in first 3 floats of each vertex - // Leave undefined to use simple quads - - depthWrite = false; - - noFog = false; - - orientation = PARTICLEORIENTATION_SCREEN; - - particleNormal = new Vec3(0, 1, 0); - - animTilesX = 1; - - animTilesY = 1; - - animStartFrame = 0; - - animNumFrames = 1; - - animNumAnimations = 1; - - animIndex = 0; - - randomizeAnimIndex = false; - - animSpeed = 1; - - animLoop = true; - - // Time-dependent parameters - /** @type {Curve|null} */ - scaleGraph = null; - - /** @type {Curve|null} */ - scaleGraph2 = null; - - /** @type {CurveSet|null} */ - colorGraph = null; - - /** @type {CurveSet|null} */ - colorGraph2 = null; - - /** @type {Curve|null} */ - alphaGraph = null; - - /** @type {Curve|null} */ - alphaGraph2 = null; - - /** @type {CurveSet|null} */ - localVelocityGraph = null; - - /** @type {CurveSet|null} */ - localVelocityGraph2 = null; - - /** @type {CurveSet|null} */ - velocityGraph = null; - - /** @type {CurveSet|null} */ - velocityGraph2 = null; - - /** @type {Curve|null} */ - rotationSpeedGraph = null; - - /** @type {Curve|null} */ - rotationSpeedGraph2 = null; - - /** @type {Curve|null} */ - radialSpeedGraph = null; - - /** @type {Curve|null} */ - radialSpeedGraph2 = null; - - blendType = BLEND_NORMAL; - enabled = true; - - paused = false; - - autoPlay = true; - - layers = [LAYERID_WORLD]; // assign to the default world layer } export { ParticleSystemComponentData }; diff --git a/src/framework/components/particle-system/system.js b/src/framework/components/particle-system/system.js index 66a3a9f7fda..e30469c1585 100644 --- a/src/framework/components/particle-system/system.js +++ b/src/framework/components/particle-system/system.js @@ -2,8 +2,9 @@ import { Curve } from '../../../core/math/curve.js'; import { CurveSet } from '../../../core/math/curve-set.js'; import { Vec3 } from '../../../core/math/vec3.js'; import { Asset } from '../../asset/asset.js'; +import { Component } from '../component.js'; import { ComponentSystem } from '../system.js'; -import { ParticleSystemComponent } from './component.js'; +import { _properties, ParticleSystemComponent } from './component.js'; import { ParticleSystemComponentData } from './data.js'; import { particleChunksGLSL } from '../../../scene/shader-lib/glsl/collections/particle-chunks-glsl.js'; import { particleChunksWGSL } from '../../../scene/shader-lib/wgsl/collections/particle-chunks-wgsl.js'; @@ -14,71 +15,28 @@ import { ShaderChunks } from '../../../scene/shader-lib/shader-chunks.js'; * @import { AppBase } from '../../app-base.js' */ -const _schema = [ - 'enabled', - 'autoPlay', - 'numParticles', - 'lifetime', - 'rate', - 'rate2', - 'startAngle', - 'startAngle2', - 'loop', - 'preWarm', - 'lighting', - 'halfLambert', - 'intensity', - 'depthWrite', - 'noFog', - 'depthSoftening', - 'sort', - 'blendType', - 'stretch', - 'alignToMotion', - 'emitterShape', - 'emitterExtents', - 'emitterExtentsInner', - 'emitterRadius', - 'emitterRadiusInner', - 'initialVelocity', - 'wrap', - 'wrapBounds', - 'localSpace', - 'screenSpace', - 'colorMapAsset', - 'normalMapAsset', - 'mesh', - 'meshAsset', - 'renderAsset', - 'orientation', - 'particleNormal', - 'localVelocityGraph', - 'localVelocityGraph2', - 'velocityGraph', - 'velocityGraph2', - 'rotationSpeedGraph', - 'rotationSpeedGraph2', - 'radialSpeedGraph', - 'radialSpeedGraph2', - 'scaleGraph', - 'scaleGraph2', - 'colorGraph', - 'colorGraph2', - 'alphaGraph', - 'alphaGraph2', - 'colorMap', - 'normalMap', - 'animTilesX', - 'animTilesY', - 'animStartFrame', - 'animNumFrames', - 'animNumAnimations', - 'animIndex', - 'randomizeAnimIndex', - 'animSpeed', - 'animLoop', - 'layers' -]; +const _schema = ['enabled']; + +const _propertyTypes = { + emitterExtents: 'vec3', + emitterExtentsInner: 'vec3', + particleNormal: 'vec3', + wrapBounds: 'vec3', + localVelocityGraph: 'curveset', + localVelocityGraph2: 'curveset', + velocityGraph: 'curveset', + velocityGraph2: 'curveset', + colorGraph: 'curveset', + colorGraph2: 'curveset', + alphaGraph: 'curve', + alphaGraph2: 'curve', + rotationSpeedGraph: 'curve', + rotationSpeedGraph2: 'curve', + radialSpeedGraph: 'curve', + radialSpeedGraph2: 'curve', + scaleGraph: 'curve', + scaleGraph2: 'curve' +}; /** * Allows an Entity to render a particle system. @@ -102,27 +60,6 @@ class ParticleSystemComponentSystem extends ComponentSystem { this.schema = _schema; - this.propertyTypes = { - emitterExtents: 'vec3', - emitterExtentsInner: 'vec3', - particleNormal: 'vec3', - wrapBounds: 'vec3', - localVelocityGraph: 'curveset', - localVelocityGraph2: 'curveset', - velocityGraph: 'curveset', - velocityGraph2: 'curveset', - colorGraph: 'curveset', - colorGraph2: 'curveset', - alphaGraph: 'curve', - alphaGraph2: 'curve', - rotationSpeedGraph: 'curve', - rotationSpeedGraph2: 'curve', - radialSpeedGraph: 'curve', - radialSpeedGraph2: 'curve', - scaleGraph: 'curve', - scaleGraph2: 'curve' - }; - this.on('beforeremove', this.onBeforeRemove, this); this.app.systems.on('update', this.onUpdate, this); @@ -131,75 +68,70 @@ class ParticleSystemComponentSystem extends ComponentSystem { ShaderChunks.get(app.graphicsDevice, SHADERLANGUAGE_WGSL).add(particleChunksWGSL); } - initializeComponentData(component, _data, properties) { - const data = {}; - - properties = []; - const types = this.propertyTypes; + initializeComponentData(component, _data) { + // duplicate input data as we are modifying it + const data = { ..._data }; // we store the mesh asset id as "mesh" (it should be "meshAsset") // this re-maps "mesh" into "meshAsset" if it is an asset or an asset id - if (_data.mesh instanceof Asset || typeof _data.mesh === 'number') { + if (data.mesh instanceof Asset || typeof data.mesh === 'number') { // migrate into meshAsset property - _data.meshAsset = _data.mesh; - delete _data.mesh; + data.meshAsset = data.mesh; + delete data.mesh; } - for (const prop in _data) { - if (_data.hasOwnProperty(prop)) { - properties.push(prop); - // duplicate input data as we are modifying it - data[prop] = _data[prop]; - } - - if (types[prop] === 'vec3') { + for (const prop in data) { + const type = _propertyTypes[prop]; + if (type === 'vec3') { if (Array.isArray(data[prop])) { data[prop] = new Vec3(data[prop][0], data[prop][1], data[prop][2]); } - } else if (types[prop] === 'curve') { + } else if (type === 'curve') { if (!(data[prop] instanceof Curve)) { const t = data[prop].type; data[prop] = new Curve(data[prop].keys); data[prop].type = t; } - } else if (types[prop] === 'curveset') { + } else if (type === 'curveset') { if (!(data[prop] instanceof CurveSet)) { const t = data[prop].type; data[prop] = new CurveSet(data[prop].keys); data[prop].type = t; } } + } - // duplicate layer list - if (data.layers && Array.isArray(data.layers)) { - data.layers = data.layers.slice(0); + // duplicate layer list + if (data.layers && Array.isArray(data.layers)) { + data.layers = data.layers.slice(0); + } + + for (let i = 0; i < _properties.length; i++) { + const property = _properties[i]; + if (data.hasOwnProperty(property)) { + component[property] = data[property]; } } - super.initializeComponentData(component, data, properties); + super.initializeComponentData(component, data, _schema); } cloneComponent(entity, clone) { - const source = entity.particlesystem.data; - const schema = this.schema; - - const data = {}; + const c = entity.particlesystem; - for (let i = 0, len = schema.length; i < len; i++) { - const prop = schema[i]; - let sourceProp = source[prop]; - if (sourceProp instanceof Vec3 || - sourceProp instanceof Curve || - sourceProp instanceof CurveSet) { + const data = { + enabled: c.enabled + }; - sourceProp = sourceProp.clone(); - data[prop] = sourceProp; + for (let i = 0; i < _properties.length; i++) { + const prop = _properties[i]; + const value = c[prop]; + if (value instanceof Vec3 || value instanceof Curve || value instanceof CurveSet) { + data[prop] = value.clone(); } else if (prop === 'layers') { - data.layers = source.layers.slice(0); - } else { - if (sourceProp !== null && sourceProp !== undefined) { - data[prop] = sourceProp; - } + data.layers = value.slice(0); + } else if (value !== null && value !== undefined) { + data[prop] = value; } } @@ -218,17 +150,16 @@ class ParticleSystemComponentSystem extends ComponentSystem { for (const id in components) { if (components.hasOwnProperty(id)) { - const component = components[id]; + const component = components[id].entity.particlesystem; const entity = component.entity; - const data = component.data; - if (data.enabled && entity.enabled) { - const emitter = entity.particlesystem.emitter; + if (component.enabled && entity.enabled) { + const emitter = component.emitter; if (!emitter?.meshInstance.visible) continue; // if emitter is using lighting, enable light cube on all layers it is assigned to if (emitter.lighting) { - const layers = data.layers; + const layers = component.layers; for (let i = 0; i < layers.length; i++) { const layer = composition.getLayerById(layers[i]); if (layer) { @@ -237,7 +168,7 @@ class ParticleSystemComponentSystem extends ComponentSystem { } } - if (!data.paused) { + if (!component._paused) { let numSteps = 0; emitter.simTime += dt; if (emitter.simTime >= emitter.fixedTimeStep) { @@ -271,4 +202,6 @@ class ParticleSystemComponentSystem extends ComponentSystem { } } +Component._buildAccessors(ParticleSystemComponent.prototype, _schema); + export { ParticleSystemComponentSystem }; diff --git a/test/framework/components/particlesystem/component.test.mjs b/test/framework/components/particlesystem/component.test.mjs index fc22fd30643..120928bcb09 100644 --- a/test/framework/components/particlesystem/component.test.mjs +++ b/test/framework/components/particlesystem/component.test.mjs @@ -1,8 +1,13 @@ import { expect } from 'chai'; +import { CURVE_SPLINE } from '../../../../src/core/math/constants.js'; +import { CurveSet } from '../../../../src/core/math/curve-set.js'; +import { Curve } from '../../../../src/core/math/curve.js'; +import { Vec3 } from '../../../../src/core/math/vec3.js'; import { AssetListLoader } from '../../../../src/framework/asset/asset-list-loader.js'; import { Asset } from '../../../../src/framework/asset/asset.js'; import { Entity } from '../../../../src/framework/entity.js'; +import { BLEND_NORMAL, EMITTERSHAPE_BOX, LAYERID_WORLD, PARTICLEORIENTATION_SCREEN } from '../../../../src/scene/constants.js'; import { createApp } from '../../../app.mjs'; import { jsdomSetup, jsdomTeardown } from '../../../jsdom.mjs'; @@ -65,6 +70,182 @@ describe('ParticleSystemComponent', function () { expect(e.particlesystem).to.not.exist; }); + it('Add particlesystem with default values', function () { + const e = new Entity(); + + e.addComponent('particlesystem'); + + const c = e.particlesystem; + expect(c.enabled).to.be.true; + expect(c.autoPlay).to.be.true; + expect(c.numParticles).to.equal(1); + expect(c.lifetime).to.equal(50); + expect(c.rate).to.equal(1); + expect(c.rate2).to.be.null; + expect(c.startAngle).to.equal(0); + expect(c.startAngle2).to.be.null; + expect(c.loop).to.be.true; + expect(c.preWarm).to.be.false; + expect(c.lighting).to.be.false; + expect(c.halfLambert).to.be.false; + expect(c.intensity).to.equal(1); + expect(c.depthWrite).to.be.false; + expect(c.noFog).to.be.false; + expect(c.depthSoftening).to.equal(0); + expect(c.sort).to.equal(0); + expect(c.blendType).to.equal(BLEND_NORMAL); + expect(c.stretch).to.equal(0); + expect(c.alignToMotion).to.be.false; + expect(c.emitterShape).to.equal(EMITTERSHAPE_BOX); + expect(c.emitterExtents).to.be.an.instanceof(Vec3); + expect(c.emitterExtents.equals(new Vec3())).to.be.true; + expect(c.emitterExtentsInner.equals(new Vec3())).to.be.true; + expect(c.emitterRadius).to.equal(0); + expect(c.emitterRadiusInner).to.equal(0); + expect(c.initialVelocity).to.equal(0); + expect(c.wrap).to.be.false; + expect(c.wrapBounds).to.be.an.instanceof(Vec3); + expect(c.wrapBounds.equals(new Vec3())).to.be.true; + expect(c.localSpace).to.be.false; + expect(c.screenSpace).to.be.false; + expect(c.colorMapAsset).to.be.null; + expect(c.normalMapAsset).to.be.null; + expect(c.colorMap).to.be.null; + expect(c.normalMap).to.be.null; + expect(c.mesh).to.be.null; + expect(c.meshAsset).to.be.null; + expect(c.renderAsset).to.be.null; + expect(c.orientation).to.equal(PARTICLEORIENTATION_SCREEN); + expect(c.particleNormal.equals(new Vec3(0, 1, 0))).to.be.true; + expect(c.localVelocityGraph).to.be.null; + expect(c.localVelocityGraph2).to.be.null; + expect(c.velocityGraph).to.be.null; + expect(c.velocityGraph2).to.be.null; + expect(c.rotationSpeedGraph).to.be.null; + expect(c.rotationSpeedGraph2).to.be.null; + expect(c.radialSpeedGraph).to.be.null; + expect(c.radialSpeedGraph2).to.be.null; + expect(c.scaleGraph).to.be.null; + expect(c.scaleGraph2).to.be.null; + expect(c.colorGraph).to.be.null; + expect(c.colorGraph2).to.be.null; + expect(c.alphaGraph).to.be.null; + expect(c.alphaGraph2).to.be.null; + expect(c.animTilesX).to.equal(1); + expect(c.animTilesY).to.equal(1); + expect(c.animStartFrame).to.equal(0); + expect(c.animNumFrames).to.equal(1); + expect(c.animNumAnimations).to.equal(1); + expect(c.animIndex).to.equal(0); + expect(c.randomizeAnimIndex).to.be.false; + expect(c.animSpeed).to.equal(1); + expect(c.animLoop).to.be.true; + expect(c.layers).to.deep.equal([LAYERID_WORLD]); + expect(c.drawOrder).to.equal(0); + }); + + it('Add particlesystem with enabled false', function () { + const e = new Entity(); + + e.addComponent('particlesystem', { + enabled: false + }); + + expect(e.particlesystem.enabled).to.be.false; + }); + + it('Initializes vec3 and curve properties from JSON-style data', function () { + const e = new Entity(); + + e.addComponent('particlesystem', { + emitterExtents: [1, 2, 3], + wrapBounds: [4, 5, 6], + alphaGraph: { type: CURVE_SPLINE, keys: [0, 0, 1, 1] }, + colorGraph: { type: CURVE_SPLINE, keys: [[0, 0, 1, 1], [0, 0, 1, 1], [0, 0, 1, 1]] } + }); + + const c = e.particlesystem; + expect(c.emitterExtents).to.be.an.instanceof(Vec3); + expect(c.emitterExtents.equals(new Vec3(1, 2, 3))).to.be.true; + expect(c.wrapBounds.equals(new Vec3(4, 5, 6))).to.be.true; + expect(c.alphaGraph).to.be.an.instanceof(Curve); + expect(c.alphaGraph.type).to.equal(CURVE_SPLINE); + expect(c.colorGraph).to.be.an.instanceof(CurveSet); + expect(c.colorGraph.type).to.equal(CURVE_SPLINE); + }); + + it('Initializes from Vec3 and Curve instances', function () { + const extents = new Vec3(1, 2, 3); + const alphaGraph = new Curve([0, 0, 1, 1]); + const e = new Entity(); + + e.addComponent('particlesystem', { + emitterExtents: extents, + alphaGraph: alphaGraph + }); + + expect(e.particlesystem.emitterExtents).to.equal(extents); + expect(e.particlesystem.alphaGraph).to.equal(alphaGraph); + }); + + it('Remaps legacy mesh asset id to meshAsset', function () { + const e = new Entity(); + app.root.addChild(e); + e.addComponent('particlesystem', { + mesh: assets.mesh.id + }); + + expect(e.particlesystem.meshAsset).to.equal(assets.mesh.id); + }); + + it('Stores the asset id when assigned an Asset instance', function () { + const e = new Entity(); + app.root.addChild(e); + e.addComponent('particlesystem'); + + e.particlesystem.colorMapAsset = assets.colorMap; + + expect(e.particlesystem.colorMapAsset).to.equal(assets.colorMap.id); + }); + + it('Clones a particlesystem component', function () { + const e = new Entity(); + e.addComponent('particlesystem', { + numParticles: 10, + loop: false, + emitterExtents: [1, 2, 3], + alphaGraph: { type: CURVE_SPLINE, keys: [0, 0, 1, 1] }, + colorGraph: { type: CURVE_SPLINE, keys: [[0, 0, 1, 1], [0, 0, 1, 1], [0, 0, 1, 1]] } + }); + app.root.addChild(e); + + const clone = e.clone(); + const c = clone.particlesystem; + const src = e.particlesystem; + + expect(c.numParticles).to.equal(10); + expect(c.loop).to.be.false; + expect(c.emitterExtents.equals(new Vec3(1, 2, 3))).to.be.true; + expect(c.emitterExtents).to.not.equal(src.emitterExtents); + expect(c.alphaGraph).to.be.an.instanceof(Curve); + expect(c.alphaGraph).to.not.equal(src.alphaGraph); + expect(c.alphaGraph.type).to.equal(CURVE_SPLINE); + expect(c.colorGraph).to.be.an.instanceof(CurveSet); + expect(c.colorGraph).to.not.equal(src.colorGraph); + expect(c.layers).to.deep.equal(src.layers); + expect(c.layers).to.not.equal(src.layers); + }); + + it('pause() prevents the particle system from playing', function () { + const e = new Entity(); + e.addComponent('particlesystem'); + app.root.addChild(e); + + e.particlesystem.pause(); + + expect(e.particlesystem.isPlaying()).to.be.false; + }); + it('ColorMap Asset unbinds on destroy', function () { const e = new Entity(); app.root.addChild(e); From 020903d226de0211e618575eb398edfa5f860300 Mon Sep 17 00:00:00 2001 From: Will Eastcott Date: Thu, 11 Jun 2026 16:04:50 +0100 Subject: [PATCH 2/5] Address review feedback: skip undefined values during init, guard isPlaying() Skip present-but-undefined keys in initializeComponentData so class-field defaults are preserved (matching the base initializer) and curve deserialization cannot throw on undefined. Make isPlaying() return false when no emitter exists instead of throwing. Co-Authored-By: Claude Fable 5 --- .../components/particle-system/component.js | 4 ++-- .../components/particle-system/system.js | 4 +++- .../particlesystem/component.test.mjs | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/framework/components/particle-system/component.js b/src/framework/components/particle-system/component.js index 883781e3b39..12fa099a31f 100644 --- a/src/framework/components/particle-system/component.js +++ b/src/framework/components/particle-system/component.js @@ -2302,10 +2302,10 @@ class ParticleSystemComponent extends Component { * @returns {boolean} True if the particle system is currently playing and false otherwise. */ isPlaying() { - if (this._paused) { + if (this._paused || !this.emitter) { return false; } - if (this.emitter && this.emitter.loop) { + if (this.emitter.loop) { return true; } diff --git a/src/framework/components/particle-system/system.js b/src/framework/components/particle-system/system.js index e30469c1585..0e7b828f1e1 100644 --- a/src/framework/components/particle-system/system.js +++ b/src/framework/components/particle-system/system.js @@ -81,6 +81,8 @@ class ParticleSystemComponentSystem extends ComponentSystem { } for (const prop in data) { + if (data[prop] === undefined) continue; + const type = _propertyTypes[prop]; if (type === 'vec3') { if (Array.isArray(data[prop])) { @@ -108,7 +110,7 @@ class ParticleSystemComponentSystem extends ComponentSystem { for (let i = 0; i < _properties.length; i++) { const property = _properties[i]; - if (data.hasOwnProperty(property)) { + if (data[property] !== undefined) { component[property] = data[property]; } } diff --git a/test/framework/components/particlesystem/component.test.mjs b/test/framework/components/particlesystem/component.test.mjs index 120928bcb09..da7653727e0 100644 --- a/test/framework/components/particlesystem/component.test.mjs +++ b/test/framework/components/particlesystem/component.test.mjs @@ -174,6 +174,21 @@ describe('ParticleSystemComponent', function () { expect(c.colorGraph.type).to.equal(CURVE_SPLINE); }); + it('Preserves defaults for properties initialized with undefined', function () { + const e = new Entity(); + + e.addComponent('particlesystem', { + numParticles: undefined, + emitterExtents: undefined, + alphaGraph: undefined + }); + + const c = e.particlesystem; + expect(c.numParticles).to.equal(1); + expect(c.emitterExtents.equals(new Vec3())).to.be.true; + expect(c.alphaGraph).to.be.null; + }); + it('Initializes from Vec3 and Curve instances', function () { const extents = new Vec3(1, 2, 3); const alphaGraph = new Curve([0, 0, 1, 1]); @@ -246,6 +261,15 @@ describe('ParticleSystemComponent', function () { expect(e.particlesystem.isPlaying()).to.be.false; }); + it('isPlaying() returns false when no emitter exists', function () { + const e = new Entity(); + e.addComponent('particlesystem'); + app.root.addChild(e); + + // NullGraphicsDevice disables particle systems, so no emitter is created + expect(e.particlesystem.isPlaying()).to.be.false; + }); + it('ColorMap Asset unbinds on destroy', function () { const e = new Entity(); app.root.addChild(e); From cacef609d1a7a7eb5a7a7815d1b547db61cfc4d6 Mon Sep 17 00:00:00 2001 From: Will Eastcott Date: Thu, 11 Jun 2026 16:16:33 +0100 Subject: [PATCH 3/5] Address review feedback: allow null curve init data, fix asset property JSDoc Skip explicit nulls in curve/curveset deserialization so component creation cannot throw on { alphaGraph: null }. Correct the JSDoc on the four asset properties to Asset|number|null (they normalize Asset instances to ids), matching the ModelComponent convention. Co-Authored-By: Claude Fable 5 --- .../components/particle-system/component.js | 32 +++++++++---------- .../components/particle-system/system.js | 2 +- .../particlesystem/component.test.mjs | 12 +++++++ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/framework/components/particle-system/component.js b/src/framework/components/particle-system/component.js index 12fa099a31f..e0c679a38d9 100644 --- a/src/framework/components/particle-system/component.js +++ b/src/framework/components/particle-system/component.js @@ -1036,9 +1036,9 @@ class ParticleSystemComponent extends Component { } /** - * Sets the {@link Asset} used to set the colorMap. + * Sets the {@link Asset} (or asset id) used to set the colorMap. * - * @type {Asset} + * @type {Asset|number|null} */ set colorMapAsset(arg) { const assets = this.system.app.assets; @@ -1070,18 +1070,18 @@ class ParticleSystemComponent extends Component { } /** - * Gets the {@link Asset} used to set the colorMap. + * Gets the id of the {@link Asset} used to set the colorMap. * - * @type {Asset} + * @type {Asset|number|null} */ get colorMapAsset() { return this._colorMapAsset; } /** - * Sets the {@link Asset} used to set the normalMap. + * Sets the {@link Asset} (or asset id) used to set the normalMap. * - * @type {Asset} + * @type {Asset|number|null} */ set normalMapAsset(arg) { const assets = this.system.app.assets; @@ -1113,9 +1113,9 @@ class ParticleSystemComponent extends Component { } /** - * Gets the {@link Asset} used to set the normalMap. + * Gets the id of the {@link Asset} used to set the normalMap. * - * @type {Asset} + * @type {Asset|number|null} */ get normalMapAsset() { return this._normalMapAsset; @@ -1147,9 +1147,9 @@ class ParticleSystemComponent extends Component { } /** - * Sets the {@link Asset} used to set the mesh. + * Sets the {@link Asset} (or asset id) used to set the mesh. * - * @type {Asset} + * @type {Asset|number|null} */ set meshAsset(arg) { const assets = this.system.app.assets; @@ -1177,18 +1177,18 @@ class ParticleSystemComponent extends Component { } /** - * Gets the {@link Asset} used to set the mesh. + * Gets the id of the {@link Asset} used to set the mesh. * - * @type {Asset} + * @type {Asset|number|null} */ get meshAsset() { return this._meshAsset; } /** - * Sets the Render {@link Asset} used to set the mesh. + * Sets the Render {@link Asset} (or asset id) used to set the mesh. * - * @type {Asset} + * @type {Asset|number|null} */ set renderAsset(arg) { const assets = this.system.app.assets; @@ -1216,9 +1216,9 @@ class ParticleSystemComponent extends Component { } /** - * Gets the Render {@link Asset} used to set the mesh. + * Gets the id of the Render {@link Asset} used to set the mesh. * - * @type {Asset} + * @type {Asset|number|null} */ get renderAsset() { return this._renderAsset; diff --git a/src/framework/components/particle-system/system.js b/src/framework/components/particle-system/system.js index 0e7b828f1e1..137a5070afa 100644 --- a/src/framework/components/particle-system/system.js +++ b/src/framework/components/particle-system/system.js @@ -81,7 +81,7 @@ class ParticleSystemComponentSystem extends ComponentSystem { } for (const prop in data) { - if (data[prop] === undefined) continue; + if (data[prop] === undefined || data[prop] === null) continue; const type = _propertyTypes[prop]; if (type === 'vec3') { diff --git a/test/framework/components/particlesystem/component.test.mjs b/test/framework/components/particlesystem/component.test.mjs index da7653727e0..1152d23c9d1 100644 --- a/test/framework/components/particlesystem/component.test.mjs +++ b/test/framework/components/particlesystem/component.test.mjs @@ -189,6 +189,18 @@ describe('ParticleSystemComponent', function () { expect(c.alphaGraph).to.be.null; }); + it('Accepts explicit null for curve properties', function () { + const e = new Entity(); + + e.addComponent('particlesystem', { + alphaGraph: null, + colorGraph: null + }); + + expect(e.particlesystem.alphaGraph).to.be.null; + expect(e.particlesystem.colorGraph).to.be.null; + }); + it('Initializes from Vec3 and Curve instances', function () { const extents = new Vec3(1, 2, 3); const alphaGraph = new Curve([0, 0, 1, 1]); From 0a4970522db2a41966174785ce3fe3414ef98b4d Mon Sep 17 00:00:00 2001 From: Will Eastcott Date: Thu, 11 Jun 2026 16:21:03 +0100 Subject: [PATCH 4/5] Limit public asset property types to Asset|null Per the convention from the collision migration discussion, the public API documents encouraged usage (Asset|null) rather than everything tolerated (ids remain accepted and stored internally). Co-Authored-By: Claude Fable 5 --- .../components/particle-system/component.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/framework/components/particle-system/component.js b/src/framework/components/particle-system/component.js index e0c679a38d9..dd5e609393e 100644 --- a/src/framework/components/particle-system/component.js +++ b/src/framework/components/particle-system/component.js @@ -1036,9 +1036,9 @@ class ParticleSystemComponent extends Component { } /** - * Sets the {@link Asset} (or asset id) used to set the colorMap. + * Sets the {@link Asset} used to set the colorMap. * - * @type {Asset|number|null} + * @type {Asset|null} */ set colorMapAsset(arg) { const assets = this.system.app.assets; @@ -1070,18 +1070,18 @@ class ParticleSystemComponent extends Component { } /** - * Gets the id of the {@link Asset} used to set the colorMap. + * Gets the {@link Asset} used to set the colorMap. * - * @type {Asset|number|null} + * @type {Asset|null} */ get colorMapAsset() { return this._colorMapAsset; } /** - * Sets the {@link Asset} (or asset id) used to set the normalMap. + * Sets the {@link Asset} used to set the normalMap. * - * @type {Asset|number|null} + * @type {Asset|null} */ set normalMapAsset(arg) { const assets = this.system.app.assets; @@ -1113,9 +1113,9 @@ class ParticleSystemComponent extends Component { } /** - * Gets the id of the {@link Asset} used to set the normalMap. + * Gets the {@link Asset} used to set the normalMap. * - * @type {Asset|number|null} + * @type {Asset|null} */ get normalMapAsset() { return this._normalMapAsset; @@ -1147,9 +1147,9 @@ class ParticleSystemComponent extends Component { } /** - * Sets the {@link Asset} (or asset id) used to set the mesh. + * Sets the {@link Asset} used to set the mesh. * - * @type {Asset|number|null} + * @type {Asset|null} */ set meshAsset(arg) { const assets = this.system.app.assets; @@ -1177,18 +1177,18 @@ class ParticleSystemComponent extends Component { } /** - * Gets the id of the {@link Asset} used to set the mesh. + * Gets the {@link Asset} used to set the mesh. * - * @type {Asset|number|null} + * @type {Asset|null} */ get meshAsset() { return this._meshAsset; } /** - * Sets the Render {@link Asset} (or asset id) used to set the mesh. + * Sets the Render {@link Asset} used to set the mesh. * - * @type {Asset|number|null} + * @type {Asset|null} */ set renderAsset(arg) { const assets = this.system.app.assets; @@ -1216,9 +1216,9 @@ class ParticleSystemComponent extends Component { } /** - * Gets the id of the Render {@link Asset} used to set the mesh. + * Gets the Render {@link Asset} used to set the mesh. * - * @type {Asset|number|null} + * @type {Asset|null} */ get renderAsset() { return this._renderAsset; From 388b48ef1ec3661e24082fa627a0c4c79d88ece6 Mon Sep 17 00:00:00 2001 From: Will Eastcott Date: Thu, 11 Jun 2026 16:32:21 +0100 Subject: [PATCH 5/5] Apply enabled state before other properties during initialization Asset setters gate deferred loads on the component's enabled state, so the intended enabled value must be in the data store before they run. Previously they always saw the default (true) and could trigger loads for components initialized disabled. Co-Authored-By: Claude Fable 5 --- .../components/particle-system/system.js | 7 +++++ .../particlesystem/component.test.mjs | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/framework/components/particle-system/system.js b/src/framework/components/particle-system/system.js index 137a5070afa..76a865b3cc9 100644 --- a/src/framework/components/particle-system/system.js +++ b/src/framework/components/particle-system/system.js @@ -108,6 +108,13 @@ class ParticleSystemComponentSystem extends ComponentSystem { data.layers = data.layers.slice(0); } + // store the enabled state before applying the other properties, so that + // initialization-time side effects in their setters (e.g. asset loading) + // respect the intended enabled state + if (data.enabled !== undefined) { + component.data.enabled = data.enabled; + } + for (let i = 0; i < _properties.length; i++) { const property = _properties[i]; if (data[property] !== undefined) { diff --git a/test/framework/components/particlesystem/component.test.mjs b/test/framework/components/particlesystem/component.test.mjs index 1152d23c9d1..4e2e2b7f6b9 100644 --- a/test/framework/components/particlesystem/component.test.mjs +++ b/test/framework/components/particlesystem/component.test.mjs @@ -235,6 +235,33 @@ describe('ParticleSystemComponent', function () { expect(e.particlesystem.colorMapAsset).to.equal(assets.colorMap.id); }); + it('Does not trigger asset loads when initialized disabled', function () { + const asset = new Asset('Unloaded', 'texture', { + url: '/test/assets/test.png' + }); + app.assets.add(asset); + + const e = new Entity(); + app.root.addChild(e); + e.addComponent('particlesystem', { + enabled: false, + colorMapAsset: asset.id + }); + + expect(e.particlesystem.enabled).to.be.false; + expect(asset.loading).to.be.false; + expect(asset.loaded).to.be.false; + + // the same data on an enabled component does trigger the load + const e2 = new Entity(); + app.root.addChild(e2); + e2.addComponent('particlesystem', { + colorMapAsset: asset.id + }); + + expect(asset.loading || asset.loaded).to.be.true; + }); + it('Clones a particlesystem component', function () { const e = new Entity(); e.addComponent('particlesystem', {