@gg-web-engine/matter 0.0.76 → 0.0.78

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.
@@ -7,6 +7,7 @@ export declare class MatterRigidBodyComponent implements IRigidBody2dComponent<M
7
7
  readonly shape: Shape2DDescriptor;
8
8
  readonly bodyType: BodyType;
9
9
  readonly ccd: boolean;
10
+ readonly canSleep: boolean;
10
11
  get position(): Point2;
11
12
  set position(value: Point2);
12
13
  get rotation(): number;
@@ -43,7 +44,7 @@ export declare class MatterRigidBodyComponent implements IRigidBody2dComponent<M
43
44
  * `notifyCollisionStart`/`notifyCollisionEnd`, called by `MatterWorldComponent`'s single
44
45
  * world-wide `collisionStart`/`collisionEnd` listener - not touched directly by anything else. */
45
46
  protected readonly currentContacts: Set<MatterRigidBodyComponent>;
46
- constructor(nativeBody: Body, shape: Shape2DDescriptor, bodyType?: BodyType, ccd?: boolean);
47
+ constructor(nativeBody: Body, shape: Shape2DDescriptor, bodyType?: BodyType, ccd?: boolean, canSleep?: boolean);
47
48
  /** @internal called by `MatterWorldComponent`'s global `collisionStart` listener - not part of
48
49
  * this component's public API surface. */
49
50
  notifyCollisionStart(collisionEvent: CollisionEvent<Point2, MatterRigidBodyComponent>): void;
@@ -80,7 +81,8 @@ export declare class MatterRigidBodyComponent implements IRigidBody2dComponent<M
80
81
  */
81
82
  wakeUp(): void;
82
83
  /**
83
- * No-op on a body that reports `isStatic` (see `isSleeping`'s own doc). Forces sleep immediately,
84
+ * No-op on a body that reports `isStatic` (see `isSleeping`'s own doc) or was created with
85
+ * `canSleep: false`. Forces sleep immediately,
84
86
  * regardless of whether the world's `Matter.Engine` has `enableSleeping` turned on - unlike a
85
87
  * body naturally falling asleep from inactivity (which requires that engine flag), an explicit
86
88
  * `Sleeping.set(body, true)` call takes effect either way.
@@ -1,5 +1,17 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
1
12
  import { BitMask, DebugBody2DSettings, Pnt2, } from '@gg-web-engine/core';
2
13
  import { Body, Composite, Sleeping, Vector } from 'matter-js';
14
+ import { buildMatterRigidBody } from '../matter-rigid-body-builder';
3
15
  import { Subject } from 'rxjs';
4
16
  // FIXME why this needs to be introduced? investigate units in matter.js
5
17
  const MATTER_VELOCITY_SCALE = 0.0166667;
@@ -47,6 +59,7 @@ export class MatterRigidBodyComponent {
47
59
  friction: this.nativeBody.friction,
48
60
  restitution: this.nativeBody.restitution,
49
61
  ccd: this.ccd,
62
+ canSleep: this.canSleep,
50
63
  ownCollisionGroups: this.ownCollisionGroups,
51
64
  interactWithCollisionGroups: this.interactWithCollisionGroups,
52
65
  };
@@ -57,11 +70,12 @@ export class MatterRigidBodyComponent {
57
70
  get onCollisionEnd() {
58
71
  return this.onCollisionEnd$.asObservable();
59
72
  }
60
- constructor(nativeBody, shape, bodyType = 'dynamic', ccd = false) {
73
+ constructor(nativeBody, shape, bodyType = 'dynamic', ccd = false, canSleep = true) {
61
74
  this.nativeBody = nativeBody;
62
75
  this.shape = shape;
63
76
  this.bodyType = bodyType;
64
77
  this.ccd = ccd;
78
+ this.canSleep = canSleep;
65
79
  this.name = '';
66
80
  this.entity = null;
67
81
  // matter-js has no native kinematic body - a body requested as `kinematic_pos`/`kinematic_vel`
@@ -83,6 +97,11 @@ export class MatterRigidBodyComponent {
83
97
  * world-wide `collisionStart`/`collisionEnd` listener - not touched directly by anything else. */
84
98
  this.currentContacts = new Set();
85
99
  this.updateCollisionFilter();
100
+ if (!canSleep) {
101
+ // this adapter's engine never sleeps bodies on its own, but an app may turn on
102
+ // `engine.enableSleeping` - an infinite threshold keeps this body awake even then
103
+ nativeBody.sleepThreshold = Infinity;
104
+ }
86
105
  }
87
106
  /** @internal called by `MatterWorldComponent`'s global `collisionStart` listener - not part of
88
107
  * this component's public API surface. */
@@ -134,11 +153,15 @@ export class MatterRigidBodyComponent {
134
153
  this.nativeBody.collisionFilter = Object.assign(Object.assign({}, this.nativeBody.collisionFilter), { category: this._ownCGsMask, mask: this._interactWithCGsMask });
135
154
  }
136
155
  clone() {
137
- const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
138
- const component = new MatterRigidBodyComponent(clonedBody, this.shape, this.bodyType, this.ccd);
139
- component.ownCollisionGroups = this.ownCollisionGroups;
140
- component.interactWithCollisionGroups = this.interactWithCollisionGroups;
141
- return component;
156
+ // Rebuilt from the shape and options rather than `Body.create({ ...this.nativeBody })`: a native
157
+ // body references itself (`parts[0]` and `parent` are the body itself), so matter-js's deep
158
+ // option merge recurses forever on it.
159
+ const _a = this.bodyOptions, { mass } = _a, options = __rest(_a, ["mass"]);
160
+ return buildMatterRigidBody({
161
+ shape: this.shape,
162
+ // a static body reports an infinite mass, which isn't a valid option to build one from
163
+ body: this.bodyType === 'dynamic' ? Object.assign(Object.assign({}, options), { mass }) : options,
164
+ }, { position: this.position, rotation: this.rotation });
142
165
  }
