@combos-fun/plugin-cannon 0.0.42 → 0.0.44

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  @combos-fun/plugin-cannon — part of the Combos Fun engine monorepo.
4
4
 
5
- Keywords: `cannon-es`, `3d`, `physics`, `collision`, `rigid-body`, `gravity`.
5
+ Keywords: `cannon-es`, `3d`, `physics`, `collision`, `rigid-body`, `gravity`, `trimesh`, `compound`.
6
6
 
7
7
  ## Documentation
8
8
 
package/agent-skill.md CHANGED
@@ -6,7 +6,7 @@ Pattern mirrors `plugin-matterjs` (2D): Component stores params, System observes
6
6
 
7
7
  ## When to read
8
8
 
9
- Read for any 3D physics task: gravity, collisions, rigid bodies, ground planes, cylinders / spheres, body-mesh sync.
9
+ Read for any 3D physics task: gravity, collisions, rigid bodies, ground planes, cylinders / cones / capsules / tori, body-mesh sync, GLB colliders.
10
10
 
11
11
  ## Public API
12
12
 
@@ -15,7 +15,12 @@ import {
15
15
  Physics3DSystem,
16
16
  Physics3D,
17
17
  Physics3DType,
18
+ Physics3DConstraint,
19
+ Physics3DConstraintSystem,
18
20
  type Physics3DParams,
21
+ type Physics3DPartParams,
22
+ type Physics3DCollider,
23
+ type Physics3DConstraintParams,
19
24
  type Physics3DSystemParams,
20
25
  } from '@combos-fun/plugin-cannon';
