UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

315 lines (277 loc) 9.36 kB
import type { BufferGeometry, LineBasicMaterial, LineSegments, MeshBasicMaterial } from 'three'; import { ArrowHelper, AxesHelper, Box3, Box3Helper, Color, GridHelper, Mesh, Vector3, type Material, type Object3D, } from 'three'; import type { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'; import type OBB from '../core/OBB.js'; import { isMaterial } from '../utils/predicates'; import { nonNull } from '../utils/tsutils'; import OBBHelper from './OBBHelper'; export class VolumeHelper extends OBBHelper { readonly isvolumeHelper = true; } export class SphereHelper extends Mesh<BufferGeometry, MeshBasicMaterial> { readonly isHelper = true; } export class BoundingBoxHelper extends Box3Helper { readonly isHelper = true; readonly isvolumeHelper = true; } interface HasBoundingBox extends Object3D { boundingBox: Box3; } interface HasVolumeHelper extends Object3D { volumeHelper: VolumeHelper; } interface HasBoundingBoxHelper extends Object3D { volumeHelper: BoundingBoxHelper; } export function hasVolumeHelper(obj: unknown): obj is HasVolumeHelper { return (obj as HasVolumeHelper)?.volumeHelper !== undefined; } interface HasSelectionHelper extends Object3D { selectionHelper: BoundingBoxHelper; } interface HasBoundingVolumeHelper extends Object3D { boundingVolumeHelper: { object3d: | SphereHelper | VolumeHelper | LineSegments<LineSegmentsGeometry | BufferGeometry, LineBasicMaterial> | Mesh<BufferGeometry, MeshBasicMaterial>; absolute: boolean; }; } export function hasBoundingVolumeHelper(obj: unknown): obj is HasBoundingVolumeHelper { return (obj as HasBoundingVolumeHelper)?.boundingVolumeHelper !== undefined; } interface HasGeometry extends Object3D { geometry: BufferGeometry; } const _vector = new Vector3(); let _axisSize = 500; /** * @param colorDesc - A THREE color or hex string. * @returns The THREE color. */ function getColor(colorDesc: Color | string) { if (typeof colorDesc === 'string' || colorDesc instanceof String) { return new Color(colorDesc); } return colorDesc; } /** * This function creates a Box3 by matching the object's bounding box, * without including its children. * * @param object - The object to expand. * @param precise - If true, the computation uses the vertices from the geometry. * @returns The expanded box. */ function makeLocalBbox(object: Object3D, precise = false): Box3 { // The object provides a specific bounding box if ((object as HasBoundingBox).boundingBox != null) { return (object as HasBoundingBox).boundingBox; } const box = new Box3(); const geometry = (object as HasGeometry).geometry; if (geometry !== undefined) { if ( precise && geometry.attributes !== undefined && geometry.attributes.position !== undefined ) { const position = geometry.attributes.position; for (let i = 0, l = position.count; i < l; i++) { _vector.fromBufferAttribute(position, i); box.expandByPoint(_vector); } } else { if (geometry.boundingBox === null) { geometry.computeBoundingBox(); } box.copy(nonNull(geometry.boundingBox)); } } return box; } /** * Provides utility functions to create scene helpers, such as bounding boxes, grids, axes... * */ class Helpers { /** * Adds a bounding box helper to the object. * If a bounding box is already present, it is updated instead. * * @param obj - The object to decorate. * @param color - The color. * @example * // add a bounding box to 'obj' * Helpers.addBoundingBox(obj, 'green'); */ static addBoundingBox(obj: Object3D, color: Color | string) { // Don't add a bounding box helper to a bounding box helper ! if ((obj as BoundingBoxHelper).isvolumeHelper) { return; } if (hasVolumeHelper(obj)) { obj.volumeHelper.updateMatrixWorld(true); } else { const helper = Helpers.createBoxHelper(makeLocalBbox(obj), getColor(color)); obj.add(helper); (obj as HasBoundingBoxHelper).volumeHelper = helper; helper.updateMatrixWorld(true); } } static createBoxHelper(box: Box3, color: Color) { const helper = new BoundingBoxHelper(box, color); helper.name = 'bounding box'; if (isMaterial(helper.material)) { helper.material.transparent = true; helper.material.needsUpdate = true; } return helper; } static set axisSize(v) { _axisSize = v; } static get axisSize() { return _axisSize; } /** * Creates a selection bounding box helper around the specified object. * * @param obj - The object to decorate. * @param color - The color. * @returns the created box helper. * @example * // add a bounding box to 'obj' * Helpers.createSelectionBox(obj, 'green'); */ static createSelectionBox(obj: Object3D, color: Color) { const helper = Helpers.createBoxHelper(makeLocalBbox(obj), getColor(color)); (obj as HasSelectionHelper).selectionHelper = helper; obj.add(helper); obj.updateMatrixWorld(true); return helper; } /** * Adds an oriented bounding box (OBB) helper to the object. * If a bounding box is already present, it is updated instead. * * @param obj - The object to decorate. * @param obb - The OBB. * @param color - The color. * @example * // add an OBB to 'obj' * Helpers.addOBB(obj, obj.OBB, 'green'); */ static addOBB(obj: Object3D, obb: OBB, color: Color) { if (hasVolumeHelper(obj)) { obj.volumeHelper.update(obb, color); } else { const helper = new VolumeHelper(obb, color); helper.name = 'OBBHelper'; obj.add(helper); (obj as HasVolumeHelper).volumeHelper = helper; helper.updateMatrixWorld(true); } } static removeOBB(obj: Object3D) { if (hasVolumeHelper(obj)) { const helper = (obj as HasVolumeHelper).volumeHelper; helper.removeFromParent(); helper.dispose(); // @ts-expect-error cannot remove "mandatory" property delete obj.volumeHelper; } } /** * Create a grid on the XZ plane. * * @param origin - The grid origin. * @param size - The size of the grid. * @param subdivs - The number of grid subdivisions. */ static createGrid(origin: Vector3, size: number, subdivs: number) { const grid = new GridHelper(size, subdivs); grid.name = 'grid'; // Rotate the grid to be in the XZ plane. grid.rotateX(Math.PI / 2); grid.position.copy(origin); grid.updateMatrixWorld(); return grid; } /** * Create an axis helper. * * @param size - The size of the helper. */ static createAxes(size: number) { const axes = new AxesHelper(size); // We want the axes to be always visible, // and rendered on top of any other object in the scene. axes.renderOrder = 9999; (axes.material as Material).depthTest = false; return axes; } static remove3DTileBoundingVolume(obj: Object3D) { if (hasBoundingVolumeHelper(obj)) { // The helper is not necessarily attached to the object, in the // case of helpers with absolute position. const obj3d = obj.boundingVolumeHelper.object3d; obj3d.removeFromParent(); obj3d.geometry?.dispose(); obj3d.material?.dispose(); // @ts-expect-error cannot remove "mandatory" property delete obj.boundingVolumeHelper; } } static update3DTileBoundingVolume(obj: Object3D, properties: { color: Color }) { if (!hasBoundingVolumeHelper(obj)) { return; } if (properties.color != null) { obj.boundingVolumeHelper.object3d.material.color = properties.color; } } /** * Creates an arrow between the two points. * * @param start - The starting point. * @param end - The end point. */ static createArrow(start: Vector3, end: Vector3) { const length = start.distanceTo(end); const dir = end.sub(start).normalize(); const arrow = new ArrowHelper(dir, start, length); return arrow; } /** * Removes an existing bounding box from the object, if any. * * @param obj - The object to update. * @example * Helpers.removeBoundingBox(obj); */ static removeBoundingBox(obj: Object3D) { if (hasVolumeHelper(obj)) { const volumeHelper = (obj as HasVolumeHelper).volumeHelper; obj.remove(volumeHelper); volumeHelper.dispose(); // @ts-expect-error cannot remove "mandatory" property delete obj.volumeHelper; } } } export default Helpers;