diff --git a/src/framework/components/collision/component.js b/src/framework/components/collision/component.js index 38ae16a6c90..9abe41fc872 100644 --- a/src/framework/components/collision/component.js +++ b/src/framework/components/collision/component.js @@ -121,6 +121,60 @@ class CollisionComponent extends Component { */ static EVENT_TRIGGERLEAVE = 'triggerleave'; + /** @private */ + _type = 'box'; + + /** @private */ + _halfExtents = new Vec3(0.5, 0.5, 0.5); + + /** @private */ + _linearOffset = new Vec3(); + + /** @private */ + _angularOffset = new Quat(); + + /** @private */ + _radius = 0.5; + + /** @private */ + _axis = 1; + + /** @private */ + _height = 2; + + /** + * @type {number|null} + * @private + */ + _asset = null; + + /** + * @type {number|null} + * @private + */ + _renderAsset = null; + + /** @private */ + _convexHull = false; + + /** @private */ + _shape = null; + + /** + * @type {Model|null} + * @private + */ + _model = null; + + /** @private */ + _render = null; + + /** @private */ + _checkVertexDuplicates = true; + + /** @private */ + _initialized = false; + /** @private */ _compoundParent = null; @@ -137,37 +191,6 @@ class CollisionComponent extends Component { super(system, entity); this.entity.on('insert', this._onInsert, this); - - this.on('set_type', this.onSetType, this); - this.on('set_convexHull', this.onSetModel, this); - this.on('set_halfExtents', this.onSetHalfExtents, this); - this.on('set_linearOffset', this.onSetOffset, this); - this.on('set_angularOffset', this.onSetOffset, this); - this.on('set_radius', this.onSetRadius, this); - this.on('set_height', this.onSetHeight, this); - this.on('set_axis', this.onSetAxis, this); - this.on('set_asset', this.onSetAsset, this); - this.on('set_renderAsset', this.onSetRenderAsset, this); - this.on('set_model', this.onSetModel, this); - this.on('set_render', this.onSetRender, this); - } - - /** - * Sets the enabled state of the component. - * - * @type {boolean} - */ - set enabled(arg) { - this._setValue('enabled', arg); - } - - /** - * Gets the enabled state of the component. - * - * @type {boolean} - */ - get enabled() { - return this.data.enabled; } /** @@ -187,7 +210,13 @@ class CollisionComponent extends Component { * @type {string} */ set type(arg) { - this._setValue('type', arg); + if (this._type === arg) { + return; + } + + const previous = this._type; + this._type = arg; + this.system.changeType(this, previous, arg); } /** @@ -196,7 +225,7 @@ class CollisionComponent extends Component { * @type {string} */ get type() { - return this.data.type; + return this._type; } /** @@ -206,7 +235,15 @@ class CollisionComponent extends Component { * @type {Vec3} */ set halfExtents(arg) { - this._setValue('halfExtents', arg); + if (arg instanceof Vec3) { + this._halfExtents.copy(arg); + } else { + this._halfExtents.set(arg[0], arg[1], arg[2]); + } + + if (this._initialized && this._type === 'box') { + this.system.recreatePhysicalShapes(this); + } } /** @@ -215,7 +252,7 @@ class CollisionComponent extends Component { * @type {Vec3} */ get halfExtents() { - return this.data.halfExtents; + return this._halfExtents; } /** @@ -225,7 +262,17 @@ class CollisionComponent extends Component { * @type {Vec3} */ set linearOffset(arg) { - this._setValue('linearOffset', arg); + if (arg instanceof Vec3) { + this._linearOffset.copy(arg); + } else { + this._linearOffset.set(arg[0], arg[1], arg[2]); + } + + this._updateHasOffset(); + + if (this._initialized) { + this.system.recreatePhysicalShapes(this); + } } /** @@ -235,7 +282,7 @@ class CollisionComponent extends Component { * @type {Vec3} */ get linearOffset() { - return this.data.linearOffset; + return this._linearOffset; } /** @@ -245,7 +292,20 @@ class CollisionComponent extends Component { * @type {Quat} */ set angularOffset(arg) { - this._setValue('angularOffset', arg); + if (arg instanceof Quat) { + this._angularOffset.copy(arg); + } else if (arg.length === 3) { + // allow for euler angles to be passed as a 3 length array + this._angularOffset.setFromEulerAngles(arg[0], arg[1], arg[2]); + } else { + this._angularOffset.set(arg[0], arg[1], arg[2], arg[3]); + } + + this._updateHasOffset(); + + if (this._initialized) { + this.system.recreatePhysicalShapes(this); + } } /** @@ -254,7 +314,7 @@ class CollisionComponent extends Component { * @type {Quat} */ get angularOffset() { - return this.data.angularOffset; + return this._angularOffset; } /** @@ -264,7 +324,12 @@ class CollisionComponent extends Component { * @type {number} */ set radius(arg) { - this._setValue('radius', arg); + this._radius = arg; + + const t = this._type; + if (this._initialized && (t === 'sphere' || t === 'capsule' || t === 'cylinder' || t === 'cone')) { + this.system.recreatePhysicalShapes(this); + } } /** @@ -273,7 +338,7 @@ class CollisionComponent extends Component { * @type {number} */ get radius() { - return this.data.radius; + return this._radius; } /** @@ -283,7 +348,12 @@ class CollisionComponent extends Component { * @type {number} */ set axis(arg) { - this._setValue('axis', arg); + this._axis = arg; + + const t = this._type; + if (this._initialized && (t === 'capsule' || t === 'cylinder' || t === 'cone')) { + this.system.recreatePhysicalShapes(this); + } } /** @@ -293,7 +363,7 @@ class CollisionComponent extends Component { * @type {number} */ get axis() { - return this.data.axis; + return this._axis; } /** @@ -303,7 +373,12 @@ class CollisionComponent extends Component { * @type {number} */ set height(arg) { - this._setValue('height', arg); + this._height = arg; + + const t = this._type; + if (this._initialized && (t === 'capsule' || t === 'cylinder' || t === 'cone')) { + this.system.recreatePhysicalShapes(this); + } } /** @@ -313,7 +388,7 @@ class CollisionComponent extends Component { * @type {number} */ get height() { - return this.data.height; + return this._height; } /** @@ -322,7 +397,36 @@ class CollisionComponent extends Component { * @type {Asset|number|null} */ set asset(arg) { - this._setValue('asset', arg); + const assets = this.system.app.assets; + + if (this._asset) { + // remove the listener registered on the previous asset + const asset = assets.get(this._asset); + if (asset) { + asset.off('remove', this.onAssetRemoved, this); + } + } + + this._asset = arg instanceof Asset ? arg.id : arg; + + if (arg) { + const asset = assets.get(this._asset); + if (asset) { + // make sure we don't subscribe twice + asset.off('remove', this.onAssetRemoved, this); + asset.on('remove', this.onAssetRemoved, this); + } + } + + if (this._initialized && this._type === 'mesh') { + if (!arg) { + // if asset is null set model to null so that the shape is + // removed from the simulation - write the private field so + // recreatePhysicalShapes below performs the single rebuild + this._model = null; + } + this.system.recreatePhysicalShapes(this); + } } /** @@ -331,7 +435,7 @@ class CollisionComponent extends Component { * @type {Asset|number|null} */ get asset() { - return this.data.asset; + return this._asset; } /** @@ -341,7 +445,36 @@ class CollisionComponent extends Component { * @type {Asset|number|null} */ set renderAsset(arg) { - this._setValue('renderAsset', arg); + const assets = this.system.app.assets; + + if (this._renderAsset) { + // remove the listener registered on the previous asset + const asset = assets.get(this._renderAsset); + if (asset) { + asset.off('remove', this.onRenderAssetRemoved, this); + } + } + + this._renderAsset = arg instanceof Asset ? arg.id : arg; + + if (arg) { + const asset = assets.get(this._renderAsset); + if (asset) { + // make sure we don't subscribe twice + asset.off('remove', this.onRenderAssetRemoved, this); + asset.on('remove', this.onRenderAssetRemoved, this); + } + } + + if (this._initialized && this._type === 'mesh') { + if (!arg) { + // if render asset is null set render to null so that the + // shape is removed from the simulation - write the private + // field so recreatePhysicalShapes performs the single rebuild + this._render = null; + } + this.system.recreatePhysicalShapes(this); + } } /** @@ -350,7 +483,7 @@ class CollisionComponent extends Component { * @type {Asset|number|null} */ get renderAsset() { - return this.data.renderAsset; + return this._renderAsset; } /** @@ -361,7 +494,11 @@ class CollisionComponent extends Component { * @type {boolean} */ set convexHull(arg) { - this._setValue('convexHull', arg); + this._convexHull = arg; + + if (this._initialized && this._type === 'mesh') { + this.system.implementations.mesh.doRecreatePhysicalShape(this); + } } /** @@ -370,15 +507,15 @@ class CollisionComponent extends Component { * @type {boolean} */ get convexHull() { - return this.data.convexHull; + return this._convexHull; } set shape(arg) { - this._setValue('shape', arg); + this._shape = arg; } get shape() { - return this.data.shape; + return this._shape; } /** @@ -387,7 +524,14 @@ class CollisionComponent extends Component { * @type {Model | null} */ set model(arg) { - this._setValue('model', arg); + this._model = arg; + + if (this._initialized && this._type === 'mesh') { + // recreate physical shapes skipping loading the model + // from the 'asset' as the model passed in might + // have been created procedurally + this.system.implementations.mesh.doRecreatePhysicalShape(this); + } } /** @@ -396,15 +540,21 @@ class CollisionComponent extends Component { * @type {Model | null} */ get model() { - return this.data.model; + return this._model; } set render(arg) { - this._setValue('render', arg); + this._render = arg; + + if (this._initialized && this._type === 'mesh') { + // recreate physical shapes skipping loading the render asset + // as the render passed in might have been created procedurally + this.system.implementations.mesh.doRecreatePhysicalShape(this); + } } get render() { - return this.data.render; + return this._render; } /** @@ -413,7 +563,7 @@ class CollisionComponent extends Component { * @type {boolean} */ set checkVertexDuplicates(arg) { - this._setValue('checkVertexDuplicates', arg); + this._checkVertexDuplicates = arg; } /** @@ -422,200 +572,14 @@ class CollisionComponent extends Component { * @type {boolean} */ get checkVertexDuplicates() { - return this.data.checkVertexDuplicates; + return this._checkVertexDuplicates; } - /** @ignore */ - _setValue(name, value) { - const data = this.data; - const oldValue = data[name]; - data[name] = value; - this.fire('set', name, oldValue, value); - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetType(name, oldValue, newValue) { - if (oldValue !== newValue) { - this.system.changeType(this, oldValue, newValue); - } - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetHalfExtents(name, oldValue, newValue) { - const t = this.data.type; - if (this.data.initialized && t === 'box') { - this.system.recreatePhysicalShapes(this); - } - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetOffset(name, oldValue, newValue) { + /** @private */ + _updateHasOffset() { this._hasOffset = - !this.data.linearOffset.equals(Vec3.ZERO) || - !this.data.angularOffset.equals(Quat.IDENTITY); - - if (this.data.initialized) { - this.system.recreatePhysicalShapes(this); - } - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetRadius(name, oldValue, newValue) { - const t = this.data.type; - if (this.data.initialized && (t === 'sphere' || t === 'capsule' || t === 'cylinder' || t === 'cone')) { - this.system.recreatePhysicalShapes(this); - } - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetHeight(name, oldValue, newValue) { - const t = this.data.type; - if (this.data.initialized && (t === 'capsule' || t === 'cylinder' || t === 'cone')) { - this.system.recreatePhysicalShapes(this); - } - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetAxis(name, oldValue, newValue) { - const t = this.data.type; - if (this.data.initialized && (t === 'capsule' || t === 'cylinder' || t === 'cone')) { - this.system.recreatePhysicalShapes(this); - } - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetAsset(name, oldValue, newValue) { - const assets = this.system.app.assets; - - if (oldValue) { - // Remove old listeners - const asset = assets.get(oldValue); - if (asset) { - asset.off('remove', this.onAssetRemoved, this); - } - } - - if (newValue) { - if (newValue instanceof Asset) { - this.data.asset = newValue.id; - } - - const asset = assets.get(this.data.asset); - if (asset) { - // make sure we don't subscribe twice - asset.off('remove', this.onAssetRemoved, this); - asset.on('remove', this.onAssetRemoved, this); - } - } - - if (this.data.initialized && this.data.type === 'mesh') { - if (!newValue) { - // if asset is null set model to null - // so that it's going to be removed from the simulation - this.data.model = null; - } - this.system.recreatePhysicalShapes(this); - } - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetRenderAsset(name, oldValue, newValue) { - const assets = this.system.app.assets; - - if (oldValue) { - // Remove old listeners - const asset = assets.get(oldValue); - if (asset) { - asset.off('remove', this.onRenderAssetRemoved, this); - } - } - - if (newValue) { - if (newValue instanceof Asset) { - this.data.renderAsset = newValue.id; - } - - const asset = assets.get(this.data.renderAsset); - if (asset) { - // make sure we don't subscribe twice - asset.off('remove', this.onRenderAssetRemoved, this); - asset.on('remove', this.onRenderAssetRemoved, this); - } - } - - if (this.data.initialized && this.data.type === 'mesh') { - if (!newValue) { - // if render asset is null set render to null - // so that it's going to be removed from the simulation - this.data.render = null; - } - this.system.recreatePhysicalShapes(this); - } - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetModel(name, oldValue, newValue) { - if (this.data.initialized && this.data.type === 'mesh') { - // recreate physical shapes skipping loading the model - // from the 'asset' as the model passed in newValue might - // have been created procedurally - this.system.implementations.mesh.doRecreatePhysicalShape(this); - } - } - - /** - * @param {string} name - Property name. - * @param {*} oldValue - Previous value of the property. - * @param {*} newValue - New value of the property. - * @private - */ - onSetRender(name, oldValue, newValue) { - this.onSetModel(name, oldValue, newValue); + !this._linearOffset.equals(Vec3.ZERO) || + !this._angularOffset.equals(Quat.IDENTITY); } /** @@ -624,7 +588,7 @@ class CollisionComponent extends Component { */ onAssetRemoved(asset) { asset.off('remove', this.onAssetRemoved, this); - if (this.data.asset === asset.id) { + if (this._asset === asset.id) { this.asset = null; } } @@ -635,7 +599,7 @@ class CollisionComponent extends Component { */ onRenderAssetRemoved(asset) { asset.off('remove', this.onRenderAssetRemoved, this); - if (this.data.renderAsset === asset.id) { + if (this._renderAsset === asset.id) { this.renderAsset = null; } } @@ -646,7 +610,7 @@ class CollisionComponent extends Component { * @private */ getCompoundChildShapeIndex(shape) { - const compound = this.data.shape; + const compound = this._shape; const shapes = compound.getNumChildShapes(); for (let i = 0; i < shapes; i++) { @@ -730,7 +694,7 @@ class CollisionComponent extends Component { if (this._hasOffset) { const rot = this.entity.getRotation(); - const lo = this.data.linearOffset; + const lo = this._linearOffset; _quat.copy(rot).transformVector(lo, _vec3); return _vec3.add(pos); @@ -748,18 +712,18 @@ class CollisionComponent extends Component { const rot = this.entity.getRotation(); if (this._hasOffset) { - return _quat.copy(rot).mul(this.data.angularOffset); + return _quat.copy(rot).mul(this._angularOffset); } return rot; } onEnable() { - if (this.data.type === 'mesh' && (this.data.asset || this.data.renderAsset) && this.data.initialized) { - const asset = this.system.app.assets.get(this.data.asset || this.data.renderAsset); + if (this._type === 'mesh' && (this._asset || this._renderAsset) && this._initialized) { + const asset = this.system.app.assets.get(this._asset || this._renderAsset); // recreate the collision shape if the model asset is not loaded // or the shape does not exist - if (asset && (!asset.resource || !this.data.shape)) { + if (asset && (!asset.resource || !this._shape)) { this.system.recreatePhysicalShapes(this); return; } @@ -774,7 +738,7 @@ class CollisionComponent extends Component { this.system.recreatePhysicalShapes(this._compoundParent); } else { const transform = this.system._getNodeTransform(this.entity, this._compoundParent.entity); - this._compoundParent.shape.addChildShape(transform, this.data.shape); + this._compoundParent.shape.addChildShape(transform, this._shape); Ammo.destroy(transform); if (this._compoundParent.entity.rigidbody) { @@ -791,7 +755,7 @@ class CollisionComponent extends Component { this.entity.rigidbody.disableSimulation(); } else if (this._compoundParent && this !== this._compoundParent) { if (!this._compoundParent.entity._destroying) { - this.system._removeCompoundChild(this._compoundParent, this.data.shape); + this.system._removeCompoundChild(this._compoundParent, this._shape); if (this._compoundParent.entity.rigidbody) { this._compoundParent.entity.rigidbody.activate(); diff --git a/src/framework/components/collision/data.js b/src/framework/components/collision/data.js index 1e6c45c3c2d..dee92c1da74 100644 --- a/src/framework/components/collision/data.js +++ b/src/framework/components/collision/data.js @@ -1,47 +1,5 @@ -import { Quat } from '../../../core/math/quat.js'; -import { Vec3 } from '../../../core/math/vec3.js'; - -/** - * @import { Asset } from '../../../framework/asset/asset.js' - * @import { Model } from '../../../scene/model.js' - */ - class CollisionComponentData { enabled = true; - - type = 'box'; - - halfExtents = new Vec3(0.5, 0.5, 0.5); - - linearOffset = new Vec3(); - - angularOffset = new Quat(); - - radius = 0.5; - - axis = 1; - - height = 2; - - convexHull = false; - - /** @type {Asset|number|null} */ - asset = null; - - /** @type {Asset|number|null} */ - renderAsset = null; - - checkVertexDuplicates = true; - - // Non-serialized properties - shape = null; - - /** @type {Model|null} */ - model = null; - - render = null; - - initialized = false; } export { CollisionComponentData }; diff --git a/src/framework/components/collision/system.js b/src/framework/components/collision/system.js index 19df92048c7..4c8de0a2674 100644 --- a/src/framework/components/collision/system.js +++ b/src/framework/components/collision/system.js @@ -5,6 +5,7 @@ import { Vec3 } from '../../../core/math/vec3.js'; import { SEMANTIC_POSITION } from '../../../platform/graphics/constants.js'; import { GraphNode } from '../../../scene/graph-node.js'; import { Model } from '../../../scene/model.js'; +import { Component } from '../component.js'; import { ComponentSystem } from '../system.js'; import { CollisionComponent } from './component.js'; import { CollisionComponentData } from './data.js'; @@ -20,21 +21,21 @@ const p2 = new Vec3(); const quat = new Quat(); const tempGraphNode = new GraphNode(); -const _schema = [ - 'enabled', - 'type', +const _schema = ['enabled']; + +const _properties = [ 'halfExtents', - 'linearOffset', - 'angularOffset', 'radius', 'axis', 'height', 'convexHull', - 'asset', - 'renderAsset', 'shape', 'model', + 'asset', 'render', + 'renderAsset', + 'linearOffset', + 'angularOffset', 'checkVertexDuplicates' ]; @@ -45,29 +46,29 @@ class CollisionSystemImpl { } // Called before the call to system.super.initializeComponentData is made - beforeInitialize(component, data) { - data.shape = null; + beforeInitialize(component) { + component._shape = null; - data.model = new Model(); - data.model.graph = new GraphNode(); + const model = new Model(); + model.graph = new GraphNode(); + component._model = model; } // Called after the call to system.super.initializeComponentData is made - afterInitialize(component, data) { + afterInitialize(component) { this.recreatePhysicalShapes(component); - component.data.initialized = true; + component._initialized = true; } // Called when a collision component changes type in order to recreate debug and physical shapes - reset(component, data) { - this.beforeInitialize(component, data); - this.afterInitialize(component, data); + reset(component) { + this.beforeInitialize(component); + this.afterInitialize(component); } // Re-creates rigid bodies / triggers recreatePhysicalShapes(component) { const entity = component.entity; - const data = component.data; if (typeof Ammo !== 'undefined') { if (entity.trigger) { @@ -75,10 +76,10 @@ class CollisionSystemImpl { delete entity.trigger; } - if (data.shape) { + if (component._shape) { if (component._compoundParent) { if (component !== component._compoundParent) { - this.system._removeCompoundChild(component._compoundParent, data.shape); + this.system._removeCompoundChild(component._compoundParent, component._shape); } if (component._compoundParent.entity.rigidbody) { @@ -86,18 +87,18 @@ class CollisionSystemImpl { } } - this.destroyShape(data); + this.destroyShape(component); } - data.shape = this.createPhysicalShape(component.entity, data); + component._shape = this.createPhysicalShape(entity, component); const firstCompoundChild = !component._compoundParent; - if (data.type === 'compound' && (!component._compoundParent || component === component._compoundParent)) { + if (component._type === 'compound' && (!component._compoundParent || component === component._compoundParent)) { component._compoundParent = component; entity.forEach(this._addEachDescendant, component); - } else if (data.type !== 'compound') { + } else if (component._type !== 'compound') { if (!component.rigidbody) { component._compoundParent = null; let parent = entity.parent; @@ -134,9 +135,9 @@ class CollisionSystemImpl { } } else if (!component._compoundParent) { if (!entity.trigger) { - entity.trigger = new Trigger(this.system.app, component, data); + entity.trigger = new Trigger(this.system.app, component); } else { - entity.trigger.initialize(data); + entity.trigger.initialize(); } } } @@ -145,7 +146,7 @@ class CollisionSystemImpl { // Creates a physical shape for the collision. This consists // of the actual shape that will be used for the rigid bodies / triggers of // the collision. - createPhysicalShape(entity, data) { + createPhysicalShape(entity, component) { return undefined; } @@ -155,17 +156,17 @@ class CollisionSystemImpl { } } - destroyShape(data) { - if (data.shape) { - Ammo.destroy(data.shape); - data.shape = null; + destroyShape(component) { + if (component._shape) { + Ammo.destroy(component._shape); + component._shape = null; } } beforeRemove(entity, component) { - if (component.data.shape) { + if (component._shape) { if (component._compoundParent && !component._compoundParent.entity._destroying) { - this.system._removeCompoundChild(component._compoundParent, component.data.shape); + this.system._removeCompoundChild(component._compoundParent, component._shape); if (component._compoundParent.entity.rigidbody) { component._compoundParent.entity.rigidbody.activate(); @@ -174,52 +175,16 @@ class CollisionSystemImpl { component._compoundParent = null; - this.destroyShape(component.data); - } - } - - // Called when the collision is removed - remove(entity, data) { - if (entity.rigidbody && entity.rigidbody.body) { - entity.rigidbody.disableSimulation(); - } - - if (entity.trigger) { - entity.trigger.destroy(); - delete entity.trigger; + this.destroyShape(component); } } - - // Called when the collision is cloned to another entity - clone(entity, clone) { - const src = this.system.store[entity.guid]; - - const data = { - enabled: src.data.enabled, - type: src.data.type, - halfExtents: [src.data.halfExtents.x, src.data.halfExtents.y, src.data.halfExtents.z], - linearOffset: [src.data.linearOffset.x, src.data.linearOffset.y, src.data.linearOffset.z], - angularOffset: [src.data.angularOffset.x, src.data.angularOffset.y, src.data.angularOffset.z, src.data.angularOffset.w], - radius: src.data.radius, - axis: src.data.axis, - height: src.data.height, - convexHull: src.data.convexHull, - asset: src.data.asset, - renderAsset: src.data.renderAsset, - model: src.data.model, - render: src.data.render, - checkVertexDuplicates: src.data.checkVertexDuplicates - }; - - return this.system.addComponent(clone, data); - } } // Box Collision System class CollisionBoxSystemImpl extends CollisionSystemImpl { - createPhysicalShape(entity, data) { + createPhysicalShape(entity, component) { if (typeof Ammo !== 'undefined') { - const he = data.halfExtents; + const he = component.halfExtents; const ammoHe = new Ammo.btVector3(he ? he.x : 0.5, he ? he.y : 0.5, he ? he.z : 0.5); const shape = new Ammo.btBoxShape(ammoHe); Ammo.destroy(ammoHe); @@ -231,9 +196,9 @@ class CollisionBoxSystemImpl extends CollisionSystemImpl { // Sphere Collision System class CollisionSphereSystemImpl extends CollisionSystemImpl { - createPhysicalShape(entity, data) { + createPhysicalShape(entity, component) { if (typeof Ammo !== 'undefined') { - return new Ammo.btSphereShape(data.radius); + return new Ammo.btSphereShape(component.radius); } return undefined; } @@ -241,10 +206,10 @@ class CollisionSphereSystemImpl extends CollisionSystemImpl { // Capsule Collision System class CollisionCapsuleSystemImpl extends CollisionSystemImpl { - createPhysicalShape(entity, data) { - const axis = data.axis ?? 1; - const radius = data.radius ?? 0.5; - const height = Math.max((data.height ?? 2) - 2 * radius, 0); + createPhysicalShape(entity, component) { + const axis = component.axis ?? 1; + const radius = component.radius ?? 0.5; + const height = Math.max((component.height ?? 2) - 2 * radius, 0); let shape = null; @@ -268,10 +233,10 @@ class CollisionCapsuleSystemImpl extends CollisionSystemImpl { // Cylinder Collision System class CollisionCylinderSystemImpl extends CollisionSystemImpl { - createPhysicalShape(entity, data) { - const axis = data.axis ?? 1; - const radius = data.radius ?? 0.5; - const height = data.height ?? 1; + createPhysicalShape(entity, component) { + const axis = component.axis ?? 1; + const radius = component.radius ?? 0.5; + const height = component.height ?? 1; let halfExtents = null; let shape = null; @@ -303,10 +268,10 @@ class CollisionCylinderSystemImpl extends CollisionSystemImpl { // Cone Collision System class CollisionConeSystemImpl extends CollisionSystemImpl { - createPhysicalShape(entity, data) { - const axis = data.axis ?? 1; - const radius = data.radius ?? 0.5; - const height = data.height ?? 1; + createPhysicalShape(entity, component) { + const axis = component.axis ?? 1; + const radius = component.radius ?? 0.5; + const height = component.height ?? 1; let shape = null; @@ -332,7 +297,7 @@ class CollisionConeSystemImpl extends CollisionSystemImpl { class CollisionMeshSystemImpl extends CollisionSystemImpl { // override for the mesh implementation because the asset model needs // special handling - beforeInitialize(component, data) {} + beforeInitialize(component) {} createAmmoHull(mesh, node, shape, scale) { const hull = new Ammo.btConvexHullShape(); @@ -450,28 +415,28 @@ class CollisionMeshSystemImpl extends CollisionSystemImpl { Ammo.destroy(transform); } - createPhysicalShape(entity, data) { + createPhysicalShape(entity, component) { if (typeof Ammo === 'undefined') return undefined; - if (data.model || data.render) { + if (component._model || component._render) { const shape = new Ammo.btCompoundShape(); const entityTransform = entity.getWorldTransform(); const scale = entityTransform.getScale(); - if (data.render) { - const meshes = data.render.meshes; + if (component._render) { + const meshes = component._render.meshes; for (let i = 0; i < meshes.length; i++) { - if (data.convexHull) { + if (component._convexHull) { this.createAmmoHull(meshes[i], tempGraphNode, shape, scale); } else { - this.createAmmoMesh(meshes[i], tempGraphNode, shape, scale, data.checkVertexDuplicates); + this.createAmmoMesh(meshes[i], tempGraphNode, shape, scale, component._checkVertexDuplicates); } } - } else if (data.model) { - const meshInstances = data.model.meshInstances; + } else if (component._model) { + const meshInstances = component._model.meshInstances; for (let i = 0; i < meshInstances.length; i++) { - this.createAmmoMesh(meshInstances[i].mesh, meshInstances[i].node, shape, null, data.checkVertexDuplicates); + this.createAmmoMesh(meshInstances[i].mesh, meshInstances[i].node, shape, null, component._checkVertexDuplicates); } const vec = new Ammo.btVector3(scale.x, scale.y, scale.z); shape.setLocalScaling(vec); @@ -485,14 +450,12 @@ class CollisionMeshSystemImpl extends CollisionSystemImpl { } recreatePhysicalShapes(component) { - const data = component.data; - - if (data.renderAsset || data.asset) { + if (component._renderAsset || component._asset) { if (component.enabled && component.entity.enabled) { this.loadAsset( component, - data.renderAsset || data.asset, - data.renderAsset ? 'render' : 'model' + component._renderAsset || component._asset, + component._renderAsset ? 'render' : 'model' ); return; } @@ -502,16 +465,18 @@ class CollisionMeshSystemImpl extends CollisionSystemImpl { } loadAsset(component, id, property) { - const data = component.data; const assets = this.system.app.assets; - const previousPropertyValue = data[property]; + // write the loaded resource to the private field - the public setter + // would trigger a second shape rebuild via doRecreatePhysicalShape + const privateProperty = `_${property}`; + const previousPropertyValue = component[privateProperty]; const onAssetFullyReady = (asset) => { - if (data[property] !== previousPropertyValue) { + if (component[privateProperty] !== previousPropertyValue) { // the asset has changed since we started loading it, so ignore this callback return; } - data[property] = asset.resource; + component[privateProperty] = asset.resource; this.doRecreatePhysicalShape(component); }; @@ -545,12 +510,11 @@ class CollisionMeshSystemImpl extends CollisionSystemImpl { doRecreatePhysicalShape(component) { const entity = component.entity; - const data = component.data; - if (data.model || data.render) { - this.destroyShape(data); + if (component._model || component._render) { + this.destroyShape(component); - data.shape = this.createPhysicalShape(entity, data); + component._shape = this.createPhysicalShape(entity, component); if (entity.rigidbody) { entity.rigidbody.disableSimulation(); @@ -561,14 +525,14 @@ class CollisionMeshSystemImpl extends CollisionSystemImpl { } } else { if (!entity.trigger) { - entity.trigger = new Trigger(this.system.app, component, data); + entity.trigger = new Trigger(this.system.app, component); } else { - entity.trigger.initialize(data); + entity.trigger.initialize(); } } } else { this.beforeRemove(entity, component); - this.remove(entity, data); + this.system.onRemove(entity); } } @@ -589,25 +553,25 @@ class CollisionMeshSystemImpl extends CollisionSystemImpl { super.updateTransform(component, position, rotation, scale); } - destroyShape(data) { - if (!data.shape) { + destroyShape(component) { + if (!component._shape) { return; } - const numShapes = data.shape.getNumChildShapes(); + const numShapes = component._shape.getNumChildShapes(); for (let i = 0; i < numShapes; i++) { - const shape = data.shape.getChildShape(i); + const shape = component._shape.getChildShape(i); Ammo.destroy(shape); } - Ammo.destroy(data.shape); - data.shape = null; + Ammo.destroy(component._shape); + component._shape = null; } } // Compound Collision System class CollisionCompoundSystemImpl extends CollisionSystemImpl { - createPhysicalShape(entity, data) { + createPhysicalShape(entity, component) { if (typeof Ammo !== 'undefined') { return new Ammo.btCompoundShape(); } @@ -681,81 +645,44 @@ class CollisionComponentSystem extends ComponentSystem { this.on('remove', this.onRemove, this); } - initializeComponentData(component, _data, properties) { - properties = [ - 'type', - 'halfExtents', - 'radius', - 'axis', - 'height', - 'convexHull', - 'shape', - 'model', - 'asset', - 'render', - 'renderAsset', - 'enabled', - 'linearOffset', - 'angularOffset', - 'checkVertexDuplicates' - ]; - - // duplicate the input data because we are modifying it - const data = {}; - for (let i = 0, len = properties.length; i < len; i++) { - const property = properties[i]; - data[property] = _data[property]; + initializeComponentData(component, data, properties) { + // resolve the type first - falsy values fall back to the current + // type, matching the old initializer, and the private field is + // written directly so the type setter does not fire changeType + // before the component is initialized + if (data.type) { + component._type = data.type; } - // asset takes priority over model - // but they are both trying to change the mesh - // so remove one of them to avoid conflicts + // asset takes priority over model and render but they are all trying + // to change the mesh, so remove the conflicting inputs + properties = _properties.slice(); let idx; - if (_data.hasOwnProperty('asset')) { + if (data.asset !== undefined) { idx = properties.indexOf('model'); - if (idx !== -1) { - properties.splice(idx, 1); - } + properties.splice(idx, 1); idx = properties.indexOf('render'); - if (idx !== -1) { - properties.splice(idx, 1); - } - } else if (_data.hasOwnProperty('model')) { + properties.splice(idx, 1); + } else if (data.model !== undefined) { idx = properties.indexOf('asset'); - if (idx !== -1) { - properties.splice(idx, 1); - } - } - - if (!data.type) { - data.type = component.data.type; + properties.splice(idx, 1); } - component.data.type = data.type; - if (Array.isArray(data.halfExtents)) { - data.halfExtents = new Vec3(data.halfExtents); - } - - if (Array.isArray(data.linearOffset)) { - data.linearOffset = new Vec3(data.linearOffset); - } - - if (Array.isArray(data.angularOffset)) { - // Allow for euler angles to be passed as a 3 length array - const values = data.angularOffset; - if (values.length === 3) { - data.angularOffset = new Quat().setFromEulerAngles(values[0], values[1], values[2]); - } else { - data.angularOffset = new Quat(data.angularOffset); + // apply the user-supplied properties through the public setters - all + // side effects are gated on _initialized, which is still false here + for (let i = 0; i < properties.length; i++) { + const property = properties[i]; + if (data[property] !== undefined) { + component[property] = data[property]; } } - const impl = this._createImplementation(data.type); - impl.beforeInitialize(component, data); + const impl = this._createImplementation(component._type); + impl.beforeInitialize(component); - super.initializeComponentData(component, data, properties); + super.initializeComponentData(component, data, _schema); - impl.afterInitialize(component, data); + impl.afterInitialize(component); } // Creates an implementation based on the collision type and caches it @@ -794,22 +721,40 @@ class CollisionComponentSystem extends ComponentSystem { return this.implementations[type]; } - // Gets an existing implementation for the specified entity - _getImplementation(entity) { - return this.implementations[entity.collision.data.type]; - } - cloneComponent(entity, clone) { - return this._getImplementation(entity).clone(entity, clone); + const c = entity.collision; + return this.addComponent(clone, { + enabled: c.enabled, + type: c.type, + halfExtents: c.halfExtents, + linearOffset: c.linearOffset, + angularOffset: c.angularOffset, + radius: c.radius, + axis: c.axis, + height: c.height, + convexHull: c.convexHull, + asset: c.asset, + renderAsset: c.renderAsset, + model: c.model, + render: c.render, + checkVertexDuplicates: c.checkVertexDuplicates + }); } onBeforeRemove(entity, component) { - this.implementations[component.data.type].beforeRemove(entity, component); + this.implementations[component.type].beforeRemove(entity, component); component.onBeforeRemove(); } - onRemove(entity, data) { - this.implementations[data.type].remove(entity, data); + onRemove(entity) { + if (entity.rigidbody && entity.rigidbody.body) { + entity.rigidbody.disableSimulation(); + } + + if (entity.trigger) { + entity.trigger.destroy(); + delete entity.trigger; + } } updateCompoundChildTransform(entity, forceUpdate) { @@ -820,7 +765,7 @@ class CollisionComponentSystem extends ComponentSystem { const transform = this._getNodeTransform(entity, parentComponent.entity); const idx = parentComponent.getCompoundChildShapeIndex(entity.collision.shape); if (idx === null) { - parentComponent.shape.addChildShape(transform, entity.collision.data.shape); + parentComponent.shape.addChildShape(transform, entity.collision.shape); } else { parentComponent.shape.updateChildTransform(idx, transform, true); } @@ -844,19 +789,19 @@ class CollisionComponentSystem extends ComponentSystem { } onTransformChanged(component, position, rotation, scale) { - this.implementations[component.data.type].updateTransform(component, position, rotation, scale); + this.implementations[component.type].updateTransform(component, position, rotation, scale); } // Destroys the previous collision type and creates a new one based on the new type provided changeType(component, previousType, newType) { this.implementations[previousType].beforeRemove(component.entity, component); - this.implementations[previousType].remove(component.entity, component.data); - this._createImplementation(newType).reset(component, component.data); + this.onRemove(component.entity); + this._createImplementation(newType).reset(component); } // Recreates rigid bodies or triggers for the specified component recreatePhysicalShapes(component) { - this.implementations[component.data.type].recreatePhysicalShapes(component); + this.implementations[component.type].recreatePhysicalShapes(component); } _calculateNodeRelativeTransform(node, relative) { @@ -898,8 +843,8 @@ class CollisionComponentSystem extends ComponentSystem { const component = node.collision; if (component && component._hasOffset) { - const lo = component.data.linearOffset; - const ao = component.data.angularOffset; + const lo = component.linearOffset; + const ao = component.angularOffset; const newOrigin = p2; quat.copy(rot).transformVector(lo, newOrigin); @@ -930,4 +875,6 @@ class CollisionComponentSystem extends ComponentSystem { } } +Component._buildAccessors(CollisionComponent.prototype, _schema); + export { CollisionComponentSystem }; diff --git a/src/framework/components/collision/trigger.js b/src/framework/components/collision/trigger.js index c9a17783647..c5bbf22b7bb 100644 --- a/src/framework/components/collision/trigger.js +++ b/src/framework/components/collision/trigger.js @@ -2,7 +2,7 @@ import { BODYFLAG_NORESPONSE_OBJECT, BODYMASK_NOT_STATIC, BODYGROUP_TRIGGER, BOD /** * @import { AppBase } from '../../app-base.js' - * @import { Component } from '../component.js' + * @import { CollisionComponent } from './component.js' */ let _ammoVec1, _ammoQuat, _ammoTransform; @@ -16,10 +16,9 @@ class Trigger { * Create a new Trigger instance. * * @param {AppBase} app - The running {@link AppBase}. - * @param {Component} component - The component for which the trigger will be created. - * @param {object} data - The data for the component. + * @param {CollisionComponent} component - The component for which the trigger will be created. */ - constructor(app, component, data) { + constructor(app, component) { this.entity = component.entity; this.component = component; this.app = app; @@ -30,12 +29,12 @@ class Trigger { _ammoTransform = new Ammo.btTransform(); } - this.initialize(data); + this.initialize(); } - initialize(data) { + initialize() { const entity = this.entity; - const shape = data.shape; + const shape = this.component.shape; if (shape && typeof Ammo !== 'undefined') { if (entity.trigger) { diff --git a/src/framework/components/rigid-body/component.js b/src/framework/components/rigid-body/component.js index 7b641f6376b..040765957d7 100644 --- a/src/framework/components/rigid-body/component.js +++ b/src/framework/components/rigid-body/component.js @@ -1094,8 +1094,8 @@ class RigidBodyComponent extends Component { const component = entity.collision; if (component && component._hasOffset) { - const lo = component.data.linearOffset; - const ao = component.data.angularOffset; + const lo = component.linearOffset; + const ao = component.angularOffset; // Un-rotate the angular offset and then use the new rotation to // un-translate the linear offset in local space diff --git a/test/framework/components/collision/component.test.mjs b/test/framework/components/collision/component.test.mjs new file mode 100644 index 00000000000..ec5498ff512 --- /dev/null +++ b/test/framework/components/collision/component.test.mjs @@ -0,0 +1,463 @@ +import { expect } from 'chai'; + +import { Quat } from '../../../../src/core/math/quat.js'; +import { Vec3 } from '../../../../src/core/math/vec3.js'; +import { Asset } from '../../../../src/framework/asset/asset.js'; +import { Entity } from '../../../../src/framework/entity.js'; +import { Model } from '../../../../src/scene/model.js'; +import { createApp } from '../../../app.mjs'; +import { jsdomSetup, jsdomTeardown } from '../../../jsdom.mjs'; + +describe('CollisionComponent', function () { + let app; + + beforeEach(function () { + jsdomSetup(); + app = createApp(); + }); + + afterEach(function () { + app?.destroy(); + app = null; + jsdomTeardown(); + }); + + describe('#addComponent', function () { + + it('creates a component with sensible defaults', function () { + const e = new Entity(); + e.addComponent('collision'); + + expect(e.collision).to.exist; + expect(e.collision.enabled).to.equal(true); + expect(e.collision.type).to.equal('box'); + expect(e.collision.halfExtents.equals(new Vec3(0.5, 0.5, 0.5))).to.equal(true); + expect(e.collision.linearOffset.equals(new Vec3())).to.equal(true); + expect(e.collision.angularOffset.equals(new Quat())).to.equal(true); + expect(e.collision.radius).to.equal(0.5); + expect(e.collision.axis).to.equal(1); + expect(e.collision.height).to.equal(2); + expect(e.collision.convexHull).to.equal(false); + expect(e.collision.asset).to.equal(null); + expect(e.collision.renderAsset).to.equal(null); + expect(e.collision.checkVertexDuplicates).to.equal(true); + expect(e.collision.shape).to.equal(null); + expect(e.collision.render).to.equal(null); + // non-mesh initialization creates a placeholder model + expect(e.collision.model).to.be.an.instanceof(Model); + }); + + it('round-trips every property passed via the data argument', function () { + const e = new Entity(); + e.addComponent('collision', { + enabled: false, + type: 'capsule', + halfExtents: new Vec3(1, 2, 3), + linearOffset: new Vec3(4, 5, 6), + angularOffset: new Quat().setFromEulerAngles(0, 90, 0), + radius: 2, + axis: 0, + height: 5, + convexHull: true, + asset: 42, + renderAsset: 43, + checkVertexDuplicates: false + }); + + const c = e.collision; + expect(c.enabled).to.equal(false); + expect(c.type).to.equal('capsule'); + expect(c.halfExtents.equals(new Vec3(1, 2, 3))).to.equal(true); + expect(c.linearOffset.equals(new Vec3(4, 5, 6))).to.equal(true); + expect(c.angularOffset.equals(new Quat().setFromEulerAngles(0, 90, 0))).to.equal(true); + expect(c.radius).to.equal(2); + expect(c.axis).to.equal(0); + expect(c.height).to.equal(5); + expect(c.convexHull).to.equal(true); + expect(c.asset).to.equal(42); + expect(c.renderAsset).to.equal(43); + expect(c.checkVertexDuplicates).to.equal(false); + }); + + it('converts arrays to Vec3 for halfExtents and linearOffset', function () { + const e = new Entity(); + e.addComponent('collision', { + halfExtents: [1, 2, 3], + linearOffset: [4, 5, 6] + }); + + expect(e.collision.halfExtents).to.be.an.instanceof(Vec3); + expect(e.collision.halfExtents.equals(new Vec3(1, 2, 3))).to.equal(true); + expect(e.collision.linearOffset).to.be.an.instanceof(Vec3); + expect(e.collision.linearOffset.equals(new Vec3(4, 5, 6))).to.equal(true); + }); + + it('converts a 4 element array to Quat for angularOffset', function () { + const source = new Quat().setFromEulerAngles(10, 20, 30); + + const e = new Entity(); + e.addComponent('collision', { + angularOffset: [source.x, source.y, source.z, source.w] + }); + + expect(e.collision.angularOffset).to.be.an.instanceof(Quat); + expect(e.collision.angularOffset.equals(source)).to.equal(true); + }); + + it('treats a 3 element array as euler angles for angularOffset', function () { + const e = new Entity(); + e.addComponent('collision', { + angularOffset: [0, 90, 0] + }); + + expect(e.collision.angularOffset.equals(new Quat().setFromEulerAngles(0, 90, 0))).to.equal(true); + }); + + it('preserves class-field defaults when properties are passed as explicit undefined', function () { + const e = new Entity(); + e.addComponent('collision', { + halfExtents: undefined, + radius: undefined, + type: undefined + }); + + expect(e.collision.type).to.equal('box'); + expect(e.collision.halfExtents.equals(new Vec3(0.5, 0.5, 0.5))).to.equal(true); + expect(e.collision.radius).to.equal(0.5); + }); + + it('falls back to the default type for falsy type values', function () { + const e = new Entity(); + e.addComponent('collision', { type: null }); + + expect(e.collision.type).to.equal('box'); + expect(app.systems.collision.implementations.box).to.exist; + }); + + it('copies Vec3 inputs so caller mutations do not leak into component state', function () { + const source = new Vec3(1, 2, 3); + + const e = new Entity(); + e.addComponent('collision', { halfExtents: source }); + + expect(e.collision.halfExtents).to.not.equal(source); + + source.x = 9; + expect(e.collision.halfExtents.x).to.equal(1); + }); + + it('ignores model and render when an asset is also supplied', function () { + const model = new Model(); + + const e = new Entity(); + e.addComponent('collision', { type: 'mesh', asset: 99, model: model }); + + expect(e.collision.asset).to.equal(99); + expect(e.collision.model).to.equal(null); + }); + + it('accepts a model when no asset is supplied', function () { + const model = new Model(); + + const e = new Entity(); + e.addComponent('collision', { type: 'mesh', model: model }); + + expect(e.collision.model).to.equal(model); + }); + + }); + + describe('#asset', function () { + + it('normalizes an Asset instance to its id', function () { + const asset = new Asset('model', 'model'); + app.assets.add(asset); + + const e = new Entity(); + e.addComponent('collision'); + + e.collision.asset = asset; + expect(e.collision.asset).to.equal(asset.id); + }); + + it('normalizes an Asset instance to its id for renderAsset', function () { + const asset = new Asset('render', 'render'); + app.assets.add(asset); + + const e = new Entity(); + e.addComponent('collision'); + + e.collision.renderAsset = asset; + expect(e.collision.renderAsset).to.equal(asset.id); + }); + + it('clears the asset property when the asset is removed from the registry', function () { + const asset = new Asset('model', 'model'); + app.assets.add(asset); + + const e = new Entity(); + e.addComponent('collision', { asset: asset.id }); + + app.assets.remove(asset); + expect(e.collision.asset).to.equal(null); + }); + + it('unsubscribes from the previous asset when reassigned', function () { + const asset1 = new Asset('model1', 'model'); + const asset2 = new Asset('model2', 'model'); + app.assets.add(asset1); + app.assets.add(asset2); + + const e = new Entity(); + e.addComponent('collision'); + + e.collision.asset = asset1; + expect(asset1.hasEvent('remove')).to.equal(true); + + e.collision.asset = asset2; + expect(asset1.hasEvent('remove')).to.equal(false); + expect(asset2.hasEvent('remove')).to.equal(true); + }); + + }); + + describe('#type', function () { + + it('changes type and creates the new implementation', function () { + const e = new Entity(); + e.addComponent('collision'); + + e.collision.type = 'sphere'; + + expect(e.collision.type).to.equal('sphere'); + expect(app.systems.collision.implementations.sphere).to.exist; + }); + + it('is a no-op when the type is unchanged', function () { + const e = new Entity(); + e.addComponent('collision'); + + const system = app.systems.collision; + let calls = 0; + const original = system.changeType; + system.changeType = function (...args) { + calls++; + return original.apply(this, args); + }; + + e.collision.type = 'box'; + expect(calls).to.equal(0); + + system.changeType = original; + }); + + }); + + describe('shape recreation', function () { + + /** + * Patch the system-level recreatePhysicalShapes dispatcher and count calls. + * + * @returns {{ count: () => number }} The call counter. + */ + function patchRecreate() { + const system = app.systems.collision; + let calls = 0; + system.recreatePhysicalShapes = function () { + calls++; + }; + return { count: () => calls }; + } + + it('recreates the shape when halfExtents is set on a box', function () { + const e = new Entity(); + e.addComponent('collision'); + + const counter = patchRecreate(); + e.collision.halfExtents = new Vec3(1, 2, 3); + + expect(counter.count()).to.equal(1); + }); + + it('recreates the shape when the returned halfExtents is mutated and reassigned', function () { + const e = new Entity(); + e.addComponent('collision'); + + const counter = patchRecreate(); + const he = e.collision.halfExtents; + he.x = 2; + e.collision.halfExtents = he; + + expect(counter.count()).to.equal(1); + expect(e.collision.halfExtents.x).to.equal(2); + }); + + it('converts arrays assigned after initialization', function () { + const e = new Entity(); + e.addComponent('collision'); + + patchRecreate(); + e.collision.halfExtents = [1, 2, 3]; + e.collision.linearOffset = [4, 5, 6]; + + expect(e.collision.halfExtents.equals(new Vec3(1, 2, 3))).to.equal(true); + expect(e.collision.linearOffset.equals(new Vec3(4, 5, 6))).to.equal(true); + }); + + it('recreates the shape for radius, height and axis on a capsule but not a box', function () { + const box = new Entity(); + box.addComponent('collision'); + + const capsule = new Entity(); + capsule.addComponent('collision', { type: 'capsule' }); + + const counter = patchRecreate(); + + box.collision.radius = 1; + box.collision.height = 3; + box.collision.axis = 0; + expect(counter.count()).to.equal(0); + + capsule.collision.radius = 1; + capsule.collision.height = 3; + capsule.collision.axis = 0; + expect(counter.count()).to.equal(3); + }); + + it('routes model, render and convexHull changes to the mesh implementation', function () { + const box = new Entity(); + box.addComponent('collision'); + + const mesh = new Entity(); + mesh.addComponent('collision', { type: 'mesh' }); + + const impl = app.systems.collision.implementations.mesh; + let calls = 0; + impl.doRecreatePhysicalShape = function () { + calls++; + }; + + box.collision.convexHull = true; + box.collision.model = null; + expect(calls).to.equal(0); + + mesh.collision.convexHull = true; + mesh.collision.model = new Model(); + mesh.collision.render = null; + expect(calls).to.equal(3); + }); + + }); + + describe('offsets', function () { + + it('applies linear and angular offsets to the shape transform', function () { + const e = new Entity(); + e.addComponent('collision'); + + e.collision.linearOffset = new Vec3(1, 2, 3); + expect(e.collision.getShapePosition().equals(new Vec3(1, 2, 3))).to.equal(true); + + const offset = new Quat().setFromEulerAngles(0, 90, 0); + e.collision.angularOffset = offset; + expect(e.collision.getShapeRotation().equals(offset)).to.equal(true); + }); + + it('returns the entity transform when the offsets are cleared', function () { + const e = new Entity(); + e.addComponent('collision', { linearOffset: [1, 2, 3], angularOffset: [0, 90, 0] }); + + e.collision.linearOffset = Vec3.ZERO; + e.collision.angularOffset = Quat.IDENTITY; + + expect(e.collision.getShapePosition().equals(e.getPosition())).to.equal(true); + expect(e.collision.getShapeRotation().equals(e.getRotation())).to.equal(true); + }); + + }); + + describe('#cloneComponent', function () { + + it('clones every property', function () { + const e = new Entity(); + e.addComponent('collision', { + enabled: false, + type: 'capsule', + halfExtents: new Vec3(1, 2, 3), + linearOffset: new Vec3(4, 5, 6), + angularOffset: new Quat().setFromEulerAngles(0, 90, 0), + radius: 2, + axis: 0, + height: 5, + convexHull: true, + asset: 42, + renderAsset: 43, + checkVertexDuplicates: false + }); + + const clone = e.clone(); + const c = clone.collision; + + expect(c).to.exist; + expect(c.enabled).to.equal(false); + expect(c.type).to.equal('capsule'); + expect(c.halfExtents.equals(new Vec3(1, 2, 3))).to.equal(true); + expect(c.halfExtents).to.not.equal(e.collision.halfExtents); + expect(c.linearOffset.equals(new Vec3(4, 5, 6))).to.equal(true); + expect(c.linearOffset).to.not.equal(e.collision.linearOffset); + expect(c.angularOffset.equals(new Quat().setFromEulerAngles(0, 90, 0))).to.equal(true); + expect(c.angularOffset).to.not.equal(e.collision.angularOffset); + expect(c.radius).to.equal(2); + expect(c.axis).to.equal(0); + expect(c.height).to.equal(5); + expect(c.convexHull).to.equal(true); + expect(c.asset).to.equal(42); + expect(c.renderAsset).to.equal(43); + expect(c.checkVertexDuplicates).to.equal(false); + }); + + }); + + describe('lifecycle', function () { + + it('detaches listeners when the component is removed', function () { + const asset = new Asset('model', 'model'); + app.assets.add(asset); + + const e = new Entity(); + app.root.addChild(e); + e.addComponent('collision', { asset: asset.id }); + + expect(asset.hasEvent('remove')).to.equal(true); + expect(e.hasEvent('insert')).to.equal(true); + + e.removeComponent('collision'); + + expect(asset.hasEvent('remove')).to.equal(false); + expect(e.hasEvent('insert')).to.equal(false); + }); + + it('survives a disable and enable round trip', function () { + const e = new Entity(); + app.root.addChild(e); + e.addComponent('collision'); + + e.collision.enabled = false; + expect(e.collision.enabled).to.equal(false); + + e.collision.enabled = true; + expect(e.collision.enabled).to.equal(true); + }); + + it('destroys the entity without throwing', function () { + const e = new Entity(); + app.root.addChild(e); + e.addComponent('collision'); + + expect(() => e.destroy()).to.not.throw(); + expect(e.collision).to.not.exist; + }); + + }); + +});