@math.gl/culling 4.2.0-alpha.3 → 4.2.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +51 -0
- package/README.md +8 -1
- package/dist/index.cjs +618 -89
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/plane.d.ts +2 -2
- package/dist/lib/plane.d.ts.map +1 -1
- package/dist/lib/plane.js +3 -6
- package/dist/lib/plane.js.map +1 -1
- package/dist/lib/shapes/box-shape.d.ts +19 -0
- package/dist/lib/shapes/box-shape.d.ts.map +1 -0
- package/dist/lib/shapes/box-shape.js +69 -0
- package/dist/lib/shapes/box-shape.js.map +1 -0
- package/dist/lib/shapes/capsule-shape.d.ts +23 -0
- package/dist/lib/shapes/capsule-shape.d.ts.map +1 -0
- package/dist/lib/shapes/capsule-shape.js +151 -0
- package/dist/lib/shapes/capsule-shape.js.map +1 -0
- package/dist/lib/shapes/cylinder-shape.d.ts +22 -0
- package/dist/lib/shapes/cylinder-shape.d.ts.map +1 -0
- package/dist/lib/shapes/cylinder-shape.js +99 -0
- package/dist/lib/shapes/cylinder-shape.js.map +1 -0
- package/dist/lib/shapes/implicit-shape.d.ts +55 -0
- package/dist/lib/shapes/implicit-shape.d.ts.map +1 -0
- package/dist/lib/shapes/implicit-shape.js +143 -0
- package/dist/lib/shapes/implicit-shape.js.map +1 -0
- package/dist/lib/shapes/plane-shape.d.ts +26 -0
- package/dist/lib/shapes/plane-shape.d.ts.map +1 -0
- package/dist/lib/shapes/plane-shape.js +72 -0
- package/dist/lib/shapes/plane-shape.js.map +1 -0
- package/dist/lib/shapes/sphere-shape.d.ts +18 -0
- package/dist/lib/shapes/sphere-shape.d.ts.map +1 -0
- package/dist/lib/shapes/sphere-shape.js +43 -0
- package/dist/lib/shapes/sphere-shape.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +12 -0
- package/src/lib/plane.ts +3 -6
- package/src/lib/shapes/box-shape.ts +78 -0
- package/src/lib/shapes/capsule-shape.ts +232 -0
- package/src/lib/shapes/cylinder-shape.ts +133 -0
- package/src/lib/shapes/implicit-shape.ts +209 -0
- package/src/lib/shapes/plane-shape.ts +78 -0
- package/src/lib/shapes/sphere-shape.ts +60 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {Matrix4, Vector3} from '@math.gl/core';
|
|
6
|
+
import {INTERSECTION} from '../../constants';
|
|
7
|
+
import {AxisAlignedBoundingBox} from '../bounding-volumes/axis-aligned-bounding-box';
|
|
8
|
+
import {BoundingSphere} from '../bounding-volumes/bounding-sphere';
|
|
9
|
+
import type {BoundingVolume} from '../bounding-volumes/bounding-volume';
|
|
10
|
+
import type {Plane} from '../plane';
|
|
11
|
+
import type {Ray} from '../ray';
|
|
12
|
+
|
|
13
|
+
export type RayIntersection = {
|
|
14
|
+
distance: number;
|
|
15
|
+
point: Vector3;
|
|
16
|
+
normal: Vector3;
|
|
17
|
+
entering: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface ImplicitShape extends BoundingVolume {
|
|
21
|
+
readonly type: 'box' | 'capsule' | 'cylinder' | 'plane' | 'sphere';
|
|
22
|
+
readonly matrix: Matrix4;
|
|
23
|
+
clone(): ImplicitShape;
|
|
24
|
+
equals(shape: ImplicitShape): boolean;
|
|
25
|
+
containsPoint(point: readonly number[]): boolean;
|
|
26
|
+
intersectRay(ray: Ray): RayIntersection | undefined;
|
|
27
|
+
getAxisAlignedBoundingBox(): AxisAlignedBoundingBox | undefined;
|
|
28
|
+
getBoundingSphere(): BoundingSphere | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type LocalRayIntersection = {distance: number; normal: readonly number[]};
|
|
32
|
+
|
|
33
|
+
/** Shared affine transform, support mapping and query implementation for finite convex shapes. */
|
|
34
|
+
export abstract class ImplicitShapeBase implements ImplicitShape {
|
|
35
|
+
abstract readonly type: ImplicitShape['type'];
|
|
36
|
+
readonly matrix: Matrix4;
|
|
37
|
+
protected inverseMatrix: Matrix4;
|
|
38
|
+
|
|
39
|
+
constructor(matrix?: readonly number[]) {
|
|
40
|
+
this.matrix = new Matrix4();
|
|
41
|
+
if (matrix) this.matrix.copy(matrix);
|
|
42
|
+
validateAffineMatrix(this.matrix);
|
|
43
|
+
this.inverseMatrix = this.matrix.clone().invert();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
abstract clone(): ImplicitShapeBase;
|
|
47
|
+
abstract containsLocalPoint(point: readonly number[]): boolean;
|
|
48
|
+
abstract distanceToLocalPoint(point: readonly number[]): number;
|
|
49
|
+
abstract supportLocal(direction: readonly number[]): readonly number[];
|
|
50
|
+
abstract intersectLocalRay(
|
|
51
|
+
origin: readonly number[],
|
|
52
|
+
direction: readonly number[]
|
|
53
|
+
): LocalRayIntersection | undefined;
|
|
54
|
+
|
|
55
|
+
equals(shape: ImplicitShape): boolean {
|
|
56
|
+
if (this.type !== shape.type) return false;
|
|
57
|
+
for (let i = 0; i < 16; i++) if (this.matrix[i] !== shape.matrix[i]) return false;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
transform(transform: readonly number[]): this {
|
|
62
|
+
const next = new Matrix4().copy(transform);
|
|
63
|
+
validateAffineMatrix(next);
|
|
64
|
+
this.matrix.multiplyLeft(next);
|
|
65
|
+
validateAffineMatrix(this.matrix);
|
|
66
|
+
this.inverseMatrix.copy(this.matrix).invert();
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
containsPoint(point: readonly number[]): boolean {
|
|
71
|
+
return this.containsLocalPoint(this.inverseMatrix.transformAsPoint(point) as readonly number[]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
distanceSquaredTo(point: readonly number[]): number {
|
|
75
|
+
const distance = this.distanceTo(point);
|
|
76
|
+
return distance * distance;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Exact for rigid/uniform transforms; conservative estimate for non-uniform scale or shear. */
|
|
80
|
+
distanceTo(point: readonly number[]): number {
|
|
81
|
+
const localPoint = this.inverseMatrix.transformAsPoint(point);
|
|
82
|
+
const localDistance = this.distanceToLocalPoint(localPoint as readonly number[]);
|
|
83
|
+
const scales = this.matrix.getScale();
|
|
84
|
+
return localDistance * Math.min(scales[0], scales[1], scales[2]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
intersectPlane(plane: Plane): number {
|
|
88
|
+
const maximum = this.getSupportPoint(plane.normal);
|
|
89
|
+
const minimum = this.getSupportPoint([-plane.normal[0], -plane.normal[1], -plane.normal[2]]);
|
|
90
|
+
const maxDistance = plane.getPointDistance(maximum);
|
|
91
|
+
const minDistance = plane.getPointDistance(minimum);
|
|
92
|
+
if (minDistance > 0) return INTERSECTION.INSIDE;
|
|
93
|
+
if (maxDistance < 0) return INTERSECTION.OUTSIDE;
|
|
94
|
+
return INTERSECTION.INTERSECTING;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
intersectRay(ray: Ray): RayIntersection | undefined {
|
|
98
|
+
const localOrigin = this.inverseMatrix.transformAsPoint(ray.origin);
|
|
99
|
+
const localDirection = this.inverseMatrix.transformAsVector(ray.direction);
|
|
100
|
+
const hit = this.intersectLocalRay(
|
|
101
|
+
localOrigin as readonly number[],
|
|
102
|
+
localDirection as readonly number[]
|
|
103
|
+
);
|
|
104
|
+
if (!hit || hit.distance < 0) return undefined;
|
|
105
|
+
const point = new Vector3(ray.direction).scale(hit.distance).add(ray.origin);
|
|
106
|
+
const inverse = this.inverseMatrix;
|
|
107
|
+
const normal = new Vector3(
|
|
108
|
+
inverse[0] * hit.normal[0] + inverse[1] * hit.normal[1] + inverse[2] * hit.normal[2],
|
|
109
|
+
inverse[4] * hit.normal[0] + inverse[5] * hit.normal[1] + inverse[6] * hit.normal[2],
|
|
110
|
+
inverse[8] * hit.normal[0] + inverse[9] * hit.normal[1] + inverse[10] * hit.normal[2]
|
|
111
|
+
).normalize();
|
|
112
|
+
return {distance: hit.distance, point, normal, entering: ray.direction.dot(normal) < 0};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getAxisAlignedBoundingBox(): AxisAlignedBoundingBox | undefined {
|
|
116
|
+
const minimum = new Vector3();
|
|
117
|
+
const maximum = new Vector3();
|
|
118
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
119
|
+
const direction = [0, 0, 0];
|
|
120
|
+
direction[axis] = 1;
|
|
121
|
+
maximum[axis] = this.getSupportPoint(direction)[axis];
|
|
122
|
+
direction[axis] = -1;
|
|
123
|
+
minimum[axis] = this.getSupportPoint(direction)[axis];
|
|
124
|
+
}
|
|
125
|
+
return new AxisAlignedBoundingBox(minimum, maximum);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
getBoundingSphere(): BoundingSphere | undefined {
|
|
129
|
+
const box = this.getAxisAlignedBoundingBox();
|
|
130
|
+
if (!box) return undefined;
|
|
131
|
+
return new BoundingSphere().fromCornerPoints(box.minimum, box.maximum);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected getSupportPoint(worldDirection: readonly number[]): Vector3 {
|
|
135
|
+
const matrix = this.matrix;
|
|
136
|
+
const localDirection = [
|
|
137
|
+
matrix[0] * worldDirection[0] + matrix[1] * worldDirection[1] + matrix[2] * worldDirection[2],
|
|
138
|
+
matrix[4] * worldDirection[0] + matrix[5] * worldDirection[1] + matrix[6] * worldDirection[2],
|
|
139
|
+
matrix[8] * worldDirection[0] + matrix[9] * worldDirection[1] + matrix[10] * worldDirection[2]
|
|
140
|
+
];
|
|
141
|
+
return new Vector3(this.supportLocal(localDirection)).transform(this.matrix);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function validateAffineMatrix(matrix: readonly number[]): void {
|
|
146
|
+
if (matrix.length !== 16 || matrix.some(value => !Number.isFinite(value))) {
|
|
147
|
+
throw new Error('Shape matrix must contain 16 finite values');
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
Math.abs(matrix[3]) > 1e-12 ||
|
|
151
|
+
Math.abs(matrix[7]) > 1e-12 ||
|
|
152
|
+
Math.abs(matrix[11]) > 1e-12 ||
|
|
153
|
+
Math.abs(matrix[15] - 1) > 1e-12
|
|
154
|
+
) {
|
|
155
|
+
throw new Error('Shape matrix must be affine');
|
|
156
|
+
}
|
|
157
|
+
if (Math.abs(new Matrix4().copy(matrix).determinant()) < 1e-15)
|
|
158
|
+
throw new Error('Shape matrix must be invertible');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function solveQuadratic(a: number, b: number, c: number): number[] {
|
|
162
|
+
if (Math.abs(a) < 1e-15) return Math.abs(b) < 1e-15 ? [] : [-c / b];
|
|
163
|
+
const discriminant = b * b - 4 * a * c;
|
|
164
|
+
if (discriminant < 0) return [];
|
|
165
|
+
const root = Math.sqrt(Math.max(0, discriminant));
|
|
166
|
+
return [(-b - root) / (2 * a), (-b + root) / (2 * a)].sort((left, right) => left - right);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function closestDistanceToSegment2D(
|
|
170
|
+
px: number,
|
|
171
|
+
py: number,
|
|
172
|
+
ax: number,
|
|
173
|
+
ay: number,
|
|
174
|
+
bx: number,
|
|
175
|
+
by: number
|
|
176
|
+
): number {
|
|
177
|
+
const dx = bx - ax;
|
|
178
|
+
const dy = by - ay;
|
|
179
|
+
const denominator = dx * dx + dy * dy;
|
|
180
|
+
const t = denominator
|
|
181
|
+
? Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / denominator))
|
|
182
|
+
: 0;
|
|
183
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Distance in the radial/Y half-plane to a spherical profile arc selected by normal Y range. */
|
|
187
|
+
export function closestDistanceToCircleArc2D(
|
|
188
|
+
px: number,
|
|
189
|
+
py: number,
|
|
190
|
+
centerY: number,
|
|
191
|
+
radius: number,
|
|
192
|
+
minimumNormalY: number,
|
|
193
|
+
maximumNormalY: number
|
|
194
|
+
): number {
|
|
195
|
+
if (radius === 0) return Math.hypot(px, py - centerY);
|
|
196
|
+
const dx = px;
|
|
197
|
+
const dy = py - centerY;
|
|
198
|
+
const length = Math.hypot(dx, dy);
|
|
199
|
+
const normalY = length ? dy / length : 0;
|
|
200
|
+
if (normalY >= minimumNormalY && normalY <= maximumNormalY) {
|
|
201
|
+
return Math.abs(length - radius);
|
|
202
|
+
}
|
|
203
|
+
const minimumRadius = radius * Math.sqrt(Math.max(0, 1 - minimumNormalY ** 2));
|
|
204
|
+
const maximumRadius = radius * Math.sqrt(Math.max(0, 1 - maximumNormalY ** 2));
|
|
205
|
+
return Math.min(
|
|
206
|
+
Math.hypot(px - minimumRadius, py - (centerY + radius * minimumNormalY)),
|
|
207
|
+
Math.hypot(px - maximumRadius, py - (centerY + radius * maximumNormalY))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {Vector3} from '@math.gl/core';
|
|
6
|
+
import {INTERSECTION} from '../../constants';
|
|
7
|
+
import {ImplicitShapeBase, type ImplicitShape, type LocalRayIntersection} from './implicit-shape';
|
|
8
|
+
import type {AxisAlignedBoundingBox} from '../bounding-volumes/axis-aligned-bounding-box';
|
|
9
|
+
import type {BoundingSphere} from '../bounding-volumes/bounding-sphere';
|
|
10
|
+
import type {Plane} from '../plane';
|
|
11
|
+
|
|
12
|
+
export type PlaneShapeProps = {sizeX?: number; sizeZ?: number; matrix?: readonly number[]};
|
|
13
|
+
|
|
14
|
+
/** Analytic glTF plane. The surface faces +Y and the negative-Y half-space is inside. */
|
|
15
|
+
export class PlaneShape extends ImplicitShapeBase {
|
|
16
|
+
readonly type = 'plane' as const;
|
|
17
|
+
readonly sizeX?: number;
|
|
18
|
+
readonly sizeZ?: number;
|
|
19
|
+
constructor(props: PlaneShapeProps = {}) {
|
|
20
|
+
super(props.matrix);
|
|
21
|
+
this.sizeX = props.sizeX;
|
|
22
|
+
this.sizeZ = props.sizeZ;
|
|
23
|
+
if (this.sizeX !== undefined && (!Number.isFinite(this.sizeX) || this.sizeX <= 0))
|
|
24
|
+
throw new Error('Plane sizeX must be greater than zero');
|
|
25
|
+
if (this.sizeZ !== undefined && (!Number.isFinite(this.sizeZ) || this.sizeZ <= 0))
|
|
26
|
+
throw new Error('Plane sizeZ must be greater than zero');
|
|
27
|
+
}
|
|
28
|
+
clone(): PlaneShape {
|
|
29
|
+
return new PlaneShape({sizeX: this.sizeX, sizeZ: this.sizeZ, matrix: this.matrix});
|
|
30
|
+
}
|
|
31
|
+
override equals(shape: ImplicitShape): boolean {
|
|
32
|
+
return (
|
|
33
|
+
shape instanceof PlaneShape &&
|
|
34
|
+
super.equals(shape) &&
|
|
35
|
+
this.sizeX === shape.sizeX &&
|
|
36
|
+
this.sizeZ === shape.sizeZ
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
containsLocalPoint(point: readonly number[]): boolean {
|
|
40
|
+
return point[1] <= 1e-12;
|
|
41
|
+
}
|
|
42
|
+
distanceToLocalPoint(point: readonly number[]): number {
|
|
43
|
+
return Math.max(0, point[1]);
|
|
44
|
+
}
|
|
45
|
+
supportLocal(): readonly number[] {
|
|
46
|
+
throw new Error('An unbounded plane half-space has no finite support point');
|
|
47
|
+
}
|
|
48
|
+
intersectLocalRay(
|
|
49
|
+
origin: readonly number[],
|
|
50
|
+
direction: readonly number[]
|
|
51
|
+
): LocalRayIntersection | undefined {
|
|
52
|
+
if (Math.abs(direction[1]) < 1e-15) return undefined;
|
|
53
|
+
const distance = -origin[1] / direction[1];
|
|
54
|
+
if (distance < 0) return undefined;
|
|
55
|
+
const x = origin[0] + distance * direction[0];
|
|
56
|
+
const z = origin[2] + distance * direction[2];
|
|
57
|
+
if (this.sizeX !== undefined && Math.abs(x) > this.sizeX / 2 + 1e-12) return undefined;
|
|
58
|
+
if (this.sizeZ !== undefined && Math.abs(z) > this.sizeZ / 2 + 1e-12) return undefined;
|
|
59
|
+
return {distance, normal: [0, 1, 0]};
|
|
60
|
+
}
|
|
61
|
+
override intersectPlane(plane: Plane): number {
|
|
62
|
+
const boundaryPoint = new Vector3(0, 0, 0).transform(this.matrix);
|
|
63
|
+
const inverse = this.inverseMatrix;
|
|
64
|
+
const boundaryNormal = new Vector3(inverse[1], inverse[5], inverse[9]).normalize();
|
|
65
|
+
const alignment = boundaryNormal.dot(plane.normal);
|
|
66
|
+
if (Math.abs(Math.abs(alignment) - 1) > 1e-10) return INTERSECTION.INTERSECTING;
|
|
67
|
+
const boundaryDistance = plane.getPointDistance(boundaryPoint);
|
|
68
|
+
if (alignment < 0 && boundaryDistance > 0) return INTERSECTION.INSIDE;
|
|
69
|
+
if (alignment > 0 && boundaryDistance < 0) return INTERSECTION.OUTSIDE;
|
|
70
|
+
return INTERSECTION.INTERSECTING;
|
|
71
|
+
}
|
|
72
|
+
override getAxisAlignedBoundingBox(): AxisAlignedBoundingBox | undefined {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
override getBoundingSphere(): BoundingSphere | undefined {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ImplicitShapeBase,
|
|
7
|
+
solveQuadratic,
|
|
8
|
+
type ImplicitShape,
|
|
9
|
+
type LocalRayIntersection
|
|
10
|
+
} from './implicit-shape';
|
|
11
|
+
|
|
12
|
+
const EPSILON = 1e-6;
|
|
13
|
+
|
|
14
|
+
export type SphereShapeProps = {radius?: number; matrix?: readonly number[]};
|
|
15
|
+
|
|
16
|
+
/** Analytic glTF sphere shape. */
|
|
17
|
+
export class SphereShape extends ImplicitShapeBase {
|
|
18
|
+
readonly type = 'sphere' as const;
|
|
19
|
+
readonly radius: number;
|
|
20
|
+
constructor(props: SphereShapeProps = {}) {
|
|
21
|
+
super(props.matrix);
|
|
22
|
+
this.radius = props.radius ?? 0.5;
|
|
23
|
+
if (!Number.isFinite(this.radius) || this.radius <= 0)
|
|
24
|
+
throw new Error('Sphere radius must be greater than zero');
|
|
25
|
+
}
|
|
26
|
+
clone(): SphereShape {
|
|
27
|
+
return new SphereShape({radius: this.radius, matrix: this.matrix});
|
|
28
|
+
}
|
|
29
|
+
override equals(shape: ImplicitShape): boolean {
|
|
30
|
+
return shape instanceof SphereShape && super.equals(shape) && this.radius === shape.radius;
|
|
31
|
+
}
|
|
32
|
+
containsLocalPoint(point: readonly number[]): boolean {
|
|
33
|
+
return Math.hypot(...point) <= this.radius + EPSILON;
|
|
34
|
+
}
|
|
35
|
+
distanceToLocalPoint(point: readonly number[]): number {
|
|
36
|
+
return Math.max(0, Math.hypot(...point) - this.radius);
|
|
37
|
+
}
|
|
38
|
+
supportLocal(direction: readonly number[]): readonly number[] {
|
|
39
|
+
const length = Math.hypot(...direction);
|
|
40
|
+
return length ? direction.map(value => (value / length) * this.radius) : [this.radius, 0, 0];
|
|
41
|
+
}
|
|
42
|
+
intersectLocalRay(
|
|
43
|
+
origin: readonly number[],
|
|
44
|
+
direction: readonly number[]
|
|
45
|
+
): LocalRayIntersection | undefined {
|
|
46
|
+
const roots = solveQuadratic(
|
|
47
|
+
dot(direction, direction),
|
|
48
|
+
2 * dot(origin, direction),
|
|
49
|
+
dot(origin, origin) - this.radius * this.radius
|
|
50
|
+
);
|
|
51
|
+
const distance = roots.find(value => value >= 0);
|
|
52
|
+
if (distance === undefined) return undefined;
|
|
53
|
+
const point = origin.map((value, i) => value + distance * direction[i]);
|
|
54
|
+
return {distance, normal: point.map(value => value / this.radius)};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function dot(a: readonly number[], b: readonly number[]): number {
|
|
59
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
60
|
+
}
|