UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

232 lines (220 loc) 8.48 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { Color, Plane, PlaneHelper, Vector3 } from 'three'; import { hasDefaultPointOfView } from '../core/HasDefaultPointOfView'; import * as MemoryUsage from '../core/MemoryUsage'; import Helpers, { hasBoundingVolumeHelper } from '../helpers/Helpers'; import { isMaterial, isVector3 } 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. */ function traverseOnce(root, callback) { root.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').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)); } } if (hasDefaultPointOfView(entity)) { this.addController(this, 'goToEntity').name('👣 Go to'); this.addController(this, 'lookAt').name('🎥 Look at'); } this.addController(this, 'deleteEntity').name('❌ Delete entity'); this.addController(this.entity.distance, 'min').name('Min view distance').decimals(1); this.addController(this.entity.distance, 'max').name('Max view distance').decimals(1); } updateControlsWithDefaultView(defaultView) { const controls = this.instance.view.controls; if (defaultView && controls && 'target' in controls && isVector3(controls.target)) { controls.target.copy(defaultView.target); } } goToEntity() { const cast = this.entity; const defaultView = this.instance.view.goTo(cast); this.updateControlsWithDefaultView(defaultView); this.notify(); } lookAt() { const cast = this.entity; const defaultView = this.instance.view.goTo(cast, { allowTranslation: false }); this.updateControlsWithDefaultView(defaultView); this.notify(); } 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. traverseOnce(this.rootObject, 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 (hasBoundingVolumeHelper(obj)) { obj.boundingVolumeHelper.object3d.material.color = color; } }); this.notify(this.entity); } } export default EntityInspector;