@aura3d/physics 1.4.0 → 1.4.1

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.
@@ -1,205 +0,0 @@
1
- export type PlatformerAnimationState =
2
- | "idle"
3
- | "walk"
4
- | "run"
5
- | "jump"
6
- | "fall"
7
- | "doubleJump"
8
- | "wallSlide"
9
- | "land";
10
-
11
- export type PlatformerPlatformKind = "static" | "moving" | "rotating" | "falling" | "bouncy" | "disappearing";
12
- export type PlatformerCollectibleKind = "coin" | "gem" | "power-up";
13
-
14
- export interface PlatformerFixtureOptions {
15
- readonly seed?: number;
16
- readonly elapsedSeconds?: number;
17
- }
18
-
19
- export interface PlatformerPlatformSummary {
20
- readonly kind: PlatformerPlatformKind;
21
- readonly count: number;
22
- }
23
-
24
- export interface PlatformerCollectibleSummary {
25
- readonly kind: PlatformerCollectibleKind;
26
- readonly count: number;
27
- readonly scoreValue: number;
28
- }
29
-
30
- export interface PlatformerControllerFixture {
31
- readonly id: "external-parity-old-branch-platformer-controller-fixture";
32
- readonly source: "origin-master-platformer-controller-adapted";
33
- readonly config: {
34
- readonly walkSpeed: number;
35
- readonly runSpeed: number;
36
- readonly jumpForce: number;
37
- readonly doubleJumpForce: number;
38
- readonly wallJumpForce: number;
39
- readonly airControl: number;
40
- readonly coyoteTimeSeconds: number;
41
- readonly jumpBufferSeconds: number;
42
- readonly maxFallSpeed: number;
43
- };
44
- readonly controller: {
45
- readonly grounded: boolean;
46
- readonly coyoteJumpAccepted: boolean;
47
- readonly bufferedJumpAccepted: boolean;
48
- readonly doubleJumpAccepted: boolean;
49
- readonly wallJumpAccepted: boolean;
50
- readonly horizontalSpeed: number;
51
- readonly verticalVelocity: number;
52
- readonly stateSequence: readonly PlatformerAnimationState[];
53
- readonly finalState: PlatformerAnimationState;
54
- };
55
- readonly camera: {
56
- readonly distance: number;
57
- readonly minDistance: number;
58
- readonly maxDistance: number;
59
- readonly height: number;
60
- readonly followSpeed: number;
61
- readonly verticalAngle: number;
62
- readonly collisionAdjustedDistance: number;
63
- readonly shakeAmount: number;
64
- readonly lockOnSupported: boolean;
65
- };
66
- readonly level: {
67
- readonly startPosition: readonly [number, number, number];
68
- readonly goalPosition: readonly [number, number, number];
69
- readonly platformSummaries: readonly PlatformerPlatformSummary[];
70
- readonly collectibleSummaries: readonly PlatformerCollectibleSummary[];
71
- readonly checkpointCount: number;
72
- readonly hazardCount: number;
73
- readonly movingPlatformPathCount: number;
74
- readonly totalPlatforms: number;
75
- readonly totalCollectibles: number;
76
- readonly totalScoreValue: number;
77
- readonly goalDistance: number;
78
- };
79
- readonly hash: string;
80
- readonly claimBoundary: string;
81
- }
82
-
83
- const platformSummaries: readonly PlatformerPlatformSummary[] = [
84
- { kind: "static", count: 6 },
85
- { kind: "moving", count: 1 },
86
- { kind: "rotating", count: 1 },
87
- { kind: "falling", count: 3 },
88
- { kind: "bouncy", count: 1 },
89
- { kind: "disappearing", count: 2 }
90
- ];
91
-
92
- const collectibleSummaries: readonly PlatformerCollectibleSummary[] = [
93
- { kind: "coin", count: 5, scoreValue: 100 },
94
- { kind: "gem", count: 2, scoreValue: 500 },
95
- { kind: "power-up", count: 1, scoreValue: 1000 }
96
- ];
97
-
98
- export function samplePlatformerControllerFixture(options: PlatformerFixtureOptions = {}): PlatformerControllerFixture {
99
- const seed = integer(options.seed ?? 0x3d2025, "seed");
100
- const elapsedSeconds = finiteNonNegative(options.elapsedSeconds ?? 0.32, "elapsedSeconds");
101
- const coyoteTimeSeconds = 0.15;
102
- const jumpBufferSeconds = 0.1;
103
- const timeSinceGrounded = 0.08 + hash01(seed, 1) * 0.03;
104
- const timeSinceJumpPressed = 0.04 + hash01(seed, 2) * 0.02;
105
- const coyoteJumpAccepted = timeSinceGrounded <= coyoteTimeSeconds;
106
- const bufferedJumpAccepted = timeSinceJumpPressed <= jumpBufferSeconds;
107
- const doubleJumpAccepted = coyoteJumpAccepted && bufferedJumpAccepted;
108
- const wallJumpAccepted = hash01(seed, 3) > 0.25;
109
- const horizontalSpeed = Number((5 + hash01(seed, 4) * 3).toFixed(4));
110
- const verticalVelocity = Number((12 - elapsedSeconds * 9.8).toFixed(4));
111
- const stateSequence: readonly PlatformerAnimationState[] = [
112
- "idle",
113
- horizontalSpeed > 6.5 ? "run" : "walk",
114
- "jump",
115
- doubleJumpAccepted ? "doubleJump" : "fall",
116
- wallJumpAccepted ? "wallSlide" : "fall",
117
- "land"
118
- ];
119
- const totalPlatforms = platformSummaries.reduce((sum, entry) => sum + entry.count, 0);
120
- const totalCollectibles = collectibleSummaries.reduce((sum, entry) => sum + entry.count, 0);
121
- const totalScoreValue = collectibleSummaries.reduce((sum, entry) => sum + entry.count * entry.scoreValue, 0);
122
- const startPosition = [0, 2, 0] as const;
123
- const goalPosition = [56, 9, 0] as const;
124
- const goalDistance = Number(Math.hypot(goalPosition[0] - startPosition[0], goalPosition[1] - startPosition[1], goalPosition[2] - startPosition[2]).toFixed(4));
125
- return {
126
- id: "external-parity-old-branch-platformer-controller-fixture",
127
- source: "origin-master-platformer-controller-adapted",
128
- config: {
129
- walkSpeed: 5,
130
- runSpeed: 8,
131
- jumpForce: 12,
132
- doubleJumpForce: 10,
133
- wallJumpForce: 14,
134
- airControl: 0.3,
135
- coyoteTimeSeconds,
136
- jumpBufferSeconds,
137
- maxFallSpeed: 30
138
- },
139
- controller: {
140
- grounded: false,
141
- coyoteJumpAccepted,
142
- bufferedJumpAccepted,
143
- doubleJumpAccepted,
144
- wallJumpAccepted,
145
- horizontalSpeed,
146
- verticalVelocity,
147
- stateSequence,
148
- finalState: "land"
149
- },
150
- camera: {
151
- distance: 8,
152
- minDistance: 3,
153
- maxDistance: 15,
154
- height: 2,
155
- followSpeed: 8,
156
- verticalAngle: Number((0.3 + hash01(seed, 5) * 0.1).toFixed(4)),
157
- collisionAdjustedDistance: Number((8 - hash01(seed, 6) * 1.5).toFixed(4)),
158
- shakeAmount: Number((0.18 * Math.pow(0.95, elapsedSeconds * 60)).toFixed(4)),
159
- lockOnSupported: true
160
- },
161
- level: {
162
- startPosition,
163
- goalPosition,
164
- platformSummaries,
165
- collectibleSummaries,
166
- checkpointCount: 2,
167
- hazardCount: 3,
168
- movingPlatformPathCount: 2,
169
- totalPlatforms,
170
- totalCollectibles,
171
- totalScoreValue,
172
- goalDistance
173
- },
174
- hash: hashPlatformer(seed, elapsedSeconds, horizontalSpeed, verticalVelocity, totalPlatforms, totalCollectibles, goalDistance),
175
- claimBoundary: "Deterministic platformer controller, third-person camera, and level-layout telemetry adapted from the old platformer example; this is not a full character controller replacement, collision solver, camera occlusion system, or Unity/Unreal controller parity claim."
176
- };
177
- }
178
-
179
- function hashPlatformer(...values: readonly number[]): string {
180
- let hash = 0x811c9dc5;
181
- for (const value of values) {
182
- const scaled = Math.round(value * 10_000);
183
- hash ^= scaled & 0xff;
184
- hash = Math.imul(hash, 0x01000193) >>> 0;
185
- hash ^= (scaled >>> 8) & 0xff;
186
- hash = Math.imul(hash, 0x01000193) >>> 0;
187
- }
188
- return hash.toString(16).padStart(8, "0");
189
- }
190
-
191
- function hash01(seed: number, salt: number): number {
192
- let value = Math.imul(seed + salt * 1013904223, 1664525) + 1013904223;
193
- value = Math.imul(value ^ (value >>> 13), 1274126177);
194
- return ((value ^ (value >>> 16)) >>> 0) / 4294967296;
195
- }
196
-
197
- function finiteNonNegative(value: number, label: string): number {
198
- if (!Number.isFinite(value) || value < 0) throw new RangeError(`Platformer fixture ${label} must be finite and non-negative.`);
199
- return value;
200
- }
201
-
202
- function integer(value: number, label: string): number {
203
- if (!Number.isInteger(value)) throw new RangeError(`Platformer fixture ${label} must be an integer.`);
204
- return value;
205
- }
package/src/Raycast.ts DELETED
@@ -1,345 +0,0 @@
1
- import { addVec3, dotVec3, scaleVec3, subVec3, type Vec3 } from "./Shape.js";
2
- import type { Collider } from "./Collider.js";
3
- import type { RigidBody } from "./RigidBody.js";
4
-
5
- export type RaycastOptions = {
6
- readonly maxDistance?: number;
7
- readonly mask?: number;
8
- readonly includeSensors?: boolean;
9
- readonly includeBackfaces?: boolean;
10
- };
11
-
12
- export type RaycastHit = {
13
- readonly colliderId: number;
14
- readonly bodyId: number;
15
- readonly point: Vec3;
16
- readonly normal: Vec3;
17
- readonly distance: number;
18
- };
19
-
20
- export type SphereCastHit = RaycastHit & {
21
- readonly castCenter: Vec3;
22
- readonly castRadius: number;
23
- };
24
-
25
- /** Ground query result consumed by the animation foot-IK rig (matches its `GroundSample` shape). */
26
- export type GroundHeightSample = {
27
- readonly point: Vec3;
28
- readonly normal: Vec3;
29
- readonly distance: number;
30
- };
31
-
32
- /** Structural downward-ground query the `@aura3d/animation` foot-IK rig accepts. */
33
- export type GroundHeightRaycaster = {
34
- raycastDown(origin: Vec3, maxDistance: number): GroundHeightSample | undefined;
35
- };
36
-
37
- /**
38
- * Adapt any downward raycast into the ground query the foot-IK rig needs. `cast` should perform a
39
- * straight-down ray (typically a `PhysicsWorld.raycast` bound to a ground layer mask) and return
40
- * the first hit. This is a thin adapter, not a new solver — it just casts `[0,-1,0]` from the
41
- * given origin and reports the hit point/normal/distance.
42
- */
43
- export function groundHeightRaycaster(
44
- cast: (origin: Vec3, direction: Vec3, maxDistance: number) => RaycastHit | undefined
45
- ): GroundHeightRaycaster {
46
- return {
47
- raycastDown(origin: Vec3, maxDistance: number): GroundHeightSample | undefined {
48
- const hit = cast(origin, [0, -1, 0], maxDistance);
49
- if (!hit) return undefined;
50
- return { point: hit.point, normal: hit.normal, distance: hit.distance };
51
- }
52
- };
53
- }
54
-
55
- export function raycastCollider(origin: Vec3, direction: Vec3, collider: Collider, body: RigidBody, options: RaycastOptions = {}): RaycastHit | undefined {
56
- const maxDistance = options.maxDistance ?? Number.POSITIVE_INFINITY;
57
- if (collider.sensor && options.includeSensors !== true) {
58
- return undefined;
59
- }
60
- if (options.mask !== undefined && (collider.filter.layer & options.mask) === 0) {
61
- return undefined;
62
- }
63
- switch (collider.shape.kind) {
64
- case "sphere":
65
- return raycastSphere(origin, direction, body.position, collider.shape.radius, collider, maxDistance);
66
- case "box":
67
- case "capsule":
68
- return raycastAabb(origin, direction, collider.bounds(body.position), collider, body.id, maxDistance);
69
- case "plane":
70
- return raycastPlane(origin, direction, collider.shape.normal, collider.shape.constant, collider, body.id, maxDistance);
71
- case "mesh":
72
- return raycastMesh(origin, direction, collider, body, maxDistance, options.includeBackfaces === true);
73
- }
74
- }
75
-
76
- export function sphereCastCollider(origin: Vec3, radius: number, direction: Vec3, collider: Collider, body: RigidBody, options: RaycastOptions = {}): SphereCastHit | undefined {
77
- if (!Number.isFinite(radius) || radius <= 0) {
78
- throw new Error("sphereCast radius must be a finite positive number.");
79
- }
80
- const maxDistance = options.maxDistance ?? Number.POSITIVE_INFINITY;
81
- if (collider.sensor && options.includeSensors !== true) {
82
- return undefined;
83
- }
84
- if (options.mask !== undefined && (collider.filter.layer & options.mask) === 0) {
85
- return undefined;
86
- }
87
- switch (collider.shape.kind) {
88
- case "sphere":
89
- return sphereCastSphere(origin, radius, direction, body.position, collider.shape.radius, collider, maxDistance);
90
- case "box":
91
- case "capsule": {
92
- const bounds = collider.bounds(body.position);
93
- const expanded = {
94
- min: [bounds.min[0] - radius, bounds.min[1] - radius, bounds.min[2] - radius] as Vec3,
95
- max: [bounds.max[0] + radius, bounds.max[1] + radius, bounds.max[2] + radius] as Vec3
96
- };
97
- const hit = raycastAabb(origin, direction, expanded, collider, body.id, maxDistance);
98
- return hit ? toSphereCastHit(hit, origin, direction, radius) : undefined;
99
- }
100
- case "plane":
101
- return sphereCastPlane(origin, radius, direction, collider.shape.normal, collider.shape.constant, body.position, collider, body.id, maxDistance);
102
- case "mesh": {
103
- const hit = raycastMesh(origin, direction, collider, body, maxDistance, options.includeBackfaces === true);
104
- return hit ? toSphereCastHit(hit, origin, direction, radius) : undefined;
105
- }
106
- }
107
- }
108
-
109
- function raycastSphere(origin: Vec3, direction: Vec3, center: Vec3, radius: number, collider: Collider, maxDistance: number): RaycastHit | undefined {
110
- const oc = subVec3(origin, center);
111
- const b = dotVec3(oc, direction);
112
- const c = dotVec3(oc, oc) - radius * radius;
113
- const discriminant = b * b - c;
114
- if (discriminant < 0) {
115
- return undefined;
116
- }
117
- const sqrt = Math.sqrt(discriminant);
118
- const insideDistance = c <= 0 ? 0 : undefined;
119
- const distance = insideDistance ?? -b - sqrt;
120
- if (distance < 0 || distance > maxDistance) {
121
- return undefined;
122
- }
123
- const point = [origin[0] + direction[0] * distance, origin[1] + direction[1] * distance, origin[2] + direction[2] * distance] as Vec3;
124
- const normal = c <= 0 ? scaleVec3(direction, -1) : scaleVec3(subVec3(point, center), 1 / radius);
125
- return { colliderId: collider.id, bodyId: collider.bodyId, point, normal, distance };
126
- }
127
-
128
- function sphereCastSphere(origin: Vec3, castRadius: number, direction: Vec3, center: Vec3, targetRadius: number, collider: Collider, maxDistance: number): SphereCastHit | undefined {
129
- const expandedRadius = castRadius + targetRadius;
130
- const expandedHit = raycastSphere(origin, direction, center, expandedRadius, collider, maxDistance);
131
- if (!expandedHit) {
132
- return undefined;
133
- }
134
- const castCenter = pointAlongRay(origin, direction, expandedHit.distance);
135
- const normal = expandedHit.distance === 0
136
- ? scaleVec3(direction, -1)
137
- : stableVec3(scaleVec3(subVec3(castCenter, center), 1 / Math.max(1e-9, Math.hypot(castCenter[0] - center[0], castCenter[1] - center[1], castCenter[2] - center[2]))));
138
- return {
139
- colliderId: collider.id,
140
- bodyId: collider.bodyId,
141
- point: stableVec3(subVec3(castCenter, scaleVec3(normal, castRadius))),
142
- normal,
143
- distance: expandedHit.distance,
144
- castCenter,
145
- castRadius
146
- };
147
- }
148
-
149
- function raycastAabb(origin: Vec3, direction: Vec3, bounds: { readonly min: Vec3; readonly max: Vec3 }, collider: Collider, bodyId: number, maxDistance: number): RaycastHit | undefined {
150
- let tmin = 0;
151
- let tmax = maxDistance;
152
- let axis = 0;
153
- let sign = -1;
154
- const originInside = pointInsideAabb(origin, bounds);
155
- for (let i = 0; i < 3; i += 1) {
156
- const ray = direction[i]!;
157
- if (Math.abs(ray) < 1e-9) {
158
- if (origin[i]! < bounds.min[i]! || origin[i]! > bounds.max[i]!) {
159
- return undefined;
160
- }
161
- continue;
162
- }
163
- const inv = 1 / ray;
164
- let t1 = (bounds.min[i]! - origin[i]!) * inv;
165
- let t2 = (bounds.max[i]! - origin[i]!) * inv;
166
- let localSign = -Math.sign(ray);
167
- if (t1 > t2) {
168
- const temp = t1;
169
- t1 = t2;
170
- t2 = temp;
171
- localSign = Math.sign(ray);
172
- }
173
- if (t1 > tmin) {
174
- tmin = t1;
175
- axis = i;
176
- sign = localSign;
177
- }
178
- tmax = Math.min(tmax, t2);
179
- if (tmin > tmax) {
180
- return undefined;
181
- }
182
- }
183
- const point = [origin[0] + direction[0] * tmin, origin[1] + direction[1] * tmin, origin[2] + direction[2] * tmin] as Vec3;
184
- const normal: [number, number, number] = [0, 0, 0];
185
- if (originInside && tmin === 0) {
186
- normal[dominantAxis(direction)] = -Math.sign(direction[dominantAxis(direction)] || 1);
187
- } else {
188
- normal[axis] = sign;
189
- }
190
- return { colliderId: collider.id, bodyId, point, normal, distance: tmin };
191
- }
192
-
193
- function raycastPlane(origin: Vec3, direction: Vec3, normal: Vec3, constant: number, collider: Collider, bodyId: number, maxDistance: number): RaycastHit | undefined {
194
- const denom = dotVec3(normal, direction);
195
- if (Math.abs(denom) < 1e-9) {
196
- return undefined;
197
- }
198
- const distance = -(dotVec3(normal, origin) + constant) / denom;
199
- if (distance < 0 || distance > maxDistance) {
200
- return undefined;
201
- }
202
- const point = [origin[0] + direction[0] * distance, origin[1] + direction[1] * distance, origin[2] + direction[2] * distance] as Vec3;
203
- return { colliderId: collider.id, bodyId, point, normal, distance };
204
- }
205
-
206
- function sphereCastPlane(origin: Vec3, radius: number, direction: Vec3, normal: Vec3, constant: number, planePosition: Vec3, collider: Collider, bodyId: number, maxDistance: number): SphereCastHit | undefined {
207
- const signedDistance = dotVec3(normal, origin) + constant + dotVec3(normal, planePosition);
208
- if (signedDistance <= radius) {
209
- const castCenter = origin;
210
- return {
211
- colliderId: collider.id,
212
- bodyId,
213
- point: stableVec3(subVec3(castCenter, scaleVec3(normal, radius))),
214
- normal,
215
- distance: 0,
216
- castCenter,
217
- castRadius: radius
218
- };
219
- }
220
- const denom = dotVec3(normal, direction);
221
- if (denom >= -1e-9) {
222
- return undefined;
223
- }
224
- const distance = (radius - signedDistance) / denom;
225
- if (distance < 0 || distance > maxDistance) {
226
- return undefined;
227
- }
228
- const castCenter = pointAlongRay(origin, direction, distance);
229
- return {
230
- colliderId: collider.id,
231
- bodyId,
232
- point: stableVec3(subVec3(castCenter, scaleVec3(normal, radius))),
233
- normal,
234
- distance,
235
- castCenter,
236
- castRadius: radius
237
- };
238
- }
239
-
240
- function raycastMesh(origin: Vec3, direction: Vec3, collider: Collider, body: RigidBody, maxDistance: number, includeBackfaces: boolean): RaycastHit | undefined {
241
- if (collider.shape.kind !== "mesh") {
242
- return undefined;
243
- }
244
- let closest: RaycastHit | undefined;
245
- const { vertices, indices } = collider.shape;
246
- for (let index = 0; index < indices.length; index += 3) {
247
- const a = addVec3(body.position, vertices[indices[index]!]!);
248
- const b = addVec3(body.position, vertices[indices[index + 1]!]!);
249
- const c = addVec3(body.position, vertices[indices[index + 2]!]!);
250
- const hit = raycastTriangle(origin, direction, a, b, c, collider, body.id, maxDistance, includeBackfaces);
251
- if (hit && (!closest || hit.distance < closest.distance)) {
252
- closest = hit;
253
- }
254
- }
255
- return closest;
256
- }
257
-
258
- function raycastTriangle(
259
- origin: Vec3,
260
- direction: Vec3,
261
- a: Vec3,
262
- b: Vec3,
263
- c: Vec3,
264
- collider: Collider,
265
- bodyId: number,
266
- maxDistance: number,
267
- includeBackfaces: boolean
268
- ): RaycastHit | undefined {
269
- const edge1 = subVec3(b, a);
270
- const edge2 = subVec3(c, a);
271
- const p = crossVec3(direction, edge2);
272
- const det = dotVec3(edge1, p);
273
- if (includeBackfaces ? Math.abs(det) < 1e-9 : det < 1e-9) {
274
- return undefined;
275
- }
276
- const invDet = 1 / det;
277
- const t = subVec3(origin, a);
278
- const u = dotVec3(t, p) * invDet;
279
- if (u < 0 || u > 1) {
280
- return undefined;
281
- }
282
- const q = crossVec3(t, edge1);
283
- const v = dotVec3(direction, q) * invDet;
284
- if (v < 0 || u + v > 1) {
285
- return undefined;
286
- }
287
- const distance = dotVec3(edge2, q) * invDet;
288
- if (distance < 0 || distance > maxDistance) {
289
- return undefined;
290
- }
291
- const normal = normalizeForRay(crossVec3(edge1, edge2), direction);
292
- const point = [origin[0] + direction[0] * distance, origin[1] + direction[1] * distance, origin[2] + direction[2] * distance] as Vec3;
293
- return { colliderId: collider.id, bodyId, point, normal, distance };
294
- }
295
-
296
- function crossVec3(a: Vec3, b: Vec3): [number, number, number] {
297
- return [
298
- a[1] * b[2] - a[2] * b[1],
299
- a[2] * b[0] - a[0] * b[2],
300
- a[0] * b[1] - a[1] * b[0]
301
- ];
302
- }
303
-
304
- function normalizeForRay(normal: Vec3, direction: Vec3): Vec3 {
305
- const length = Math.hypot(normal[0], normal[1], normal[2]);
306
- const normalized = length <= 1e-9 ? [0, 1, 0] as Vec3 : stableVec3(scaleVec3(normal, 1 / length));
307
- return dotVec3(normalized, direction) > 0 ? stableVec3(scaleVec3(normalized, -1)) : normalized;
308
- }
309
-
310
- function pointAlongRay(origin: Vec3, direction: Vec3, distance: number): Vec3 {
311
- return [
312
- origin[0] + direction[0] * distance,
313
- origin[1] + direction[1] * distance,
314
- origin[2] + direction[2] * distance
315
- ];
316
- }
317
-
318
- function pointInsideAabb(point: Vec3, bounds: { readonly min: Vec3; readonly max: Vec3 }): boolean {
319
- return point[0] >= bounds.min[0] && point[0] <= bounds.max[0] &&
320
- point[1] >= bounds.min[1] && point[1] <= bounds.max[1] &&
321
- point[2] >= bounds.min[2] && point[2] <= bounds.max[2];
322
- }
323
-
324
- function dominantAxis(direction: Vec3): 0 | 1 | 2 {
325
- const ax = Math.abs(direction[0]);
326
- const ay = Math.abs(direction[1]);
327
- const az = Math.abs(direction[2]);
328
- if (ay >= ax && ay >= az) return 1;
329
- if (az >= ax && az >= ay) return 2;
330
- return 0;
331
- }
332
-
333
- function toSphereCastHit(hit: RaycastHit, origin: Vec3, direction: Vec3, radius: number): SphereCastHit {
334
- const castCenter = pointAlongRay(origin, direction, hit.distance);
335
- return {
336
- ...hit,
337
- point: stableVec3(subVec3(castCenter, scaleVec3(hit.normal, radius))),
338
- castCenter,
339
- castRadius: radius
340
- };
341
- }
342
-
343
- function stableVec3(value: Vec3): Vec3 {
344
- return value.map((component) => Object.is(component, -0) ? 0 : component) as unknown as Vec3;
345
- }