143
166
  addToWorld(world) {
144
167
  Composite.add(world.physicsWorld.matterWorld, this.nativeBody);
@@ -199,13 +222,14 @@ export class MatterRigidBodyComponent {
199
222
  Sleeping.set(this.nativeBody, false);
200
223
  }
201
224
  /**
202
- * No-op on a body that reports `isStatic` (see `isSleeping`'s own doc). Forces sleep immediately,
225
+ * No-op on a body that reports `isStatic` (see `isSleeping`'s own doc) or was created with
226
+ * `canSleep: false`. Forces sleep immediately,
203
227
  * regardless of whether the world's `Matter.Engine` has `enableSleeping` turned on - unlike a
204
228
  * body naturally falling asleep from inactivity (which requires that engine flag), an explicit
205
229
  * `Sleeping.set(body, true)` call takes effect either way.
206
230
  */
207
231
  sleep() {
208
- if (this.nativeBody.isStatic) {
232
+ if (this.nativeBody.isStatic || !this.canSleep) {
209
233
  return;
210
234
  }
211
235
  Sleeping.set(this.nativeBody, true);
@@ -1,6 +1,7 @@
1
1
  import { map, merge, Subject } from 'rxjs';
2
2
  import { Body, Detector, Events, Query } from 'matter-js';
3
3
  import { MatterRigidBodyComponent } from './matter-rigid-body.component';
4
+ import { buildMatterTriggerBody } from '../matter-rigid-body-builder';
4
5
  import { MatterCharacterControllerComponent } from './matter-character-controller.component';
5
6
  import { DebugBody2DSettings } from '@gg-web-engine/core';
6
7
  export class MatterTriggerComponent extends MatterRigidBodyComponent {
@@ -230,7 +231,8 @@ export class MatterTriggerComponent extends MatterRigidBodyComponent {
230
231
  }
231
232
  }
232
233
  clone() {
233
- const clonedBody = Body.create(Object.assign(Object.assign({}, this.nativeBody), { isSensor: true, collisionFilter: Object.assign({}, this.nativeBody.collisionFilter) }));
234
+ // rebuilt from the shape, for the same reason as in `MatterRigidBodyComponent.clone()`
235
+ const clonedBody = buildMatterTriggerBody(this.shape, { position: this.position, rotation: this.rotation });
234
236
  const component = new MatterTriggerComponent(clonedBody, this.shape, this.world);
235
237
  component.ownCollisionGroups = this.ownCollisionGroups;
236
238
  component.interactWithCollisionGroups = this.interactWithCollisionGroups;
@@ -19,20 +19,4 @@ export declare class MatterFactory implements IPhysicsBody2dComponentFactory<Mat
19
19
  position?: Point2;
20
20
  rotation?: number;
21
21
  }): MatterCharacterControllerComponent;
22
- private createShapeParts;
23
- private createShapeBody;
24
- /**
25
- * matter-js's own body model only has `isStatic` - no distinct kinematic body type (position-
26
- * driven, but still pushes/wakes dynamic bodies it moves into) and no continuous collision
27
- * detection at all, at any level. Both are long-standing, documented upstream limitations, not
28
- * something this adapter package failed to wire up - see `BodyOptions.kinematic_pos`/
29
- * `ccd`'s own doc in `packages/core` for what each is supposed to do.
30
- *
31
- * `kinematic_pos`/`kinematic_vel` fall back to `isStatic: true` - the closest matter-js has - so
32
- * a caller gets *a* body rather than a thrown error, but with the exact same caveat as
33
- * teleporting a `static` body's position by hand (see that doc): resting bodies on top won't be
34
- * pushed or woken correctly. `ccd: true` is silently accepted as a no-op beyond the warning below -
35
- * a fast-moving or fast-driven body can still tunnel clean through thin geometry in one step.
36
- */
37
- private transformOptions;
38
22
  }
@@ -1,203 +1,20 @@
1
- import { Pnt2, warnOnce, } from '@gg-web-engine/core';
2
- import { MatterRigidBodyComponent } from './components/matter-rigid-body.component';
3
1
  import { MatterTriggerComponent } from './components/matter-trigger.component';
4
2
  import { MatterCharacterControllerComponent } from './components/matter-character-controller.component';
5
- import { Bodies, Body, Common, Vector } from 'matter-js';
6
- import * as decomp from 'poly-decomp';
7
- /**
8
- * `Bodies.fromVertices` (used for the `POLYGON` shape below) only actually decomposes a concave
9
- * vertex set into convex parts when a decomposition library is registered via `Common.setDecomp` -
10
- * without it, matter-js silently falls back to the convex hull of the given vertices, so a concave
11
- * `POLYGON` would render/collide as if it were convex. Registering `poly-decomp` here, once, at
12
- * module load makes `Bodies.fromVertices` actually decompose concave outlines everywhere it's used.
13
- */
14
- Common.setDecomp(decomp);
15
- /**
16
- * `kinematic_pos`/`kinematic_vel`/`ccd` have no native matter-js equivalent at all - unlike
17
- * `packages/ammo`/`packages/rapier2d`/`packages/rapier3d`, this package can only warn and fall
18
- * back rather than actually implement either (see `transformOptions`'s own doc). Warned once per
19
- * distinct message (via core's `warnOnce`) rather than once per body/tick - an app that spawns many
20
- * kinematic props (or requests `ccd` on many fast-moving bodies) would otherwise flood the console
21
- * with an identical warning on every single one, which teaches a developer to ignore the console
22
- * rather than to fix the one call site that needs it.
23
- */
24
- function warnUnsupportedOnce(message) {
25
- warnOnce(`[@gg-web-engine/matter] ${message}`);
26
- }
3
+ import { buildMatterRigidBody, buildMatterTriggerBody } from './matter-rigid-body-builder';
27
4
  export class MatterFactory {
28
5
  constructor(world) {
29
6
  this.world = world;
30
7
  }
31
8
  createRigidBody(descriptor, transform) {
32
- var _a, _b, _c;
33
- let nativeBody = null;
34
- switch (descriptor.shape.shape) {
35
- case 'BOX':
36
- nativeBody = Bodies.rectangle(0, 0, descriptor.shape.dimensions.x, descriptor.shape.dimensions.y, this.transformOptions(descriptor.body));
37
- break;
38
- case 'CIRCLE':
39
- nativeBody = Bodies.circle(0, 0, descriptor.shape.radius, this.transformOptions(descriptor.body));
40
- break;
41
- case 'CAPSULE':
42
- nativeBody = Bodies.rectangle(0, 0, descriptor.shape.radius * 2, descriptor.shape.centersDistance + descriptor.shape.radius * 2, {
43
- chamfer: {
44
- radius: descriptor.shape.radius,
45
- },
46
- });
47
- break;
48
- case 'CONVEX_HULL':
49
- nativeBody = Bodies.fromVertices(0, 0, [Pnt2.hull(descriptor.shape.vertices).map(v => Vector.create(v.x, v.y))], this.transformOptions(descriptor.body));
50
- break;
51
- case 'POLYGON':
52
- nativeBody = Bodies.fromVertices(0, 0, [descriptor.shape.vertices.map(v => Vector.create(v.x, v.y))], this.transformOptions(descriptor.body));
53
- break;
54
- case 'COMPOUND':
55
- nativeBody = this.createShapeBody(descriptor.shape, this.transformOptions(descriptor.body));
56
- break;
57
- }
58
- if (!nativeBody) {
59
- throw new Error(`Shape "${descriptor.shape}" not implemented for Matter.js`);
60
- }
61
- // Must go through `Body.setPosition`/`Body.setAngle` (not a raw `nativeBody.position = ...`
62
- // field assignment) - see `gg-engine-physics-adapter-matter`'s own note on this: a body's
63
- // `vertices`/`bounds` (what every actual collision query reads) are only ever translated to
64
- // match `.position` at creation time, using whatever position was passed into
65
- // `Bodies.rectangle`/`Bodies.circle`/`Bodies.fromVertices` itself (always `(0, 0)` here) - a
66
- // later raw field write changes what `.position` *reports* without moving the real collision
67
- // geometry at all, permanently desyncing the two until something else (ordinary simulation,
68
- // which this character-controller-adjacent code path can't rely on) happens to correct it.
69
- Body.setPosition(nativeBody, Vector.create(((_a = transform === null || transform === void 0 ? void 0 : transform.position) === null || _a === void 0 ? void 0 : _a.x) || 0, ((_b = transform === null || transform === void 0 ? void 0 : transform.position) === null || _b === void 0 ? void 0 : _b.y) || 0));
70
- Body.setAngle(nativeBody, (transform === null || transform === void 0 ? void 0 : transform.rotation) || 0);
71
- const bodyType = (_c = descriptor.body.bodyType) !== null && _c !== void 0 ? _c : (descriptor.body.mass ? 'dynamic' : 'static');
72
- const component = new MatterRigidBodyComponent(nativeBody, descriptor.shape, bodyType, !!descriptor.body.ccd);
73
- // `transformOptions` (used to build `nativeBody` above) only ever reads
74
- // `bodyType`/`mass`/`restitution`/`friction` - `ownCollisionGroups`/`interactWithCollisionGroups`
75
- // must be applied through the component's own setters afterward (same as
76
- // `MatterCharacterControllerComponent`'s constructor already does for a character), or a
77
- // configured collision group is silently dropped in favor of the "all groups" default every
78
- // `MatterRigidBodyComponent` otherwise starts with.
79
- if (descriptor.body.ownCollisionGroups !== undefined) {
80
- component.ownCollisionGroups = descriptor.body.ownCollisionGroups;
81
- }
82
- if (descriptor.body.interactWithCollisionGroups !== undefined) {
83
- component.interactWithCollisionGroups = descriptor.body.interactWithCollisionGroups;
84
- }
85
- return component;
9
+ return buildMatterRigidBody(descriptor, transform);
86
10
  }
87
11
  createTrigger(descriptor, transform) {
88
- var _a, _b;
89
- let nativeBody = null;
90
- switch (descriptor.shape) {
91
- case 'BOX':
92
- nativeBody = Bodies.rectangle(0, 0, descriptor.dimensions.x, descriptor.dimensions.y, { isSensor: true });
93
- break;
94
- case 'CIRCLE':
95
- nativeBody = Bodies.circle(0, 0, descriptor.radius, { isSensor: true });
96
- break;
97
- case 'CAPSULE':
98
- nativeBody = Bodies.rectangle(0, 0, descriptor.radius * 2, descriptor.centersDistance + descriptor.radius * 2, {
99
- isSensor: true,
100
- chamfer: {
101
- radius: descriptor.radius,
102
- },
103
- });
104
- break;
105
- case 'CONVEX_HULL':
106
- nativeBody = Bodies.fromVertices(0, 0, [Pnt2.hull(descriptor.vertices).map(v => Vector.create(v.x, v.y))], {
107
- isSensor: true,
108
- });
109
- break;
110
- case 'POLYGON':
111
- nativeBody = Bodies.fromVertices(0, 0, [descriptor.vertices.map(v => Vector.create(v.x, v.y))], {
112
- isSensor: true,
113
- });
114
- break;
115
- case 'COMPOUND':
116
- nativeBody = this.createShapeBody(descriptor, { isSensor: true });
117
- break;
118
- }
119
- if (!nativeBody) {
120
- throw new Error(`Shape "${descriptor.shape}" not implemented for Matter.js`);
121
- }
122
- // See the identical fix (and its doc) in `createRigidBody` above - a trigger's collision
123
- // geometry needs the same real `Body.setPosition`/`Body.setAngle` treatment, not a raw
124
- // `.position.x`/`.position.y`/`.angle` field write.
125
- Body.setPosition(nativeBody, Vector.create(((_a = transform === null || transform === void 0 ? void 0 : transform.position) === null || _a === void 0 ? void 0 : _a.x) || 0, ((_b = transform === null || transform === void 0 ? void 0 : transform.position) === null || _b === void 0 ? void 0 : _b.y) || 0));
126
- Body.setAngle(nativeBody, (transform === null || transform === void 0 ? void 0 : transform.rotation) || 0);
127
12
  if (!this.world) {
128
13
  throw new Error('MatterFactory: World not set. Make sure the factory is created by MatterWorldComponent.');
129
14
  }
130
- return new MatterTriggerComponent(nativeBody, descriptor, this.world);
15
+ return new MatterTriggerComponent(buildMatterTriggerBody(descriptor, transform), descriptor, this.world);
131
16
  }
132
17
  createCharacterController(options, transform) {
133
18
  return new MatterCharacterControllerComponent(this.world, options, transform);
134
19
  }
135
- createShapeParts(shape, options) {
136
- switch (shape.shape) {
137
- case 'BOX':
138
- return [Bodies.rectangle(0, 0, shape.dimensions.x, shape.dimensions.y, options)];
139
- case 'CIRCLE':
140
- return [Bodies.circle(0, 0, shape.radius, options)];
141
- case 'CAPSULE':
142
- return [
143
- Bodies.rectangle(0, 0, shape.radius * 2, shape.centersDistance + shape.radius * 2, Object.assign(Object.assign({}, options), { chamfer: { radius: shape.radius } })),
144
- ];
145
- case 'CONVEX_HULL':
146
- return [Bodies.fromVertices(0, 0, [Pnt2.hull(shape.vertices).map(v => Vector.create(v.x, v.y))], options)];
147
- case 'POLYGON':
148
- return [Bodies.fromVertices(0, 0, [shape.vertices.map(v => Vector.create(v.x, v.y))], options)];
149
- case 'COMPOUND': {
150
- const parts = [];
151
- for (const { position, rotation, shape: childShape } of shape.children) {
152
- const childParts = this.createShapeParts(childShape, options);
153
- for (const part of childParts) {
154
- Body.setPosition(part, Vector.add(Vector.rotate(part.position, rotation || 0), Vector.create((position === null || position === void 0 ? void 0 : position.x) || 0, (position === null || position === void 0 ? void 0 : position.y) || 0)));
155
- Body.setAngle(part, part.angle + (rotation || 0));
156
- }
157
- parts.push(...childParts);
158
- }
159
- return parts;
160
- }
161
- }
162
- }
163
- createShapeBody(shape, options) {
164
- return Body.create(Object.assign({ parts: this.createShapeParts(shape, options) }, options));
165
- }
166
- /**
167
- * matter-js's own body model only has `isStatic` - no distinct kinematic body type (position-
168
- * driven, but still pushes/wakes dynamic bodies it moves into) and no continuous collision
169
- * detection at all, at any level. Both are long-standing, documented upstream limitations, not
170
- * something this adapter package failed to wire up - see `BodyOptions.kinematic_pos`/
171
- * `ccd`'s own doc in `packages/core` for what each is supposed to do.
172
- *
173
- * `kinematic_pos`/`kinematic_vel` fall back to `isStatic: true` - the closest matter-js has - so
174
- * a caller gets *a* body rather than a thrown error, but with the exact same caveat as
175
- * teleporting a `static` body's position by hand (see that doc): resting bodies on top won't be
176
- * pushed or woken correctly. `ccd: true` is silently accepted as a no-op beyond the warning below -
177
- * a fast-moving or fast-driven body can still tunnel clean through thin geometry in one step.
178
- */
179
- transformOptions(options) {
180
- if (options.bodyType === 'kinematic_pos' || options.bodyType === 'kinematic_vel') {
181
- warnUnsupportedOnce(`bodyType: '${options.bodyType}' has no matter-js equivalent - falling back to a plain static ` +
182
- "body. Resting bodies won't be pushed or woken when you move it, the same as manually " +
183
- "teleporting a static body's position. See Body2DOptions.kinematic_pos/kinematic_vel's doc.");
184
- }
185
- if (options.ccd) {
186
- warnUnsupportedOnce('ccd: true has no matter-js equivalent (no continuous collision detection at all) - ignored. ' +
187
- 'A fast-moving or fast-driven body can still tunnel through thin geometry. See ' +
188
- "Body2DOptions.ccd's doc.");
189
- }
190
- const res = {
191
- isStatic: options.bodyType !== undefined ? options.bodyType != 'dynamic' : !options.mass,
192
- mass: options.mass,
193
- restitution: options.restitution,
194
- friction: options.friction,
195
- };
196
- for (const key in res) {
197
- if (res[key] === undefined) {
198
- delete res[key];
199
- }
200
- }
201
- return res;
202
- }
203
20
  }
@@ -0,0 +1,24 @@
1
+ import { BodyShape2DDescriptor, Point2, Shape2DDescriptor } from '@gg-web-engine/core';
2
+ import { Body, IChamferableBodyDefinition } from 'matter-js';
3
+ import { MatterRigidBodyComponent } from './components/matter-rigid-body.component';
4
+ /**
5
+ * Builds a rigid body for `MatterFactory.createRigidBody`. A plain function because building one
6
+ * never needs the world (it's added to one later, in `addToWorld`), which lets
7
+ * `MatterRigidBodyComponent.clone()` rebuild its body from its own shape and options without a
8
+ * factory instance at hand, and without importing the factory module: that module imports
9
+ * `MatterTriggerComponent`, which extends `MatterRigidBodyComponent`, so importing it from the
10
+ * rigid body's own module is a circular import that breaks the `extends`.
11
+ */
12
+ export declare function buildMatterRigidBody(descriptor: BodyShape2DDescriptor, transform?: {
13
+ position?: Point2;
14
+ rotation?: number;
15
+ }): MatterRigidBodyComponent;
16
+ export declare function createShapeBody(shape: Shape2DDescriptor, options: IChamferableBodyDefinition): Body;
17
+ /**
18
+ * Builds the sensor body of a trigger, for `MatterFactory.createTrigger` and
19
+ * `MatterTriggerComponent.clone()`.
20
+ */
21
+ export declare function buildMatterTriggerBody(shape: Shape2DDescriptor, transform?: {
22
+ position?: Point2;
23
+ rotation?: number;
24
+ }): Body;
@@ -0,0 +1,168 @@
1
+ import { Pnt2, warnOnce, } from '@gg-web-engine/core';
2
+ import { Bodies, Body, Common, Vector } from 'matter-js';
3
+ import * as decomp from 'poly-decomp';
4
+ import { MatterRigidBodyComponent } from './components/matter-rigid-body.component';
5
+ /**
6
+ * `Bodies.fromVertices` (used for the `POLYGON` shape below) only actually decomposes a concave
7
+ * vertex set into convex parts when a decomposition library is registered via `Common.setDecomp` -
8
+ * without it, matter-js silently falls back to the convex hull of the given vertices, so a concave
9
+ * `POLYGON` would render/collide as if it were convex. Registering `poly-decomp` here, once, at
10
+ * module load makes `Bodies.fromVertices` actually decompose concave outlines everywhere it's used.
11
+ */
12
+ Common.setDecomp(decomp);
13
+ /**
14
+ * `kinematic_pos`/`kinematic_vel`/`ccd` have no native matter-js equivalent at all - unlike
15
+ * `packages/ammo`/`packages/rapier2d`/`packages/rapier3d`, this package can only warn and fall
16
+ * back rather than actually implement either (see `transformOptions`'s own doc). Warned once per
17
+ * distinct message (via core's `warnOnce`) rather than once per body/tick - an app that spawns many
18
+ * kinematic props (or requests `ccd` on many fast-moving bodies) would otherwise flood the console
19
+ * with an identical warning on every single one, which teaches a developer to ignore the console
20
+ * rather than to fix the one call site that needs it.
21
+ */
22
+ function warnUnsupportedOnce(message) {
23
+ warnOnce(`[@gg-web-engine/matter] ${message}`);
24
+ }
25
+ /**
26
+ * Builds a rigid body for `MatterFactory.createRigidBody`. A plain function because building one
27
+ * never needs the world (it's added to one later, in `addToWorld`), which lets
28
+ * `MatterRigidBodyComponent.clone()` rebuild its body from its own shape and options without a
29
+ * factory instance at hand, and without importing the factory module: that module imports
30
+ * `MatterTriggerComponent`, which extends `MatterRigidBodyComponent`, so importing it from the
31
+ * rigid body's own module is a circular import that breaks the `extends`.
32
+ */
33
+ export function buildMatterRigidBody(descriptor, transform) {
34
+ var _a, _b, _c;
35
+ let nativeBody = null;
36
+ switch (descriptor.shape.shape) {
37
+ case 'BOX':
38
+ nativeBody = Bodies.rectangle(0, 0, descriptor.shape.dimensions.x, descriptor.shape.dimensions.y, transformOptions(descriptor.body));
39
+ break;
40
+ case 'CIRCLE':
41
+ nativeBody = Bodies.circle(0, 0, descriptor.shape.radius, transformOptions(descriptor.body));
42
+ break;
43
+ case 'CAPSULE':
44
+ nativeBody = Bodies.rectangle(0, 0, descriptor.shape.radius * 2, descriptor.shape.centersDistance + descriptor.shape.radius * 2, Object.assign(Object.assign({}, transformOptions(descriptor.body)), { chamfer: {
45
+ radius: descriptor.shape.radius,
46
+ } }));
47
+ break;
48
+ case 'CONVEX_HULL':
49
+ nativeBody = Bodies.fromVertices(0, 0, [Pnt2.hull(descriptor.shape.vertices).map(v => Vector.create(v.x, v.y))], transformOptions(descriptor.body));
50
+ break;
51
+ case 'POLYGON':
52
+ nativeBody = Bodies.fromVertices(0, 0, [descriptor.shape.vertices.map(v => Vector.create(v.x, v.y))], transformOptions(descriptor.body));
53
+ break;
54
+ case 'COMPOUND':
55
+ nativeBody = createShapeBody(descriptor.shape, transformOptions(descriptor.body));
56
+ break;
57
+ }
58
+ if (!nativeBody) {
59
+ throw new Error(`Shape "${descriptor.shape}" not implemented for Matter.js`);
60
+ }
61
+ // Must go through `Body.setPosition`/`Body.setAngle` (not a raw `nativeBody.position = ...`
62
+ // field assignment) - see `gg-engine-physics-adapter-matter`'s own note on this: a body's
63
+ // `vertices`/`bounds` (what every actual collision query reads) are only ever translated to
64
+ // match `.position` at creation time, using whatever position was passed into
65
+ // `Bodies.rectangle`/`Bodies.circle`/`Bodies.fromVertices` itself (always `(0, 0)` here) - a
66
+ // later raw field write changes what `.position` *reports* without moving the real collision
67
+ // geometry at all, permanently desyncing the two until something else (ordinary simulation,
68
+ // which this character-controller-adjacent code path can't rely on) happens to correct it.
69
+ Body.setPosition(nativeBody, Vector.create(((_a = transform === null || transform === void 0 ? void 0 : transform.position) === null || _a === void 0 ? void 0 : _a.x) || 0, ((_b = transform === null || transform === void 0 ? void 0 : transform.position) === null || _b === void 0 ? void 0 : _b.y) || 0));
70
+ Body.setAngle(nativeBody, (transform === null || transform === void 0 ? void 0 : transform.rotation) || 0);
71
+ const bodyType = (_c = descriptor.body.bodyType) !== null && _c !== void 0 ? _c : (descriptor.body.mass ? 'dynamic' : 'static');
72
+ const component = new MatterRigidBodyComponent(nativeBody, descriptor.shape, bodyType, !!descriptor.body.ccd, descriptor.body.canSleep !== false);
73
+ // `transformOptions` (used to build `nativeBody` above) only ever reads
74
+ // `bodyType`/`mass`/`restitution`/`friction` - `ownCollisionGroups`/`interactWithCollisionGroups`
75
+ // must be applied through the component's own setters afterward (same as
76
+ // `MatterCharacterControllerComponent`'s constructor already does for a character), or a
77
+ // configured collision group is silently dropped in favor of the "all groups" default every
78
+ // `MatterRigidBodyComponent` otherwise starts with.
79
+ if (descriptor.body.ownCollisionGroups !== undefined) {
80
+ component.ownCollisionGroups = descriptor.body.ownCollisionGroups;
81
+ }
82
+ if (descriptor.body.interactWithCollisionGroups !== undefined) {
83
+ component.interactWithCollisionGroups = descriptor.body.interactWithCollisionGroups;
84
+ }
85
+ return component;
86
+ }
87
+ function createShapeParts(shape, options) {
88
+ switch (shape.shape) {
89
+ case 'BOX':
90
+ return [Bodies.rectangle(0, 0, shape.dimensions.x, shape.dimensions.y, options)];
91
+ case 'CIRCLE':
92
+ return [Bodies.circle(0, 0, shape.radius, options)];
93
+ case 'CAPSULE':
94
+ return [
95
+ Bodies.rectangle(0, 0, shape.radius * 2, shape.centersDistance + shape.radius * 2, Object.assign(Object.assign({}, options), { chamfer: { radius: shape.radius } })),
96
+ ];
97
+ case 'CONVEX_HULL':
98
+ return [Bodies.fromVertices(0, 0, [Pnt2.hull(shape.vertices).map(v => Vector.create(v.x, v.y))], options)];
99
+ case 'POLYGON':
100
+ return [Bodies.fromVertices(0, 0, [shape.vertices.map(v => Vector.create(v.x, v.y))], options)];
101
+ case 'COMPOUND': {
102
+ const parts = [];
103
+ for (const { position, rotation, shape: childShape } of shape.children) {
104
+ const childParts = createShapeParts(childShape, options);
105
+ for (const part of childParts) {
106
+ Body.setPosition(part, Vector.add(Vector.rotate(part.position, rotation || 0), Vector.create((position === null || position === void 0 ? void 0 : position.x) || 0, (position === null || position === void 0 ? void 0 : position.y) || 0)));
107
+ Body.setAngle(part, part.angle + (rotation || 0));
108
+ }
109
+ parts.push(...childParts);
110
+ }
111
+ return parts;
112
+ }
113
+ }
114
+ }
115
+ export function createShapeBody(shape, options) {
116
+ return Body.create(Object.assign({ parts: createShapeParts(shape, options) }, options));
117
+ }
118
+ /**
119
+ * Builds the sensor body of a trigger, for `MatterFactory.createTrigger` and
120
+ * `MatterTriggerComponent.clone()`.
121
+ */
122
+ export function buildMatterTriggerBody(shape, transform) {
123
+ var _a, _b;
124
+ const options = { isSensor: true };
125
+ const nativeBody = shape.shape === 'COMPOUND' ? createShapeBody(shape, options) : createShapeParts(shape, options)[0];
126
+ // `Body.setPosition`/`Body.setAngle` rather than raw field writes, for the same reason as in
127
+ // `buildMatterRigidBody` above
128
+ Body.setPosition(nativeBody, Vector.create(((_a = transform === null || transform === void 0 ? void 0 : transform.position) === null || _a === void 0 ? void 0 : _a.x) || 0, ((_b = transform === null || transform === void 0 ? void 0 : transform.position) === null || _b === void 0 ? void 0 : _b.y) || 0));
129
+ Body.setAngle(nativeBody, (transform === null || transform === void 0 ? void 0 : transform.rotation) || 0);
130
+ return nativeBody;
131
+ }
132
+ /**
133
+ * matter-js's own body model only has `isStatic` - no distinct kinematic body type (position-
134
+ * driven, but still pushes/wakes dynamic bodies it moves into) and no continuous collision
135
+ * detection at all, at any level. Both are long-standing, documented upstream limitations, not
136
+ * something this adapter package failed to wire up - see `BodyOptions.kinematic_pos`/
137
+ * `ccd`'s own doc in `packages/core` for what each is supposed to do.
138
+ *
139
+ * `kinematic_pos`/`kinematic_vel` fall back to `isStatic: true` - the closest matter-js has - so
140
+ * a caller gets *a* body rather than a thrown error, but with the exact same caveat as
141
+ * teleporting a `static` body's position by hand (see that doc): resting bodies on top won't be
142
+ * pushed or woken correctly. `ccd: true` is silently accepted as a no-op beyond the warning below -
143
+ * a fast-moving or fast-driven body can still tunnel clean through thin geometry in one step.
144
+ */
145
+ function transformOptions(options) {
146
+ if (options.bodyType === 'kinematic_pos' || options.bodyType === 'kinematic_vel') {
147
+ warnUnsupportedOnce(`bodyType: '${options.bodyType}' has no matter-js equivalent - falling back to a plain static ` +
148
+ "body. Resting bodies won't be pushed or woken when you move it, the same as manually " +
149
+ "teleporting a static body's position. See Body2DOptions.kinematic_pos/kinematic_vel's doc.");
150
+ }
151
+ if (options.ccd) {
152
+ warnUnsupportedOnce('ccd: true has no matter-js equivalent (no continuous collision detection at all) - ignored. ' +
153
+ 'A fast-moving or fast-driven body can still tunnel through thin geometry. See ' +
154
+ "Body2DOptions.ccd's doc.");
155
+ }
156
+ const res = {
157
+ isStatic: options.bodyType !== undefined ? options.bodyType != 'dynamic' : !options.mass,
158
+ mass: options.mass,
159
+ restitution: options.restitution,
160
+ friction: options.friction,
161
+ };
162
+ for (const key in res) {
163
+ if (res[key] === undefined) {
164
+ delete res[key];
165
+ }
166
+ }
167
+ return res;
168
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gg-web-engine/matter",
3
- "version": "0.0.76",
3
+ "version": "0.0.78",
4
4
  "description": "An attempt to create open source game engine for browser",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,7 +30,7 @@
30
30
  "poly-decomp": "^0.3.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@gg-web-engine/core": "0.0.76",
33
+ "@gg-web-engine/core": "0.0.78",
34
34
  "@types/jest": "^30.0.0",
35
35
  "@types/matter-js": "0.20.2",
36
36
  "@types/node": "^26.4.0",
@@ -43,7 +43,7 @@
43
43
  "typescript": "~6.0.3"
44
44
  },
45
45
  "peerDependencies": {
46
- "@gg-web-engine/core": "0.0.76",
46
+ "@gg-web-engine/core": "0.0.78",
47
47
  "matter-js": "0.20.0",
48
48
  "rxjs": "7.8.2"
49
49
  },
@@ -76,4 +76,17 @@ describe('MatterRigidBodyComponent sleep API', () => {
76
76
  floor.wakeUp();
77
77
  expect(floor.isSleeping).toBe(false);
78
78
  });
79
+
80
+ it('a canSleep: false body ignores sleep() and keeps that option through clone()', () => {
81
+ const body = factory.createRigidBody(
82
+ { shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 1, canSleep: false } },
83
+ { position: { x: 0, y: 0 } },
84
+ );
85
+ body.addToWorld({ physicsWorld: world } as any);
86
+
87
+ body.sleep();
88
+ expect(body.isSleeping).toBe(false);
89
+ expect(body.bodyOptions.canSleep).toBe(false);
90
+ expect(body.clone().bodyOptions.canSleep).toBe(false);
91
+ });
79
92
  });
