@aura3d/physics 1.0.6
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/README.md +26 -0
- package/dist/CharacterController.d.ts +52 -0
- package/dist/CharacterController.d.ts.map +1 -0
- package/dist/CharacterController.js +146 -0
- package/dist/CharacterController.js.map +1 -0
- package/dist/ClothFixtures.d.ts +87 -0
- package/dist/ClothFixtures.d.ts.map +1 -0
- package/dist/ClothFixtures.js +264 -0
- package/dist/ClothFixtures.js.map +1 -0
- package/dist/Collider.d.ts +27 -0
- package/dist/Collider.d.ts.map +1 -0
- package/dist/Collider.js +45 -0
- package/dist/Collider.js.map +1 -0
- package/dist/CollisionEvents.d.ts +26 -0
- package/dist/CollisionEvents.d.ts.map +1 -0
- package/dist/CollisionEvents.js +46 -0
- package/dist/CollisionEvents.js.map +1 -0
- package/dist/CollisionVolumes.d.ts +57 -0
- package/dist/CollisionVolumes.d.ts.map +1 -0
- package/dist/CollisionVolumes.js +159 -0
- package/dist/CollisionVolumes.js.map +1 -0
- package/dist/Constraint.d.ts +31 -0
- package/dist/Constraint.d.ts.map +1 -0
- package/dist/Constraint.js +103 -0
- package/dist/Constraint.js.map +1 -0
- package/dist/Constraints.d.ts +2 -0
- package/dist/Constraints.d.ts.map +1 -0
- package/dist/Constraints.js +2 -0
- package/dist/Constraints.js.map +1 -0
- package/dist/Crowd.d.ts +64 -0
- package/dist/Crowd.d.ts.map +1 -0
- package/dist/Crowd.js +209 -0
- package/dist/Crowd.js.map +1 -0
- package/dist/ECSPhysicsBridge.d.ts +17 -0
- package/dist/ECSPhysicsBridge.d.ts.map +1 -0
- package/dist/ECSPhysicsBridge.js +37 -0
- package/dist/ECSPhysicsBridge.js.map +1 -0
- package/dist/FireSmokeFixtures.d.ts +103 -0
- package/dist/FireSmokeFixtures.d.ts.map +1 -0
- package/dist/FireSmokeFixtures.js +271 -0
- package/dist/FireSmokeFixtures.js.map +1 -0
- package/dist/FluidFixtures.d.ts +75 -0
- package/dist/FluidFixtures.d.ts.map +1 -0
- package/dist/FluidFixtures.js +219 -0
- package/dist/FluidFixtures.js.map +1 -0
- package/dist/FractureFixtures.d.ts +68 -0
- package/dist/FractureFixtures.d.ts.map +1 -0
- package/dist/FractureFixtures.js +199 -0
- package/dist/FractureFixtures.js.map +1 -0
- package/dist/HitboxWorld.d.ts +250 -0
- package/dist/HitboxWorld.d.ts.map +1 -0
- package/dist/HitboxWorld.js +771 -0
- package/dist/HitboxWorld.js.map +1 -0
- package/dist/KinematicBody.d.ts +157 -0
- package/dist/KinematicBody.d.ts.map +1 -0
- package/dist/KinematicBody.js +405 -0
- package/dist/KinematicBody.js.map +1 -0
- package/dist/KinematicWorld.d.ts +58 -0
- package/dist/KinematicWorld.d.ts.map +1 -0
- package/dist/KinematicWorld.js +246 -0
- package/dist/KinematicWorld.js.map +1 -0
- package/dist/Navigation.d.ts +67 -0
- package/dist/Navigation.d.ts.map +1 -0
- package/dist/Navigation.js +261 -0
- package/dist/Navigation.js.map +1 -0
- package/dist/PhysicsDebugDraw.d.ts +11 -0
- package/dist/PhysicsDebugDraw.d.ts.map +1 -0
- package/dist/PhysicsDebugDraw.js +64 -0
- package/dist/PhysicsDebugDraw.js.map +1 -0
- package/dist/PhysicsSandboxFixtures.d.ts +53 -0
- package/dist/PhysicsSandboxFixtures.d.ts.map +1 -0
- package/dist/PhysicsSandboxFixtures.js +280 -0
- package/dist/PhysicsSandboxFixtures.js.map +1 -0
- package/dist/PhysicsStepper.d.ts +15 -0
- package/dist/PhysicsStepper.d.ts.map +1 -0
- package/dist/PhysicsStepper.js +37 -0
- package/dist/PhysicsStepper.js.map +1 -0
- package/dist/PhysicsWorld.d.ts +101 -0
- package/dist/PhysicsWorld.d.ts.map +1 -0
- package/dist/PhysicsWorld.js +868 -0
- package/dist/PhysicsWorld.js.map +1 -0
- package/dist/PlatformerFixtures.d.ts +70 -0
- package/dist/PlatformerFixtures.d.ts.map +1 -0
- package/dist/PlatformerFixtures.js +120 -0
- package/dist/PlatformerFixtures.js.map +1 -0
- package/dist/Raycast.d.ts +23 -0
- package/dist/Raycast.d.ts.map +1 -0
- package/dist/Raycast.js +271 -0
- package/dist/Raycast.js.map +1 -0
- package/dist/RigidBody.d.ts +69 -0
- package/dist/RigidBody.d.ts.map +1 -0
- package/dist/RigidBody.js +255 -0
- package/dist/RigidBody.js.map +1 -0
- package/dist/ScenePhysicsBridge.d.ts +42 -0
- package/dist/ScenePhysicsBridge.d.ts.map +1 -0
- package/dist/ScenePhysicsBridge.js +168 -0
- package/dist/ScenePhysicsBridge.js.map +1 -0
- package/dist/Shape.d.ts +50 -0
- package/dist/Shape.d.ts.map +1 -0
- package/dist/Shape.js +128 -0
- package/dist/Shape.js.map +1 -0
- package/dist/SoftBodyFixtures.d.ts +70 -0
- package/dist/SoftBodyFixtures.d.ts.map +1 -0
- package/dist/SoftBodyFixtures.js +272 -0
- package/dist/SoftBodyFixtures.js.map +1 -0
- package/dist/Steering.d.ts +161 -0
- package/dist/Steering.d.ts.map +1 -0
- package/dist/Steering.js +347 -0
- package/dist/Steering.js.map +1 -0
- package/dist/VehicleDynamics.d.ts +152 -0
- package/dist/VehicleDynamics.d.ts.map +1 -0
- package/dist/VehicleDynamics.js +335 -0
- package/dist/VehicleDynamics.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/package.json +17 -0
- package/src/CharacterController.ts +184 -0
- package/src/ClothFixtures.ts +359 -0
- package/src/Collider.ts +67 -0
- package/src/CollisionEvents.ts +70 -0
- package/src/CollisionVolumes.ts +218 -0
- package/src/Constraint.ts +120 -0
- package/src/Constraints.ts +1 -0
- package/src/Crowd.ts +283 -0
- package/src/ECSPhysicsBridge.ts +53 -0
- package/src/FireSmokeFixtures.ts +382 -0
- package/src/FluidFixtures.ts +301 -0
- package/src/FractureFixtures.ts +277 -0
- package/src/HitboxWorld.ts +1179 -0
- package/src/KinematicBody.ts +553 -0
- package/src/KinematicWorld.ts +310 -0
- package/src/Navigation.ts +321 -0
- package/src/PhysicsDebugDraw.ts +71 -0
- package/src/PhysicsSandboxFixtures.ts +394 -0
- package/src/PhysicsStepper.ts +47 -0
- package/src/PhysicsWorld.ts +986 -0
- package/src/PlatformerFixtures.ts +205 -0
- package/src/Raycast.ts +315 -0
- package/src/RigidBody.ts +311 -0
- package/src/ScenePhysicsBridge.ts +207 -0
- package/src/Shape.ts +182 -0
- package/src/SoftBodyFixtures.ts +363 -0
- package/src/Steering.ts +531 -0
- package/src/VehicleDynamics.ts +524 -0
- package/src/index.ts +28 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { cloneVec3, validateFiniteVec3, type Bounds, type Vec3 } from "./Shape.js";
|
|
2
|
+
|
|
3
|
+
export type CombatFacing = -1 | 1;
|
|
4
|
+
export type CollisionOwnerId = string | number;
|
|
5
|
+
export type CollisionVolumeKind = "hitbox" | "hurtbox" | "pushbox" | "guardbox";
|
|
6
|
+
|
|
7
|
+
export type CollisionVolumeDescriptor = {
|
|
8
|
+
readonly id?: string;
|
|
9
|
+
readonly ownerId?: CollisionOwnerId;
|
|
10
|
+
readonly offset?: Vec3;
|
|
11
|
+
readonly halfExtents: Vec3;
|
|
12
|
+
readonly enabled?: boolean;
|
|
13
|
+
readonly tags?: readonly string[];
|
|
14
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CollisionVolume = {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly kind: CollisionVolumeKind;
|
|
20
|
+
readonly offset: Vec3;
|
|
21
|
+
readonly halfExtents: Vec3;
|
|
22
|
+
readonly enabled: boolean;
|
|
23
|
+
readonly tags: readonly string[];
|
|
24
|
+
readonly ownerId?: CollisionOwnerId;
|
|
25
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ResolvedCollisionVolume = CollisionVolume & {
|
|
29
|
+
readonly center: Vec3;
|
|
30
|
+
readonly facing: CombatFacing;
|
|
31
|
+
readonly bounds: Bounds;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type CollisionPenetration = {
|
|
35
|
+
readonly normal: Vec3;
|
|
36
|
+
readonly depth: number;
|
|
37
|
+
readonly axis: 0 | 1 | 2;
|
|
38
|
+
readonly overlap: Vec3;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function collisionVolume(kind: CollisionVolumeKind, descriptor: CollisionVolumeDescriptor): CollisionVolume {
|
|
42
|
+
validateHalfExtents(descriptor.halfExtents, `${kind} halfExtents`);
|
|
43
|
+
const volume = {
|
|
44
|
+
id: descriptor.id ?? kind,
|
|
45
|
+
kind,
|
|
46
|
+
offset: cloneVec3(descriptor.offset ?? [0, 0, 0]),
|
|
47
|
+
halfExtents: cloneVec3(descriptor.halfExtents),
|
|
48
|
+
enabled: descriptor.enabled ?? true,
|
|
49
|
+
tags: [...(descriptor.tags ?? [])],
|
|
50
|
+
...(descriptor.ownerId === undefined ? {} : { ownerId: descriptor.ownerId }),
|
|
51
|
+
...(descriptor.metadata === undefined ? {} : { metadata: { ...descriptor.metadata } })
|
|
52
|
+
};
|
|
53
|
+
validateFiniteVec3(volume.offset, `${kind} offset`);
|
|
54
|
+
return volume;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hitbox(descriptor: CollisionVolumeDescriptor): CollisionVolume {
|
|
58
|
+
return collisionVolume("hitbox", descriptor);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hurtbox(descriptor: CollisionVolumeDescriptor): CollisionVolume {
|
|
62
|
+
return collisionVolume("hurtbox", descriptor);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function pushbox(descriptor: CollisionVolumeDescriptor): CollisionVolume {
|
|
66
|
+
return collisionVolume("pushbox", descriptor);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function guardbox(descriptor: CollisionVolumeDescriptor): CollisionVolume {
|
|
70
|
+
return collisionVolume("guardbox", descriptor);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const createHitbox = hitbox;
|
|
74
|
+
export const createHurtbox = hurtbox;
|
|
75
|
+
export const createPushbox = pushbox;
|
|
76
|
+
export const createGuardbox = guardbox;
|
|
77
|
+
export const createGuardBox = guardbox;
|
|
78
|
+
|
|
79
|
+
export function cloneCollisionVolume(volume: CollisionVolume): CollisionVolume {
|
|
80
|
+
return {
|
|
81
|
+
id: volume.id,
|
|
82
|
+
kind: volume.kind,
|
|
83
|
+
offset: cloneVec3(volume.offset),
|
|
84
|
+
halfExtents: cloneVec3(volume.halfExtents),
|
|
85
|
+
enabled: volume.enabled,
|
|
86
|
+
tags: [...volume.tags],
|
|
87
|
+
...(volume.ownerId === undefined ? {} : { ownerId: volume.ownerId }),
|
|
88
|
+
...(volume.metadata === undefined ? {} : { metadata: { ...volume.metadata } })
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function withVolumeOwner(volume: CollisionVolume, ownerId: CollisionOwnerId): CollisionVolume {
|
|
93
|
+
return {
|
|
94
|
+
...cloneCollisionVolume(volume),
|
|
95
|
+
ownerId
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resolveCollisionVolume(volume: CollisionVolume, ownerPosition: Vec3, facing: CombatFacing = 1): ResolvedCollisionVolume {
|
|
100
|
+
validateFiniteVec3(ownerPosition, "volume owner position");
|
|
101
|
+
const resolvedFacing = normalizeFacing(facing);
|
|
102
|
+
const center: Vec3 = [
|
|
103
|
+
ownerPosition[0] + volume.offset[0] * resolvedFacing,
|
|
104
|
+
ownerPosition[1] + volume.offset[1],
|
|
105
|
+
ownerPosition[2] + volume.offset[2]
|
|
106
|
+
];
|
|
107
|
+
return {
|
|
108
|
+
...cloneCollisionVolume(volume),
|
|
109
|
+
center,
|
|
110
|
+
facing: resolvedFacing,
|
|
111
|
+
bounds: aabbFromCenter(center, volume.halfExtents)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function aabbFromCenter(center: Vec3, halfExtents: Vec3): Bounds {
|
|
116
|
+
validateFiniteVec3(center, "aabb center");
|
|
117
|
+
validateHalfExtents(halfExtents, "aabb halfExtents");
|
|
118
|
+
return {
|
|
119
|
+
min: [center[0] - halfExtents[0], center[1] - halfExtents[1], center[2] - halfExtents[2]],
|
|
120
|
+
max: [center[0] + halfExtents[0], center[1] + halfExtents[1], center[2] + halfExtents[2]]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function boundsCenter(bounds: Bounds): Vec3 {
|
|
125
|
+
validateBounds(bounds, "bounds");
|
|
126
|
+
return [
|
|
127
|
+
(bounds.min[0] + bounds.max[0]) * 0.5,
|
|
128
|
+
(bounds.min[1] + bounds.max[1]) * 0.5,
|
|
129
|
+
(bounds.min[2] + bounds.max[2]) * 0.5
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function boundsHalfExtents(bounds: Bounds): Vec3 {
|
|
134
|
+
validateBounds(bounds, "bounds");
|
|
135
|
+
return [
|
|
136
|
+
(bounds.max[0] - bounds.min[0]) * 0.5,
|
|
137
|
+
(bounds.max[1] - bounds.min[1]) * 0.5,
|
|
138
|
+
(bounds.max[2] - bounds.min[2]) * 0.5
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function overlapsAabb(a: Bounds, b: Bounds): boolean {
|
|
143
|
+
validateBounds(a, "a bounds");
|
|
144
|
+
validateBounds(b, "b bounds");
|
|
145
|
+
return (
|
|
146
|
+
a.min[0] < b.max[0] &&
|
|
147
|
+
a.max[0] > b.min[0] &&
|
|
148
|
+
a.min[1] < b.max[1] &&
|
|
149
|
+
a.max[1] > b.min[1] &&
|
|
150
|
+
a.min[2] < b.max[2] &&
|
|
151
|
+
a.max[2] > b.min[2]
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function overlapsVolume(a: ResolvedCollisionVolume, b: ResolvedCollisionVolume): boolean {
|
|
156
|
+
if (!a.enabled || !b.enabled) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return overlapsAabb(a.bounds, b.bounds);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function aabbPenetration(a: Bounds, b: Bounds): CollisionPenetration | null {
|
|
163
|
+
if (!overlapsAabb(a, b)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const overlap: Vec3 = [
|
|
167
|
+
Math.min(a.max[0] - b.min[0], b.max[0] - a.min[0]),
|
|
168
|
+
Math.min(a.max[1] - b.min[1], b.max[1] - a.min[1]),
|
|
169
|
+
Math.min(a.max[2] - b.min[2], b.max[2] - a.min[2])
|
|
170
|
+
];
|
|
171
|
+
let axis: 0 | 1 | 2 = 0;
|
|
172
|
+
if (overlap[1] < overlap[axis]) {
|
|
173
|
+
axis = 1;
|
|
174
|
+
}
|
|
175
|
+
if (overlap[2] < overlap[axis]) {
|
|
176
|
+
axis = 2;
|
|
177
|
+
}
|
|
178
|
+
const centerA = boundsCenter(a);
|
|
179
|
+
const centerB = boundsCenter(b);
|
|
180
|
+
const normal: [number, number, number] = [0, 0, 0];
|
|
181
|
+
normal[axis] = centerA[axis] <= centerB[axis] ? -1 : 1;
|
|
182
|
+
return {
|
|
183
|
+
normal,
|
|
184
|
+
depth: overlap[axis],
|
|
185
|
+
axis,
|
|
186
|
+
overlap
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function volumePairKey(a: CollisionVolume | ResolvedCollisionVolume, b: CollisionVolume | ResolvedCollisionVolume): string {
|
|
191
|
+
const keyA = volumeKey(a);
|
|
192
|
+
const keyB = volumeKey(b);
|
|
193
|
+
return keyA < keyB ? `${keyA}:${keyB}` : `${keyB}:${keyA}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function normalizeFacing(value: number): CombatFacing {
|
|
197
|
+
return value < 0 ? -1 : 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function validateBounds(bounds: Bounds, name: string): void {
|
|
201
|
+
validateFiniteVec3(bounds.min, `${name} min`);
|
|
202
|
+
validateFiniteVec3(bounds.max, `${name} max`);
|
|
203
|
+
if (bounds.min[0] > bounds.max[0] || bounds.min[1] > bounds.max[1] || bounds.min[2] > bounds.max[2]) {
|
|
204
|
+
throw new Error(`${name} min values must be less than or equal to max values.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateHalfExtents(value: Vec3, name: string): void {
|
|
209
|
+
validateFiniteVec3(value, name);
|
|
210
|
+
if (value[0] <= 0 || value[1] <= 0 || value[2] <= 0) {
|
|
211
|
+
throw new Error(`${name} must contain finite positive values.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function volumeKey(volume: CollisionVolume | ResolvedCollisionVolume): string {
|
|
216
|
+
const owner = volume.ownerId === undefined ? "none" : String(volume.ownerId);
|
|
217
|
+
return `${owner}/${volume.kind}/${volume.id}`;
|
|
218
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { addVec3, dotVec3, normalizeVec3, scaleVec3, subVec3, validateFiniteVec3, type Vec3 } from "./Shape.js";
|
|
2
|
+
import type { RigidBody } from "./RigidBody.js";
|
|
3
|
+
|
|
4
|
+
export type ConstraintType = "fixed" | "hinge" | "slider" | "spring";
|
|
5
|
+
|
|
6
|
+
export type ConstraintDescriptor = {
|
|
7
|
+
readonly type: ConstraintType;
|
|
8
|
+
readonly bodyA: RigidBody;
|
|
9
|
+
readonly bodyB: RigidBody;
|
|
10
|
+
readonly localAnchorA?: Vec3;
|
|
11
|
+
readonly localAnchorB?: Vec3;
|
|
12
|
+
readonly restLength?: number;
|
|
13
|
+
readonly stiffness?: number;
|
|
14
|
+
readonly axis?: Vec3;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class Constraint {
|
|
18
|
+
readonly type: ConstraintType;
|
|
19
|
+
readonly bodyA: RigidBody;
|
|
20
|
+
readonly bodyB: RigidBody;
|
|
21
|
+
readonly localAnchorA: Vec3;
|
|
22
|
+
readonly localAnchorB: Vec3;
|
|
23
|
+
readonly restOffset: Vec3;
|
|
24
|
+
readonly restLength: number;
|
|
25
|
+
readonly stiffness: number;
|
|
26
|
+
readonly axis: Vec3;
|
|
27
|
+
|
|
28
|
+
constructor(descriptor: ConstraintDescriptor) {
|
|
29
|
+
if (descriptor.bodyA.id === descriptor.bodyB.id) {
|
|
30
|
+
throw new Error("Constraint requires two different bodies.");
|
|
31
|
+
}
|
|
32
|
+
this.localAnchorA = descriptor.localAnchorA ?? [0, 0, 0];
|
|
33
|
+
this.localAnchorB = descriptor.localAnchorB ?? [0, 0, 0];
|
|
34
|
+
validateFiniteVec3(this.localAnchorA, "constraint localAnchorA");
|
|
35
|
+
validateFiniteVec3(this.localAnchorB, "constraint localAnchorB");
|
|
36
|
+
this.axis = normalizeVec3(descriptor.axis ?? [1, 0, 0]);
|
|
37
|
+
this.type = descriptor.type;
|
|
38
|
+
this.bodyA = descriptor.bodyA;
|
|
39
|
+
this.bodyB = descriptor.bodyB;
|
|
40
|
+
const anchorA = this.anchorAWorld();
|
|
41
|
+
const anchorB = this.anchorBWorld();
|
|
42
|
+
this.restOffset = subVec3(anchorB, anchorA);
|
|
43
|
+
this.restLength = descriptor.restLength ?? Math.hypot(this.restOffset[0], this.restOffset[1], this.restOffset[2]);
|
|
44
|
+
this.stiffness = descriptor.stiffness ?? 1;
|
|
45
|
+
if (!Number.isFinite(this.restLength) || this.restLength < 0) {
|
|
46
|
+
throw new Error("Constraint restLength must be finite and non-negative.");
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isFinite(this.stiffness) || this.stiffness < 0 || this.stiffness > 1) {
|
|
49
|
+
throw new Error("Constraint stiffness must be in the [0, 1] range.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
solve(): void {
|
|
54
|
+
if (this.stiffness === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (this.type === "fixed") {
|
|
58
|
+
const currentOffset = subVec3(this.anchorBWorld(), this.anchorAWorld());
|
|
59
|
+
this.applyError(subVec3(currentOffset, this.restOffset));
|
|
60
|
+
this.applyVelocityError(subVec3(this.bodyB.velocity, this.bodyA.velocity));
|
|
61
|
+
} else if (this.type === "hinge") {
|
|
62
|
+
this.applyError(subVec3(this.anchorBWorld(), this.anchorAWorld()));
|
|
63
|
+
this.applyVelocityError(subVec3(this.bodyB.velocity, this.bodyA.velocity));
|
|
64
|
+
} else if (this.type === "slider") {
|
|
65
|
+
const currentOffset = subVec3(this.anchorBWorld(), this.anchorAWorld());
|
|
66
|
+
const projected = scaleVec3(this.axis, dotVec3(currentOffset, this.axis));
|
|
67
|
+
this.applyError(subVec3(currentOffset, projected));
|
|
68
|
+
const relativeVelocity = subVec3(this.bodyB.velocity, this.bodyA.velocity);
|
|
69
|
+
const projectedVelocity = scaleVec3(this.axis, dotVec3(relativeVelocity, this.axis));
|
|
70
|
+
this.applyVelocityError(subVec3(relativeVelocity, projectedVelocity));
|
|
71
|
+
} else if (this.type === "spring") {
|
|
72
|
+
const currentOffset = subVec3(this.anchorBWorld(), this.anchorAWorld());
|
|
73
|
+
const currentLength = Math.hypot(currentOffset[0], currentOffset[1], currentOffset[2]);
|
|
74
|
+
if (currentLength <= 1e-9) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const lengthError = currentLength - this.restLength;
|
|
78
|
+
this.applyError(scaleVec3(currentOffset, lengthError / currentLength));
|
|
79
|
+
const relativeVelocity = subVec3(this.bodyB.velocity, this.bodyA.velocity);
|
|
80
|
+
const axis = scaleVec3(currentOffset, 1 / currentLength);
|
|
81
|
+
this.applyVelocityError(scaleVec3(axis, dotVec3(relativeVelocity, axis) * Math.min(1, this.stiffness)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private anchorAWorld(): Vec3 {
|
|
86
|
+
return addVec3(this.bodyA.position, this.localAnchorA);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private anchorBWorld(): Vec3 {
|
|
90
|
+
return addVec3(this.bodyB.position, this.localAnchorB);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private applyError(error: Vec3): void {
|
|
94
|
+
const inverseMassSum = this.bodyA.inverseMass + this.bodyB.inverseMass;
|
|
95
|
+
if (inverseMassSum <= 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const weightedError = scaleVec3(error, this.stiffness);
|
|
99
|
+
if (this.bodyA.inverseMass > 0) {
|
|
100
|
+
this.bodyA.setPosition(addVec3(this.bodyA.position, scaleVec3(weightedError, this.bodyA.inverseMass / inverseMassSum)));
|
|
101
|
+
}
|
|
102
|
+
if (this.bodyB.inverseMass > 0) {
|
|
103
|
+
this.bodyB.setPosition(subVec3(this.bodyB.position, scaleVec3(weightedError, this.bodyB.inverseMass / inverseMassSum)));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private applyVelocityError(errorVelocity: Vec3): void {
|
|
108
|
+
const inverseMassSum = this.bodyA.inverseMass + this.bodyB.inverseMass;
|
|
109
|
+
if (inverseMassSum <= 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const correctionImpulse = scaleVec3(errorVelocity, -this.stiffness / inverseMassSum);
|
|
113
|
+
if (this.bodyA.inverseMass > 0) {
|
|
114
|
+
this.bodyA.applyImpulse(scaleVec3(correctionImpulse, -1));
|
|
115
|
+
}
|
|
116
|
+
if (this.bodyB.inverseMass > 0) {
|
|
117
|
+
this.bodyB.applyImpulse(correctionImpulse);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Constraint.js";
|
package/src/Crowd.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { arriveSteering, type SteeringPoint, type SteeringVector } from "./Steering";
|
|
2
|
+
|
|
3
|
+
export type CrowdFormationType = "line" | "wedge" | "column" | "circle";
|
|
4
|
+
|
|
5
|
+
export interface CrowdAgentOptions {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly position: SteeringPoint;
|
|
8
|
+
readonly velocity?: SteeringVector;
|
|
9
|
+
readonly radius?: number;
|
|
10
|
+
readonly maxSpeed?: number;
|
|
11
|
+
readonly priority?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CrowdSimulationOptions {
|
|
15
|
+
readonly neighborRadius?: number;
|
|
16
|
+
readonly maxNeighbors?: number;
|
|
17
|
+
readonly separationWeight?: number;
|
|
18
|
+
readonly alignmentWeight?: number;
|
|
19
|
+
readonly cohesionWeight?: number;
|
|
20
|
+
readonly formationWeight?: number;
|
|
21
|
+
readonly maxForce?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CrowdFormationOptions {
|
|
25
|
+
readonly type: CrowdFormationType;
|
|
26
|
+
readonly center: SteeringPoint;
|
|
27
|
+
readonly forward?: SteeringVector;
|
|
28
|
+
readonly spacing?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CrowdAgentSnapshot {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly position: SteeringPoint;
|
|
34
|
+
readonly velocity: SteeringVector;
|
|
35
|
+
readonly speed: number;
|
|
36
|
+
readonly neighborCount: number;
|
|
37
|
+
readonly formationSlot: SteeringPoint;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CrowdSimulationSnapshot {
|
|
41
|
+
readonly agentCount: number;
|
|
42
|
+
readonly neighborPairs: number;
|
|
43
|
+
readonly averageNeighbors: number;
|
|
44
|
+
readonly averageSpeed: number;
|
|
45
|
+
readonly maxSpeed: number;
|
|
46
|
+
readonly formationType: CrowdFormationType | "none";
|
|
47
|
+
readonly center: SteeringPoint;
|
|
48
|
+
readonly agents: readonly CrowdAgentSnapshot[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type CrowdAgentState = {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
position: SteeringPoint;
|
|
54
|
+
velocity: SteeringVector;
|
|
55
|
+
readonly radius: number;
|
|
56
|
+
readonly maxSpeed: number;
|
|
57
|
+
readonly priority: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export class CrowdSimulation {
|
|
61
|
+
private readonly agents: CrowdAgentState[] = [];
|
|
62
|
+
private readonly neighborRadius: number;
|
|
63
|
+
private readonly maxNeighbors: number;
|
|
64
|
+
private readonly separationWeight: number;
|
|
65
|
+
private readonly alignmentWeight: number;
|
|
66
|
+
private readonly cohesionWeight: number;
|
|
67
|
+
private readonly formationWeight: number;
|
|
68
|
+
private readonly maxForce: number;
|
|
69
|
+
private formation: CrowdFormationOptions | undefined;
|
|
70
|
+
private lastNeighbors = new Map<string, readonly CrowdAgentState[]>();
|
|
71
|
+
private lastSlots = new Map<string, SteeringPoint>();
|
|
72
|
+
|
|
73
|
+
constructor(options: CrowdSimulationOptions = {}) {
|
|
74
|
+
this.neighborRadius = positive(options.neighborRadius ?? 0.55, "neighborRadius");
|
|
75
|
+
this.maxNeighbors = Math.max(1, Math.floor(options.maxNeighbors ?? 6));
|
|
76
|
+
this.separationWeight = nonNegative(options.separationWeight ?? 1.8, "separationWeight");
|
|
77
|
+
this.alignmentWeight = nonNegative(options.alignmentWeight ?? 0.55, "alignmentWeight");
|
|
78
|
+
this.cohesionWeight = nonNegative(options.cohesionWeight ?? 0.42, "cohesionWeight");
|
|
79
|
+
this.formationWeight = nonNegative(options.formationWeight ?? 0.85, "formationWeight");
|
|
80
|
+
this.maxForce = positive(options.maxForce ?? 1.2, "maxForce");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addAgent(options: CrowdAgentOptions): void {
|
|
84
|
+
if (this.agents.some((agent) => agent.id === options.id)) throw new Error(`Crowd agent already exists: ${options.id}`);
|
|
85
|
+
this.agents.push({
|
|
86
|
+
id: options.id,
|
|
87
|
+
position: options.position,
|
|
88
|
+
velocity: options.velocity ?? [0, 0],
|
|
89
|
+
radius: positive(options.radius ?? 0.1, "radius"),
|
|
90
|
+
maxSpeed: positive(options.maxSpeed ?? 0.65, "maxSpeed"),
|
|
91
|
+
priority: nonNegative(options.priority ?? 50, "priority")
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
removeAgent(id: string): boolean {
|
|
96
|
+
const index = this.agents.findIndex((agent) => agent.id === id);
|
|
97
|
+
if (index < 0) return false;
|
|
98
|
+
this.agents.splice(index, 1);
|
|
99
|
+
this.lastNeighbors.delete(id);
|
|
100
|
+
this.lastSlots.delete(id);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setFormation(options: CrowdFormationOptions | undefined): void {
|
|
105
|
+
this.formation = options;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
update(deltaSeconds: number): CrowdSimulationSnapshot {
|
|
109
|
+
const dt = Math.max(0, deltaSeconds);
|
|
110
|
+
const neighborsById = new Map(this.agents.map((agent) => [agent.id, this.neighborsFor(agent)]));
|
|
111
|
+
const slots = formationSlots(this.agents.length, this.formation);
|
|
112
|
+
this.lastNeighbors = neighborsById;
|
|
113
|
+
this.lastSlots = new Map(this.agents.map((agent, index) => [agent.id, slots[index] ?? agent.position]));
|
|
114
|
+
|
|
115
|
+
for (let index = 0; index < this.agents.length; index += 1) {
|
|
116
|
+
const agent = this.agents[index]!;
|
|
117
|
+
const neighbors = neighborsById.get(agent.id) ?? [];
|
|
118
|
+
const slot = slots[index] ?? agent.position;
|
|
119
|
+
const force = clampMagnitude(addMany([
|
|
120
|
+
scale(separationForce(agent, neighbors), this.separationWeight),
|
|
121
|
+
scale(alignmentForce(agent, neighbors), this.alignmentWeight),
|
|
122
|
+
scale(cohesionForce(agent, neighbors), this.cohesionWeight),
|
|
123
|
+
scale(formationForce(agent, slot), this.formationWeight)
|
|
124
|
+
]), this.maxForce);
|
|
125
|
+
const priorityDamping = Math.max(0.35, Math.min(1, agent.priority / 100));
|
|
126
|
+
const velocity = clampMagnitude([
|
|
127
|
+
agent.velocity[0] + force[0] * dt * priorityDamping,
|
|
128
|
+
agent.velocity[1] + force[1] * dt * priorityDamping
|
|
129
|
+
], agent.maxSpeed);
|
|
130
|
+
agent.velocity = velocity;
|
|
131
|
+
agent.position = [
|
|
132
|
+
round3(agent.position[0] + velocity[0] * dt),
|
|
133
|
+
round3(agent.position[1] + velocity[1] * dt)
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return this.snapshot();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
snapshot(): CrowdSimulationSnapshot {
|
|
141
|
+
const agentSnapshots = this.agents.map((agent) => {
|
|
142
|
+
const neighbors = this.lastNeighbors.get(agent.id) ?? [];
|
|
143
|
+
return {
|
|
144
|
+
id: agent.id,
|
|
145
|
+
position: [round3(agent.position[0]), round3(agent.position[1])] as const,
|
|
146
|
+
velocity: [round3(agent.velocity[0]), round3(agent.velocity[1])] as const,
|
|
147
|
+
speed: round3(length(agent.velocity)),
|
|
148
|
+
neighborCount: neighbors.length,
|
|
149
|
+
formationSlot: this.lastSlots.get(agent.id) ?? agent.position
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
const neighborPairs = agentSnapshots.reduce((sum, agent) => sum + agent.neighborCount, 0) / 2;
|
|
153
|
+
const averageSpeed = agentSnapshots.length > 0
|
|
154
|
+
? agentSnapshots.reduce((sum, agent) => sum + agent.speed, 0) / agentSnapshots.length
|
|
155
|
+
: 0;
|
|
156
|
+
return {
|
|
157
|
+
agentCount: agentSnapshots.length,
|
|
158
|
+
neighborPairs: round3(neighborPairs),
|
|
159
|
+
averageNeighbors: agentSnapshots.length > 0 ? round3((neighborPairs * 2) / agentSnapshots.length) : 0,
|
|
160
|
+
averageSpeed: round3(averageSpeed),
|
|
161
|
+
maxSpeed: round3(Math.max(0, ...agentSnapshots.map((agent) => agent.speed))),
|
|
162
|
+
formationType: this.formation?.type ?? "none",
|
|
163
|
+
center: centerOf(agentSnapshots.map((agent) => agent.position)),
|
|
164
|
+
agents: agentSnapshots
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private neighborsFor(agent: CrowdAgentState): readonly CrowdAgentState[] {
|
|
169
|
+
return this.agents
|
|
170
|
+
.filter((candidate) => candidate !== agent && distance(agent.position, candidate.position) <= this.neighborRadius)
|
|
171
|
+
.sort((left, right) => distance(agent.position, left.position) - distance(agent.position, right.position) || left.id.localeCompare(right.id))
|
|
172
|
+
.slice(0, this.maxNeighbors);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function separationForce(agent: CrowdAgentState, neighbors: readonly CrowdAgentState[]): SteeringVector {
|
|
177
|
+
const contributions = neighbors.map((neighbor) => {
|
|
178
|
+
const away = subtract(agent.position, neighbor.position);
|
|
179
|
+
const gap = Math.max(0.001, length(away) - agent.radius - neighbor.radius);
|
|
180
|
+
return scale(normalize(away), 1 / gap);
|
|
181
|
+
});
|
|
182
|
+
return normalize(addMany(contributions));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function alignmentForce(agent: CrowdAgentState, neighbors: readonly CrowdAgentState[]): SteeringVector {
|
|
186
|
+
if (neighbors.length === 0) return [0, 0];
|
|
187
|
+
const average = scale(addMany(neighbors.map((neighbor) => neighbor.velocity)), 1 / neighbors.length);
|
|
188
|
+
return subtract(clampMagnitude(average, agent.maxSpeed), agent.velocity);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function cohesionForce(agent: CrowdAgentState, neighbors: readonly CrowdAgentState[]): SteeringVector {
|
|
192
|
+
if (neighbors.length === 0) return [0, 0];
|
|
193
|
+
const center = centerOf(neighbors.map((neighbor) => neighbor.position));
|
|
194
|
+
return arriveSteering({ position: agent.position, velocity: agent.velocity, target: center, maxSpeed: agent.maxSpeed, slowingRadius: 0.45 }).force;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formationForce(agent: CrowdAgentState, slot: SteeringPoint): SteeringVector {
|
|
198
|
+
return arriveSteering({ position: agent.position, velocity: agent.velocity, target: slot, maxSpeed: agent.maxSpeed, slowingRadius: 0.35, tolerance: 0.025 }).force;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formationSlots(count: number, formation: CrowdFormationOptions | undefined): readonly SteeringPoint[] {
|
|
202
|
+
if (!formation) return [];
|
|
203
|
+
const spacing = formation.spacing ?? 0.24;
|
|
204
|
+
const forward = normalize(formation.forward ?? [1, 0]);
|
|
205
|
+
const right: SteeringVector = [forward[1], -forward[0]];
|
|
206
|
+
return Array.from({ length: count }, (_, index) => {
|
|
207
|
+
switch (formation.type) {
|
|
208
|
+
case "line":
|
|
209
|
+
return offset(formation.center, right, (index - (count - 1) / 2) * spacing);
|
|
210
|
+
case "column":
|
|
211
|
+
return offset(formation.center, forward, -index * spacing);
|
|
212
|
+
case "circle": {
|
|
213
|
+
const angle = (Math.PI * 2 * index) / Math.max(1, count);
|
|
214
|
+
return [round3(formation.center[0] + Math.cos(angle) * spacing), round3(formation.center[1] + Math.sin(angle) * spacing)];
|
|
215
|
+
}
|
|
216
|
+
case "wedge": {
|
|
217
|
+
if (index === 0) return formation.center;
|
|
218
|
+
const side = index % 2 === 0 ? 1 : -1;
|
|
219
|
+
const row = Math.ceil(index / 2);
|
|
220
|
+
return add(offset(formation.center, forward, -row * spacing), scale(right, side * row * spacing));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function offset(origin: SteeringPoint, direction: SteeringVector, amount: number): SteeringPoint {
|
|
227
|
+
return [round3(origin[0] + direction[0] * amount), round3(origin[1] + direction[1] * amount)];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function add(left: SteeringPoint, right: SteeringVector): SteeringPoint {
|
|
231
|
+
return [round3(left[0] + right[0]), round3(left[1] + right[1])];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function subtract(left: SteeringPoint, right: SteeringPoint): SteeringVector {
|
|
235
|
+
return [left[0] - right[0], left[1] - right[1]];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function scale(value: SteeringVector, scalar: number): SteeringVector {
|
|
239
|
+
return [value[0] * scalar, value[1] * scalar];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function addMany(values: readonly SteeringVector[]): SteeringVector {
|
|
243
|
+
return values.reduce<SteeringVector>((sum, value) => [sum[0] + value[0], sum[1] + value[1]], [0, 0]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function distance(left: SteeringPoint, right: SteeringPoint): number {
|
|
247
|
+
return length(subtract(left, right));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function length(value: SteeringVector): number {
|
|
251
|
+
return Math.hypot(value[0], value[1]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalize(value: SteeringVector): SteeringVector {
|
|
255
|
+
const magnitude = length(value);
|
|
256
|
+
return magnitude === 0 ? [0, 0] : [value[0] / magnitude, value[1] / magnitude];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function clampMagnitude(value: SteeringVector, maxMagnitude: number): SteeringVector {
|
|
260
|
+
const magnitude = length(value);
|
|
261
|
+
if (magnitude === 0 || magnitude <= maxMagnitude) return value;
|
|
262
|
+
return scale(value, maxMagnitude / magnitude);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function centerOf(points: readonly SteeringPoint[]): SteeringPoint {
|
|
266
|
+
if (points.length === 0) return [0, 0];
|
|
267
|
+
const sum = points.reduce<SteeringVector>((total, point) => [total[0] + point[0], total[1] + point[1]], [0, 0]);
|
|
268
|
+
return [round3(sum[0] / points.length), round3(sum[1] / points.length)];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function positive(value: number, name: string): number {
|
|
272
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`Crowd ${name} must be a finite positive number.`);
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function nonNegative(value: number, name: string): number {
|
|
277
|
+
if (!Number.isFinite(value) || value < 0) throw new RangeError(`Crowd ${name} must be a finite non-negative number.`);
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function round3(value: number): number {
|
|
282
|
+
return Number(value.toFixed(3));
|
|
283
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PhysicsWorld } from "./PhysicsWorld.js";
|
|
2
|
+
import { interpolatePosition } from "./ScenePhysicsBridge.js";
|
|
3
|
+
import type { Vec3 } from "./Shape.js";
|
|
4
|
+
|
|
5
|
+
export type ECSTransformLike = {
|
|
6
|
+
position: [number, number, number];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type ECSPhysicsBinding = {
|
|
10
|
+
readonly bodyId: number;
|
|
11
|
+
readonly transform: ECSTransformLike;
|
|
12
|
+
readonly mode?: "dynamic" | "kinematic";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class ECSPhysicsBridge {
|
|
16
|
+
private readonly bindings = new Map<number, ECSPhysicsBinding>();
|
|
17
|
+
|
|
18
|
+
bind(binding: ECSPhysicsBinding): void {
|
|
19
|
+
if (this.bindings.has(binding.bodyId)) {
|
|
20
|
+
throw new Error(`Body ${binding.bodyId} is already bound to an ECS transform.`);
|
|
21
|
+
}
|
|
22
|
+
this.bindings.set(binding.bodyId, binding);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
unbind(bodyId: number): void {
|
|
26
|
+
this.bindings.delete(bodyId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pushKinematic(world: Pick<PhysicsWorld, "getBody">): void {
|
|
30
|
+
for (const binding of this.bindings.values()) {
|
|
31
|
+
if (binding.mode !== "kinematic") {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
world.getBody(binding.bodyId)?.setPosition(binding.transform.position);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pullDynamic(world: Pick<PhysicsWorld, "getBody">, alpha = 1): void {
|
|
39
|
+
for (const binding of this.bindings.values()) {
|
|
40
|
+
if (binding.mode === "kinematic") {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const body = world.getBody(binding.bodyId);
|
|
44
|
+
if (!body) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const interpolated = interpolatePosition(body.previousPosition, body.position, alpha);
|
|
48
|
+
binding.transform.position[0] = interpolated[0];
|
|
49
|
+
binding.transform.position[1] = interpolated[1];
|
|
50
|
+
binding.transform.position[2] = interpolated[2];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|