@aura3d/physics 1.3.3 → 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.
package/src/Collider.ts DELETED
@@ -1,67 +0,0 @@
1
- import { Shape, type Bounds, type PhysicsShape, type Vec3 } from "./Shape.js";
2
-
3
- export type CollisionFilter = {
4
- readonly layer: number;
5
- readonly mask: number;
6
- };
7
-
8
- export type ColliderMaterial = {
9
- readonly restitution: number;
10
- readonly friction: number;
11
- };
12
-
13
- export type ColliderDescriptor = {
14
- readonly shape: PhysicsShape;
15
- readonly sensor?: boolean;
16
- readonly filter?: Partial<CollisionFilter>;
17
- readonly material?: Partial<ColliderMaterial>;
18
- };
19
-
20
- export class Collider {
21
- readonly id: number;
22
- readonly bodyId: number;
23
- readonly shape: PhysicsShape;
24
- readonly sensor: boolean;
25
- readonly filter: CollisionFilter;
26
- readonly material: ColliderMaterial;
27
-
28
- constructor(id: number, bodyId: number, descriptor: ColliderDescriptor) {
29
- if (!Number.isInteger(id) || id <= 0) {
30
- throw new Error("Collider id must be a positive integer.");
31
- }
32
- if (!Number.isInteger(bodyId) || bodyId <= 0) {
33
- throw new Error("Collider body id must be a positive integer.");
34
- }
35
- this.id = id;
36
- this.bodyId = bodyId;
37
- this.shape = descriptor.shape;
38
- this.sensor = descriptor.sensor ?? false;
39
- this.filter = {
40
- layer: descriptor.filter?.layer ?? 1,
41
- mask: descriptor.filter?.mask ?? 0xffffffff
42
- };
43
- this.material = {
44
- restitution: descriptor.material?.restitution ?? 0,
45
- friction: descriptor.material?.friction ?? 0.5
46
- };
47
- if (this.filter.layer < 0 || this.filter.mask < 0) {
48
- throw new Error("Collider filter layer and mask must be non-negative bit masks.");
49
- }
50
- if (
51
- !Number.isFinite(this.material.restitution) ||
52
- !Number.isFinite(this.material.friction) ||
53
- this.material.restitution < 0 ||
54
- this.material.friction < 0
55
- ) {
56
- throw new Error("Collider material values must be finite and non-negative.");
57
- }
58
- }
59
-
60
- canCollideWith(other: Collider): boolean {
61
- return (this.filter.mask & other.filter.layer) !== 0 && (other.filter.mask & this.filter.layer) !== 0;
62
- }
63
-
64
- bounds(position: Vec3): Bounds {
65
- return Shape.bounds(this.shape, position);
66
- }
67
- }
@@ -1,70 +0,0 @@
1
- import type { Vec3 } from "./Shape.js";
2
-
3
- export type Contact = {
4
- readonly colliderA: number;
5
- readonly colliderB: number;
6
- readonly bodyA: number;
7
- readonly bodyB: number;
8
- readonly normal: Vec3;
9
- readonly penetration: number;
10
- readonly sensor: boolean;
11
- };
12
-
13
- export type CollisionEventType = "begin" | "stay" | "end";
14
-
15
- export type CollisionEvent = {
16
- readonly type: CollisionEventType;
17
- readonly pairKey: string;
18
- readonly contact: Contact;
19
- };
20
-
21
- export function contactPairKey(colliderA: number, colliderB: number): string {
22
- return colliderA < colliderB ? `${colliderA}:${colliderB}` : `${colliderB}:${colliderA}`;
23
- }
24
-
25
- export class CollisionEventQueue {
26
- private previousContacts = new Map<string, Contact>();
27
- private events: CollisionEvent[] = [];
28
-
29
- update(currentContacts: readonly Contact[]): readonly CollisionEvent[] {
30
- const next = new Map<string, Contact>();
31
- const events: CollisionEvent[] = [];
32
- for (const contact of currentContacts) {
33
- const pairKey = contactPairKey(contact.colliderA, contact.colliderB);
34
- next.set(pairKey, contact);
35
- events.push({ type: this.previousContacts.has(pairKey) ? "stay" : "begin", pairKey, contact });
36
- }
37
- for (const [pairKey, contact] of this.previousContacts) {
38
- if (!next.has(pairKey)) {
39
- events.push({ type: "end", pairKey, contact });
40
- }
41
- }
42
- events.sort((a, b) => a.pairKey.localeCompare(b.pairKey) || a.type.localeCompare(b.type));
43
- this.previousContacts = next;
44
- this.events = events;
45
- return this.drain();
46
- }
47
-
48
- removeCollider(colliderId: number): readonly CollisionEvent[] {
49
- const removedEvents: CollisionEvent[] = [];
50
- for (const [pairKey, contact] of this.previousContacts) {
51
- if (contact.colliderA === colliderId || contact.colliderB === colliderId) {
52
- const event = { type: "end" as const, pairKey, contact };
53
- this.events.push(event);
54
- removedEvents.push(event);
55
- this.previousContacts.delete(pairKey);
56
- }
57
- }
58
- return removedEvents;
59
- }
60
-
61
- drain(): readonly CollisionEvent[] {
62
- const events = this.events;
63
- this.events = [];
64
- return events;
65
- }
66
-
67
- snapshotContacts(): readonly Contact[] {
68
- return Array.from(this.previousContacts.values());
69
- }
70
- }
@@ -1,303 +0,0 @@
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 debug?: boolean | CollisionVolumeDebugOptions;
15
- readonly metadata?: Readonly<Record<string, unknown>>;
16
- };
17
-
18
- export type CollisionVolumeDebugOptions = {
19
- readonly enabled?: boolean;
20
- readonly color?: string;
21
- readonly opacity?: number;
22
- readonly label?: string;
23
- };
24
-
25
- export type CollisionVolumeDebugMetadata = {
26
- readonly enabled: boolean;
27
- readonly visibleInNormalPass: false;
28
- readonly color: string;
29
- readonly opacity: number;
30
- readonly label?: string;
31
- };
32
-
33
- export type CollisionVolume = {
34
- readonly id: string;
35
- readonly kind: CollisionVolumeKind;
36
- readonly offset: Vec3;
37
- readonly halfExtents: Vec3;
38
- readonly enabled: boolean;
39
- readonly tags: readonly string[];
40
- readonly debug: CollisionVolumeDebugMetadata;
41
- readonly ownerId?: CollisionOwnerId;
42
- readonly metadata?: Readonly<Record<string, unknown>>;
43
- };
44
-
45
- export type ResolvedCollisionVolume = CollisionVolume & {
46
- readonly center: Vec3;
47
- readonly facing: CombatFacing;
48
- readonly bounds: Bounds;
49
- };
50
-
51
- export type CollisionPenetration = {
52
- readonly normal: Vec3;
53
- readonly depth: number;
54
- readonly axis: 0 | 1 | 2;
55
- readonly overlap: Vec3;
56
- };
57
-
58
- export type CollisionVolumeDebugDrawOptions = {
59
- readonly enabled?: boolean;
60
- };
61
-
62
- export type CollisionVolumeDebugDrawDescriptor = {
63
- readonly id: string;
64
- readonly kind: CollisionVolumeKind;
65
- readonly center: Vec3;
66
- readonly halfExtents: Vec3;
67
- readonly color: string;
68
- readonly opacity: number;
69
- readonly label: string;
70
- };
71
-
72
- export function collisionVolume(kind: CollisionVolumeKind, descriptor: CollisionVolumeDescriptor): CollisionVolume {
73
- validateHalfExtents(descriptor.halfExtents, `${kind} halfExtents`);
74
- const volume = {
75
- id: descriptor.id ?? kind,
76
- kind,
77
- offset: cloneVec3(descriptor.offset ?? [0, 0, 0]),
78
- halfExtents: cloneVec3(descriptor.halfExtents),
79
- enabled: descriptor.enabled ?? true,
80
- tags: [...(descriptor.tags ?? [])],
81
- debug: normalizeCollisionVolumeDebug(kind, descriptor.id ?? kind, descriptor.debug),
82
- ...(descriptor.ownerId === undefined ? {} : { ownerId: descriptor.ownerId }),
83
- ...(descriptor.metadata === undefined ? {} : { metadata: { ...descriptor.metadata } })
84
- };
85
- validateFiniteVec3(volume.offset, `${kind} offset`);
86
- return volume;
87
- }
88
-
89
- export function hitbox(descriptor: CollisionVolumeDescriptor): CollisionVolume {
90
- return collisionVolume("hitbox", descriptor);
91
- }
92
-
93
- export function hurtbox(descriptor: CollisionVolumeDescriptor): CollisionVolume {
94
- return collisionVolume("hurtbox", descriptor);
95
- }
96
-
97
- export function pushbox(descriptor: CollisionVolumeDescriptor): CollisionVolume {
98
- return collisionVolume("pushbox", descriptor);
99
- }
100
-
101
- export function guardbox(descriptor: CollisionVolumeDescriptor): CollisionVolume {
102
- return collisionVolume("guardbox", descriptor);
103
- }
104
-
105
- export const createHitbox = hitbox;
106
- export const createHurtbox = hurtbox;
107
- export const createPushbox = pushbox;
108
- export const createGuardbox = guardbox;
109
- export const createGuardBox = guardbox;
110
-
111
- export function cloneCollisionVolume(volume: CollisionVolume): CollisionVolume {
112
- return {
113
- id: volume.id,
114
- kind: volume.kind,
115
- offset: cloneVec3(volume.offset),
116
- halfExtents: cloneVec3(volume.halfExtents),
117
- enabled: volume.enabled,
118
- tags: [...volume.tags],
119
- debug: { ...volume.debug },
120
- ...(volume.ownerId === undefined ? {} : { ownerId: volume.ownerId }),
121
- ...(volume.metadata === undefined ? {} : { metadata: { ...volume.metadata } })
122
- };
123
- }
124
-
125
- export function withVolumeOwner(volume: CollisionVolume, ownerId: CollisionOwnerId): CollisionVolume {
126
- return {
127
- ...cloneCollisionVolume(volume),
128
- ownerId
129
- };
130
- }
131
-
132
- export function resolveCollisionVolume(volume: CollisionVolume, ownerPosition: Vec3, facing: CombatFacing = 1): ResolvedCollisionVolume {
133
- validateFiniteVec3(ownerPosition, "volume owner position");
134
- const resolvedFacing = normalizeFacing(facing);
135
- const center: Vec3 = [
136
- ownerPosition[0] + volume.offset[0] * resolvedFacing,
137
- ownerPosition[1] + volume.offset[1],
138
- ownerPosition[2] + volume.offset[2]
139
- ];
140
- return {
141
- ...cloneCollisionVolume(volume),
142
- center,
143
- facing: resolvedFacing,
144
- bounds: aabbFromCenter(center, volume.halfExtents)
145
- };
146
- }
147
-
148
- export function collisionVolumeDebugDrawDescriptors(
149
- volumes: readonly ResolvedCollisionVolume[],
150
- options: CollisionVolumeDebugDrawOptions = {}
151
- ): readonly CollisionVolumeDebugDrawDescriptor[] {
152
- if (options.enabled !== true) {
153
- return [];
154
- }
155
- return volumes
156
- .filter((volume) => volume.enabled && volume.debug.enabled)
157
- .map((volume) => ({
158
- id: volume.id,
159
- kind: volume.kind,
160
- center: cloneVec3(volume.center),
161
- halfExtents: cloneVec3(volume.halfExtents),
162
- color: volume.debug.color,
163
- opacity: volume.debug.opacity,
164
- label: volume.debug.label ?? `${volume.kind}:${volume.id}`
165
- }));
166
- }
167
-
168
- export function collisionVolumeVisibleInNormalPass(volume: CollisionVolume): false {
169
- return volume.debug.visibleInNormalPass;
170
- }
171
-
172
- export function aabbFromCenter(center: Vec3, halfExtents: Vec3): Bounds {
173
- validateFiniteVec3(center, "aabb center");
174
- validateHalfExtents(halfExtents, "aabb halfExtents");
175
- return {
176
- min: [center[0] - halfExtents[0], center[1] - halfExtents[1], center[2] - halfExtents[2]],
177
- max: [center[0] + halfExtents[0], center[1] + halfExtents[1], center[2] + halfExtents[2]]
178
- };
179
- }
180
-
181
- export function boundsCenter(bounds: Bounds): Vec3 {
182
- validateBounds(bounds, "bounds");
183
- return [
184
- (bounds.min[0] + bounds.max[0]) * 0.5,
185
- (bounds.min[1] + bounds.max[1]) * 0.5,
186
- (bounds.min[2] + bounds.max[2]) * 0.5
187
- ];
188
- }
189
-
190
- export function boundsHalfExtents(bounds: Bounds): Vec3 {
191
- validateBounds(bounds, "bounds");
192
- return [
193
- (bounds.max[0] - bounds.min[0]) * 0.5,
194
- (bounds.max[1] - bounds.min[1]) * 0.5,
195
- (bounds.max[2] - bounds.min[2]) * 0.5
196
- ];
197
- }
198
-
199
- export function overlapsAabb(a: Bounds, b: Bounds): boolean {
200
- validateBounds(a, "a bounds");
201
- validateBounds(b, "b bounds");
202
- return (
203
- a.min[0] < b.max[0] &&
204
- a.max[0] > b.min[0] &&
205
- a.min[1] < b.max[1] &&
206
- a.max[1] > b.min[1] &&
207
- a.min[2] < b.max[2] &&
208
- a.max[2] > b.min[2]
209
- );
210
- }
211
-
212
- export function overlapsVolume(a: ResolvedCollisionVolume, b: ResolvedCollisionVolume): boolean {
213
- if (!a.enabled || !b.enabled) {
214
- return false;
215
- }
216
- return overlapsAabb(a.bounds, b.bounds);
217
- }
218
-
219
- export function aabbPenetration(a: Bounds, b: Bounds): CollisionPenetration | null {
220
- if (!overlapsAabb(a, b)) {
221
- return null;
222
- }
223
- const overlap: Vec3 = [
224
- Math.min(a.max[0] - b.min[0], b.max[0] - a.min[0]),
225
- Math.min(a.max[1] - b.min[1], b.max[1] - a.min[1]),
226
- Math.min(a.max[2] - b.min[2], b.max[2] - a.min[2])
227
- ];
228
- let axis: 0 | 1 | 2 = 0;
229
- if (overlap[1] < overlap[axis]) {
230
- axis = 1;
231
- }
232
- if (overlap[2] < overlap[axis]) {
233
- axis = 2;
234
- }
235
- const centerA = boundsCenter(a);
236
- const centerB = boundsCenter(b);
237
- const normal: [number, number, number] = [0, 0, 0];
238
- normal[axis] = centerA[axis] <= centerB[axis] ? -1 : 1;
239
- return {
240
- normal,
241
- depth: overlap[axis],
242
- axis,
243
- overlap
244
- };
245
- }
246
-
247
- export function volumePairKey(a: CollisionVolume | ResolvedCollisionVolume, b: CollisionVolume | ResolvedCollisionVolume): string {
248
- const keyA = volumeKey(a);
249
- const keyB = volumeKey(b);
250
- return keyA < keyB ? `${keyA}:${keyB}` : `${keyB}:${keyA}`;
251
- }
252
-
253
- export function normalizeFacing(value: number): CombatFacing {
254
- return value < 0 ? -1 : 1;
255
- }
256
-
257
- export function validateBounds(bounds: Bounds, name: string): void {
258
- validateFiniteVec3(bounds.min, `${name} min`);
259
- validateFiniteVec3(bounds.max, `${name} max`);
260
- if (bounds.min[0] > bounds.max[0] || bounds.min[1] > bounds.max[1] || bounds.min[2] > bounds.max[2]) {
261
- throw new Error(`${name} min values must be less than or equal to max values.`);
262
- }
263
- }
264
-
265
- function validateHalfExtents(value: Vec3, name: string): void {
266
- validateFiniteVec3(value, name);
267
- if (value[0] <= 0 || value[1] <= 0 || value[2] <= 0) {
268
- throw new Error(`${name} must contain finite positive values.`);
269
- }
270
- }
271
-
272
- function normalizeCollisionVolumeDebug(
273
- kind: CollisionVolumeKind,
274
- id: string,
275
- debug: boolean | CollisionVolumeDebugOptions | undefined
276
- ): CollisionVolumeDebugMetadata {
277
- const options: CollisionVolumeDebugOptions = typeof debug === "object" && debug !== null ? debug : {};
278
- return {
279
- enabled: typeof debug === "boolean" ? debug : options.enabled ?? false,
280
- visibleInNormalPass: false,
281
- color: options.color ?? defaultDebugColor(kind),
282
- opacity: options.opacity ?? 0.28,
283
- label: options.label ?? `${kind}:${id}`
284
- };
285
- }
286
-
287
- function defaultDebugColor(kind: CollisionVolumeKind): string {
288
- switch (kind) {
289
- case "hitbox":
290
- return "#ef4444";
291
- case "hurtbox":
292
- return "#22c55e";
293
- case "guardbox":
294
- return "#38bdf8";
295
- case "pushbox":
296
- return "#facc15";
297
- }
298
- }
299
-
300
- function volumeKey(volume: CollisionVolume | ResolvedCollisionVolume): string {
301
- const owner = volume.ownerId === undefined ? "none" : String(volume.ownerId);
302
- return `${owner}/${volume.kind}/${volume.id}`;
303
- }
package/src/Constraint.ts DELETED
@@ -1,120 +0,0 @@
1
- import { addVec3, dotVec3, normalizeVec3, rotateVec3ByQuat, 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, rotateVec3ByQuat(this.localAnchorA, this.bodyA.rotation));
87
- }
88
-
89
- private anchorBWorld(): Vec3 {
90
- return addVec3(this.bodyB.position, rotateVec3ByQuat(this.localAnchorB, this.bodyB.rotation));
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
- }
@@ -1 +0,0 @@
1
- export * from "./Constraint.js";