UNPKG

@itwin/frontend-devtools

Version:

Debug menu and supporting UI widgets

177 lines 7.43 kB
/*--------------------------------------------------------------------------------------------- * 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 */ import { Point3d, Range1d, Vector3d } from "@itwin/core-geometry"; import { TextureTransparency } from "@itwin/core-common"; import { GraphicType, imageElementFromUrl, IModelApp, ParticleCollectionBuilder, Tool, } from "@itwin/core-frontend"; import { randomFloat, randomFloatInRange, randomIntegerInRange, randomPositionInRange } from "./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 = Range1d.createXX(1, 2); /** Range from which each particle's lifetime in seconds will be selected. */ lifetimeRange = Range1d.createXX(5, 10); /** Range from which each particle's size in meters will be selected. */ sizeRange = Range1d.createXX(0.2, 1.0); /** Range from which the number of particles emitted will be selected. */ numParticlesRange = Range1d.createXX(1600, 2200); /** Emit an explosion of particles from the center of the sphere. */ emit() { const particles = []; const numParticles = randomIntegerInRange(this.numParticlesRange); for (let i = 0; i < numParticles; i++) { const velocity = new Vector3d(randomFloat(-1.0, 1.0), randomFloat(-1.0, 1.0), randomFloat(-1.0, 1.0)); velocity.normalizeInPlace(); velocity.scaleInPlace(randomFloatInRange(this.speedRange)); const lifetime = randomFloatInRange(this.lifetimeRange); const size = randomFloatInRange(this.sizeRange); particles.push(new Particle(new Point3d(0, 0, 0), velocity, lifetime, size)); } return particles; } } class ParticleSystem { _origin; _pickableId; _emitter = new ParticleEmitter(); _numEmissions; _texture; _lastUpdateTime; _particles = []; _scratchVector3d = new Vector3d(); _dispose; /** Acceleration in Z applied to particles, in meters per second squared. */ gravity = -3; static numEmissionsRange = 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 = randomPositionInRange(iModel.projectExtents); this._dispose = iModel.onClose.addListener(() => this[Symbol.dispose]()); } [Symbol.dispose]() { if (this._dispose) { this._dispose(); this._dispose = undefined; } 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 = 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(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 imageElementFromUrl(`${IModelApp.publicPath}sprites/particle_explosion.png`); const texture = IModelApp.renderSystem.createTexture({ ownership: "external", image: { source: image, transparency: TextureTransparency.Mixed }, }); if (texture) IModelApp.viewManager.addDecorator(new ParticleSystem(texture, iModel, randomIntegerInRange(this.numEmissionsRange))); } } /** This tool applies an explosion particle effect used for testing [ParticleCollectionBuilder]($frontend). * @beta */ export class ExplosionEffect extends Tool { static toolId = "ExplosionEffect"; /** This method runs the tool, applying an explosion particle effect. */ async run() { const vp = IModelApp.viewManager.selectedView; if (vp) await ParticleSystem.addDecorator(vp.iModel); return true; } } //# sourceMappingURL=Explosion.js.map