@ue-too/dynamics 0.14.1 → 0.15.0

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 CHANGED
@@ -25,11 +25,13 @@
25
25
  ## Installation
26
26
 
27
27
  Using Bun:
28
+
28
29
  ```bash
29
30
  bun add @ue-too/dynamics
30
31
  ```
31
32
 
32
33
  Using npm:
34
+
33
35
  ```bash
34
36
  npm install @ue-too/dynamics
35
37
  ```
@@ -39,41 +41,42 @@ npm install @ue-too/dynamics
39
41
  Here's a simple example creating a physics world with a falling ball:
40
42
 
41
43
  ```typescript
42
- import { World, Circle, Polygon } from '@ue-too/dynamics';
44
+ import { Circle, Polygon, World } from '@ue-too/dynamics';
43
45
 
44
46
  // Create a physics world (2000x2000 world size)
45
47
  const world = new World(2000, 2000, 'dynamictree');
46
48
 
47
49
  // Create static ground
48
50
  const ground = new Polygon(
49
- { x: 0, y: -100 }, // Position
50
- [ // Vertices (local space)
51
- { x: -1000, y: 0 },
52
- { x: 1000, y: 0 },
53
- { x: 1000, y: 50 },
54
- { x: -1000, y: 50 }
55
- ],
56
- 0, // Rotation
57
- 100, // Mass (ignored for static bodies)
58
- true // isStatic
51
+ { x: 0, y: -100 }, // Position
52
+ [
53
+ // Vertices (local space)
54
+ { x: -1000, y: 0 },
55
+ { x: 1000, y: 0 },
56
+ { x: 1000, y: 50 },
57
+ { x: -1000, y: 50 },
58
+ ],
59
+ 0, // Rotation
60
+ 100, // Mass (ignored for static bodies)
61
+ true // isStatic
59
62
  );
60
63
  world.addRigidBody('ground', ground);
61
64
 
62
65
  // Create dynamic ball
63
66
  const ball = new Circle(
64
- { x: 0, y: 200 }, // Position
65
- 20, // Radius
66
- 0, // Rotation
67
- 10, // Mass
68
- false // isStatic
67
+ { x: 0, y: 200 }, // Position
68
+ 20, // Radius
69
+ 0, // Rotation
70
+ 10, // Mass
71
+ false // isStatic
69
72
  );
70
73
  world.addRigidBody('ball', ball);
71
74
 
72
75
  // Simulation loop (60 FPS)
73
76
  setInterval(() => {
74
- world.step(1/60); // deltaTime in seconds
77
+ world.step(1 / 60); // deltaTime in seconds
75
78
 
76
- console.log('Ball position:', ball.position);
79
+ console.log('Ball position:', ball.position);
77
80
  }, 16);
78
81
  ```
79
82
 
@@ -99,15 +102,16 @@ Rigid bodies are objects that don't deform. They have:
99
102
  ### Collision Detection Phases
100
103
 
101
104
  1. **Broad Phase**: Uses spatial indexing to quickly find potentially colliding pairs
102
- - QuadTree: Good for static worlds
103
- - Dynamic Tree: Good for mixed static/dynamic
104
- - Sweep-and-Prune: Best for many dynamic bodies
105
+ - QuadTree: Good for static worlds
106
+ - Dynamic Tree: Good for mixed static/dynamic
107
+ - Sweep-and-Prune: Best for many dynamic bodies
105
108
 
106
109
  2. **Narrow Phase**: Precise collision detection using Separating Axis Theorem (SAT)
107
110
 
108
111
  ### Collision Filtering
109
112
 
110
113
  Bodies can be filtered by:
114
+
111
115
  - **Category**: What category this body belongs to (bit flags)
112
116
  - **Mask**: Which categories this body collides with (bit flags)
113
117
  - **Group**: Positive groups collide only with same group, negative never collide
@@ -119,6 +123,7 @@ Bodies can be filtered by:
119
123
  Main physics world container.
120
124
 
121
125
  **Constructor:**
