Created
May 26, 2026 18:56
-
-
Save gstreetmedia/f9dae7e50fbc3d3eb5b6dbb24e961d1c to your computer and use it in GitHub Desktop.
An Oriented version of the Three.js Box3 class.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // TypeScript | |
| import {Box3, Vector3, Matrix4, Quaternion, Plane, LineCurve3, Euler} from 'three'; | |
| /* | |
| const obbA = new OrientedBox3().setFromBox3(boxA); | |
| obbA.center.copy(centerA); | |
| obbA.rotation.setFromEuler(new Euler(0, Math.PI / 4, 0)); | |
| obbA.update(); | |
| const obbB = new OrientedBox3().setFromBox3(boxB); | |
| obbB.center.copy(centerB); | |
| obbB.rotation.setFromEuler(new Euler(0, 0, 0)); | |
| obbB.update(); | |
| const overlap = obbA.intersectOrientedBox(obbB); | |
| if (overlap) { | |
| // overlap is an OrientedBox3 aligned with obbA’s orientation | |
| // use overlap.min/max in A’s local frame, or overlap.center/halfSize/axes | |
| } | |
| */ | |
| export const namedCorners = { | |
| rightTopFront: "rightTopFront", | |
| rightTopBack: "rightTopBack", | |
| rightBottomFront: "rightBottomFront", | |
| rightBottomBack: "rightBottomBack", | |
| rightTopCenter: "rightTopCenter", | |
| rightBottomCenter: "rightBottomCenter", | |
| leftTopFront: "leftTopFront", | |
| leftTopBack: "leftTopBack", | |
| leftBottomFront: "leftBottomFront", | |
| leftBottomBack: "leftBottomBack", | |
| leftTopCenter: "leftTopCenter", | |
| leftBottomCenter: "leftBottomCenter", | |
| center: "center", | |
| leftCenterCenter: "leftCenterCenter", | |
| rightCenterCenter: "rightCenterCenter", | |
| centerBottomFront: "centerBottomFront", | |
| centerTopFront: "centerTopFront", | |
| centerTopBack: "centerTopBack", | |
| centerBottomBack: "centerBottomBack", | |
| } | |
| export class OrientedBox3 extends Box3 { | |
| center: Vector3; | |
| rotation: Quaternion; | |
| scale: Vector3; | |
| readonly matrixWorld: Matrix4; | |
| readonly axes: [Vector3, Vector3, Vector3]; | |
| readonly halfSize: Vector3; | |
| constructor(min?: Vector3, max?: Vector3) { | |
| super( | |
| min ? min.clone() : new Vector3(+Infinity, +Infinity, +Infinity), | |
| max ? max.clone() : new Vector3(-Infinity, -Infinity, -Infinity) | |
| ); | |
| this.center = new Vector3(); | |
| this.rotation = new Quaternion(); | |
| this.scale = new Vector3(1, 1, 1); | |
| this.matrixWorld = new Matrix4(); | |
| this.axes = [ | |
| new Vector3(1, 0, 0), | |
| new Vector3(0, 1, 0), | |
| new Vector3(0, 0, 1), | |
| ]; | |
| this.halfSize = new Vector3(); | |
| if (min && max) { | |
| super.getCenter(this.center); | |
| const size = new Vector3(); | |
| super.getSize(size); | |
| this.halfSize.set(size.x / 2, size.y / 2, size.z / 2); | |
| this.update(); | |
| } | |
| return this; | |
| } | |
| get dimensions() { | |
| let width = this.max.x - this.min.x; | |
| let height = this.max.y - this.min.y; | |
| let length = this.max.z - this.min.z; | |
| return {width, height, length}; | |
| } | |
| get hasRotation() { | |
| let {x, y, z} = this.rotation; | |
| return x !== 0 || y !== 0 || z !== 0 | |
| } | |
| removeRotation() { | |
| this.rotation.setFromEuler(new Euler(0, 0, 0)); | |
| this.update(); | |
| } | |
| copy(source: OrientedBox3): this { | |
| this.min.copy(source.min); | |
| this.max.copy(source.max); | |
| this.center.copy(source.center); | |
| this.rotation.copy(source.rotation); | |
| this.scale.copy(source.scale); | |
| this.halfSize.copy(source.halfSize); | |
| // Keep derived data consistent | |
| return this.update(); | |
| } | |
| clone(): OrientedBox3 { | |
| return new OrientedBox3().copy(this); | |
| } | |
| setFromCenterAndSize(center: Vector3, size: Vector3): this { | |
| this.center.copy(center); | |
| this.halfSize.set(size.x / 2, size.y / 2, size.z / 2); | |
| this.min.set( | |
| center.x - this.halfSize.x, | |
| center.y - this.halfSize.y, | |
| center.z - this.halfSize.z | |
| ); | |
| this.max.set( | |
| center.x + this.halfSize.x, | |
| center.y + this.halfSize.y, | |
| center.z + this.halfSize.z | |
| ); | |
| return this.update(); | |
| } | |
| setFromBox3(box: Box3): this { | |
| this.min.copy(box.min); | |
| this.max.copy(box.max); | |
| const size = new Vector3(); | |
| box.getCenter(this.center); | |
| box.getSize(size); | |
| this.halfSize.set(size.x / 2, size.y / 2, size.z / 2); | |
| this.rotation.identity(); | |
| this.scale.set(1, 1, 1); | |
| return this.update(); | |
| } | |
| update(): this { | |
| this.matrixWorld.compose(this.center, this.rotation, this.scale); | |
| const e = this.matrixWorld.elements; | |
| this.axes[0].set(e[0], e[1], e[2]).normalize(); | |
| this.axes[1].set(e[4], e[5], e[6]).normalize(); | |
| this.axes[2].set(e[8], e[9], e[10]).normalize(); | |
| return this; | |
| } | |
| /** | |
| * Calculates and returns the corner points of an oriented bounding box. | |
| * The corner points are affected by the center, axes, half sizes, and scale of the box. | |
| * | |
| * @param {Vector3[]} [target] - Optional array to store the resulting corner points. If not provided, a new array is created. | |
| * @return {Vector3[]} An array of 8 corner points represented as `Vector3` objects. | |
| */ | |
| getCorners(target?: Vector3[]): Vector3[] { | |
| const result = target ?? new Array<Vector3>(8); | |
| const hx = this.halfSize.x * Math.abs(this.scale.x); | |
| const hy = this.halfSize.y * Math.abs(this.scale.y); | |
| const hz = this.halfSize.z * Math.abs(this.scale.z); | |
| const ax = this.axes[0], ay = this.axes[1], az = this.axes[2]; | |
| const c = this.center; | |
| const dx = ax.clone().multiplyScalar(hx); | |
| const dy = ay.clone().multiplyScalar(hy); | |
| const dz = az.clone().multiplyScalar(hz); | |
| const combos: [number, number, number][] = [ | |
| [+1, +1, +1], | |
| [+1, +1, -1], | |
| [+1, -1, +1], | |
| [+1, -1, -1], | |
| [-1, +1, +1], | |
| [-1, +1, -1], | |
| [-1, -1, +1], | |
| [-1, -1, -1], | |
| [-1, 0, 0], | |
| [1, 0, 0], | |
| [0, 0, 0], | |
| [1, 1, 0], //right top center 11 | |
| [1, -1, 0], //right bottom center 12 | |
| [-1, 1, 0], //left top center 13 | |
| [-1, -1, 0] //left bottom center 14 | |
| ]; | |
| for (let i = 0; i < combos.length; i++) { | |
| const [sx, sy, sz] = combos[i]; | |
| const v = result[i] ?? new Vector3(); | |
| v.copy(c) | |
| .addScaledVector(dx, sx) | |
| .addScaledVector(dy, sy) | |
| .addScaledVector(dz, sz); | |
| result[i] = v; | |
| } | |
| return result; | |
| } | |
| /** | |
| * Retrieves an object containing the named corners and specific calculated points of a structure. | |
| * The method uses internal corner data and additional computations to return a comprehensive set of | |
| * labeled corner points. | |
| * | |
| * @return {Object} An object where each key represents a named corner or calculated point, corresponding | |
| * to its 3D coordinates: | |
| * - rightTopFront: Top-front corner on the right side. | |
| * - rightTopBack: Top-back corner on the right side. | |
| * - rightBottomFront: Bottom-front corner on the right side. | |
| * - rightBottomBack: Bottom-back corner on the right side. | |
| * - leftTopFrom: Top-front corner on the left side. | |
| * - leftTopBack: Top-back corner on the left side. | |
| * - leftBottomFront: Bottom-front corner on the left side. | |
| * - leftBottomBack: Bottom-back corner on the left side. | |
| * - center: The central point of the structure. | |
| * - leftCenterCenter: Center of the left side. | |
| * - rightCenterCenter: Center of the right side. | |
| * - centerBottomFront: Computed center-bottom-front point based on specific coordinates. | |
| * - centerTopBack: Computed center-top-back point based on specific coordinates. | |
| */ | |
| getNamedCorners() { | |
| let corners = this.getCorners(); | |
| return { | |
| rightTopFront: corners[0], | |
| rightTopBack: corners[1], | |
| rightBottomFront: corners[2], | |
| rightBottomBack: corners[3], | |
| rightTopCenter: corners[11], | |
| rightBottomCenter: corners[12], | |
| leftTopFront: corners[4], | |
| leftTopBack: corners[5], | |
| leftBottomFront: corners[6], | |
| leftBottomBack: corners[7], | |
| leftTopCenter: corners[13], | |
| leftBottomCenter: corners[14], | |
| center: this.center, | |
| leftCenterCenter: corners[8], | |
| rightCenterCenter: corners[9], | |
| centerBottomFront: new Vector3(this.center.x, corners[2].y, corners[2].z), | |
| centerTopFront: new Vector3(this.center.x, corners[0].y, corners[0].z), | |
| centerTopBack: new Vector3(this.center.x, corners[1].y, corners[1].z), | |
| centerBottomBack: new Vector3(this.center.x, corners[3].y, corners[3].z), | |
| } | |
| } | |
| /** | |
| * Retrieves a specific named corner based on the provided name. | |
| * | |
| * @param {string} name - The name of the corner to retrieve. | |
| * @return {*} The corner object associated with the given name. | |
| */ | |
| getNamedCorner(name) { | |
| return this.getNamedCorners()[name] | |
| } | |
| /** | |
| * Calculates the position of a given vector relative to the center point. | |
| * | |
| * @param {Vector3} absoluteVector3 - The absolute vector in 3D space. | |
| * @return {Vector3} A new vector representing the position relative to the center. | |
| */ | |
| getRelativePosition(absoluteVector3) { | |
| return absoluteVector3.clone().sub(this.center); | |
| } | |
| /** | |
| * Calculates the absolute position by adding the given relative vector to the center position. | |
| * | |
| * @param {Vector3} relativeVector3 - The relative vector represented as a Vector3 object. | |
| * @return {Vector3} The absolute position as a new Vector3 object. | |
| */ | |
| getAbsolutePosition(relativeVector3) { | |
| return relativeVector3.clone().add(this.center); | |
| } | |
| get rightTopFront() { | |
| return this.getNamedCorner("rightTopFront"); | |
| } | |
| get rightTopBack() { | |
| return this.getNamedCorner("rightTopBack"); | |
| } | |
| get rightBottomFront() { | |
| return this.getNamedCorner("rightBottomFront"); | |
| } | |
| get rightBottomBack() { | |
| return this.getNamedCorner("rightBottomBack"); | |
| } | |
| get leftTopFront() { | |
| return this.getNamedCorner("leftTopFront"); | |
| } | |
| get leftTopBack() { | |
| return this.getNamedCorner("leftTopBack"); | |
| } | |
| get leftBottomFront() { | |
| return this.getNamedCorner("leftBottomFront"); | |
| } | |
| get leftBottomBack() { | |
| return this.getNamedCorner("leftBottomBack"); | |
| } | |
| get leftCenterCenter() { | |
| return this.getNamedCorner("leftCenterCenter"); | |
| } | |
| get rightCenterCenter() { | |
| return this.getNamedCorner("rightCenterCenter"); | |
| } | |
| get centerBottomFront() { | |
| return this.getNamedCorner("centerBottomFront"); | |
| } | |
| get centerTopBack() { | |
| return this.getNamedCorner("centerTopBack"); | |
| } | |
| getPlanes(target?: Plane[]): Plane[] { | |
| const planes = target ?? new Array<Plane>(6); | |
| const ax = this.axes[0], ay = this.axes[1], az = this.axes[2]; | |
| const c = this.center; | |
| const hx = this.halfSize.x * Math.abs(this.scale.x); | |
| const hy = this.halfSize.y * Math.abs(this.scale.y); | |
| const hz = this.halfSize.z * Math.abs(this.scale.z); | |
| const offsets = [ | |
| ax.clone().multiplyScalar(hx), | |
| ax.clone().multiplyScalar(-hx), | |
| ay.clone().multiplyScalar(hy), | |
| ay.clone().multiplyScalar(-hy), | |
| az.clone().multiplyScalar(hz), | |
| az.clone().multiplyScalar(-hz), | |
| ]; | |
| const normals = [ | |
| ax.clone(), | |
| ax.clone().negate(), | |
| ay.clone(), | |
| ay.clone().negate(), | |
| az.clone(), | |
| az.clone().negate(), | |
| ]; | |
| for (let i = 0; i < 6; i++) { | |
| const plane = planes[i] ?? new Plane(); | |
| const p = new Vector3().copy(c).add(offsets[i]); | |
| plane.setFromNormalAndCoplanarPoint(normals[i], p); | |
| planes[i] = plane; | |
| } | |
| return planes; | |
| } | |
| /** | |
| * Boolean test (like Box3.intersectsBox) but for oriented boxes. | |
| */ | |
| intersectsOrientedBox(box: OrientedBox3): boolean { | |
| this.update(); | |
| box.update(); | |
| return OrientedBox3._obbIntersectsObb(this, box); | |
| } | |
| /** | |
| * Returns the world-space axis-aligned bounding box (AABB) of the intersection | |
| * volume between this OBB and another OBB. Returns null if they do not intersect. | |
| */ | |
| intersectOrientedBoxAABB(box: OrientedBox3, target?: Box3): Box3 | null { | |
| this.update(); | |
| box.update(); | |
| if (!OrientedBox3._obbIntersectsObb(this, box)) return null; | |
| const points = OrientedBox3._obbIntersectionPoints(this, box); | |
| if (points.length === 0) return null; | |
| const out = target ?? new Box3(); | |
| out.setFromPoints(points); | |
| return out; | |
| } | |
| /** | |
| * Box3-like `intersect`: returns the overlap as another OrientedBox3 | |
| * expressed in this box's orientation, or null if no overlap. | |
| */ | |
| intersectOrientedBox( | |
| box: OrientedBox3, | |
| target?: OrientedBox3 | |
| ): OrientedBox3 | null { | |
| this.update(); | |
| box.update(); | |
| if (!OrientedBox3._obbIntersectsObb(this, box)) return null; | |
| const points = OrientedBox3._obbIntersectionPoints(this, box); | |
| if (points.length === 0) return null; | |
| // Express intersection vertices in this box's local frame | |
| const inv = new Matrix4().copy(this.matrixWorld).invert(); | |
| const localPoints = points.map((p) => p.clone().applyMatrix4(inv)); | |
| const localBox = new Box3().setFromPoints(localPoints); | |
| const size = new Vector3(); | |
| const centerLocal = new Vector3(); | |
| localBox.getSize(size); | |
| localBox.getCenter(centerLocal); | |
| // Turn center back into world space (still using this.rotation / this.scale) | |
| const centerWorld = centerLocal.applyMatrix4(this.matrixWorld); | |
| const result = target ?? new OrientedBox3(); | |
| result.setFromCenterAndSize(centerWorld, size); | |
| result.rotation.copy(this.rotation); | |
| result.scale.copy(this.scale); | |
| return result.update(); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Internal helpers | |
| // --------------------------------------------------------------------------- | |
| private static _obbIntersectsObb(a: OrientedBox3, b: OrientedBox3): boolean { | |
| const EPS = 1e-6; | |
| const A = a.axes; | |
| const B = b.axes; | |
| const EA = new Vector3( | |
| a.halfSize.x * Math.abs(a.scale.x), | |
| a.halfSize.y * Math.abs(a.scale.y), | |
| a.halfSize.z * Math.abs(a.scale.z) | |
| ); | |
| const EB = new Vector3( | |
| b.halfSize.x * Math.abs(b.scale.x), | |
| b.halfSize.y * Math.abs(b.scale.y), | |
| b.halfSize.z * Math.abs(b.scale.z) | |
| ); | |
| const R: number[][] = [ | |
| [0, 0, 0], | |
| [0, 0, 0], | |
| [0, 0, 0], | |
| ]; | |
| const AbsR: number[][] = [ | |
| [0, 0, 0], | |
| [0, 0, 0], | |
| [0, 0, 0], | |
| ]; | |
| for (let i = 0; i < 3; i++) { | |
| for (let j = 0; j < 3; j++) { | |
| const r = A[i].dot(B[j]); | |
| R[i][j] = r; | |
| AbsR[i][j] = Math.abs(r) + EPS; | |
| } | |
| } | |
| const tWorld = new Vector3().subVectors(b.center, a.center); | |
| const T = [ | |
| tWorld.dot(A[0]), | |
| tWorld.dot(A[1]), | |
| tWorld.dot(A[2]), | |
| ]; | |
| let ra: number; | |
| let rb: number; | |
| // Axes A0, A1, A2 | |
| for (let i = 0; i < 3; i++) { | |
| ra = EA.getComponent(i); | |
| rb = EB.x * AbsR[i][0] + EB.y * AbsR[i][1] + EB.z * AbsR[i][2]; | |
| if (Math.abs(T[i]) > ra + rb) return false; | |
| } | |
| // Axes B0, B1, B2 | |
| for (let j = 0; j < 3; j++) { | |
| ra = EA.x * AbsR[0][j] + EA.y * AbsR[1][j] + EA.z * AbsR[2][j]; | |
| rb = EB.getComponent(j); | |
| const proj = T[0] * R[0][j] + T[1] * R[1][j] + T[2] * R[2][j]; | |
| if (Math.abs(proj) > ra + rb) return false; | |
| } | |
| // Cross products Ai x Bj | |
| // A0 x B0 | |
| ra = EA.y * AbsR[2][0] + EA.z * AbsR[1][0]; | |
| rb = EB.y * AbsR[0][2] + EB.z * AbsR[0][1]; | |
| if (Math.abs(T[2] * R[1][0] - T[1] * R[2][0]) > ra + rb) return false; | |
| // A0 x B1 | |
| ra = EA.y * AbsR[2][1] + EA.z * AbsR[1][1]; | |
| rb = EB.x * AbsR[0][2] + EB.z * AbsR[0][0]; | |
| if (Math.abs(T[2] * R[1][1] - T[1] * R[2][1]) > ra + rb) return false; | |
| // A0 x B2 | |
| ra = EA.y * AbsR[2][2] + EA.z * AbsR[1][2]; | |
| rb = EB.x * AbsR[0][1] + EB.y * AbsR[0][0]; | |
| if (Math.abs(T[2] * R[1][2] - T[1] * R[2][2]) > ra + rb) return false; | |
| // A1 x B0 | |
| ra = EA.x * AbsR[2][0] + EA.z * AbsR[0][0]; | |
| rb = EB.y * AbsR[1][2] + EB.z * AbsR[1][1]; | |
| if (Math.abs(T[0] * R[2][0] - T[2] * R[0][0]) > ra + rb) return false; | |
| // A1 x B1 | |
| ra = EA.x * AbsR[2][1] + EA.z * AbsR[0][1]; | |
| rb = EB.x * AbsR[1][2] + EB.z * AbsR[1][0]; | |
| if (Math.abs(T[0] * R[2][1] - T[2] * R[0][1]) > ra + rb) return false; | |
| // A1 x B2 | |
| ra = EA.x * AbsR[2][2] + EA.z * AbsR[0][2]; | |
| rb = EB.x * AbsR[1][1] + EB.y * AbsR[1][0]; | |
| if (Math.abs(T[0] * R[2][2] - T[2] * R[0][2]) > ra + rb) return false; | |
| // A2 x B0 | |
| ra = EA.x * AbsR[1][0] + EA.y * AbsR[0][0]; | |
| rb = EB.y * AbsR[2][2] + EB.z * AbsR[2][1]; | |
| if (Math.abs(T[1] * R[0][0] - T[0] * R[1][0]) > ra + rb) return false; | |
| // A2 x B1 | |
| ra = EA.x * AbsR[1][1] + EA.y * AbsR[0][1]; | |
| rb = EB.x * AbsR[2][2] + EB.z * AbsR[2][0]; | |
| if (Math.abs(T[1] * R[0][1] - T[0] * R[1][1]) > ra + rb) return false; | |
| // A2 x B2 | |
| ra = EA.x * AbsR[1][2] + EA.y * AbsR[0][2]; | |
| rb = EB.x * AbsR[2][1] + EB.y * AbsR[2][0]; | |
| if (Math.abs(T[1] * R[0][2] - T[0] * R[1][2]) > ra + rb) return false; | |
| return true; | |
| } | |
| private static _obbIntersectionPoints( | |
| a: OrientedBox3, | |
| b: OrientedBox3 | |
| ): Vector3[] { | |
| const result: Vector3[] = []; | |
| const cornersA = a.getCorners(); | |
| const cornersB = b.getCorners(); | |
| const planesA = a.getPlanes(); | |
| const planesB = b.getPlanes(); | |
| // 1) Corners of A inside B | |
| for (const p of cornersA) { | |
| if (OrientedBox3._pointInsideOBB(p, b, planesB)) { | |
| result.push(p.clone()); | |
| } | |
| } | |
| // 2) Corners of B inside A | |
| for (const p of cornersB) { | |
| if (OrientedBox3._pointInsideOBB(p, a, planesA)) { | |
| result.push(p.clone()); | |
| } | |
| } | |
| const edgeIndices: [number, number][] = [ | |
| [0, 1], [0, 2], [1, 3], [2, 3], | |
| [4, 5], [4, 6], [5, 7], [6, 7], | |
| [0, 4], [1, 5], [2, 6], [3, 7], | |
| ]; | |
| const tmp = new Vector3(); | |
| // 3) Edges of A vs planes of B | |
| for (const [i0, i1] of edgeIndices) { | |
| const p0 = cornersA[i0]; | |
| const p1 = cornersA[i1]; | |
| for (const plane of planesB) { | |
| if ( | |
| OrientedBox3._segmentIntersectsPlane(p0, p1, plane, tmp) && | |
| OrientedBox3._pointInsideOBB(tmp, a, planesA) && | |
| OrientedBox3._pointInsideOBB(tmp, b, planesB) | |
| ) { | |
| result.push(tmp.clone()); | |
| } | |
| } | |
| } | |
| // 4) Edges of B vs planes of A | |
| for (const [i0, i1] of edgeIndices) { | |
| const p0 = cornersB[i0]; | |
| const p1 = cornersB[i1]; | |
| for (const plane of planesA) { | |
| if ( | |
| OrientedBox3._segmentIntersectsPlane(p0, p1, plane, tmp) && | |
| OrientedBox3._pointInsideOBB(tmp, a, planesA) && | |
| OrientedBox3._pointInsideOBB(tmp, b, planesB) | |
| ) { | |
| result.push(tmp.clone()); | |
| } | |
| } | |
| } | |
| // 5) Deduplicate | |
| const unique: Vector3[] = []; | |
| const EPS = 1e-5; | |
| outer: for (const p of result) { | |
| for (const q of unique) { | |
| if (p.distanceToSquared(q) < EPS * EPS) continue outer; | |
| } | |
| unique.push(p); | |
| } | |
| return unique; | |
| } | |
| private static _pointInsideOBB( | |
| p: Vector3, | |
| box: OrientedBox3, | |
| planes: Plane[] | |
| ): boolean { | |
| const EPS = 1e-6; | |
| for (const pl of planes) { | |
| if (pl.distanceToPoint(p) > EPS) return false; | |
| } | |
| return true; | |
| } | |
| private static _segmentIntersectsPlane( | |
| p0: Vector3, | |
| p1: Vector3, | |
| plane: Plane, | |
| target: Vector3 | |
| ): boolean { | |
| const dir = new Vector3().subVectors(p1, p0); | |
| const denom = plane.normal.dot(dir); | |
| const EPS = 1e-8; | |
| if (Math.abs(denom) < EPS) return false; | |
| const t = -(plane.normal.dot(p0) + plane.constant) / denom; | |
| if (t < 0 || t > 1) return false; | |
| target.copy(p0).addScaledVector(dir, t); | |
| return true; | |
| } | |
| } | |
| export type OrientedBoxFace = 'top' | 'bottom' | 'left' | 'right' | 'front' | 'back'; | |
| export class OrientedBox3FaceHelper { | |
| /** | |
| * Returns the world-space center of the requested face. | |
| * | |
| * Face conventions: | |
| * - left / right : -X / +X | |
| * - bottom / top : -Y / +Y | |
| * - back / front : -Z / +Z | |
| */ | |
| static getFaceCenter(box: OrientedBox3, face: OrientedBoxFace, target?: Vector3): Vector3 { | |
| box.update(); | |
| const result = target ?? new Vector3(); | |
| const ax = box.axes[0]; // local X in world | |
| const ay = box.axes[1]; // local Y in world | |
| const az = box.axes[2]; // local Z in world | |
| const hx = box.halfSize.x * Math.abs(box.scale.x); | |
| const hy = box.halfSize.y * Math.abs(box.scale.y); | |
| const hz = box.halfSize.z * Math.abs(box.scale.z); | |
| result.copy(box.center); | |
| switch (face) { | |
| case 'right': // +X | |
| return result.addScaledVector(ax, hx); | |
| case 'left': // -X | |
| return result.addScaledVector(ax, -hx); | |
| case 'top': // +Y | |
| return result.addScaledVector(ay, hy); | |
| case 'bottom': // -Y | |
| return result.addScaledVector(ay, -hy); | |
| case 'front': // +Z | |
| return result.addScaledVector(az, hz); | |
| case 'back': // -Z | |
| return result.addScaledVector(az, -hz); | |
| } | |
| } | |
| /** | |
| * Returns the opposite (matching) face for a given face. | |
| * | |
| * - top ↔ bottom | |
| * - left ↔ right | |
| * - front ↔ back | |
| */ | |
| static getOppositeFace(face: OrientedBoxFace): OrientedBoxFace { | |
| switch (face) { | |
| case 'top': | |
| return 'bottom'; | |
| case 'bottom': | |
| return 'top'; | |
| case 'left': | |
| return 'right'; | |
| case 'right': | |
| return 'left'; | |
| case 'front': | |
| return 'back'; | |
| case 'back': | |
| return 'front'; | |
| } | |
| } | |
| /** | |
| * Creates a LineCurve3 between two named faces on the same box. | |
| * | |
| * Useful for ExtrudeGeometry.extrudePath, e.g. extrude from "bottom" to "top", | |
| * or from "back" to "front". | |
| */ | |
| static createFaceCenterCurve( | |
| box: OrientedBox3, | |
| fromFace: OrientedBoxFace, | |
| toFace?: OrientedBoxFace | |
| ): LineCurve3 { | |
| const faceB = toFace ?? OrientedBox3FaceHelper.getOppositeFace(fromFace); | |
| const p1 = OrientedBox3FaceHelper.getFaceCenter(box, fromFace); | |
| const p2 = OrientedBox3FaceHelper.getFaceCenter(box, faceB); | |
| return new LineCurve3(p1, p2); | |
| } | |
| /** | |
| * Convenience helpers for the three canonical matching pairs: | |
| * - top ↔ bottom | |
| * - left ↔ right | |
| * - front ↔ back | |
| */ | |
| static createTopBottomCurve(box: OrientedBox3, fromTop: boolean = false): LineCurve3 { | |
| const from: OrientedBoxFace = fromTop ? 'top' : 'bottom'; | |
| const to: OrientedBoxFace = fromTop ? 'bottom' : 'top'; | |
| return OrientedBox3FaceHelper.createFaceCenterCurve(box, from, to); | |
| } | |
| static createLeftRightCurve(box: OrientedBox3, fromLeft: boolean = true): LineCurve3 { | |
| const from: OrientedBoxFace = fromLeft ? 'left' : 'right'; | |
| const to: OrientedBoxFace = fromLeft ? 'right' : 'left'; | |
| return OrientedBox3FaceHelper.createFaceCenterCurve(box, from, to); | |
| } | |
| static createFrontBackCurve(box: OrientedBox3, fromFront: boolean = true): LineCurve3 { | |
| const from: OrientedBoxFace = fromFront ? 'front' : 'back'; | |
| const to: OrientedBoxFace = fromFront ? 'back' : 'front'; | |
| return OrientedBox3FaceHelper.createFaceCenterCurve(box, from, to); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment