import { Box3, Color, type ColorRepresentation, LineSegments, Object3D, Vector3 } from "three"; import { CreateWireCube, Gizmos } from "../engine/engine_gizmos.js"; import { getBoundingBox, getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js"; import { getParam } from "../engine/engine_utils.js"; import { Behaviour } from "./Component.js"; const gizmos = getParam("gizmos"); const debug = getParam("debugboxhelper"); /** * A component that creates a bounding box around an object and provides intersection testing functionality. * * Debug mode can be enabled with the URL parameter `?debugboxhelper`, which will visualize intersection tests. * Helper visualization can be enabled with the URL parameter `?gizmos`. * * @category Helpers * @group Components */ export class BoxHelperComponent extends Behaviour { /** The bounding box for this component */ private box: Box3 | null = null; private static testBox: Box3 = new Box3(); private _lastMatrixUpdateFrame: number = -1; private static _position: Vector3 = new Vector3(); private static _size: Vector3 = new Vector3(.01, .01, .01); private static _emptyObjectSize: Vector3 = new Vector3(.01, .01, .01); /** * Tests if an object intersects with this helper's bounding box * @param obj The object to test for intersection * @returns True if objects intersect, false if not, undefined if the provided object is invalid */ public isInBox(obj: Object3D): boolean | undefined { if (!obj) return undefined; if (!this.box) { this.box = new Box3(); } getBoundingBox([obj], undefined, undefined, BoxHelperComponent.testBox); if (BoxHelperComponent.testBox.isEmpty()) { const wp = getWorldPosition(obj, BoxHelperComponent._position); BoxHelperComponent.testBox.setFromCenterAndSize(wp, BoxHelperComponent._emptyObjectSize); } this.updateBox(); const intersects = this.box?.intersectsBox(BoxHelperComponent.testBox); if (intersects) { if (debug) Gizmos.DrawWireBox3(BoxHelperComponent.testBox, 0xff0000, 5); } return intersects; } /** * Tests if this helper's bounding box intersects with another box * @param box The {@link Box3} to test for intersection * @returns True if boxes intersect, false otherwise */ public intersects(box: Box3): boolean { if (!box) return false; return this.updateBox(false).intersectsBox(box); } /** * Updates the helper's bounding box based on the gameObject's position and scale * @param force Whether to force an update regardless of frame count * @returns The updated {@link Box3} */ public updateBox(force: boolean = false): Box3 { if (!this.box) { this.box = new Box3(); } if (force || this.context.time.frameCount != this._lastMatrixUpdateFrame) { const firstUpdate = this._lastMatrixUpdateFrame < 0; this._lastMatrixUpdateFrame = this.context.time.frameCount; const updateParents: boolean = firstUpdate; // updating parents seems to cause falsely calculated positions sometimes? const wp = getWorldPosition(this.gameObject, BoxHelperComponent._position, updateParents); const size = getWorldScale(this.gameObject, BoxHelperComponent._size); this.box.setFromCenterAndSize(wp, size); } return this.box; } private _helper: LineSegments | null = null; private _color: Color | null = null; awake(): void { this._helper = null; this._color = null; this.box = null; } /** * Creates and displays a visual wireframe representation of this box helper * @param col Optional color for the wireframe. If not provided, uses default color * @param force If true, shows the helper even if gizmos are disabled */ public showHelper(col: ColorRepresentation | null = null, force: boolean = false) { if (!gizmos && !force) return; if (this._helper) { if (col) this._color?.set(col); this.gameObject.add(this._helper); return; } this._helper = CreateWireCube(col); this.gameObject.add(this._helper); } }