@@ -96,3 +96,108 @@ describe('MatterRigidBodyComponent.rotation', () => {
96
96
  expect(height).toBeCloseTo(2);
97
97
  });
98
98
  });
99
+
100
+ describe('MatterRigidBodyComponent.clone', () => {
101
+ let world: MatterWorldComponent;
102
+ let factory: MatterFactory;
103
+
104
+ beforeEach(async () => {
105
+ if (world) {
106
+ world.dispose();
107
+ }
108
+ world = new MatterWorldComponent();
109
+ factory = new MatterFactory(world);
110
+ await world.init();
111
+ world.gravity = Pnt2.O;
112
+ });
113
+
114
+ afterAll(() => {
115
+ world.dispose();
116
+ });
117
+
118
+ it('copies shape, transform and body options into an independent native body', () => {
119
+ const body = factory.createRigidBody(
120
+ {
121
+ shape: { shape: 'BOX', dimensions: { x: 2, y: 1 } },
122
+ body: { bodyType: 'dynamic', mass: 5, friction: 0.3, restitution: 0.7, canSleep: false },
123
+ },
124
+ { position: { x: 3, y: 4 }, rotation: 0.5 },
125
+ );
126
+ body.ownCollisionGroups = [2];
127
+ body.interactWithCollisionGroups = [3];
128
+
129
+ const clone = body.clone();
130
+
131
+ expect(clone.nativeBody).not.toBe(body.nativeBody);
132
+ expect(clone.shape).toEqual(body.shape);
133
+ expect(clone.position.x).toBeCloseTo(3);
134
+ expect(clone.position.y).toBeCloseTo(4);
135
+ expect(clone.rotation).toBeCloseTo(0.5);
136
+ expect(clone.bodyOptions).toEqual(body.bodyOptions);
137
+
138
+ clone.position = { x: 10, y: 10 };
139
+ expect(body.position.x).toBeCloseTo(3);
140
+ });
141
+
142
+ it('clones a body that is already in a world, and the clone can be added too', () => {
143
+ const body = factory.createRigidBody(
144
+ { shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 1 } },
145
+ { position: { x: 0, y: 0 } },
146
+ );
147
+ body.addToWorld({ physicsWorld: world } as any);
148
+
149
+ const clone = body.clone();
150
+ clone.addToWorld({ physicsWorld: world } as any);
151
+
152
+ expect(world.matterWorld!.bodies).toContain(clone.nativeBody);
153
+ expect(world.matterWorld!.bodies).toContain(body.nativeBody);
154
+ });
155
+
156
+ it('keeps a static body static', () => {
157
+ const floor = factory.createRigidBody(
158
+ { shape: { shape: 'BOX', dimensions: { x: 10, y: 1 } }, body: { bodyType: 'static' } },
159
+ { position: { x: 0, y: 5 } },
160
+ );
161
+
162
+ const clone = floor.clone();
163
+
164
+ expect(clone.nativeBody.isStatic).toBe(true);
165
+ expect(clone.bodyOptions.bodyType).toBe('static');
166
+ });
167
+
168
+ it('clones a compound body', () => {
169
+ const body = factory.createRigidBody(
170
+ {
171
+ shape: {
172
+ shape: 'COMPOUND',
173
+ children: [
174
+ { shape: { shape: 'BOX', dimensions: { x: 1, y: 1 } }, position: { x: -1, y: 0 } },
175
+ { shape: { shape: 'CIRCLE', radius: 0.5 }, position: { x: 1, y: 0 } },
176
+ ],
177
+ },
178
+ body: { bodyType: 'dynamic', mass: 2 },
179
+ },
180
+ { position: { x: 0, y: 0 } },
181
+ );
182
+
183
+ const clone = body.clone();
184
+
185
+ expect(clone.nativeBody.parts.length).toBe(body.nativeBody.parts.length);
186
+ expect(clone.bodyOptions.mass).toBeCloseTo(2);
187
+ });
188
+ });
189
+
190
+ describe('MatterFactory capsule rigid body', () => {
191
+ it('applies body options to a capsule, like every other shape', async () => {
192
+ const world = new MatterWorldComponent();
193
+ await world.init();
194
+ const capsule = new MatterFactory(world).createRigidBody(
195
+ { shape: { shape: 'CAPSULE', radius: 0.5, centersDistance: 1 }, body: { bodyType: 'dynamic', mass: 3, friction: 0.2 } },
196
+ { position: { x: 0, y: 0 } },
197
+ );
198
+
199
+ expect(capsule.nativeBody.mass).toBeCloseTo(3);
200
+ expect(capsule.nativeBody.friction).toBe(0.2);
201
+ world.dispose();
202
+ });
203
+ });
@@ -250,4 +250,46 @@ describe(`MatterTriggerComponent`, () => {
250
250
  await Promise.resolve();
251
251
  expect(exitRegistered).toBe(true);
252
252
  });