21
26
  ```
@@ -24,29 +29,54 @@ import {
24
29
 
25
30
  | Value | Shape | Notes |
26
31
  |-------|-------|-------|
27
- | `BOX` | `CANNON.Box` | Half-extents from `width`/`height`/`depth` |
32
+ | `BOX` | `CANNON.Box` | Half-extents from `width` / `height` / `depth` |
28
33
  | `SPHERE` | `CANNON.Sphere` | `radius` |
29
- | `CYLINDER` | `CANNON.Cylinder` | `radiusTop`, `radiusBottom`, `height`, `segments` |
30
- | `PLANE` | `CANNON.Plane` | Infinite ground; always `mass = 0` |
34
+ | `CYLINDER` | `CANNON.Cylinder` | `radiusTop`, `radiusBottom`, `height`, `segments` (Y axis) |
35
+ | `CONE` | `CANNON.Cylinder(≈0, radius, height)` | Matches `Graphics3D` cone |
36
+ | `PLANE` | `CANNON.Plane` or thin box | Infinite unless `finite: true` |
37
+ | `TORUS` | Compound spheres | 8–12 spheres; tube = `tube` or `0.4 × radius` |
38
+ | `CAPSULE` | Cylinder + 2 spheres | `height` is total height |
39
+ | `COMPOUND` | `Body.addShape` list | From `parts` or sibling `Graphics3D.parts` |
40
+
41
+ If `type` is omitted: use `parts` → `COMPOUND`, else sibling `Graphics3D.shape`, else `BOX`. Missing size fields fall back to the sibling `Graphics3D`.
42
+
43
+ ### `Physics3DCollider` (models)
44
+
45
+ | Value | When | Notes |
46
+ |-------|------|-------|
47
+ | `manual` | default without `Model3D` | Use `type` / `parts` |
48
+ | `aabb` | **default when a sibling `Model3D` exists and `type` / `parts` are omitted** | `Box3` after load, includes `scale*` |
49
+ | `hull` | convex props | Convex hull of sampled vertices; holes fill in |
50
+ | `trimesh` | static scenery only | Triangle mesh; `mass` is forced to `0` |
51
+
52
+ Mesh colliders wait until `Model3D` has attached a `Group`. Changing `scale*` rebuilds the body. Skinned animation stays on the bind-pose mesh. Do not use `trimesh` on two dynamic models that must hit each other.
31
53
 
32
54
  ### `Physics3DParams`
33
55
 
34
56
  | Field | Type | Default | Notes |
35
57
  |-------|------|---------|-------|
36
- | `type` | `Physics3DType` | `BOX` | |
37
- | `mass` | `number` | `1` | `0` = static; `PLANE` always static |
58
+ | `type` | `Physics3DType` | inferred | |
59
+ | `mass` | `number` | `1` | `0` = static; infinite `PLANE` always 0; `trimesh` always 0 |
38
60
  | `bodyOptions` | `object` | `{}` | `friction`, `restitution`, `linearDamping`, `angularDamping`, `fixedRotation` |
39
- | `position` | `{x?, y?, z?}` | `0,0,0` | |
40
- | `rotation` | `{x?, y?, z?}` | `0,0,0` | Euler → quaternion |
41
- | `width`/`height`/`depth` | `number` | `1` | Box dimensions |
42
- | `radius` | `number` | `0.5` | Sphere / cylinder radius |
43
- | `radiusTop`/`radiusBottom` | `number` | `radius` | Cylinder top/bottom |
44
- | `segments` | `number` | `16` | Cylinder radial segments |
61
+ | `position` | `{x?, y?, z?}` | sibling pose or `0` | |
62
+ | `rotation` | `{x?, y?, z?}` | sibling pose or `0` | Euler → quaternion |
63
+ | `width`/`height`/`depth` | `number` | sibling or `1` | Box / finite plane |
64
+ | `radius` | `number` | sibling or `0.5` | Sphere / cylinder / cone / torus / capsule |
65
+ | `radiusTop`/`radiusBottom` | `number` | `radius` | Cylinder taper |
66
+ | `segments` | `number` | `12` | |
67
+ | `tube` | `number` | `0.4 × radius` | Torus tube |
68
+ | `finite` | `boolean` | `false` | `PLANE` → thin box (`width` × `height` × 0.02) in XY |
69
+ | `collider` | `Physics3DCollider` | see above | |
70
+ | `parts` | `Physics3DPartParams[]` | sibling `Graphics3D.parts` | Local offsets; no nested `COMPOUND` |
45
71
  | `stopRotation` | `boolean` | `false` | Lock rotation sync |
46
72
 
73
+ `PLANE` stays **infinite** unless `finite: true`. A 10×10 visual ground that should drop you at the edge is a thin `BOX` or `finite: true`. Infinite ground still needs `rotation: { x: -Math.PI/2 }`.
74
+
47
75
  Synced public fields (read each `update`):
48
76
 
49
- `positionX`, `positionY`, `positionZ`, `rotationX`, `rotationY`, `rotationZ`. The system writes these back to **all sibling components** on the same `GameObject` that have those fields (duck-type check). So `Physics3D` + `Graphics3D` on the same node stay in sync without custom glue code.
77
+ `positionX`, `positionY`, `positionZ`, `rotationX`, `rotationY`, `rotationZ`. The system writes these back to **all sibling components** on the same `GameObject` that have those fields (duck-type check), including `Transform3D`. So `Physics3D` + `Transform3D` / `Graphics3D` / `Model3D` on the same node stay in sync without custom glue code.
78
+
79
+ If the visual is parented (`addChild`), Cannon stays in world space and `Physics3D` converts the body pose to **local** before writing siblings. A moving car with physics + visual-only wheels as children does not need physics on the wheels.
50
80
 
51
81
  `body: CANNON.Body` is injected by the engine after the body is created.
52
82
 
@@ -66,15 +96,33 @@ Listen on the `Physics3D` instance:
66
96
 
67
97
  ```ts
68
98
  physics3d.on('collisionStart', (otherGameObject, selfGameObject) => { ... });
99
+ physics3d.on('collisionActive', (otherGameObject, selfGameObject) => { ... });
69
100
  physics3d.on('collisionEnd', (otherGameObject, selfGameObject) => { ... });
