@itwin/frontend-devtools
Version:
Debug menu and supporting UI widgets
181 lines • 7.85 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Effects
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExplosionEffect = void 0;
const core_geometry_1 = require("@itwin/core-geometry");
const core_common_1 = require("@itwin/core-common");
const core_frontend_1 = require("@itwin/core-frontend");
const Random_1 = require("./Random");
/** Represents one particle in the system. */
class Particle {
/** Current position in the particle system's local coordinate space. */
position;
/** Current velocity in meters per second. */
velocity;
/** Current age in seconds, incremented each frame. */
age = 0;
/** Maximum age in seconds. When `this.age` exceeds `this.lifetime`, the particle expires. */
lifetime;
/** Particle size in meters. */
size;
/** Particle transparency in [0..255]. */
transparency = 0;
get x() { return this.position.x; }
get y() { return this.position.y; }
get z() { return this.position.z; }
constructor(position, velocity, lifetime, size) {
this.position = position;
this.velocity = velocity;
this.lifetime = lifetime;
this.size = size;
}
get isExpired() { return this.age >= this.lifetime; }
}
/** Emits particles in a sphere with its center at the origin.
* Each particle is emitted from the center of the sphere with random velocity toward the surface of the sphere.
*/
class ParticleEmitter {
/** Range from which each particle's initial speed in meters per second will be selected. */
speedRange = core_geometry_1.Range1d.createXX(1, 2);
/** Range from which each particle's lifetime in seconds will be selected. */
lifetimeRange = core_geometry_1.Range1d.createXX(5, 10);
/** Range from which each particle's size in meters will be selected. */
sizeRange = core_geometry_1.Range1d.createXX(0.2, 1.0);
/** Range from which the number of particles emitted will be selected. */
numParticlesRange = core_geometry_1.Range1d.createXX(1600, 2200);
/** Emit an explosion of particles from the center of the sphere. */
emit() {
const particles = [];
const numParticles = (0, Random_1.randomIntegerInRange)(this.numParticlesRange);
for (let i = 0; i < numParticles; i++) {
const velocity = new core_geometry_1.Vector3d((0, Random_1.randomFloat)(-1.0, 1.0), (0, Random_1.randomFloat)(-1.0, 1.0), (0, Random_1.randomFloat)(-1.0, 1.0));
velocity.normalizeInPlace();
velocity.scaleInPlace((0, Random_1.randomFloatInRange)(this.speedRange));
const lifetime = (0, Random_1.randomFloatInRange)(this.lifetimeRange);
const size = (0, Random_1.randomFloatInRange)(this.sizeRange);
particles.push(new Particle(new core_geometry_1.Point3d(0, 0, 0), velocity, lifetime, size));
}
return particles;
}
}
class ParticleSystem {
_origin;
_pickableId;
_emitter = new ParticleEmitter();
_numEmissions;
_texture;
_lastUpdateTime;
_particles = [];
_scratchVector3d = new core_geometry_1.Vector3d();
_dispose;
/** Acceleration in Z applied to particles, in meters per second squared. */
gravity = -3;
static numEmissionsRange = core_geometry_1.Range1d.createXX(1, 5);
constructor(texture, iModel, numEmissions) {
this._texture = texture;
this._pickableId = iModel.transientIds.getNext();
this._numEmissions = numEmissions;
this._lastUpdateTime = Date.now();
this._origin = (0, Random_1.randomPositionInRange)(iModel.projectExtents);
this._dispose = iModel.onClose.addListener(() => this[Symbol.dispose]());
}
[Symbol.dispose]() {
if (this._dispose) {
this._dispose();
this._dispose = undefined;
}
core_frontend_1.IModelApp.viewManager.dropDecorator(this);
this._texture[Symbol.dispose]();
}
update() {
const now = Date.now();
let deltaMillis = now - this._lastUpdateTime;
deltaMillis = Math.min(100, deltaMillis);
this._lastUpdateTime = now;
let numParticles = this._particles.length;
if (numParticles === 0) {
this._numEmissions--;
if (this._numEmissions < 0)
this[Symbol.dispose]();
else
this._particles = this._emitter.emit();
return;
}
const elapsedSeconds = deltaMillis / 1000;
for (let i = 0; i < numParticles; i++) {
const particle = this._particles[i];
this.updateParticle(particle, elapsedSeconds);
if (particle.isExpired) {
this._particles[i] = this._particles[numParticles - 1];
--i;
--numParticles;
}
}
this._particles.length = numParticles;
}
updateParticle(particle, elapsedSeconds) {
const velocity = particle.velocity.clone(this._scratchVector3d);
velocity.scale(elapsedSeconds, velocity);
velocity.z += elapsedSeconds * this.gravity;
particle.position.addInPlace(velocity);
particle.transparency = 255 * (particle.age / particle.lifetime);
particle.age += elapsedSeconds;
}
decorate(context) {
if (!context.viewport.view.isSpatialView())
return;
this.update();
const builder = core_frontend_1.ParticleCollectionBuilder.create({
viewport: context.viewport,
texture: this._texture,
size: (this._emitter.sizeRange.high - this._emitter.sizeRange.low) / 2,
transparency: 0,
origin: this._origin,
pickableId: this._pickableId,
});
for (const particle of this._particles)
builder.addParticle(particle);
const graphic = builder.finish();
if (graphic) {
context.addDecoration(core_frontend_1.GraphicType.WorldDecoration, graphic);
context.viewport.onRender.addOnce((vp) => vp.invalidateDecorations());
}
}
testDecorationHit(id) {
return id === this._pickableId;
}
async getDecorationToolTip(_hit) {
return "Explosion effect";
}
static async addDecorator(iModel) {
// Note: The decorator takes ownership of the texture, and disposes of it when the decorator is disposed.
const image = await (0, core_frontend_1.imageElementFromUrl)(`${core_frontend_1.IModelApp.publicPath}sprites/particle_explosion.png`);
const texture = core_frontend_1.IModelApp.renderSystem.createTexture({
ownership: "external",
image: { source: image, transparency: core_common_1.TextureTransparency.Mixed },
});
if (texture)
core_frontend_1.IModelApp.viewManager.addDecorator(new ParticleSystem(texture, iModel, (0, Random_1.randomIntegerInRange)(this.numEmissionsRange)));
}
}
/** This tool applies an explosion particle effect used for testing [ParticleCollectionBuilder]($frontend).
* @beta
*/
class ExplosionEffect extends core_frontend_1.Tool {
static toolId = "ExplosionEffect";
/** This method runs the tool, applying an explosion particle effect. */
async run() {
const vp = core_frontend_1.IModelApp.viewManager.selectedView;
if (vp)
await ParticleSystem.addDecorator(vp.iModel);
return true;
}
}
exports.ExplosionEffect = ExplosionEffect;
//# sourceMappingURL=Explosion.js.map