@combos-fun/plugin-cannon 0.0.41 → 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.
@@ -1,6 +1,9 @@
1
1
  import { __decorate } from 'tslib';
2
2
  import { Component, System, OBSERVER_TYPE, decorators } from '@combos-fun/engine';
3
3
  import * as CANNON from 'cannon-es';
4
+ import { Vector3, Quaternion, Euler, Box3 } from 'three';
5
+ import { Renderer3DSystem } from '@combos-fun/plugin-renderer-3d';
6
+ import { ConvexGeometry } from 'three/addons/geometries/ConvexGeometry.js';
4
7
 
5
8
  var Physics3DType;
6
9
  (function (Physics3DType) {
@@ -8,6 +11,10 @@ var Physics3DType;
8
11
  Physics3DType["SPHERE"] = "sphere";
9
12
  Physics3DType["CYLINDER"] = "cylinder";
10
13
  Physics3DType["PLANE"] = "plane";
14
+ Physics3DType["CONE"] = "cone";
15
+ Physics3DType["TORUS"] = "torus";
16
+ Physics3DType["CAPSULE"] = "capsule";
17
+ Physics3DType["COMPOUND"] = "compound";
11
18
  })(Physics3DType || (Physics3DType = {}));
12
19
  class Physics3D extends Component {
13
20
  constructor() {
@@ -20,10 +27,14 @@ class Physics3D extends Component {
20
27
  this.rotationY = 0;
21
28
  this.rotationZ = 0;
22
29
  this._euler = new CANNON.Vec3();
30
+ this._worldPos = new Vector3();
31
+ this._worldQuat = new Quaternion();
32
+ this._bodyQuat = new Quaternion();
33
+ this._localEuler = new Euler();
23
34
  }
24
35
  static { this.componentName = 'Physics3D'; }
25
36
  init(params) {
26
- this.bodyParams = params;
37
+ this.bodyParams = params || {};
27
38
  }
28
39
  update() {
29
40
  if (this.body && this.gameObject) {
@@ -38,7 +49,8 @@ class Physics3D extends Component {
38
49
  this.rotationY = this._euler.y;
39
50
  this.rotationZ = this._euler.z;
40
51
  }
41
- // Auto-sync to sibling 3D render components on the same GameObject
52
+ this._worldPoseToLocalIfParented();
53
+ // Auto-sync to sibling Transform3D / visual components on the same GameObject
42
54
  this._syncToSiblings();
43
55
  }
44
56
  }
@@ -47,6 +59,28 @@ class Physics3D extends Component {
47
59
  this.world.removeBody(this.body);
48
60
  }
49
61
  }
62
+ _worldPoseToLocalIfParented() {
63
+ const renderer = this.game?.getSystem(Renderer3DSystem);
64
+ const node = renderer?.threeContext?.object3DFor(this.gameObject.id);
65
+ const scene = renderer?.threeContext?.scene;
66
+ if (!node?.parent || node.parent === scene)
67
+ return;
68
+ node.parent.updateWorldMatrix(true, false);
69
+ this._worldPos.set(this.positionX, this.positionY, this.positionZ);
70
+ node.parent.worldToLocal(this._worldPos);
71
+ this.positionX = this._worldPos.x;
72
+ this.positionY = this._worldPos.y;
73
+ this.positionZ = this._worldPos.z;
74
+ if (!this.bodyParams.stopRotation) {
75
+ node.parent.getWorldQuaternion(this._worldQuat).invert();
76
+ this._bodyQuat.set(this.body.quaternion.x, this.body.quaternion.y, this.body.quaternion.z, this.body.quaternion.w);
77
+ this._bodyQuat.premultiply(this._worldQuat);
78
+ this._localEuler.setFromQuaternion(this._bodyQuat);
79
+ this.rotationX = this._localEuler.x;
80
+ this.rotationY = this._localEuler.y;
81
+ this.rotationZ = this._localEuler.z;
82
+ }
83
+ }
50
84
  _syncToSiblings() {
51
85
  const components = this.gameObject.components;
52
86
  if (!components)
@@ -69,74 +103,385 @@ class Physics3D extends Component {
69
103
  }
70
104
  }
71
105
 
72
- class BodiesFactory {
73
- create(component) {
74
- const { bodyParams } = component;
75
- const type = bodyParams.type ?? Physics3DType.BOX;
76
- const mass = bodyParams.mass ?? 1;
77
- const options = bodyParams.bodyOptions ?? {};
78
- // Create shape
79
- let shape;
80
- switch (type) {
81
- case Physics3DType.SPHERE: {
82
- const radius = bodyParams.radius ?? 0.5;
83
- shape = new CANNON.Sphere(radius);
84
- break;
106
+ const TYPE_BY_SHAPE = {
107
+ box: Physics3DType.BOX,
108
+ sphere: Physics3DType.SPHERE,
109
+ cylinder: Physics3DType.CYLINDER,
110
+ plane: Physics3DType.PLANE,
111
+ cone: Physics3DType.CONE,
112
+ torus: Physics3DType.TORUS,
113
+ capsule: Physics3DType.CAPSULE,
114
+ compound: Physics3DType.COMPOUND,
115
+ };
116
+ function normalizeType(type) {
117
+ if (!type)
118
+ return Physics3DType.BOX;
119
+ return TYPE_BY_SHAPE[type] ?? type;
120
+ }
121
+ function torusTube(radius, tube) {
122
+ return tube && tube > 0 ? tube : radius * 0.4;
123
+ }
124
+ function dimsFromParams(params, fallback) {
125
+ const radius = params.radius ?? fallback?.radius ?? 0.5;
126
+ return {
127
+ width: params.width ?? fallback?.width ?? 1,
128
+ height: params.height ?? fallback?.height ?? 1,
129
+ depth: params.depth ?? fallback?.depth ?? 1,
130
+ radius,
131
+ radiusTop: params.radiusTop ?? radius,
132
+ radiusBottom: params.radiusBottom ?? radius,
133
+ segments: Math.max(6, Math.round(params.segments ?? fallback?.segments ?? 12)),
134
+ tube: torusTube(radius, params.tube ?? fallback?.tube),
135
+ finite: params.finite ?? fallback?.finite,
136
+ };
137
+ }
138
+ function offsetFromPart(part) {
139
+ return new CANNON.Vec3(part.positionX ?? part.position?.x ?? 0, part.positionY ?? part.position?.y ?? 0, part.positionZ ?? part.position?.z ?? 0);
140
+ }
141
+ function quaternionFromPart(part) {
142
+ const x = part.rotationX ?? part.rotation?.x ?? 0;
143
+ const y = part.rotationY ?? part.rotation?.y ?? 0;
144
+ const z = part.rotationZ ?? part.rotation?.z ?? 0;
145
+ if (!x && !y && !z)
146
+ return undefined;
147
+ const q = new CANNON.Quaternion();
148
+ q.setFromEuler(x, y, z);
149
+ return q;
150
+ }
151
+ function placedShapesForType(type, dims) {
152
+ const kind = normalizeType(type);
153
+ switch (kind) {
154
+ case Physics3DType.SPHERE:
155
+ return [{ shape: new CANNON.Sphere(dims.radius) }];
156
+ case Physics3DType.CYLINDER:
157
+ return [{
158
+ shape: new CANNON.Cylinder(dims.radiusTop, dims.radiusBottom, dims.height, dims.segments),
159
+ }];
160
+ case Physics3DType.CONE:
161
+ return [{
162
+ shape: new CANNON.Cylinder(0.001, dims.radius, dims.height, dims.segments),
163
+ }];
164
+ case Physics3DType.PLANE:
165
+ if (dims.finite) {
166
+ return [{ shape: new CANNON.Box(new CANNON.Vec3(dims.width / 2, dims.height / 2, 0.01)) }];
85
167
  }
86
- case Physics3DType.CYLINDER: {
87
- const radiusTop = bodyParams.radiusTop ?? bodyParams.radius ?? 0.5;
88
- const radiusBottom = bodyParams.radiusBottom ?? bodyParams.radius ?? 0.5;
89
- const height = bodyParams.height ?? 1;
90
- const segments = bodyParams.segments ?? 16;
91
- shape = new CANNON.Cylinder(radiusTop, radiusBottom, height, segments);
92
- break;
168
+ return [{ shape: new CANNON.Plane() }];
169
+ case Physics3DType.TORUS:
170
+ return torusShapes(dims);
171
+ case Physics3DType.CAPSULE:
172
+ return capsuleShapes(dims);
173
+ case Physics3DType.BOX:
174
+ default:
175
+ return [{
176
+ shape: new CANNON.Box(new CANNON.Vec3(dims.width / 2, dims.height / 2, dims.depth / 2)),
177
+ }];
178
+ }
179
+ }
180
+ function torusShapes(dims) {
181
+ const n = Math.max(8, Math.min(12, dims.segments));
182
+ const placed = [];
183
+ for (let i = 0; i < n; i++) {
184
+ const angle = (i / n) * Math.PI * 2;
185
+ placed.push({
186
+ shape: new CANNON.Sphere(dims.tube),
187
+ offset: new CANNON.Vec3(Math.cos(angle) * dims.radius, 0, Math.sin(angle) * dims.radius),
188
+ });
189
+ }
190
+ return placed;
191
+ }
192
+ function capsuleShapes(dims) {
193
+ const cyl = Math.max(0, dims.height - 2 * dims.radius);
194
+ const placed = [];
195
+ if (cyl > 0.001) {
196
+ placed.push({
197
+ shape: new CANNON.Cylinder(dims.radius, dims.radius, cyl, dims.segments),
198
+ });
199
+ }
200
+ placed.push({
201
+ shape: new CANNON.Sphere(dims.radius),
202
+ offset: new CANNON.Vec3(0, cyl / 2, 0),
203
+ });
204
+ placed.push({
205
+ shape: new CANNON.Sphere(dims.radius),
206
+ offset: new CANNON.Vec3(0, -cyl / 2, 0),
207
+ });
208
+ return placed;
209
+ }
210
+
211
+ const _box = new Box3();
212
+ const _size = new Vector3();
213
+ const _center = new Vector3();
214
+ const _vertex = new Vector3();
215
+ function withIdentityPose(root, fn) {
216
+ const px = root.position.x;
217
+ const py = root.position.y;
218
+ const pz = root.position.z;
219
+ const rx = root.rotation.x;
220
+ const ry = root.rotation.y;
221
+ const rz = root.rotation.z;
222
+ root.position.set(0, 0, 0);
223
+ root.rotation.set(0, 0, 0);
224
+ root.updateMatrixWorld(true);
225
+ try {
226
+ return fn();
227
+ }
228
+ finally {
229
+ root.position.set(px, py, pz);
230
+ root.rotation.set(rx, ry, rz);
231
+ root.updateMatrixWorld(true);
232
+ }
233
+ }
234
+ function addModelCollider(body, root, collider) {
235
+ return withIdentityPose(root, () => {
236
+ if (collider === 'trimesh') {
237
+ return addTrimesh(body, root);
238
+ }
239
+ if (collider === 'hull') {
240
+ return addHull(body, root);
241
+ }
242
+ return addAabb(body, root);
243
+ });
244
+ }
245
+ function addAabb(body, root) {
246
+ _box.setFromObject(root);
247
+ if (_box.isEmpty())
248
+ return false;
249
+ _box.getSize(_size);
250
+ _box.getCenter(_center);
251
+ if (_size.x <= 0 && _size.y <= 0 && _size.z <= 0)
252
+ return false;
253
+ body.addShape(new CANNON.Box(new CANNON.Vec3(Math.max(_size.x / 2, 0.01), Math.max(_size.y / 2, 0.01), Math.max(_size.z / 2, 0.01))), new CANNON.Vec3(_center.x, _center.y, _center.z));
254
+ return true;
255
+ }
256
+ function addTrimesh(body, root) {
257
+ let added = false;
258
+ root.traverse((child) => {
259
+ const mesh = child;
260
+ const geometry = mesh.geometry;
261
+ const position = geometry?.attributes?.position;
262
+ if (!position)
263
+ return;
264
+ const vertices = [];
265
+ for (let i = 0; i < position.count; i++) {
266
+ _vertex.fromBufferAttribute(position, i).applyMatrix4(mesh.matrixWorld);
267
+ vertices.push(_vertex.x, _vertex.y, _vertex.z);
268
+ }
269
+ const indices = [];
270
+ if (geometry.index) {
271
+ const array = geometry.index.array;
272
+ for (let i = 0; i < array.length; i++) {
273
+ indices.push(array[i]);
93
274
  }
94
- case Physics3DType.PLANE: {
95
- shape = new CANNON.Plane();
96
- break;
275
+ }
276
+ else {
277
+ for (let i = 0; i < position.count; i++) {
278
+ indices.push(i);
97
279
  }
98
- case Physics3DType.BOX:
99
- default: {
100
- const w = bodyParams.width ?? 1;
101
- const h = bodyParams.height ?? 1;
102
- const d = bodyParams.depth ?? 1;
103
- shape = new CANNON.Box(new CANNON.Vec3(w / 2, h / 2, d / 2));
104
- break;
280
+ }
281
+ if (indices.length < 3)
282
+ return;
283
+ body.addShape(new CANNON.Trimesh(vertices, indices));
284
+ added = true;
285
+ });
286
+ return added;
287
+ }
288
+ function addHull(body, root) {
289
+ const points = [];
290
+ root.traverse((child) => {
291
+ const mesh = child;
292
+ const position = mesh.geometry?.attributes?.position;
293
+ if (!position)
294
+ return;
295
+ const stride = position.count > 128 ? Math.ceil(position.count / 128) : 1;
296
+ for (let i = 0; i < position.count; i += stride) {
297
+ points.push(_vertex.fromBufferAttribute(position, i).applyMatrix4(mesh.matrixWorld).clone());
298
+ }
299
+ });
300
+ if (points.length < 4) {
301
+ return addAabb(body, root);
302
+ }
303
+ try {
304
+ const geometry = new ConvexGeometry(points);
305
+ const pos = geometry.attributes.position;
306
+ const vertices = [];
307
+ const indexOf = new Map();
308
+ const add = (x, y, z) => {
309
+ const key = `${x.toFixed(5)},${y.toFixed(5)},${z.toFixed(5)}`;
310
+ const existing = indexOf.get(key);
311
+ if (existing != null)
312
+ return existing;
313
+ const i = vertices.length;
314
+ vertices.push(new CANNON.Vec3(x, y, z));
315
+ indexOf.set(key, i);
316
+ return i;
317
+ };
318
+ const faces = [];
319
+ if (geometry.index) {
320
+ const array = geometry.index.array;
321
+ for (let i = 0; i + 2 < array.length; i += 3) {
322
+ faces.push([
323
+ add(pos.getX(array[i]), pos.getY(array[i]), pos.getZ(array[i])),
324
+ add(pos.getX(array[i + 1]), pos.getY(array[i + 1]), pos.getZ(array[i + 1])),
325
+ add(pos.getX(array[i + 2]), pos.getY(array[i + 2]), pos.getZ(array[i + 2])),
326
+ ]);
105
327
  }
106
328
  }
107
- // Create body
108
- const body = new CANNON.Body({
109
- mass: type === Physics3DType.PLANE ? 0 : mass,
110
- shape,
329
+ else {
330
+ for (let i = 0; i + 2 < pos.count; i += 3) {
331
+ faces.push([
332
+ add(pos.getX(i), pos.getY(i), pos.getZ(i)),
333
+ add(pos.getX(i + 1), pos.getY(i + 1), pos.getZ(i + 1)),
334
+ add(pos.getX(i + 2), pos.getY(i + 2), pos.getZ(i + 2)),
335
+ ]);
336
+ }
337
+ }
338
+ geometry.dispose();
339
+ if (vertices.length < 4 || faces.length < 4) {
340
+ return addAabb(body, root);
341
+ }
342
+ body.addShape(new CANNON.ConvexPolyhedron({ vertices, faces }));
343
+ return true;
344
+ }
345
+ catch {
346
+ return addAabb(body, root);
347
+ }
348
+ }
349
+
350
+ function resolveCollider(params, model) {
351
+ if (params.collider)
352
+ return params.collider;
353
+ if (params.type || (params.parts && params.parts.length > 0))
354
+ return 'manual';
355
+ return model ? 'aabb' : 'manual';
356
+ }
357
+ function resolveType(params, graphics) {
358
+ if (params.type)
359
+ return normalizeType(params.type);
360
+ if ((params.parts && params.parts.length > 0) || (graphics?.parts && graphics.parts.length > 0)) {
361
+ return Physics3DType.COMPOUND;
362
+ }
363
+ if (graphics?.shape)
364
+ return normalizeType(graphics.shape);
365
+ return Physics3DType.BOX;
366
+ }
367
+ function meshColliderFingerprint(collider, model) {
368
+ if (!model || collider === 'manual')
369
+ return undefined;
370
+ return `${collider}:${model.resource}:${model.scaleX}:${model.scaleY}:${model.scaleZ}`;
371
+ }
372
+
373
+ function applyBodyOptions(body, options) {
374
+ if (!options)
375
+ return;
376
+ if (options.friction !== undefined) {
377
+ body.material = new CANNON.Material({
378
+ friction: options.friction,
379
+ restitution: options.restitution ?? 0.3,
380
+ });
381
+ }
382
+ if (options.restitution !== undefined && !body.material) {
383
+ body.material = new CANNON.Material({ friction: 0.3, restitution: options.restitution });
384
+ }
385
+ if (options.linearDamping !== undefined)
386
+ body.linearDamping = options.linearDamping;
387
+ if (options.angularDamping !== undefined)
388
+ body.angularDamping = options.angularDamping;
389
+ if (options.fixedRotation !== undefined)
390
+ body.fixedRotation = options.fixedRotation;
391
+ body.updateMassProperties();
392
+ }
393
+ function addPlaced(body, type, dims, part) {
394
+ const placed = placedShapesForType(type, dims);
395
+ const extraOffset = part ? offsetFromPart(part) : new CANNON.Vec3();
396
+ const extraQuat = part ? quaternionFromPart(part) : undefined;
397
+ for (const item of placed) {
398
+ const local = (item.offset ? item.offset.clone() : new CANNON.Vec3());
399
+ if (extraQuat)
400
+ extraQuat.vmult(local, local);
401
+ const offset = extraOffset.vadd(local);
402
+ const quat = extraQuat && item.quaternion
403
+ ? extraQuat.mult(item.quaternion)
404
+ : (extraQuat || item.quaternion);
405
+ body.addShape(item.shape, offset, quat);
406
+ }
407
+ }
408
+ class BodiesFactory {
409
+ create(component, context = {}) {
410
+ const { bodyParams } = component;
411
+ const graphics = context.graphics;
412
+ const model = context.model;
413
+ const collider = resolveCollider(bodyParams, model);
414
+ if (collider !== 'manual') {
415
+ if (!context.modelGroup)
416
+ return null;
417
+ let mass = bodyParams.mass ?? 1;
418
+ if (collider === 'trimesh' && mass !== 0) {
419
+ mass = 0;
420
+ }
421
+ const body = this.createBodyShell(component, mass, context.pose || graphics);
422
+ const ok = addModelCollider(body, context.modelGroup, collider);
423
+ if (!ok)
424
+ return null;
425
+ applyBodyOptions(body, bodyParams.bodyOptions);
426
+ component.meshColliderKey = meshColliderFingerprint(collider, model);
427
+ return body;
428
+ }
429
+ const type = resolveType(bodyParams, graphics);
430
+ const dims = dimsFromParams(bodyParams, {
431
+ width: graphics?.width,
432
+ height: graphics?.height,
433
+ depth: graphics?.depth,
434
+ radius: graphics?.radius,
435
+ segments: graphics?.segments,
436
+ tube: graphics?.tube,
437
+ finite: bodyParams.finite,
111
438
  });
112
- // Set position
113
- const pos = bodyParams.position;
114
- if (pos) {
115
- body.position.set(pos.x ?? 0, pos.y ?? 0, pos.z ?? 0);
116
- }
117
- // Set rotation (Euler to quaternion)
118
- const rot = bodyParams.rotation;
119
- if (rot) {
120
- body.quaternion.setFromEuler(rot.x ?? 0, rot.y ?? 0, rot.z ?? 0);
121
- }
122
- // Apply body options
123
- if (options.friction !== undefined)
124
- body.material = new CANNON.Material({ friction: options.friction, restitution: options.restitution ?? 0.3 });
125
- if (options.restitution !== undefined && !body.material)
126
- body.material = new CANNON.Material({ friction: 0.3, restitution: options.restitution });
127
- if (options.linearDamping !== undefined)
128
- body.linearDamping = options.linearDamping;
129
- if (options.angularDamping !== undefined)
130
- body.angularDamping = options.angularDamping;
131
- if (options.fixedRotation !== undefined)
132
- body.fixedRotation = options.fixedRotation;
133
- body.updateMassProperties();
439
+ const infinitePlane = type === Physics3DType.PLANE && !dims.finite;
440
+ const mass = infinitePlane ? 0 : (bodyParams.mass ?? 1);
441
+ const body = this.createBodyShell(component, mass, context.pose || graphics);
442
+ if (type === Physics3DType.COMPOUND) {
443
+ const parts = bodyParams.parts?.length ? bodyParams.parts : (graphics?.parts || []);
444
+ if (!parts.length) {
445
+ addPlaced(body, Physics3DType.BOX, dims);
446
+ }
447
+ else {
448
+ for (const part of parts) {
449
+ const partType = part.type || part.shape || Physics3DType.BOX;
450
+ if (normalizeType(partType) === Physics3DType.COMPOUND)
451
+ continue;
452
+ const partDims = dimsFromParams(part, dims);
453
+ addPlaced(body, partType, partDims, part);
454
+ }
455
+ }
456
+ }
457
+ else {
458
+ addPlaced(body, type, dims);
459
+ }
460
+ applyBodyOptions(body, bodyParams.bodyOptions);
461
+ component.meshColliderKey = undefined;
462
+ return body;
463
+ }
464
+ createBodyShell(component, mass, graphics) {
465
+ const body = new CANNON.Body({ mass });
466
+ const pos = component.bodyParams.position;
467
+ body.position.set(pos?.x ?? graphics?.positionX ?? 0, pos?.y ?? graphics?.positionY ?? 0, pos?.z ?? graphics?.positionZ ?? 0);
468
+ const rot = component.bodyParams.rotation;
469
+ const rx = rot?.x ?? graphics?.rotationX ?? 0;
470
+ const ry = rot?.y ?? graphics?.rotationY ?? 0;
471
+ const rz = rot?.z ?? graphics?.rotationZ ?? 0;
472
+ if (rx || ry || rz) {
473
+ body.quaternion.setFromEuler(rx, ry, rz);
474
+ }
134
475
  return body;
135
476
  }
136
477
  }
137
478
 
138
479
  class Physics3DEngine {
139
480
  constructor(game, options) {
481
+ this.pending = new Set();
482
+ this.pendingConstraints = new Set();
483
+ this.constraints = new Set();
484
+ this.contactPairs = new Map();
140
485
  this.enabled = false;
141
486
  this.game = game;
142
487
  this.options = options ?? {};
@@ -144,10 +489,8 @@ class Physics3DEngine {
144
489
  }
145
490
  start() {
146
491
  this.world = new CANNON.World();
147
- // Set gravity
148
492
  const gravity = this.options.gravity ?? { x: 0, y: -9.82, z: 0 };
149
493
  this.world.gravity.set(gravity.x, gravity.y, gravity.z);
150
- // Configure solver
151
494
  if (this.options.solver) {
152
495
  const solver = this.world.solver;
153
496
  if (this.options.solver.iterations !== undefined) {
@@ -165,10 +508,14 @@ class Physics3DEngine {
165
508
  update(e) {
166
509
  if (!this.world || !this.enabled)
167
510
  return;
511
+ this.flushPending();
512
+ this.rebuildStaleMeshColliders();
168
513
  const fixedTimeStep = this.options.fixedTimeStep ?? 1 / 60;
169
514
  const maxSubSteps = this.options.maxSubSteps ?? 3;
170
515
  const dt = (e.deltaTime || 16.67) / 1000;
516
+ this.flushConstraints();
171
517
  this.world.step(fixedTimeStep, dt, maxSubSteps);
518
+ this.emitCollisionActive();
172
519
  }
173
520
  stop() {
174
521
  this.enabled = false;
@@ -177,8 +524,16 @@ class Physics3DEngine {
177
524
  this.enabled = true;
178
525
  }
179
526
  destroy() {
527
+ this.pending.clear();
528
+ this.pendingConstraints.clear();
529
+ this.contactPairs.clear();
180
530
  if (this.world) {
181
- // Remove all bodies
531
+ for (const constraint of this.constraints) {
532
+ if (constraint.constraint)
533
+ this.world.removeConstraint(constraint.constraint);
534
+ constraint.constraint = undefined;
535
+ }
536
+ this.constraints.clear();
182
537
  while (this.world.bodies.length > 0) {
183
538
  this.world.removeBody(this.world.bodies[0]);
184
539
  }
@@ -186,32 +541,131 @@ class Physics3DEngine {
186
541
  }
187
542
  }
188
543
  add(component) {
189
- const body = this.bodiesFactory.create(component);
190
- this.world.addBody(body);
191
- // Inject references into component
192
- component.body = body;
193
- component.world = this.world;
194
- // Back-link for collision event lookup
195
- body.component = component;
544
+ const body = this.tryCreate(component);
545
+ if (!body) {
546
+ this.pending.add(component);
547
+ return;
548
+ }
549
+ this.attach(component, body);
196
550
  }
197
551
  change(component) {
198
- // Remove old body, create and add new one
552
+ this.detachConstraintsFor(component);
553
+ this.clearContactsFor(component);
199
554
  if (component.body) {
200
555
  this.world.removeBody(component.body);
556
+ component.body = undefined;
201
557
  }
202
- const newBody = this.bodiesFactory.create(component);
203
- this.world.addBody(newBody);
204
- component.body = newBody;
205
- newBody.component = component;
558
+ this.pending.delete(component);
559
+ this.add(component);
206
560
  }
207
561
  remove(component) {
562
+ this.detachConstraintsFor(component);
563
+ this.clearContactsFor(component);
564
+ this.pending.delete(component);
208
565
  if (component.body) {
209
566
  this.world.removeBody(component.body);
210
567
  component.body = undefined;
211
568
  }
569
+ component.meshColliderKey = undefined;
570
+ }
571
+ addConstraint(component) {
572
+ this.removeConstraint(component);
573
+ this.pendingConstraints.add(component);
574
+ this.flushConstraints();
575
+ }
576
+ removeConstraint(component) {
577
+ this.pendingConstraints.delete(component);
578
+ this.constraints.delete(component);
579
+ if (component.constraint && this.world) {
580
+ this.world.removeConstraint(component.constraint);
581
+ }
582
+ component.constraint = undefined;
583
+ }
584
+ flushConstraints() {
585
+ if (!this.world)
586
+ return;
587
+ for (const component of [...this.pendingConstraints]) {
588
+ if (!component.gameObject) {
589
+ this.pendingConstraints.delete(component);
590
+ continue;
591
+ }
592
+ if (this.tryAttachConstraint(component)) {
593
+ this.pendingConstraints.delete(component);
594
+ }
595
+ }
596
+ }
597
+ tryCreate(component) {
598
+ const context = this.contextFor(component);
599
+ return this.bodiesFactory.create(component, context);
600
+ }
601
+ attach(component, body) {
602
+ this.world.addBody(body);
603
+ component.body = body;
604
+ component.world = this.world;
605
+ body.component = component;
606
+ }
607
+ contextFor(component) {
608
+ const gameObject = component.gameObject;
609
+ const graphics = gameObject?.getComponent('Graphics3D');
610
+ const model = gameObject?.getComponent('Model3D');
611
+ const transform3d = gameObject?.getComponent('Transform3D');
612
+ const renderer = this.game.getSystem(Renderer3DSystem);
613
+ const modelGroup = gameObject ? renderer?.threeContext?.models.get(gameObject.id) : undefined;
614
+ const node = gameObject ? renderer?.threeContext?.object3DFor(gameObject.id) : undefined;
615
+ const scene = renderer?.threeContext?.scene;
616
+ let worldPose;
617
+ if (node?.parent && node.parent !== scene) {
618
+ node.updateWorldMatrix(true, false);
619
+ const position = new Vector3();
620
+ const euler = new Euler();
621
+ node.getWorldPosition(position);
622
+ euler.setFromRotationMatrix(node.matrixWorld);
623
+ worldPose = {
624
+ positionX: position.x,
625
+ positionY: position.y,
626
+ positionZ: position.z,
627
+ rotationX: euler.x,
628
+ rotationY: euler.y,
629
+ rotationZ: euler.z,
630
+ };
631
+ }
632
+ return { graphics, model, modelGroup, pose: worldPose || transform3d || graphics };
633
+ }
634
+ flushPending() {
635
+ if (!this.pending.size)
636
+ return;
637
+ for (const component of [...this.pending]) {
638
+ if (!component.gameObject) {
639
+ this.pending.delete(component);
640
+ continue;
641
+ }
642
+ const body = this.tryCreate(component);
643
+ if (body) {
644
+ this.attach(component, body);
645
+ this.pending.delete(component);
646
+ }
647
+ }
648
+ }
649
+ rebuildStaleMeshColliders() {
650
+ if (!this.world)
651
+ return;
652
+ const stale = [];
653
+ for (const body of this.world.bodies) {
654
+ const component = body.component;
655
+ if (!component?.gameObject)
656
+ continue;
657
+ const model = component.gameObject.getComponent('Model3D');
658
+ const collider = resolveCollider(component.bodyParams, model);
659
+ const key = meshColliderFingerprint(collider, model);
660
+ if (!key || key === component.meshColliderKey)
661
+ continue;
662
+ stale.push(component);
663
+ }
664
+ for (const component of stale) {
665
+ this.change(component);
666
+ }
212
667
  }
213
668
  initCollisionEvents() {
214
- // beginContact: emitted when two bodies start touching
215
669
  this.world.addEventListener('beginContact', (event) => {
216
670
  const bodyA = event.bodyA;
217
671
  const bodyB = event.bodyB;
@@ -220,9 +674,10 @@ class Physics3DEngine {
220
674
  if (componentA && componentB) {
221
675
  componentA.emit('collisionStart', componentB.gameObject, componentA.gameObject);
222
676
  componentB.emit('collisionStart', componentA.gameObject, componentB.gameObject);
677
+ const pair = this.pairKey(bodyA, bodyB);
678
+ this.contactPairs.set(pair, [bodyA.component, bodyB.component]);
223
679
  }
224
680
  });
225
- // endContact: emitted when two bodies stop touching
226
681
  this.world.addEventListener('endContact', (event) => {
227
682
  const bodyA = event.bodyA;
228
683
  const bodyB = event.bodyB;
@@ -232,8 +687,82 @@ class Physics3DEngine {
232
687
  componentA.emit('collisionEnd', componentB.gameObject, componentA.gameObject);
233
688
  componentB.emit('collisionEnd', componentA.gameObject, componentB.gameObject);
234
689
  }
690
+ this.contactPairs.delete(this.pairKey(bodyA, bodyB));
235
691
  });
236
692
  }
693
+ emitCollisionActive() {
694
+ for (const [componentA, componentB] of this.contactPairs.values()) {
695
+ if (!componentA?.gameObject || !componentB?.gameObject)
696
+ continue;
697
+ componentA.emit('collisionActive', componentB.gameObject, componentA.gameObject);
698
+ componentB.emit('collisionActive', componentA.gameObject, componentB.gameObject);
699
+ }
700
+ }
701
+ pairKey(bodyA, bodyB) {
702
+ return bodyA.id < bodyB.id ? `${bodyA.id}:${bodyB.id}` : `${bodyB.id}:${bodyA.id}`;
703
+ }
704
+ clearContactsFor(component) {
705
+ for (const [key, pair] of this.contactPairs) {
706
+ if (pair[0] === component || pair[1] === component) {
707
+ this.contactPairs.delete(key);
708
+ }
709
+ }
710
+ }
711
+ tryAttachConstraint(component) {
712
+ const physicsA = component.gameObject?.getComponent('Physics3D');
713
+ const target = this.game.gameObjects.find((go) => go.name === component.target);
714
+ const physicsB = target?.getComponent('Physics3D');
715
+ if (!physicsA?.body || !physicsB?.body || physicsA.body === physicsB.body)
716
+ return false;
717
+ const constraint = this.createConstraint(component, physicsA.body, physicsB.body);
718
+ constraint.collideConnected = component.collideConnected;
719
+ this.world.addConstraint(constraint);
720
+ component.constraint = constraint;
721
+ this.constraints.add(component);
722
+ return true;
723
+ }
724
+ createConstraint(component, bodyA, bodyB) {
725
+ const maxForce = component.maxForce;
726
+ const pivotA = new CANNON.Vec3(component.pivotAX, component.pivotAY, component.pivotAZ);
727
+ const pivotB = new CANNON.Vec3(component.pivotBX, component.pivotBY, component.pivotBZ);
728
+ const axisA = new CANNON.Vec3(component.axisAX, component.axisAY, component.axisAZ);
729
+ const axisB = new CANNON.Vec3(component.axisBX, component.axisBY, component.axisBZ);
730
+ switch (component.type) {
731
+ case 'point':
732
+ return new CANNON.PointToPointConstraint(bodyA, pivotA, bodyB, pivotB, maxForce);
733
+ case 'lock':
734
+ return new CANNON.LockConstraint(bodyA, bodyB, { maxForce });
735
+ case 'distance':
736
+ return new CANNON.DistanceConstraint(bodyA, bodyB, component.distance, maxForce);
737
+ case 'hinge':
738
+ default: {
739
+ const hinge = new CANNON.HingeConstraint(bodyA, bodyB, {
740
+ pivotA,
741
+ pivotB,
742
+ axisA,
743
+ axisB,
744
+ maxForce,
745
+ collideConnected: component.collideConnected,
746
+ });
747
+ if (component.motor) {
748
+ hinge.enableMotor();
749
+ hinge.setMotorSpeed(component.motorSpeed);
750
+ }
751
+ return hinge;
752
+ }
753
+ }
754
+ }
755
+ detachConstraintsFor(physics) {
756
+ for (const constraint of [...this.constraints]) {
757
+ const physicsA = constraint.gameObject?.getComponent('Physics3D');
758
+ const target = this.game.gameObjects.find((go) => go.name === constraint.target);
759
+ const physicsB = target?.getComponent('Physics3D');
760
+ if (physicsA === physics || physicsB === physics) {
761
+ this.removeConstraint(constraint);
762
+ this.pendingConstraints.add(constraint);
763
+ }
764
+ }
765
+ }
237
766
  }
238
767
 
239
768
  let Physics3DSystem = class Physics3DSystem extends System {
@@ -268,6 +797,7 @@ let Physics3DSystem = class Physics3DSystem extends System {
268
797
  break;
269
798
  }
270
799
  case OBSERVER_TYPE.REMOVE: {
800
+ this.engine.remove(changed.component);
271
801
  break;
272
802
  }
273
803
  }
@@ -311,11 +841,97 @@ Physics3DSystem = __decorate([
311
841
  ], Physics3DSystem);
312
842
  var Physics3DSystem$1 = Physics3DSystem;
313
843
 
844
+ let Physics3DConstraintSystem = class Physics3DConstraintSystem extends System {
845
+ constructor() {
846
+ super(...arguments);
847
+ this.name = 'Physics3DConstraintSystem';
848
+ }
849
+ static { this.systemName = 'Physics3DConstraintSystem'; }
850
+ init() {
851
+ const physics = this.game.getSystem(Physics3DSystem$1);
852
+ if (!physics) {
853
+ console.warn('Physics3DConstraintSystem requires Physics3DSystem');
854
+ }
855
+ }
856
+ update() {
857
+ const engine = this.engine();
858
+ const changes = this.componentObserver.clear();
859
+ for (const changed of changes) {
860
+ this.componentChanged(changed);
861
+ }
862
+ engine?.flushConstraints();
863
+ }
864
+ componentChanged(changed) {
865
+ if (changed.componentName !== 'Physics3DConstraint')
866
+ return;
867
+ const engine = this.engine();
868
+ if (!engine)
869
+ return;
870
+ const component = changed.component;
871
+ if (changed.type === OBSERVER_TYPE.REMOVE) {
872
+ engine.removeConstraint(component);
873
+ return;
874
+ }
875
+ engine.addConstraint(component);
876
+ }
877
+ engine() {
878
+ return this.game.getSystem(Physics3DSystem$1)?.engine;
879
+ }
880
+ };
881
+ Physics3DConstraintSystem = __decorate([
882
+ decorators.componentObserver({
883
+ Physics3DConstraint: [
884
+ 'type', 'target',
885
+ 'pivotAX', 'pivotAY', 'pivotAZ',
886
+ 'pivotBX', 'pivotBY', 'pivotBZ',
887
+ 'axisAX', 'axisAY', 'axisAZ',
888
+ 'axisBX', 'axisBY', 'axisBZ',
889
+ 'distance', 'maxForce', 'collideConnected',
890
+ 'motor', 'motorSpeed',
891
+ ],
892
+ })
893
+ ], Physics3DConstraintSystem);
894
+ var Physics3DConstraintSystem$1 = Physics3DConstraintSystem;
895
+
314
896
  /** Auto-generated by scripts/build-package.mjs — do not edit. */
897
+ Object.assign(Physics3DConstraintSystem$1, {
898
+ packageName: "@combos-fun/plugin-cannon",
899
+ packageVersion: "0.0.44",
900
+ });
315
901
  Object.assign(Physics3DSystem$1, {
316
902
  packageName: "@combos-fun/plugin-cannon",
317
- packageVersion: "0.0.41",
903
+ packageVersion: "0.0.44",
318
904
  });
319
905
 
320
- export { Physics3D, Physics3DSystem$1 as Physics3DSystem, Physics3DType };
906
+ class Physics3DConstraint extends Component {
907
+ constructor() {
908
+ super(...arguments);
909
+ this.type = 'hinge';
910
+ this.target = '';
911
+ this.pivotAX = 0;
912
+ this.pivotAY = 0;
913
+ this.pivotAZ = 0;
914
+ this.pivotBX = 0;
915
+ this.pivotBY = 0;
916
+ this.pivotBZ = 0;
917
+ this.axisAX = 0;
918
+ this.axisAY = 1;
919
+ this.axisAZ = 0;
920
+ this.axisBX = 0;
921
+ this.axisBY = 1;
922
+ this.axisBZ = 0;
923
+ this.distance = 1;
924
+ this.maxForce = 1e6;
925
+ this.collideConnected = true;
926
+ this.motor = false;
927
+ this.motorSpeed = 0;
928
+ }
929
+ static { this.componentName = 'Physics3DConstraint'; }
930
+ init(obj) {
931
+ if (obj)
932
+ Object.assign(this, obj);
933
+ }
934
+ }
935
+
936
+ export { Physics3D, Physics3DConstraint, Physics3DConstraintSystem$1 as Physics3DConstraintSystem, Physics3DSystem$1 as Physics3DSystem, Physics3DType };
321
937
  //# sourceMappingURL=plugin-cannon.esm.js.map