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