126
+
122
127
  ```typescript
123
128
  const world = new World(
124
129
  worldWidth: number,
@@ -128,6 +133,7 @@ const world = new World(
128
133
  ```
129
134
 
130
135
  **Methods:**
136
+
131
137
  - `step(deltaTime: number)`: Advance simulation by deltaTime seconds
132
138
  - `addRigidBody(id: string, body: RigidBody)`: Add a rigid body
133
139
  - `removeRigidBody(id: string)`: Remove a rigid body
@@ -139,6 +145,7 @@ const world = new World(
139
145
  - `rayCast(from, to): RayCastResult[]`: Ray casting
140
146
 
141
147
  **Properties:**
148
+
142
149
  - `gravity: Point`: World gravity vector (default: `{x: 0, y: -9.8}`)
143
150
  - `sleepingEnabled: boolean`: Enable/disable sleeping system
144
151
  - `bodies: Map<string, RigidBody>`: All bodies in the world
@@ -149,6 +156,7 @@ const world = new World(
149
156
  Circular rigid body.
150
157
 
151
158
  **Constructor:**
159
+
152
160
  ```typescript
153
161
  const circle = new Circle(
154
162
  position: Point,
@@ -160,6 +168,7 @@ const circle = new Circle(
160
168
  ```
161
169
 
162
170
  **Properties:**
171
+
163
172
  - `position: Point`: World position
164
173
  - `velocity: Point`: Linear velocity
165
174
  - `rotation: number`: Rotation in radians
@@ -175,6 +184,7 @@ const circle = new Circle(
175
184
  Convex polygon rigid body.
176
185
 
177
186
  **Constructor:**
187
+
178
188
  ```typescript
179
189
  const polygon = new Polygon(
180
190
  position: Point,
@@ -186,11 +196,13 @@ const polygon = new Polygon(
186
196
  ```
187
197
 
188
198
  **Properties:**
199
+
189
200
  - Same as Circle, plus:
190
201
  - `vertices: Point[]`: Vertices in local space
191
202
  - `worldVertices: Point[]`: Vertices in world space (computed)
192
203
 
193
204
  **Methods:**
205
+
194
206
  - `updateWorldVertices()`: Recompute world vertices after transform changes
195
207
 
196
208
  ### Constraints
@@ -224,21 +236,22 @@ const fixedJoint = new FixedPinJoint(
224
236
 
225
237
  ```typescript
226
238
  type CollisionFilter = {
227
- category: number; // What category this body is (bit flags)
228
- mask: number; // What categories to collide with (bit flags)
229
- group: number; // Collision group
239
+ category: number; // What category this body is (bit flags)
240
+ mask: number; // What categories to collide with (bit flags)
241
+ group: number; // Collision group
230
242
  };
231
243
  ```
232
244
 
233
245
  **Predefined Categories:**
246
+
234
247
  ```typescript
235
248
  enum CollisionCategory {
236
- STATIC = 0x0001,
237
- PLAYER = 0x0002,
238
- ENEMY = 0x0004,
239
- PROJECTILE = 0x0008,
240
- SENSOR = 0x0010,
241
- PLATFORM = 0x0020
249
+ STATIC = 0x0001,
250
+ PLAYER = 0x0002,
251
+ ENEMY = 0x0004,
252
+ PROJECTILE = 0x0008,
253
+ SENSOR = 0x0010,
254
+ PLATFORM = 0x0020,
242
255
  }
243
256
  ```
244
257
 
@@ -247,30 +260,40 @@ enum CollisionCategory {
247
260
  ### Basic Platformer Physics
248
261
 
249
262
  ```typescript
250
- import { World, Circle, Polygon, CollisionCategory } from '@ue-too/dynamics';
263
+ import { Circle, CollisionCategory, Polygon, World } from '@ue-too/dynamics';
251
264
 
252
265
  const world = new World(2000, 2000, 'dynamictree');
253
266
  world.gravity = { x: 0, y: -20 }; // Downward gravity
254
267
 
255
268
  // Ground
256
269
  const ground = new Polygon(
257
- { x: 0, y: -150 },
258
- [{ x: -500, y: 0 }, { x: 500, y: 0 }, { x: 500, y: 50 }, { x: -500, y: 50 }],
259
- 0, 0, true
270
+ { x: 0, y: -150 },
271
+ [
272
+ { x: -500, y: 0 },
273
+ { x: 500, y: 0 },
274
+ { x: 500, y: 50 },
275
+ { x: -500, y: 50 },
276
+ ],
277
+ 0,
278
+ 0,
279
+ true
260
280
  );
261
281
  ground.collisionFilter = {
262
- category: CollisionCategory.STATIC,
263
- mask: 0xFFFF, // Collides with everything
264
- group: 0
282
+ category: CollisionCategory.STATIC,
283
+ mask: 0xffff, // Collides with everything
284
+ group: 0,
265
285
  };
266
286
  world.addRigidBody('ground', ground);
267
287
 
268
288
  // Player
269
289
  const player = new Circle({ x: 0, y: 0 }, 20, 0, 10, false);
270
290
  player.collisionFilter = {
271
- category: CollisionCategory.PLAYER,
272
- mask: CollisionCategory.STATIC | CollisionCategory.PLATFORM | CollisionCategory.ENEMY,
273
- group: 0
291
+ category: CollisionCategory.PLAYER,
292
+ mask:
293
+ CollisionCategory.STATIC |
294
+ CollisionCategory.PLATFORM |
295
+ CollisionCategory.ENEMY,
296
+ group: 0,
274
297
  };
275
298
  player.restitution = 0; // No bounce
276
299
  player.friction = 0.5;
@@ -278,20 +301,20 @@ world.addRigidBody('player', player);
278
301
 
279
302
  // Apply jump force
280
303
  function jump() {
281
- player.velocity.y = 15; // Upward velocity
304
+ player.velocity.y = 15; // Upward velocity
282
305
  }
283
306
 
284
307
  // Game loop
285
308
  function update(deltaTime: number) {
286
- world.step(deltaTime);
287
- // Render player at player.position
309
+ world.step(deltaTime);
310
+ // Render player at player.position
288
311
  }
289
312
  ```
290
313
 
291
314
  ### Pendulum with Constraints
292
315
 
293
316
  ```typescript
294
- import { World, Circle, FixedPinJoint } from '@ue-too/dynamics';
317
+ import { Circle, FixedPinJoint, World } from '@ue-too/dynamics';
295
318
 
296
319
  const world = new World(2000, 2000);
297
320
  world.gravity = { x: 0, y: -9.8 };
@@ -303,22 +326,22 @@ world.addRigidBody('bob', bob);
303
326
 
304
327
  // Fix to world origin
305
328
  const joint = new FixedPinJoint(
306
- bob,
307
- { x: 0, y: 0 }, // Bob's center
308
- { x: 0, y: 0 } // World origin
329
+ bob,
330
+ { x: 0, y: 0 }, // Bob's center
331
+ { x: 0, y: 0 } // World origin
309
332
  );
310
333
  world.addConstraint(joint);
311
334
 
312
335
  // Simulation
313
336
  function update(deltaTime: number) {
314
- world.step(deltaTime);
337
+ world.step(deltaTime);
315
338
  }
316
339
  ```
317
340
 
318
341
  ### Chain of Bodies
319
342
 
320
343
  ```typescript
321
- import { World, Circle, PinJoint } from '@ue-too/dynamics';
344
+ import { Circle, PinJoint, World } from '@ue-too/dynamics';
322
345
 
323
346
  const world = new World(2000, 2000);
324
347
  world.gravity = { x: 0, y: -9.8 };
@@ -328,27 +351,27 @@ const numLinks = 5;
328
351
 
329
352
  // Create chain links
330
353
  for (let i = 0; i < numLinks; i++) {
331
- const link = new Circle({ x: i * 30, y: 0 }, 10, 0, 5, false);
332
- world.addRigidBody(`link${i}`, link);
333
- links.push(link);
334
-
335
- if (i > 0) {
336
- // Connect to previous link
337
- const joint = new PinJoint(
338
- links[i - 1],
339
- links[i],
340
- { x: 10, y: 0 }, // Right edge of previous
341
- { x: -10, y: 0 } // Left edge of current
342
- );
343
- world.addConstraint(joint);
344
- }
354
+ const link = new Circle({ x: i * 30, y: 0 }, 10, 0, 5, false);
355
+ world.addRigidBody(`link${i}`, link);
356
+ links.push(link);
357
+
358
+ if (i > 0) {
359
+ // Connect to previous link
360
+ const joint = new PinJoint(
361
+ links[i - 1],
362
+ links[i],
363
+ { x: 10, y: 0 }, // Right edge of previous
364
+ { x: -10, y: 0 } // Left edge of current
365
+ );
366
+ world.addConstraint(joint);
367
+ }
345
368
  }
346
369
 
347
370
  // Fix first link to world
348
371
  const fixedJoint = new FixedPinJoint(
349
- links[0],
350
- { x: -10, y: 0 },
351
- { x: 0, y: 0 }
372
+ links[0],
373
+ { x: -10, y: 0 },
374
+ { x: 0, y: 0 }
352
375
  );
353
376
  world.addConstraint(fixedJoint);
354
377
  ```
@@ -361,17 +384,17 @@ import { Circle, CollisionCategory } from '@ue-too/dynamics';
361
384
  // Create a trigger zone that doesn't physically collide
362
385
  const trigger = new Circle({ x: 100, y: 100 }, 50, 0, 0, true);
363
386
  trigger.collisionFilter = {
364
- category: CollisionCategory.SENSOR,
365
- mask: CollisionCategory.PLAYER,
366
- group: -1 // Negative group = never physically collide
387
+ category: CollisionCategory.SENSOR,
388
+ mask: CollisionCategory.PLAYER,
389
+ group: -1, // Negative group = never physically collide
367
390
  };
368
391
  world.addRigidBody('trigger', trigger);
369
392
 
370
393
  // Listen for collisions
371
394
  world.onCollision((bodyA, bodyB, contacts) => {
372
- if (bodyA === trigger || bodyB === trigger) {
373
- console.log('Player entered trigger zone!');
374
- }
395
+ if (bodyA === trigger || bodyB === trigger) {
396
+ console.log('Player entered trigger zone!');
397
+ }
375
398
  });
376
399
  ```
377
400
 
@@ -380,8 +403,8 @@ world.onCollision((bodyA, bodyB, contacts) => {
380
403
  ```typescript
381
404
  // Find all bodies in a region
382
405
  const aabb = {
383
- min: { x: -50, y: -50 },
384
- max: { x: 50, y: 50 }
406
+ min: { x: -50, y: -50 },
407
+ max: { x: 50, y: 50 },
385
408
  };
386
409
  const bodiesInRegion = world.queryAABB(aabb);
387
410
 
@@ -390,12 +413,12 @@ const bodiesAtPoint = world.queryPoint({ x: 100, y: 100 });
390
413
 
391
414
  // Ray cast
392
415
  const rayResults = world.rayCast(
393
- { x: 0, y: 0 }, // From
394
- { x: 100, y: 100 } // To
416
+ { x: 0, y: 0 }, // From
417
+ { x: 100, y: 100 } // To
395
418
  );
396
419
 
397
420
  rayResults.forEach(result => {
398
- console.log('Hit:', result.body, 'at distance:', result.distance);
421
+ console.log('Hit:', result.body, 'at distance:', result.distance);
399
422
  });
400
423
  ```
401
424
 
@@ -410,8 +433,8 @@ const world = new World(2000, 2000, 'sap'); // Sweep-and-prune for many dynamic
410
433
  world.sleepingEnabled = true;
411
434
 
412
435
  // Customize sleeping thresholds per body
413
- body.sleepThreshold = 0.01; // Velocity threshold
414
- body.sleepTime = 0.5; // Seconds at rest before sleeping
436
+ body.sleepThreshold = 0.01; // Velocity threshold
437
+ body.sleepTime = 0.5; // Seconds at rest before sleeping
415
438
 
416
439
  // Get performance stats
417
440
  const stats = world.getCollisionStats();
@@ -434,13 +457,13 @@ This package is written in TypeScript with complete type definitions:
434
457
 
435
458
  ```typescript
436
459
  import {
437
- World,
438
- Circle,
439
- Polygon,
440
- RigidBody,
441
- Constraint,
442
- CollisionCategory,
443
- type Point
460
+ Circle,
461
+ CollisionCategory,
462
+ Constraint,
463
+ type Point,
464
+ Polygon,
465
+ RigidBody,
466
+ World,
444
467
  } from '@ue-too/dynamics';
445
468
 
446
469
  // Bodies are fully typed
@@ -448,13 +471,18 @@ const circle: Circle = new Circle({ x: 0, y: 0 }, 20, 0, 10, false);
448
471
  const polygon: Polygon = new Polygon(/* ... */);
449
472
 
450
473
  // Constraints are typed
451
- const joint: Constraint = new PinJoint(circle, polygon, { x: 0, y: 0 }, { x: 0, y: 0 });
474
+ const joint: Constraint = new PinJoint(
475
+ circle,
476
+ polygon,
477
+ { x: 0, y: 0 },
478
+ { x: 0, y: 0 }
479
+ );
452
480
 
453
481
  // Filters are typed
454
482
  circle.collisionFilter = {
455
- category: CollisionCategory.PLAYER,
456
- mask: CollisionCategory.STATIC | CollisionCategory.ENEMY,
457
- group: 0
483
+ category: CollisionCategory.PLAYER,
484
+ mask: CollisionCategory.STATIC | CollisionCategory.ENEMY,
485
+ group: 0,
458
486
  };
459
487
  ```
460
488
 
@@ -471,15 +499,16 @@ This physics engine follows these principles:
471
499
  ## Performance Considerations
472
500
 
473
501
  - **Spatial Indexing**: Choose based on your use case:
474
- - QuadTree: Static worlds with few dynamic objects
475
- - Dynamic Tree: Mixed static/dynamic (recommended default)
476
- - Sweep-and-Prune: Many dynamic objects moving continuously
502
+ - QuadTree: Static worlds with few dynamic objects
503
+ - Dynamic Tree: Mixed static/dynamic (recommended default)
504
+ - Sweep-and-Prune: Many dynamic objects moving continuously
477
505
 
478
506
  - **Sleeping System**: Automatically disables physics for resting bodies
479
507
  - **Collision Filtering**: Reduces narrow phase tests significantly
480
508
  - **Fixed Time Step**: Use fixed time steps (1/60) for stability
481
509
 
482
510
  **Performance Tips:**
511
+
483
512
  - Enable sleeping for worlds with many resting bodies
484
513
  - Use collision filtering to avoid unnecessary collision tests
485
514
  - Choose appropriate spatial index for your scenario
@@ -500,23 +529,29 @@ This physics engine follows these principles:
500
529
  ```typescript
501
530
  // Enable debug rendering
502
531
  world.debugDraw = (ctx: CanvasRenderingContext2D) => {
503
- // Draw all bodies
504
- world.bodies.forEach(body => {
505
- ctx.strokeStyle = body.isStatic ? 'gray' : 'blue';
506
- if (body instanceof Circle) {
507
- ctx.beginPath();
508
- ctx.arc(body.position.x, body.position.y, body.radius, 0, Math.PI * 2);
509
- ctx.stroke();
510
- } else if (body instanceof Polygon) {
511
- ctx.beginPath();
512
- ctx.moveTo(body.worldVertices[0].x, body.worldVertices[0].y);
513
- for (let i = 1; i < body.worldVertices.length; i++) {
514
- ctx.lineTo(body.worldVertices[i].x, body.worldVertices[i].y);
515
- }
516
- ctx.closePath();
517
- ctx.stroke();
518
- }
519
- });
532
+ // Draw all bodies
533
+ world.bodies.forEach(body => {
534
+ ctx.strokeStyle = body.isStatic ? 'gray' : 'blue';
535
+ if (body instanceof Circle) {
536
+ ctx.beginPath();
537
+ ctx.arc(
538
+ body.position.x,
539
+ body.position.y,
540
+ body.radius,
541
+ 0,
542
+ Math.PI * 2
543
+ );
544
+ ctx.stroke();
545
+ } else if (body instanceof Polygon) {
546
+ ctx.beginPath();
547
+ ctx.moveTo(body.worldVertices[0].x, body.worldVertices[0].y);
548
+ for (let i = 1; i < body.worldVertices.length; i++) {
549
+ ctx.lineTo(body.worldVertices[i].x, body.worldVertices[i].y);
550
+ }
551
+ ctx.closePath();
552
+ ctx.stroke();
553
+ }
554
+ });
520
555
  };
521
556
  ```
522
557
 
package/collision.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { BaseRigidBody, RigidBody } from "./rigidbody";
2
- import { Point } from "@ue-too/math";
3
- import { QuadTree } from "./quadtree";
1
+ import { Point } from '@ue-too/math';
2
+ import { QuadTree } from './quadtree';
3
+ import { BaseRigidBody, RigidBody } from './rigidbody';
4
4
  export declare function resolveCollision(bodyA: RigidBody, bodyB: RigidBody, normal: Point): void;
5
5
  export declare function resolveCollisionWithRotation(bodyA: RigidBody, bodyB: RigidBody, contactManifold: {
6
6
  normal: Point;
@@ -30,7 +30,7 @@ export declare function broadPhaseWithRigidBodyReturned(quadTree: QuadTree<Rigid
30
30
  bodyA: RigidBody;
31
31
  bodyB: RigidBody;
32
32
  }[];
33
- export declare function broadPhaseWithSpatialIndex(spatialIndex: import("./dynamic-tree").SpatialIndex<RigidBody>, bodies: RigidBody[]): {
33
+ export declare function broadPhaseWithSpatialIndex(spatialIndex: import('./dynamic-tree').SpatialIndex<RigidBody>, bodies: RigidBody[]): {
34
34
  bodyA: RigidBody;
35
35
  bodyB: RigidBody;
36
36
  }[];
@@ -38,7 +38,7 @@ export declare function broadPhase(quadTree: QuadTree<RigidBody>, bodies: BaseRi
38
38
  bodyAIndex: number;
39
39
  bodyBIndex: number;
40
40
  }[];
41
- export declare function broadPhaseWithSpatialIndexFiltered(spatialIndex: import("./dynamic-tree").SpatialIndex<RigidBody>, bodies: RigidBody[]): {
41
+ export declare function broadPhaseWithSpatialIndexFiltered(spatialIndex: import('./dynamic-tree').SpatialIndex<RigidBody>, bodies: RigidBody[]): {
42
42
  bodyA: RigidBody;
43
43
  bodyB: RigidBody;
44
44
  }[];
package/constraint.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Point } from "@ue-too/math";
2
- import { RigidBody } from "./rigidbody";
1
+ import { Point } from '@ue-too/math';
2
+ import { RigidBody } from './rigidbody';
3
3
  /**
4
4
  * Physics constraint interface.
5
5
  *
package/dynamic-tree.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Point } from "@ue-too/math";
1
+ import { Point } from '@ue-too/math';
2
2
  /**
3
3
  * Object that can be indexed spatially via AABB.
4
4
  * @category Types
package/index.d.ts CHANGED
@@ -187,11 +187,11 @@
187
187
  * @see {@link RigidBody} for physics objects
188
188
  * @see {@link Constraint} for joints and constraints
189
189
  */
190
- export * from "./rigidbody";
191
- export * from "./quadtree";
192
- export * from "./dynamic-tree";
193
- export * from "./collision";
194
- export * from "./world";
195
- export * from "./constraint";
196
- export * from "./collision-filter";
197
- export * from "./pair-manager";
190
+ export * from './rigidbody';
191
+ export * from './quadtree';
192
+ export * from './dynamic-tree';
193
+ export * from './collision';
194
+ export * from './world';
195
+ export * from './constraint';
196
+ export * from './collision-filter';
197
+ export * from './pair-manager';