UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

201 lines (190 loc) 7.36 kB
import { Color, Object3D, Plane, PlaneHelper, Vector3 } from 'three'; import * as MemoryUsage from '../core/MemoryUsage'; import Helpers, { hasVolumeHelper } from '../helpers/Helpers'; import { isMaterial } from '../utils/predicates'; import Panel from './Panel'; const _tempArray = []; /** * Traverses the object hierarchy exactly once per object, * even if the hierarchy is modified during the traversal. * * In other words, objects can be safely added * to the hierarchy without causing infinite recursion. * * @param callback - The callback to call for each visited object. */ // @ts-expect-error monkey patching // FIXME Object3D.prototype.traverseOnce = function (callback) { this.traverse(o => _tempArray.push(o)); while (_tempArray.length > 0) { const obj = _tempArray.pop(); if (obj) { callback(obj); } } }; class ClippingPlanePanel extends Panel { constructor(entity, parentGui, instance) { super(parentGui, instance, 'Clipping plane'); this.entity = entity; this.enableClippingPlane = false; this.normal = new Vector3(0, 0, 1); this.distance = 0; this.helperSize = 5; this.negate = false; this.addController(this, 'enableClippingPlane').name('Enable').onChange(() => this.updateClippingPlane()); this.addController(this.normal, 'x').name('Plane normal X').onChange(() => this.updateClippingPlane()); this.addController(this.normal, 'y').name('Plane normal Y').onChange(() => this.updateClippingPlane()); this.addController(this.normal, 'z').name('Plane normal Z').onChange(() => this.updateClippingPlane()); this.addController(this, 'distance').name('Distance').onChange(() => this.updateClippingPlane()); this.addController(this, 'helperSize').name('Helper size').onChange(() => this.updateClippingPlane()); this.addController(this, 'negate').name('Negate plane').onChange(() => this.updateClippingPlane()); } updateClippingPlane() { this.planeHelper?.removeFromParent(); this.planeHelper?.dispose(); if (this.enableClippingPlane) { const plane = new Plane(this.normal.clone(), this.distance); if (this.negate) { plane.negate(); } this.entity.clippingPlanes = [plane]; this.planeHelper = new PlaneHelper(plane, this.helperSize, 0xff0000); this.planeHelper.name = `Clipping plane for ${this.entity.id}`; this.instance.scene.add(this.planeHelper); this.planeHelper.updateMatrixWorld(); } else { this.entity.clippingPlanes = null; } this.notify(this.entity); } dispose() { this.planeHelper?.removeFromParent(); this.planeHelper?.dispose(); } } function getTitle(entity) { if (entity.name != null) { return `${entity.name} (${entity.type})`; } return entity.type; } /** * Base class for entity inspectors. To implement a custom inspector * for an entity type, you can inherit this class. */ class EntityInspector extends Panel { /** The inspected entity. */ /** The root object of the entity's hierarchy. */ /** Toggle the visibility of the entity. */ /** Toggle the visibility of the bounding boxes. */ cpuMemoryUsage = 'unknown'; gpuMemoryUsage = 'unknown'; /** * @param parentGui - The parent GUI. * @param instance - The Giro3D instance. * @param entity - The entity to inspect. * @param options - The options. */ constructor(parentGui, instance, entity, options = {}) { super(parentGui, instance, getTitle(entity)); this.entity = entity; this.rootObject = entity.object3d; this.visible = entity.visible; this.boundingBoxes = false; this.boundingBoxColor = '#FFFF00'; this.state = 'idle'; this.addController(this.entity, 'id').name('Identifier'); this.addController(this, 'cpuMemoryUsage').name('Memory usage (CPU)'); this.addController(this, 'gpuMemoryUsage').name('Memory usage (GPU)'); this.addController(this, 'state').name('Status'); this.addController(this.entity, 'renderOrder').name('Render order').onChange(() => this.notify(this.entity)); this.clippingPlanePanel = new ClippingPlanePanel(entity, this.gui, instance); if (options.visibility === true) { this.addController(this, 'visible').name('Visible').onChange(v => this.toggleVisibility(v)); } this.addController(this.entity, 'frozen').name('Freeze updates').onChange(() => this.notify(this.entity)); if (options.opacity === true) { this.addController(this.entity, 'opacity').name('Opacity').min(0).max(1).onChange(() => this.notify(this.entity)); } if (options.boundingBoxes === true) { this.addController(this, 'boundingBoxes').name('Show volumes').onChange(v => this.toggleBoundingBoxes(v)); if (options.boundingBoxColor === true) { this.addColorController(this, 'boundingBoxColor').name('Volume color').onChange(v => this.updateBoundingBoxColor(v)); } } this.addController(this, 'deleteEntity').name('Delete entity'); } deleteEntity() { this.instance.remove(this.entity); } dispose() { this.toggleBoundingBoxes(false); this.clippingPlanePanel.dispose(); } updateValues() { const ctx = { renderer: this.instance.renderer, objects: new Map() }; this.entity.getMemoryUsage(ctx); const memUsage = MemoryUsage.aggregateMemoryUsage(ctx); this.cpuMemoryUsage = MemoryUsage.format(memUsage.cpuMemory); this.gpuMemoryUsage = MemoryUsage.format(memUsage.gpuMemory); this.state = this.entity.loading ? `loading (${Math.round(this.entity.progress * 100)}%)` : 'idle'; if (this.boundingBoxes) { this.toggleBoundingBoxes(true); } } /** * Toggles the visibility of the entity in the scene. * You may override this method if the entity's visibility is not directly related * to its root object visibility. * * @param visible - The new visibility. */ toggleVisibility(visible) { this.entity.visible = visible; this.notify(this.entity); } /** * Toggles the visibility of the bounding boxes. * You may override this method to use custom bounding boxes. * * @param visible - The new state. */ toggleBoundingBoxes(visible) { const color = new Color(this.boundingBoxColor); // by default, adds axis-oriented bounding boxes to each object in the hierarchy. // custom implementations may override this to have a different behaviour. // @ts-expect-error traverseOnce() is monkey patched this.rootObject.traverseOnce(obj => this.addOrRemoveBoundingBox(obj, visible, color)); this.notify(this.entity); } /** * @param obj - The object to decorate. * @param add - If true, bounding box is added, otherwise it is removed. * @param color - The bounding box color. */ addOrRemoveBoundingBox(obj, add, color) { if (add) { if ('material' in obj && isMaterial(obj.material)) { if (obj.visible && obj.material != null && obj.material.visible) { Helpers.addBoundingBox(obj, color); } } } else { Helpers.removeBoundingBox(obj); } } updateBoundingBoxColor(colorHex) { const color = new Color(colorHex); this.rootObject.traverse(obj => { if (hasVolumeHelper(obj)) { obj.volumeHelper.material.color = color; } }); this.notify(this.entity); } } export default EntityInspector;