70
101
  ```
71
102
 
103
+ `collisionActive` fires every `world.step` while the pair is still overlapping. Compound / torus / capsule hits still emit on this parent `Physics3D`.
104
+
105
+ ### `Physics3DConstraint`
106
+
107
+ Put `Physics3DConstraint` on the same GameObject as `Physics3D`. `target` is the **other GameObject's `name`**. Both bodies must exist (pending until they do).
108
+
109
+ | `type` | Cannon constraint |
110
+ |--------|-------------------|
111
+ | `hinge` | `HingeConstraint` (`pivotA*` / `pivotB*` / `axisA*` / `axisB*`, optional `motor` / `motorSpeed`) |
112
+ | `point` | `PointToPointConstraint` |
113
+ | `lock` | `LockConstraint` |
114
+ | `distance` | `DistanceConstraint` (`distance`) |
115
+
116
+ Add `Physics3DConstraintSystem` after `Physics3DSystem`.
117
+
72
118
  ## Required setup
73
119
 
74
120
  - Add `Renderer3DSystem` (from `plugin-renderer-3d`) **before**
75
121
  `Physics3DSystem`.
122
+ - Add `Physics3DConstraintSystem` after `Physics3DSystem` when using joints.
76
123
  - Body creation is **lazy**: it happens only when `Physics3D` is added
77
124
  AND the `GameObject` has a parent (is in the scene hierarchy).
125
+ - Mesh colliders are **lazier**: they wait for `Model3D` to finish loading.
78
126
 
79
127
  ## Runtime behaviour
80
128
 
@@ -88,9 +136,13 @@ physics3d.on('collisionEnd', (otherGameObject, selfGameObject) => { ... });
88
136
 
89
137
  | Symptom | Fix |
90
138
  |---------|-----|
91
- | Bodies fall forever | `PLANE` rotated and mass 0 acts as ground; verify `rotation: { x: -Math.PI/2 }` and that it's at the right Y |
92
- | Mesh not following body | Make sure `Physics3D` and the visual (e.g. `Graphics3D`) are on the **same** `GameObject` |
93
- | Body exists but no collision | Check `Physics3DSystem` is registered after `Renderer3DSystem` and `GameObject` is actually in the scene hierarchy |
139
+ | Bodies fall forever | Infinite `PLANE` needs `rotation: { x: -Math.PI/2 }` and a sane Y |
140
+ | Walk off a 10×10 visual plane and still stand | That plane is infinite; set `finite: true` or use a `BOX` |
141
+ | Mesh not following body | `Physics3D` and the visual on the **same** `GameObject` |
142
+ | GLB uses a 1×1×1 box | Default without `Model3D` is manual `BOX`. With `Model3D`, default is `aabb` after load |
143
+ | Doorway blocked on a GLB house | `collider: 'trimesh'` and `mass: 0` (forced). `aabb` / `hull` fill holes |
144
+ | Dynamic trimesh jitter | Don't; use `aabb` / `hull` for moving objects |
145
+ | Body exists but no collision | `Physics3DSystem` after `Renderer3DSystem`; object is in the scene hierarchy |
94
146
  | Continuous low-amplitude jitter on a stack | Increase `solver.iterations` or set `allowSleep: true` |
95
147
  | Want axis-aligned visual on rotating body | `stopRotation: true` |
96
148
 
@@ -107,6 +159,7 @@ const game = new Game({
107
159
  new Renderer3DSystem({ canvas, width: 750, height: 1334 }),
108
160
  new Graphics3DSystem(),
109
161
  new Physics3DSystem({ gravity: { x: 0, y: -9.82, z: 0 } }),
162
+ new Physics3DConstraintSystem(),
110
163
  ],
111
164
  onSystemsBootstrapComplete: (g) => {
112
165
  const scene = g.scene;
@@ -130,6 +183,16 @@ const game = new Game({
130
183
  });
131
184
  ```
132
185
 
186
+ Model collider:
187
+
188
+ ```ts
189
+ hero.addComponent(new Model3D({ resource: 'hero', positionY: 0 }));
190
+ hero.addComponent(new Physics3D({ mass: 1 })); // aabb after the GLB loads
191
+
192
+ building.addComponent(new Model3D({ resource: 'house' }));
193
+ building.addComponent(new Physics3D({ collider: 'trimesh', mass: 0 }));
194
+ ```
195
+
133
196
  ## Verification
134
197
 
135
198
  - `pnpm --filter @combos-fun/plugin-cannon run build`
@@ -4,8 +4,14 @@
4
4
  "category": "physics",
5
5
  "dimension": "3d",
6
6
  "isCore": false,
7
- "keywords": ["cannon-es", "3d", "physics", "collision", "rigid-body", "gravity"],
7
+ "keywords": ["cannon-es", "3d", "physics", "collision", "rigid-body", "gravity", "trimesh", "compound", "constraint", "hinge"],
8
8
  "agentSkill": "./agent-skill.md",
9
9
  "requires": ["@combos-fun/engine", "@combos-fun/plugin-renderer-3d"],
10
- "exports": ["Physics3DSystem", "Physics3D", "Physics3DType"]
10
+ "exports": [
11
+ "Physics3DSystem",
12
+ "Physics3D",
13
+ "Physics3DType",
14
+ "Physics3DConstraint",
15
+ "Physics3DConstraintSystem"
16
+ ]
11
17
  }