253
+
254
+ it(`clones into an independent sensor of the same shape, transform and collision groups`, () => {
255
+ const trigger = factory.createTrigger(
256
+ { shape: 'BOX', dimensions: { x: 10, y: 4 } },
257
+ { position: { x: 3, y: 4 }, rotation: 0.5 },
258
+ );
259
+ trigger.ownCollisionGroups = [2];
260
+ trigger.interactWithCollisionGroups = [3];
261
+ trigger.addToWorld({ physicsWorld: world } as any);
262
+
263
+ const clone = trigger.clone();
264
+
265
+ expect(clone.nativeBody).not.toBe(trigger.nativeBody);
266
+ expect(clone.nativeBody.isSensor).toBe(true);
267
+ expect(clone.nativeBody.isStatic).toBe(true);
268
+ expect(clone.shape).toEqual(trigger.shape);
269
+ expect(clone.position.x).toBeCloseTo(3);
270
+ expect(clone.position.y).toBeCloseTo(4);
271
+ expect(clone.rotation).toBeCloseTo(0.5);
272
+ expect(clone.ownCollisionGroups).toEqual([2]);
273
+ expect(clone.interactWithCollisionGroups).toEqual([3]);
274
+
275
+ clone.addToWorld({ physicsWorld: world } as any);
276
+ expect(world.matterWorld!.bodies).toContain(clone.nativeBody);
277
+ clone.position = { x: 10, y: 10 };
278
+ expect(trigger.position.x).toBeCloseTo(3);
279
+ });
280
+
281
+ it(`clones a compound trigger`, () => {
282
+ const trigger = factory.createTrigger({
283
+ shape: 'COMPOUND',
284
+ children: [
285
+ { shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } } },
286
+ { position: { x: 3, y: 0 }, shape: { shape: 'CIRCLE', radius: 1 } },
287
+ ],
288
+ });
289
+
290
+ const clone = trigger.clone();
291
+
292
+ expect(clone.nativeBody.parts.length).toBe(trigger.nativeBody.parts.length);
293
+ expect(clone.nativeBody.isSensor).toBe(true);
294
+ });
253
295
  });