@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
315 lines (277 loc) • 9.36 kB
text/typescript
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;