@aura3d/physics 1.4.0 → 1.4.2
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/package.json +34 -2
- package/src/CharacterController.ts +0 -304
- package/src/ClothFixtures.ts +0 -359
- package/src/Collider.ts +0 -67
- package/src/CollisionEvents.ts +0 -70
- package/src/CollisionVolumes.ts +0 -303
- package/src/Constraint.ts +0 -120
- package/src/Constraints.ts +0 -1
- package/src/Crowd.ts +0 -283
- package/src/ECSPhysicsBridge.ts +0 -53
- package/src/FireSmokeFixtures.ts +0 -382
- package/src/FluidFixtures.ts +0 -301
- package/src/FractureFixtures.ts +0 -277
- package/src/HitboxWorld.ts +0 -1179
- package/src/KinematicBody.ts +0 -597
- package/src/KinematicWorld.ts +0 -316
- package/src/Navigation.ts +0 -321
- package/src/PhysicsDebugDraw.ts +0 -71
- package/src/PhysicsSandboxFixtures.ts +0 -394
- package/src/PhysicsStepper.ts +0 -47
- package/src/PhysicsWorld.ts +0 -986
- package/src/PlatformerFixtures.ts +0 -205
- package/src/Raycast.ts +0 -345
- package/src/RigidBody.ts +0 -311
- package/src/ScenePhysicsBridge.ts +0 -207
- package/src/Shape.ts +0 -195
- package/src/SoftBodyFixtures.ts +0 -363
- package/src/Steering.ts +0 -531
- package/src/VehicleDynamics.ts +0 -524
- package/src/index.ts +0 -28
package/src/KinematicWorld.ts
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import { aabbPenetration, overlapsVolume, type ResolvedCollisionVolume } from "./CollisionVolumes.js";
|
|
2
|
-
import {
|
|
3
|
-
KinematicBody,
|
|
4
|
-
type KinematicBodyDescriptor,
|
|
5
|
-
type KinematicBodyEvent,
|
|
6
|
-
type KinematicBodyId,
|
|
7
|
-
type KinematicBodySnapshot,
|
|
8
|
-
type KinematicBounds,
|
|
9
|
-
type KinematicStepOptions
|
|
10
|
-
} from "./KinematicBody.js";
|
|
11
|
-
import { cloneVec3, type Vec3 } from "./Shape.js";
|
|
12
|
-
|
|
13
|
-
export type KinematicWorldDescriptor = {
|
|
14
|
-
readonly fixedDelta?: number;
|
|
15
|
-
readonly gravity?: number;
|
|
16
|
-
readonly groundY?: number;
|
|
17
|
-
readonly groundSnapDistance?: number;
|
|
18
|
-
readonly bounds?: KinematicBounds;
|
|
19
|
-
readonly lockDepth?: boolean;
|
|
20
|
-
readonly laneZ?: number;
|
|
21
|
-
readonly laneHalfWidth?: number;
|
|
22
|
-
readonly pushboxSeparation?: boolean;
|
|
23
|
-
readonly solverIterations?: number;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type KinematicPushEvent = {
|
|
27
|
-
readonly type: "pushbox-collision";
|
|
28
|
-
readonly bodyA: KinematicBodyId;
|
|
29
|
-
readonly bodyB: KinematicBodyId;
|
|
30
|
-
readonly normal: Vec3;
|
|
31
|
-
readonly penetration: number;
|
|
32
|
-
readonly correctionA: Vec3;
|
|
33
|
-
readonly correctionB: Vec3;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type KinematicWorldEvent = KinematicBodyEvent | KinematicPushEvent;
|
|
37
|
-
|
|
38
|
-
export type KinematicWorldSnapshot = {
|
|
39
|
-
readonly frame: number;
|
|
40
|
-
readonly fixedDelta: number;
|
|
41
|
-
readonly bodies: readonly KinematicBodySnapshot[];
|
|
42
|
-
readonly events: readonly KinematicWorldEvent[];
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export class KinematicWorld {
|
|
46
|
-
readonly fixedDelta: number;
|
|
47
|
-
readonly gravity: number;
|
|
48
|
-
readonly groundY: number;
|
|
49
|
-
readonly groundSnapDistance: number;
|
|
50
|
-
readonly bounds: KinematicBounds | undefined;
|
|
51
|
-
readonly lockDepth: boolean;
|
|
52
|
-
readonly laneZ: number;
|
|
53
|
-
readonly laneHalfWidth: number | undefined;
|
|
54
|
-
readonly pushboxSeparation: boolean;
|
|
55
|
-
readonly solverIterations: number;
|
|
56
|
-
private readonly bodiesById = new Map<KinematicBodyId, KinematicBody>();
|
|
57
|
-
private nextBodyId = 1;
|
|
58
|
-
private frame = 0;
|
|
59
|
-
private lastEvents: readonly KinematicWorldEvent[] = [];
|
|
60
|
-
|
|
61
|
-
constructor(descriptor: KinematicWorldDescriptor = {}) {
|
|
62
|
-
this.fixedDelta = positiveFinite(descriptor.fixedDelta ?? 1 / 60, "kinematic fixedDelta");
|
|
63
|
-
this.gravity = nonNegativeFinite(Math.abs(descriptor.gravity ?? 24), "kinematic world gravity");
|
|
64
|
-
this.groundY = finite(descriptor.groundY ?? 0, "kinematic world groundY");
|
|
65
|
-
this.groundSnapDistance = nonNegativeFinite(descriptor.groundSnapDistance ?? 0.08, "kinematic world groundSnapDistance");
|
|
66
|
-
this.bounds = descriptor.bounds;
|
|
67
|
-
this.lockDepth = descriptor.lockDepth ?? true;
|
|
68
|
-
this.laneZ = finite(descriptor.laneZ ?? 0, "kinematic world laneZ");
|
|
69
|
-
this.laneHalfWidth = descriptor.laneHalfWidth === undefined ? undefined : nonNegativeFinite(descriptor.laneHalfWidth, "kinematic world laneHalfWidth");
|
|
70
|
-
this.pushboxSeparation = descriptor.pushboxSeparation ?? true;
|
|
71
|
-
this.solverIterations = positiveInteger(descriptor.solverIterations ?? 2, "kinematic world solverIterations");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
createBody(descriptor: KinematicBodyDescriptor = {}): KinematicBody {
|
|
75
|
-
const id = descriptor.id ?? this.nextBodyId;
|
|
76
|
-
if (typeof id === "number" && id >= this.nextBodyId) {
|
|
77
|
-
this.nextBodyId = Math.floor(id) + 1;
|
|
78
|
-
} else {
|
|
79
|
-
this.nextBodyId += 1;
|
|
80
|
-
}
|
|
81
|
-
if (this.bodiesById.has(id)) {
|
|
82
|
-
throw new Error(`Kinematic body ${String(id)} already exists.`);
|
|
83
|
-
}
|
|
84
|
-
const body = new KinematicBody(id, {
|
|
85
|
-
groundY: this.groundY,
|
|
86
|
-
groundSnapDistance: this.groundSnapDistance,
|
|
87
|
-
bounds: this.bounds,
|
|
88
|
-
lockDepth: this.lockDepth,
|
|
89
|
-
laneZ: this.laneZ,
|
|
90
|
-
laneHalfWidth: this.laneHalfWidth,
|
|
91
|
-
gravity: this.gravity,
|
|
92
|
-
...descriptor
|
|
93
|
-
});
|
|
94
|
-
this.bodiesById.set(body.id, body);
|
|
95
|
-
return body;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
addBody(body: KinematicBody): void {
|
|
99
|
-
if (this.bodiesById.has(body.id)) {
|
|
100
|
-
throw new Error(`Kinematic body ${String(body.id)} already exists.`);
|
|
101
|
-
}
|
|
102
|
-
this.bodiesById.set(body.id, body);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
removeBody(id: KinematicBodyId): void {
|
|
106
|
-
this.bodiesById.delete(id);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
getBody(id: KinematicBodyId): KinematicBody | undefined {
|
|
110
|
-
return this.bodiesById.get(id);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
requireBody(id: KinematicBodyId): KinematicBody {
|
|
114
|
-
const body = this.getBody(id);
|
|
115
|
-
if (!body) {
|
|
116
|
-
throw new Error(`Kinematic body ${String(id)} does not exist.`);
|
|
117
|
-
}
|
|
118
|
-
return body;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
bodies(): readonly KinematicBody[] {
|
|
122
|
-
return Array.from(this.bodiesById.values()).sort(compareBodies);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
step(dt = this.fixedDelta): readonly KinematicWorldEvent[] {
|
|
126
|
-
if (!Number.isFinite(dt) || dt <= 0) {
|
|
127
|
-
throw new Error("KinematicWorld.step dt must be finite and positive.");
|
|
128
|
-
}
|
|
129
|
-
const events: KinematicWorldEvent[] = [];
|
|
130
|
-
const options = this.stepOptions();
|
|
131
|
-
for (const body of this.bodies()) {
|
|
132
|
-
events.push(...body.step(dt, options));
|
|
133
|
-
}
|
|
134
|
-
if (this.pushboxSeparation) {
|
|
135
|
-
events.push(...this.resolvePushboxes());
|
|
136
|
-
}
|
|
137
|
-
this.frame += 1;
|
|
138
|
-
this.lastEvents = events;
|
|
139
|
-
return events;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
resolvePushboxes(): readonly KinematicPushEvent[] {
|
|
143
|
-
const events: KinematicPushEvent[] = [];
|
|
144
|
-
const options = this.stepOptions();
|
|
145
|
-
for (let iteration = 0; iteration < this.solverIterations; iteration += 1) {
|
|
146
|
-
const bodies = this.bodies();
|
|
147
|
-
for (let aIndex = 0; aIndex < bodies.length; aIndex += 1) {
|
|
148
|
-
for (let bIndex = aIndex + 1; bIndex < bodies.length; bIndex += 1) {
|
|
149
|
-
const bodyA = bodies[aIndex]!;
|
|
150
|
-
const bodyB = bodies[bIndex]!;
|
|
151
|
-
const pushA = bodyA.resolvedPushbox();
|
|
152
|
-
const pushB = bodyB.resolvedPushbox();
|
|
153
|
-
const event = resolvePushPair(bodyA, bodyB, pushA, pushB, this.lockDepth);
|
|
154
|
-
if (event) {
|
|
155
|
-
bodyA.constrain(options);
|
|
156
|
-
bodyB.constrain(options);
|
|
157
|
-
events.push(event);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return events;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
snapshot(): KinematicWorldSnapshot {
|
|
166
|
-
return {
|
|
167
|
-
frame: this.frame,
|
|
168
|
-
fixedDelta: this.fixedDelta,
|
|
169
|
-
bodies: this.bodies().map((body) => body.snapshot()),
|
|
170
|
-
events: this.lastEvents.map(cloneWorldEvent)
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
private stepOptions(): KinematicStepOptions {
|
|
175
|
-
return {
|
|
176
|
-
gravity: this.gravity,
|
|
177
|
-
groundY: this.groundY,
|
|
178
|
-
groundSnapDistance: this.groundSnapDistance,
|
|
179
|
-
bounds: this.bounds,
|
|
180
|
-
lockDepth: this.lockDepth,
|
|
181
|
-
laneZ: this.laneZ,
|
|
182
|
-
laneHalfWidth: this.laneHalfWidth
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function resolvePushPair(
|
|
188
|
-
bodyA: KinematicBody,
|
|
189
|
-
bodyB: KinematicBody,
|
|
190
|
-
pushA: ResolvedCollisionVolume,
|
|
191
|
-
pushB: ResolvedCollisionVolume,
|
|
192
|
-
lockDepth: boolean
|
|
193
|
-
): KinematicPushEvent | null {
|
|
194
|
-
if (!overlapsVolume(pushA, pushB)) {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
const penetration = aabbPenetration(pushA.bounds, pushB.bounds);
|
|
198
|
-
if (!penetration) {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
const overlapX = penetration.overlap[0];
|
|
202
|
-
const overlapZ = penetration.overlap[2];
|
|
203
|
-
const useZ = !lockDepth && overlapZ < overlapX;
|
|
204
|
-
const axisIndex = useZ ? 2 : 0;
|
|
205
|
-
const axis = useZ ? "z" : "x";
|
|
206
|
-
const centerA = pushA.center[axisIndex];
|
|
207
|
-
const centerB = pushB.center[axisIndex];
|
|
208
|
-
const tieBreak = compareIds(bodyA.id, bodyB.id) <= 0 ? -1 : 1;
|
|
209
|
-
const direction = centerA === centerB ? tieBreak : centerA < centerB ? -1 : 1;
|
|
210
|
-
const depth = useZ ? overlapZ : overlapX;
|
|
211
|
-
if (depth <= 0) {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
const correctionA: [number, number, number] = [0, 0, 0];
|
|
215
|
-
const correctionB: [number, number, number] = [0, 0, 0];
|
|
216
|
-
correctionA[axisIndex] = direction * depth * 0.5;
|
|
217
|
-
correctionB[axisIndex] = -direction * depth * 0.5;
|
|
218
|
-
bodyA.translate(correctionA);
|
|
219
|
-
bodyB.translate(correctionB);
|
|
220
|
-
if (bodyA.velocity[axisIndex] * direction < 0) {
|
|
221
|
-
bodyA.stopAxis(axis);
|
|
222
|
-
}
|
|
223
|
-
if (bodyB.velocity[axisIndex] * -direction < 0) {
|
|
224
|
-
bodyB.stopAxis(axis);
|
|
225
|
-
}
|
|
226
|
-
const normal: [number, number, number] = [0, 0, 0];
|
|
227
|
-
normal[axisIndex] = direction;
|
|
228
|
-
return {
|
|
229
|
-
type: "pushbox-collision",
|
|
230
|
-
bodyA: bodyA.id,
|
|
231
|
-
bodyB: bodyB.id,
|
|
232
|
-
normal,
|
|
233
|
-
penetration: depth,
|
|
234
|
-
correctionA,
|
|
235
|
-
correctionB
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function cloneWorldEvent(event: KinematicWorldEvent): KinematicWorldEvent {
|
|
240
|
-
switch (event.type) {
|
|
241
|
-
case "jump":
|
|
242
|
-
case "land":
|
|
243
|
-
case "bounds":
|
|
244
|
-
case "ground-snap":
|
|
245
|
-
return {
|
|
246
|
-
...event,
|
|
247
|
-
position: cloneVec3(event.position)
|
|
248
|
-
};
|
|
249
|
-
case "dash":
|
|
250
|
-
case "fast-fall":
|
|
251
|
-
return {
|
|
252
|
-
...event,
|
|
253
|
-
position: cloneVec3(event.position),
|
|
254
|
-
velocity: cloneVec3(event.velocity)
|
|
255
|
-
};
|
|
256
|
-
case "crouch":
|
|
257
|
-
return {
|
|
258
|
-
...event,
|
|
259
|
-
position: cloneVec3(event.position)
|
|
260
|
-
};
|
|
261
|
-
case "knockback":
|
|
262
|
-
return {
|
|
263
|
-
...event,
|
|
264
|
-
impulse: cloneVec3(event.impulse),
|
|
265
|
-
velocity: cloneVec3(event.velocity)
|
|
266
|
-
};
|
|
267
|
-
case "pushbox-collision":
|
|
268
|
-
return {
|
|
269
|
-
...event,
|
|
270
|
-
normal: cloneVec3(event.normal),
|
|
271
|
-
correctionA: cloneVec3(event.correctionA),
|
|
272
|
-
correctionB: cloneVec3(event.correctionB)
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function compareBodies(a: KinematicBody, b: KinematicBody): number {
|
|
278
|
-
return compareIds(a.id, b.id);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function compareIds(a: KinematicBodyId, b: KinematicBodyId): number {
|
|
282
|
-
const left = String(a);
|
|
283
|
-
const right = String(b);
|
|
284
|
-
if (left === right) {
|
|
285
|
-
return 0;
|
|
286
|
-
}
|
|
287
|
-
return left < right ? -1 : 1;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function finite(value: number, name: string): number {
|
|
291
|
-
if (!Number.isFinite(value)) {
|
|
292
|
-
throw new Error(`${name} must be finite.`);
|
|
293
|
-
}
|
|
294
|
-
return value;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function nonNegativeFinite(value: number, name: string): number {
|
|
298
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
299
|
-
throw new Error(`${name} must be finite and non-negative.`);
|
|
300
|
-
}
|
|
301
|
-
return value;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function positiveFinite(value: number, name: string): number {
|
|
305
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
306
|
-
throw new Error(`${name} must be finite and positive.`);
|
|
307
|
-
}
|
|
308
|
-
return value;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function positiveInteger(value: number, name: string): number {
|
|
312
|
-
if (!Number.isInteger(value) || value <= 0) {
|
|
313
|
-
throw new Error(`${name} must be a positive integer.`);
|
|
314
|
-
}
|
|
315
|
-
return value;
|
|
316
|
-
}
|
package/src/Navigation.ts
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
export type NavigationCell = readonly [number, number];
|
|
2
|
-
export type NavigationPoint = readonly [number, number];
|
|
3
|
-
|
|
4
|
-
export type NavigationPathStatus = "success" | "partial" | "failed";
|
|
5
|
-
|
|
6
|
-
export interface NavigationPath {
|
|
7
|
-
readonly status: NavigationPathStatus;
|
|
8
|
-
readonly cells: readonly NavigationCell[];
|
|
9
|
-
readonly waypoints: readonly NavigationPoint[];
|
|
10
|
-
readonly length: number;
|
|
11
|
-
readonly cost: number;
|
|
12
|
-
readonly visitedCells: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface NavigationGridOptions {
|
|
16
|
-
readonly width: number;
|
|
17
|
-
readonly height: number;
|
|
18
|
-
readonly origin?: NavigationPoint;
|
|
19
|
-
readonly cellSize?: number;
|
|
20
|
-
readonly blocked?: readonly NavigationCell[];
|
|
21
|
-
readonly costs?: readonly { readonly cell: NavigationCell; readonly cost: number }[];
|
|
22
|
-
readonly allowDiagonal?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface NavigationAgentOptions {
|
|
26
|
-
readonly position: NavigationPoint;
|
|
27
|
-
readonly speed?: number;
|
|
28
|
-
readonly waypointRadius?: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface NavigationAgentSnapshot {
|
|
32
|
-
readonly position: NavigationPoint;
|
|
33
|
-
readonly state: "idle" | "moving" | "arrived" | "blocked";
|
|
34
|
-
readonly waypointIndex: number;
|
|
35
|
-
readonly remainingWaypoints: number;
|
|
36
|
-
readonly pathLength: number;
|
|
37
|
-
readonly distanceTraveled: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
type SearchNode = {
|
|
41
|
-
readonly cell: NavigationCell;
|
|
42
|
-
readonly key: string;
|
|
43
|
-
readonly parent?: SearchNode;
|
|
44
|
-
readonly g: number;
|
|
45
|
-
readonly h: number;
|
|
46
|
-
readonly f: number;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export class NavigationGrid {
|
|
50
|
-
public readonly width: number;
|
|
51
|
-
public readonly height: number;
|
|
52
|
-
public readonly origin: NavigationPoint;
|
|
53
|
-
public readonly cellSize: number;
|
|
54
|
-
public readonly allowDiagonal: boolean;
|
|
55
|
-
private readonly blocked = new Set<string>();
|
|
56
|
-
private readonly costs = new Map<string, number>();
|
|
57
|
-
|
|
58
|
-
constructor(options: NavigationGridOptions) {
|
|
59
|
-
if (!Number.isInteger(options.width) || options.width < 2 || !Number.isInteger(options.height) || options.height < 2) {
|
|
60
|
-
throw new RangeError("NavigationGrid width and height must be integers >= 2.");
|
|
61
|
-
}
|
|
62
|
-
this.width = options.width;
|
|
63
|
-
this.height = options.height;
|
|
64
|
-
this.origin = options.origin ?? [0, 0];
|
|
65
|
-
this.cellSize = options.cellSize ?? 1;
|
|
66
|
-
this.allowDiagonal = options.allowDiagonal ?? false;
|
|
67
|
-
if (!Number.isFinite(this.cellSize) || this.cellSize <= 0) {
|
|
68
|
-
throw new RangeError("NavigationGrid cellSize must be a finite positive number.");
|
|
69
|
-
}
|
|
70
|
-
for (const cell of options.blocked ?? []) {
|
|
71
|
-
if (this.containsCell(cell)) this.blocked.add(cellKey(cell));
|
|
72
|
-
}
|
|
73
|
-
for (const entry of options.costs ?? []) {
|
|
74
|
-
if (!this.containsCell(entry.cell)) continue;
|
|
75
|
-
if (!Number.isFinite(entry.cost) || entry.cost <= 0) {
|
|
76
|
-
throw new RangeError("NavigationGrid traversal costs must be finite positive numbers.");
|
|
77
|
-
}
|
|
78
|
-
this.costs.set(cellKey(entry.cell), entry.cost);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
isBlocked(cell: NavigationCell): boolean {
|
|
83
|
-
return this.blocked.has(cellKey(cell));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
cellCost(cell: NavigationCell): number {
|
|
87
|
-
return this.costs.get(cellKey(cell)) ?? 1;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
containsCell(cell: NavigationCell): boolean {
|
|
91
|
-
return Number.isInteger(cell[0]) && Number.isInteger(cell[1]) && cell[0] >= 0 && cell[1] >= 0 && cell[0] < this.width && cell[1] < this.height;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
worldToCell(point: NavigationPoint): NavigationCell {
|
|
95
|
-
return [
|
|
96
|
-
clampInt(Math.floor((point[0] - this.origin[0]) / this.cellSize), 0, this.width - 1),
|
|
97
|
-
clampInt(Math.floor((point[1] - this.origin[1]) / this.cellSize), 0, this.height - 1),
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
cellToWorld(cell: NavigationCell): NavigationPoint {
|
|
102
|
-
return [
|
|
103
|
-
this.origin[0] + (cell[0] + 0.5) * this.cellSize,
|
|
104
|
-
this.origin[1] + (cell[1] + 0.5) * this.cellSize,
|
|
105
|
-
];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
findPath(start: NavigationPoint, goal: NavigationPoint): NavigationPath {
|
|
109
|
-
const startCell = this.nearestWalkableCell(this.worldToCell(start));
|
|
110
|
-
const goalCell = this.nearestWalkableCell(this.worldToCell(goal));
|
|
111
|
-
if (!startCell || !goalCell) return failedPath();
|
|
112
|
-
const result = this.search(startCell, goalCell);
|
|
113
|
-
if (!result) return failedPath();
|
|
114
|
-
const cells = reconstruct(result.node);
|
|
115
|
-
const waypoints = simplifyPath(cells).map((cell) => this.cellToWorld(cell));
|
|
116
|
-
const lastWaypoint = result.complete ? goal : this.cellToWorld(cells[cells.length - 1]!);
|
|
117
|
-
const normalizedWaypoints = [
|
|
118
|
-
start,
|
|
119
|
-
...waypoints.slice(1, -1),
|
|
120
|
-
lastWaypoint
|
|
121
|
-
];
|
|
122
|
-
return {
|
|
123
|
-
status: result.complete ? "success" : "partial",
|
|
124
|
-
cells,
|
|
125
|
-
waypoints: normalizedWaypoints,
|
|
126
|
-
length: pathLength(normalizedWaypoints),
|
|
127
|
-
cost: round3(result.node.g),
|
|
128
|
-
visitedCells: result.visitedCells,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private search(startCell: NavigationCell, goalCell: NavigationCell): { readonly node: SearchNode; readonly complete: boolean; readonly visitedCells: number } | undefined {
|
|
133
|
-
const open: SearchNode[] = [node(startCell, undefined, 0, manhattan(startCell, goalCell))];
|
|
134
|
-
const bestByCell = new Map<string, SearchNode>([[cellKey(startCell), open[0]!]]);
|
|
135
|
-
const closed = new Set<string>();
|
|
136
|
-
let best = open[0];
|
|
137
|
-
|
|
138
|
-
while (open.length > 0) {
|
|
139
|
-
open.sort((left, right) => left.f - right.f || left.h - right.h);
|
|
140
|
-
const current = open.shift()!;
|
|
141
|
-
if (closed.has(current.key)) continue;
|
|
142
|
-
closed.add(current.key);
|
|
143
|
-
if (current.h < best.h) best = current;
|
|
144
|
-
if (sameCell(current.cell, goalCell)) {
|
|
145
|
-
return { node: current, complete: true, visitedCells: closed.size };
|
|
146
|
-
}
|
|
147
|
-
for (const nextCell of neighbors(current.cell, this.allowDiagonal)) {
|
|
148
|
-
if (!this.containsCell(nextCell) || this.isBlocked(nextCell)) continue;
|
|
149
|
-
const key = cellKey(nextCell);
|
|
150
|
-
if (closed.has(key)) continue;
|
|
151
|
-
const stepCost = current.cell[0] !== nextCell[0] && current.cell[1] !== nextCell[1] ? Math.SQRT2 : 1;
|
|
152
|
-
const g = current.g + stepCost * this.cellCost(nextCell);
|
|
153
|
-
const h = heuristic(nextCell, goalCell, this.allowDiagonal);
|
|
154
|
-
const existing = bestByCell.get(key);
|
|
155
|
-
if (existing && existing.g <= g) continue;
|
|
156
|
-
const next = node(nextCell, current, g, h);
|
|
157
|
-
bestByCell.set(key, next);
|
|
158
|
-
open.push(next);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return best ? { node: best, complete: false, visitedCells: closed.size } : undefined;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private nearestWalkableCell(cell: NavigationCell): NavigationCell | undefined {
|
|
166
|
-
if (this.containsCell(cell) && !this.isBlocked(cell)) return cell;
|
|
167
|
-
for (let radius = 1; radius < Math.max(this.width, this.height); radius += 1) {
|
|
168
|
-
for (let y = cell[1] - radius; y <= cell[1] + radius; y += 1) {
|
|
169
|
-
for (let x = cell[0] - radius; x <= cell[0] + radius; x += 1) {
|
|
170
|
-
const candidate = [x, y] as const;
|
|
171
|
-
if (this.containsCell(candidate) && !this.isBlocked(candidate)) return candidate;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export class NavigationAgent {
|
|
180
|
-
private position: NavigationPoint;
|
|
181
|
-
private path: NavigationPath = failedPath();
|
|
182
|
-
private waypointIndex = 0;
|
|
183
|
-
private distanceTraveled = 0;
|
|
184
|
-
private readonly speed: number;
|
|
185
|
-
private readonly waypointRadius: number;
|
|
186
|
-
|
|
187
|
-
constructor(options: NavigationAgentOptions) {
|
|
188
|
-
this.position = options.position;
|
|
189
|
-
this.speed = options.speed ?? 1;
|
|
190
|
-
this.waypointRadius = options.waypointRadius ?? 0.05;
|
|
191
|
-
if (!Number.isFinite(this.speed) || this.speed <= 0 || !Number.isFinite(this.waypointRadius) || this.waypointRadius < 0) {
|
|
192
|
-
throw new RangeError("NavigationAgent speed must be positive and waypointRadius must be non-negative.");
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
setPath(path: NavigationPath): void {
|
|
197
|
-
this.path = path;
|
|
198
|
-
this.waypointIndex = path.waypoints.length > 1 ? 1 : 0;
|
|
199
|
-
this.distanceTraveled = 0;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
update(deltaSeconds: number): NavigationAgentSnapshot {
|
|
203
|
-
if (this.path.status === "failed") return this.snapshot("blocked");
|
|
204
|
-
if (this.waypointIndex >= this.path.waypoints.length) return this.snapshot("arrived");
|
|
205
|
-
const target = this.path.waypoints[this.waypointIndex]!;
|
|
206
|
-
const dx = target[0] - this.position[0];
|
|
207
|
-
const dy = target[1] - this.position[1];
|
|
208
|
-
const distance = Math.hypot(dx, dy);
|
|
209
|
-
if (distance <= this.waypointRadius) {
|
|
210
|
-
this.position = target;
|
|
211
|
-
this.waypointIndex += 1;
|
|
212
|
-
return this.update(deltaSeconds);
|
|
213
|
-
}
|
|
214
|
-
const step = Math.min(distance, Math.max(0, deltaSeconds) * this.speed);
|
|
215
|
-
if (step >= distance) {
|
|
216
|
-
this.position = target;
|
|
217
|
-
this.distanceTraveled += distance;
|
|
218
|
-
this.waypointIndex += 1;
|
|
219
|
-
return this.snapshot("moving");
|
|
220
|
-
}
|
|
221
|
-
this.position = [
|
|
222
|
-
this.position[0] + (dx / distance) * step,
|
|
223
|
-
this.position[1] + (dy / distance) * step,
|
|
224
|
-
];
|
|
225
|
-
this.distanceTraveled += step;
|
|
226
|
-
return this.snapshot("moving");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
snapshot(state: NavigationAgentSnapshot["state"] = this.path.status === "failed" ? "blocked" : "idle"): NavigationAgentSnapshot {
|
|
230
|
-
const resolvedState = this.path.status === "failed" ? "blocked" : this.waypointIndex >= this.path.waypoints.length ? "arrived" : state;
|
|
231
|
-
return {
|
|
232
|
-
position: [round3(this.position[0]), round3(this.position[1])],
|
|
233
|
-
state: resolvedState,
|
|
234
|
-
waypointIndex: this.waypointIndex,
|
|
235
|
-
remainingWaypoints: Math.max(0, this.path.waypoints.length - this.waypointIndex),
|
|
236
|
-
pathLength: round3(this.path.length),
|
|
237
|
-
distanceTraveled: round3(this.distanceTraveled),
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function node(cell: NavigationCell, parent: SearchNode | undefined, g: number, h: number): SearchNode {
|
|
243
|
-
return { cell, key: cellKey(cell), parent, g, h, f: g + h };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function neighbors(cell: NavigationCell, allowDiagonal: boolean): readonly NavigationCell[] {
|
|
247
|
-
const cardinal: NavigationCell[] = [[cell[0] + 1, cell[1]], [cell[0] - 1, cell[1]], [cell[0], cell[1] + 1], [cell[0], cell[1] - 1]];
|
|
248
|
-
return allowDiagonal
|
|
249
|
-
? [
|
|
250
|
-
...cardinal,
|
|
251
|
-
[cell[0] + 1, cell[1] + 1],
|
|
252
|
-
[cell[0] + 1, cell[1] - 1],
|
|
253
|
-
[cell[0] - 1, cell[1] + 1],
|
|
254
|
-
[cell[0] - 1, cell[1] - 1],
|
|
255
|
-
]
|
|
256
|
-
: cardinal;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function reconstruct(node: SearchNode): readonly NavigationCell[] {
|
|
260
|
-
const cells: NavigationCell[] = [];
|
|
261
|
-
let current: SearchNode | undefined = node;
|
|
262
|
-
while (current) {
|
|
263
|
-
cells.push(current.cell);
|
|
264
|
-
current = current.parent;
|
|
265
|
-
}
|
|
266
|
-
return cells.reverse();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function simplifyPath(cells: readonly NavigationCell[]): readonly NavigationCell[] {
|
|
270
|
-
if (cells.length <= 2) return cells;
|
|
271
|
-
const result: NavigationCell[] = [cells[0]!];
|
|
272
|
-
let previousDirection: NavigationCell = [cells[1]![0] - cells[0]![0], cells[1]![1] - cells[0]![1]];
|
|
273
|
-
for (let index = 2; index < cells.length; index += 1) {
|
|
274
|
-
const direction: NavigationCell = [cells[index]![0] - cells[index - 1]![0], cells[index]![1] - cells[index - 1]![1]];
|
|
275
|
-
if (!sameCell(direction, previousDirection)) {
|
|
276
|
-
result.push(cells[index - 1]!);
|
|
277
|
-
previousDirection = direction;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
result.push(cells[cells.length - 1]!);
|
|
281
|
-
return result;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function failedPath(): NavigationPath {
|
|
285
|
-
return { status: "failed", cells: [], waypoints: [], length: 0, cost: 0, visitedCells: 0 };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function pathLength(points: readonly NavigationPoint[]): number {
|
|
289
|
-
let length = 0;
|
|
290
|
-
for (let index = 1; index < points.length; index += 1) {
|
|
291
|
-
length += Math.hypot(points[index]![0] - points[index - 1]![0], points[index]![1] - points[index - 1]![1]);
|
|
292
|
-
}
|
|
293
|
-
return round3(length);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function cellKey(cell: NavigationCell): string {
|
|
297
|
-
return `${cell[0]}:${cell[1]}`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function sameCell(left: NavigationCell, right: NavigationCell): boolean {
|
|
301
|
-
return left[0] === right[0] && left[1] === right[1];
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function manhattan(left: NavigationCell, right: NavigationCell): number {
|
|
305
|
-
return Math.abs(left[0] - right[0]) + Math.abs(left[1] - right[1]);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function heuristic(left: NavigationCell, right: NavigationCell, allowDiagonal: boolean): number {
|
|
309
|
-
if (!allowDiagonal) return manhattan(left, right);
|
|
310
|
-
const dx = Math.abs(left[0] - right[0]);
|
|
311
|
-
const dy = Math.abs(left[1] - right[1]);
|
|
312
|
-
return Math.max(dx, dy) + (Math.SQRT2 - 1) * Math.min(dx, dy);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function clampInt(value: number, min: number, max: number): number {
|
|
316
|
-
return Math.max(min, Math.min(max, value));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function round3(value: number): number {
|
|
320
|
-
return Number(value.toFixed(3));
|
|
321
|
-
}
|
package/src/PhysicsDebugDraw.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { Collider } from "./Collider.js";
|
|
2
|
-
import type { PhysicsWorld } from "./PhysicsWorld.js";
|
|
3
|
-
import type { Vec3 } from "./Shape.js";
|
|
4
|
-
|
|
5
|
-
export type DebugLine = {
|
|
6
|
-
readonly from: Vec3;
|
|
7
|
-
readonly to: Vec3;
|
|
8
|
-
readonly color: Vec3;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export class PhysicsDebugDraw {
|
|
12
|
-
buildLines(world: Pick<PhysicsWorld, "colliders" | "getBody">): readonly DebugLine[] {
|
|
13
|
-
const lines: DebugLine[] = [];
|
|
14
|
-
for (const collider of world.colliders()) {
|
|
15
|
-
const body = world.getBody(collider.bodyId);
|
|
16
|
-
if (!body) {
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
appendColliderLines(lines, collider, body.position);
|
|
20
|
-
}
|
|
21
|
-
return lines;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function appendColliderLines(lines: DebugLine[], collider: Collider, position: Vec3): void {
|
|
26
|
-
const color: Vec3 = collider.sensor ? [1, 0.75, 0] : [0.1, 0.8, 1];
|
|
27
|
-
if (collider.shape.kind === "box") {
|
|
28
|
-
const h = collider.shape.halfExtents;
|
|
29
|
-
const corners = [
|
|
30
|
-
[position[0] - h[0], position[1] - h[1], position[2] - h[2]],
|
|
31
|
-
[position[0] + h[0], position[1] - h[1], position[2] - h[2]],
|
|
32
|
-
[position[0] + h[0], position[1] + h[1], position[2] - h[2]],
|
|
33
|
-
[position[0] - h[0], position[1] + h[1], position[2] - h[2]],
|
|
34
|
-
[position[0] - h[0], position[1] - h[1], position[2] + h[2]],
|
|
35
|
-
[position[0] + h[0], position[1] - h[1], position[2] + h[2]],
|
|
36
|
-
[position[0] + h[0], position[1] + h[1], position[2] + h[2]],
|
|
37
|
-
[position[0] - h[0], position[1] + h[1], position[2] + h[2]]
|
|
38
|
-
] as Vec3[];
|
|
39
|
-
const pairs: readonly (readonly [number, number])[] = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]];
|
|
40
|
-
for (const [a, b] of pairs) {
|
|
41
|
-
lines.push({ from: corners[a]!, to: corners[b]!, color });
|
|
42
|
-
}
|
|
43
|
-
} else if (collider.shape.kind === "sphere") {
|
|
44
|
-
const r = collider.shape.radius;
|
|
45
|
-
lines.push({ from: [position[0] - r, position[1], position[2]], to: [position[0] + r, position[1], position[2]], color });
|
|
46
|
-
lines.push({ from: [position[0], position[1] - r, position[2]], to: [position[0], position[1] + r, position[2]], color });
|
|
47
|
-
lines.push({ from: [position[0], position[1], position[2] - r], to: [position[0], position[1], position[2] + r], color });
|
|
48
|
-
} else if (collider.shape.kind === "plane") {
|
|
49
|
-
lines.push({ from: [-10, position[1], 0], to: [10, position[1], 0], color });
|
|
50
|
-
lines.push({ from: [0, position[1], -10], to: [0, position[1], 10], color });
|
|
51
|
-
} else if (collider.shape.kind === "capsule") {
|
|
52
|
-
const r = collider.shape.radius;
|
|
53
|
-
const h = collider.shape.halfHeight;
|
|
54
|
-
lines.push({ from: [position[0], position[1] - h - r, position[2]], to: [position[0], position[1] + h + r, position[2]], color });
|
|
55
|
-
lines.push({ from: [position[0] - r, position[1], position[2]], to: [position[0] + r, position[1], position[2]], color });
|
|
56
|
-
} else {
|
|
57
|
-
const { vertices, indices } = collider.shape;
|
|
58
|
-
for (let index = 0; index < indices.length; index += 3) {
|
|
59
|
-
const a = translate(vertices[indices[index]!]!, position);
|
|
60
|
-
const b = translate(vertices[indices[index + 1]!]!, position);
|
|
61
|
-
const c = translate(vertices[indices[index + 2]!]!, position);
|
|
62
|
-
lines.push({ from: a, to: b, color });
|
|
63
|
-
lines.push({ from: b, to: c, color });
|
|
64
|
-
lines.push({ from: c, to: a, color });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function translate(value: Vec3, position: Vec3): Vec3 {
|
|
70
|
-
return [value[0] + position[0], value[1] + position[1], value[2] + position[2]];
|
|
71
|
-
}
|