@ignifx/physics-2d 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +121 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +343 -33
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/skills/physics-2d/SKILL.md +39 -16
package/dist/index.js
CHANGED
|
@@ -80,6 +80,15 @@ var Rigidbody2D = class Rigidbody2D extends Component {
|
|
|
80
80
|
this.rebuild();
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
|
+
* Rebuilds the body after a tool changed fields from outside: an editor's inspector, or a hot
|
|
84
|
+
* reload's new default (`ComponentHooks.onValidate`).
|
|
85
|
+
*
|
|
86
|
+
* @param fields - The fields that changed.
|
|
87
|
+
*/
|
|
88
|
+
onValidate(fields) {
|
|
89
|
+
this.rebuild();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
83
92
|
* Rebuilds the body at the start of the next fixed step. Call it after changing `bodyType`,
|
|
84
93
|
* `mass`, `freezeRotation`, damping, or the entity's scale.
|
|
85
94
|
*/
|
|
@@ -276,6 +285,15 @@ var CharacterController2D = class CharacterController2D extends Component {
|
|
|
276
285
|
onDetach() {
|
|
277
286
|
this.#runtime()?.releaseController(this);
|
|
278
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Rebuilds the body after a tool changed fields from outside: an editor's inspector, or a hot
|
|
290
|
+
* reload's new default (`ComponentHooks.onValidate`).
|
|
291
|
+
*
|
|
292
|
+
* @param fields - The fields that changed.
|
|
293
|
+
*/
|
|
294
|
+
onValidate(fields) {
|
|
295
|
+
this.rebuild();
|
|
296
|
+
}
|
|
279
297
|
/** Rebuilds the capsule or box at the start of the next fixed step. */
|
|
280
298
|
rebuild() {
|
|
281
299
|
this.#runtime()?.markControllerDirty(this);
|
|
@@ -704,6 +722,15 @@ var Collider2D = class extends Component {
|
|
|
704
722
|
this.rebuild();
|
|
705
723
|
}
|
|
706
724
|
/**
|
|
725
|
+
* Rebuilds the body after a tool changed fields from outside: an editor's inspector, or a hot
|
|
726
|
+
* reload's new default (`ComponentHooks.onValidate`).
|
|
727
|
+
*
|
|
728
|
+
* @param fields - The fields that changed.
|
|
729
|
+
*/
|
|
730
|
+
onValidate(fields) {
|
|
731
|
+
this.rebuild();
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
707
734
|
* Rebuilds the entity's body and shapes at the start of the next fixed step. Call it after
|
|
708
735
|
* changing a size, an offset, `isTrigger`, or the entity's scale.
|
|
709
736
|
*
|
|
@@ -1054,18 +1081,29 @@ function degenerate(entity, component, requirement) {
|
|
|
1054
1081
|
//#endregion
|
|
1055
1082
|
//#region src/components/tilemap-collider.ts
|
|
1056
1083
|
/**
|
|
1057
|
-
* Read tile collision data through the optional 2D toolkit's type and rebuild when
|
|
1058
|
-
*
|
|
1059
|
-
*
|
|
1084
|
+
* Read tile collision data through the optional 2D toolkit's type and rebuild when it changes.
|
|
1085
|
+
* The sibling `Tilemap` is found by its registered type id, because `@ignifx/2d` is an optional
|
|
1086
|
+
* peer and must not be imported as a value. Closed polylines preserve concave tile outlines that
|
|
1087
|
+
* convex hulls would fill in. They are thin surfaces, so a body starting inside the map is not
|
|
1088
|
+
* pushed out.
|
|
1060
1089
|
*/
|
|
1090
|
+
/** The type id `@ignifx/2d` registers `Tilemap` under. */
|
|
1091
|
+
const TILEMAP_TYPE_ID = "ignifx/Tilemap";
|
|
1061
1092
|
/**
|
|
1062
1093
|
* The collision surface of a tilemap, as one static body's worth of Rapier shapes.
|
|
1063
1094
|
*
|
|
1095
|
+
* @remarks
|
|
1096
|
+
* The collider reads the `Tilemap` on its own entity, so a scene file that holds both components
|
|
1097
|
+
* has tile collision as soon as it loads. It follows that tilemap: a tile edit, a new map, or a
|
|
1098
|
+
* tilemap added later all reach physics at the next fixed step. Assign
|
|
1099
|
+
* {@link TilemapCollider2D.collisionData} only to use other geometry, such as data you built in
|
|
1100
|
+
* code or a tilemap on another entity.
|
|
1101
|
+
*
|
|
1064
1102
|
* @example
|
|
1065
1103
|
* ```ts
|
|
1066
|
-
* const
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1104
|
+
* const level = world.createEntity("Level");
|
|
1105
|
+
* level.addComponent(Tilemap).map = app.assets.load<TilemapAsset>("level.tilemap.json");
|
|
1106
|
+
* level.addComponent(TilemapCollider2D); // reads the Tilemap beside it
|
|
1069
1107
|
* ```
|
|
1070
1108
|
*
|
|
1071
1109
|
* @public
|
|
@@ -1075,9 +1113,11 @@ var TilemapCollider2D = class TilemapCollider2D extends Collider2D {
|
|
|
1075
1113
|
static typeId = "ignifx/TilemapCollider2D";
|
|
1076
1114
|
/** The serialized field declarations; the geometry itself comes from the tilemap asset. */
|
|
1077
1115
|
static schema = tilemapSchema();
|
|
1078
|
-
/**
|
|
1079
|
-
#
|
|
1080
|
-
/** The
|
|
1116
|
+
/** Geometry that code assigned, which takes the place of the sibling tilemap's. */
|
|
1117
|
+
#override = null;
|
|
1118
|
+
/** The data the current shapes were built from. */
|
|
1119
|
+
#builtData = null;
|
|
1120
|
+
/** The `version` of that data when the shapes were built. */
|
|
1081
1121
|
#builtVersion = -1;
|
|
1082
1122
|
/** Applies the shared defaults. */
|
|
1083
1123
|
constructor() {
|
|
@@ -1085,20 +1125,28 @@ var TilemapCollider2D = class TilemapCollider2D extends Collider2D {
|
|
|
1085
1125
|
Object.assign(this, createDefaults(TilemapCollider2D.schema));
|
|
1086
1126
|
}
|
|
1087
1127
|
/**
|
|
1088
|
-
* The merged chunk geometry this collider builds shapes from
|
|
1128
|
+
* The merged chunk geometry this collider builds shapes from: the assigned data, or else the
|
|
1129
|
+
* `Tilemap` on the same entity.
|
|
1130
|
+
*
|
|
1131
|
+
* @remarks
|
|
1132
|
+
* Reading it works in any app, before any body is built. An edit-mode app also builds the body
|
|
1133
|
+
* without stepping (ADR-0028, amendment of 2026-10-20), so `app.physics2d.colliderOutlines` draws
|
|
1134
|
+
* the same outlines from Rapier.
|
|
1089
1135
|
*
|
|
1090
|
-
* @returns The data, or `null` when
|
|
1136
|
+
* @returns The data, or `null` when nothing was assigned and the entity has no `Tilemap`.
|
|
1091
1137
|
*/
|
|
1092
1138
|
get collisionData() {
|
|
1093
|
-
return this.#
|
|
1139
|
+
return this.#override ?? siblingCollisionData(this);
|
|
1094
1140
|
}
|
|
1095
1141
|
/**
|
|
1096
|
-
*
|
|
1142
|
+
* Uses other geometry instead of the sibling tilemap's, and schedules a rebuild.
|
|
1097
1143
|
*
|
|
1098
|
-
* @param value - The merged chunk geometry, or `null` to
|
|
1144
|
+
* @param value - The merged chunk geometry, or `null` to go back to the `Tilemap` on the same
|
|
1145
|
+
* entity (no shapes when there is none).
|
|
1099
1146
|
*/
|
|
1100
1147
|
set collisionData(value) {
|
|
1101
|
-
this.#
|
|
1148
|
+
this.#override = value;
|
|
1149
|
+
this.#builtData = null;
|
|
1102
1150
|
this.#builtVersion = -1;
|
|
1103
1151
|
this.rebuild();
|
|
1104
1152
|
}
|
|
@@ -1106,12 +1154,19 @@ var TilemapCollider2D = class TilemapCollider2D extends Collider2D {
|
|
|
1106
1154
|
* Whether the supplied data has changed since the shapes were built. The runtime polls this once
|
|
1107
1155
|
* per fixed step, which is how a tile edit reaches physics without a cross-package subscription.
|
|
1108
1156
|
*
|
|
1157
|
+
* @remarks
|
|
1158
|
+
* A disabled collider, or one on an inactive entity, builds no shapes, so it answers `false`
|
|
1159
|
+
* rather than asking for the same empty rebuild every step. Enabling it again finds the data
|
|
1160
|
+
* changed and rebuilds.
|
|
1161
|
+
*
|
|
1109
1162
|
* @returns `true` when a rebuild is due.
|
|
1110
1163
|
*
|
|
1111
1164
|
* @internal
|
|
1112
1165
|
*/
|
|
1113
1166
|
needsRebuild() {
|
|
1114
|
-
|
|
1167
|
+
if (!this.isEnabledInHierarchy) return false;
|
|
1168
|
+
const data = this.collisionData;
|
|
1169
|
+
return data !== this.#builtData || (data?.version ?? -1) !== this.#builtVersion;
|
|
1115
1170
|
}
|
|
1116
1171
|
/**
|
|
1117
1172
|
* Appends one shape per merged outline and one per one-way edge.
|
|
@@ -1122,7 +1177,8 @@ var TilemapCollider2D = class TilemapCollider2D extends Collider2D {
|
|
|
1122
1177
|
* @internal
|
|
1123
1178
|
*/
|
|
1124
1179
|
collectShapes(scale, out) {
|
|
1125
|
-
const data = this
|
|
1180
|
+
const data = this.collisionData;
|
|
1181
|
+
this.#builtData = data;
|
|
1126
1182
|
this.#builtVersion = data?.version ?? -1;
|
|
1127
1183
|
if (data === null) return;
|
|
1128
1184
|
const offset = {
|
|
@@ -1155,6 +1211,27 @@ var TilemapCollider2D = class TilemapCollider2D extends Collider2D {
|
|
|
1155
1211
|
}
|
|
1156
1212
|
};
|
|
1157
1213
|
/**
|
|
1214
|
+
* Reads the collision data of the `Tilemap` on a collider's entity.
|
|
1215
|
+
*
|
|
1216
|
+
* @param collider - The collider.
|
|
1217
|
+
* @returns The tilemap's merged geometry, or `null` when the entity has no `Tilemap` or the 2D
|
|
1218
|
+
* toolkit is not registered.
|
|
1219
|
+
*/
|
|
1220
|
+
function siblingCollisionData(collider) {
|
|
1221
|
+
const type = collider.world.registry.get(TILEMAP_TYPE_ID);
|
|
1222
|
+
const tilemap = type === null ? null : collider.entity.getComponent(type);
|
|
1223
|
+
return tilemap !== null && isTilemap(tilemap) ? tilemap.collisionData : null;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Whether a component registered as `ignifx/Tilemap` has the shape this package reads.
|
|
1227
|
+
*
|
|
1228
|
+
* @param component - The component.
|
|
1229
|
+
* @returns `true` when it carries `collisionData`.
|
|
1230
|
+
*/
|
|
1231
|
+
function isTilemap(component) {
|
|
1232
|
+
return "collisionData" in component;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1158
1235
|
* Builds the `TilemapCollider2D` schema.
|
|
1159
1236
|
*
|
|
1160
1237
|
* @returns The schema.
|
|
@@ -1839,6 +1916,21 @@ function toRadians(degrees) {
|
|
|
1839
1916
|
return degrees * TAU / 360;
|
|
1840
1917
|
}
|
|
1841
1918
|
/**
|
|
1919
|
+
* The outline Rapier's debug renderer draws for some colliders, as world-space line segments
|
|
1920
|
+
* (`pipeline/world.d.ts`, `debugRender`; `DebugRenderBuffers.vertices`: four numbers a segment, the
|
|
1921
|
+
* `x, y` of its two ends). It reads where the colliders were after the last step, or after the
|
|
1922
|
+
* zero-length step an edit-mode app runs instead.
|
|
1923
|
+
*
|
|
1924
|
+
* @param world - The world.
|
|
1925
|
+
* @param handles - The handles of the colliders to draw.
|
|
1926
|
+
* @returns The segments; a copy, because Rapier reuses its buffer.
|
|
1927
|
+
*
|
|
1928
|
+
* @internal
|
|
1929
|
+
*/
|
|
1930
|
+
function colliderLines(world, handles) {
|
|
1931
|
+
return world.debugRender(void 0, (collider) => handles.has(collider.handle)).vertices.slice();
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1842
1934
|
* Turns Rapier's radians into the degrees `Transform.rotation2D` uses.
|
|
1843
1935
|
*
|
|
1844
1936
|
* @param radians - The angle in radians.
|
|
@@ -2114,6 +2206,12 @@ var Physics2DRuntime = class {
|
|
|
2114
2206
|
#filterFeet = 0;
|
|
2115
2207
|
#colliderSetChanged = true;
|
|
2116
2208
|
#stepped = false;
|
|
2209
|
+
/** Whether the app is an editor's scene view, where bodies exist and nothing steps (ADR-0028). */
|
|
2210
|
+
#isEditMode;
|
|
2211
|
+
/** The last failure reported for each entity whose body could not be built, in edit mode. */
|
|
2212
|
+
#failures = /* @__PURE__ */ new WeakMap();
|
|
2213
|
+
/** The entities `IGX-1157` (a body on a child entity) has been logged for, once each. */
|
|
2214
|
+
#warnedChildren = /* @__PURE__ */ new WeakSet();
|
|
2117
2215
|
#disposed = false;
|
|
2118
2216
|
#stepsThisFrame = 0;
|
|
2119
2217
|
#stepMs = 0;
|
|
@@ -2131,6 +2229,7 @@ var Physics2DRuntime = class {
|
|
|
2131
2229
|
this.#settings = options.settings;
|
|
2132
2230
|
this.#world = options.world;
|
|
2133
2231
|
this.#counters = options.counters;
|
|
2232
|
+
this.#isEditMode = options.ctx.app.isEditMode;
|
|
2134
2233
|
this.#queue = createEventQueue();
|
|
2135
2234
|
this.#contacts.push(this.#contactPoint);
|
|
2136
2235
|
setGravity(this.#world, this.#settings.gravity);
|
|
@@ -2155,6 +2254,15 @@ var Physics2DRuntime = class {
|
|
|
2155
2254
|
return this.#stepped;
|
|
2156
2255
|
}
|
|
2157
2256
|
/**
|
|
2257
|
+
* Whether this runtime belongs to an edit-mode app, where bodies are kept in sync with the world
|
|
2258
|
+
* by {@link Physics2DRuntime.syncEditMode} and never stepped (ADR-0028, amendment of 2026-10-20).
|
|
2259
|
+
*
|
|
2260
|
+
* @returns `true` in an editor's scene view.
|
|
2261
|
+
*/
|
|
2262
|
+
get isEditMode() {
|
|
2263
|
+
return this.#isEditMode;
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2158
2266
|
* Every body, in creation order.
|
|
2159
2267
|
*
|
|
2160
2268
|
* @returns The records.
|
|
@@ -2378,6 +2486,31 @@ var Physics2DRuntime = class {
|
|
|
2378
2486
|
this.#dispatchEvents();
|
|
2379
2487
|
}
|
|
2380
2488
|
/**
|
|
2489
|
+
* Brings the bodies in line with the world without stepping, which is all an edit-mode app ever
|
|
2490
|
+
* does with them (ADR-0028, amendment of 2026-10-20): it builds, rebuilds, and removes bodies for
|
|
2491
|
+
* component, layer, and tile changes, and places every body whose transform moved where the
|
|
2492
|
+
* transform now is, so a query sees what the editor shows. The world is never stepped, so nothing
|
|
2493
|
+
* falls, no contact or trigger callback fires, and no pose is written back to a transform.
|
|
2494
|
+
*
|
|
2495
|
+
* @remarks
|
|
2496
|
+
* Rapier refreshes its broadphase only inside a step, so after a change this runs the same
|
|
2497
|
+
* zero-length step a game runs before its first real one (S6.2). With a zero timestep nothing
|
|
2498
|
+
* moves; the events it reports are dropped. A changed scale rebuilds the body's colliders.
|
|
2499
|
+
*/
|
|
2500
|
+
syncEditMode() {
|
|
2501
|
+
if (this.#disposed) return;
|
|
2502
|
+
this.#syncLayers();
|
|
2503
|
+
this.#pollTilemaps();
|
|
2504
|
+
this.#followTransforms();
|
|
2505
|
+
this.#reconcile();
|
|
2506
|
+
if (this.#colliderSetChanged) {
|
|
2507
|
+
primeWorld(this.#world, this.#queue);
|
|
2508
|
+
this.#colliderSetChanged = false;
|
|
2509
|
+
drainCollisions(this.#queue, ignoreEvent);
|
|
2510
|
+
}
|
|
2511
|
+
this.#publishCounters();
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2381
2514
|
* Writes the interpolated display pose of every interpolated body.
|
|
2382
2515
|
*
|
|
2383
2516
|
* @param alpha - `time.fixedStepAlpha`, in `[0, 1)`.
|
|
@@ -2473,6 +2606,34 @@ var Physics2DRuntime = class {
|
|
|
2473
2606
|
if (record !== void 0 && record.entity.layer !== record.layer) this.markDirty(record.entity);
|
|
2474
2607
|
}
|
|
2475
2608
|
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Places every body whose transform changed since it was placed where the transform now is, for
|
|
2611
|
+
* {@link Physics2DRuntime.syncEditMode}. A body whose lossy scale changed is rebuilt instead.
|
|
2612
|
+
*/
|
|
2613
|
+
#followTransforms() {
|
|
2614
|
+
for (let index = 0; index < this.#records.length; index += 1) {
|
|
2615
|
+
const record = this.#records[index];
|
|
2616
|
+
if (record === void 0 || this.#dirty.has(record.entity)) continue;
|
|
2617
|
+
const transform = record.entity.transform;
|
|
2618
|
+
const version = transform.worldMatrixVersion;
|
|
2619
|
+
if (version === record.placedVersion) continue;
|
|
2620
|
+
const scale = transform.lossyScale;
|
|
2621
|
+
if (!sameVector(scale, record.placedScale)) {
|
|
2622
|
+
this.markDirty(record.entity);
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
transform.positionToRef(this.#scratchPose);
|
|
2626
|
+
const radians = toRadians(transform.rotation2D);
|
|
2627
|
+
this.#scratchVector.set(this.#scratchPose.x, this.#scratchPose.y);
|
|
2628
|
+
setBodyPose(record.body, this.#scratchVector, radians);
|
|
2629
|
+
record.currentPosition.copyFrom(this.#scratchVector);
|
|
2630
|
+
record.previousPosition.copyFrom(this.#scratchVector);
|
|
2631
|
+
record.currentRotation = radians;
|
|
2632
|
+
record.previousRotation = radians;
|
|
2633
|
+
record.placedVersion = version;
|
|
2634
|
+
this.#colliderSetChanged = true;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2476
2637
|
/** Marks every entity whose tilemap collision data changed since its shapes were built. */
|
|
2477
2638
|
#pollTilemaps() {
|
|
2478
2639
|
const colliders = this.#ctx.app.world.components(TilemapCollider2D);
|
|
@@ -2486,10 +2647,43 @@ var Physics2DRuntime = class {
|
|
|
2486
2647
|
if (this.#dirty.size === 0) return;
|
|
2487
2648
|
const pending = [...this.#dirty];
|
|
2488
2649
|
this.#dirty.clear();
|
|
2489
|
-
for (const entity of pending) this.#
|
|
2650
|
+
for (const entity of pending) this.#guarded(entity, () => {
|
|
2651
|
+
this.#rebuildEntity(entity);
|
|
2652
|
+
});
|
|
2490
2653
|
this.#colliderSetChanged = true;
|
|
2491
2654
|
}
|
|
2492
2655
|
/**
|
|
2656
|
+
* Runs one entity's rebuild. In an edit-mode app a failure (a collider naming an undeclared
|
|
2657
|
+
* layer, say) is reported through `app.onError` once per entity and message and the other
|
|
2658
|
+
* entities still build, so one broken entity never stops the editor's queries; the entity
|
|
2659
|
+
* builds again when its components change. A game rethrows, as before.
|
|
2660
|
+
*
|
|
2661
|
+
* @param entity - The entity being rebuilt.
|
|
2662
|
+
* @param rebuild - The work.
|
|
2663
|
+
*/
|
|
2664
|
+
#guarded(entity, rebuild) {
|
|
2665
|
+
if (!this.#isEditMode) {
|
|
2666
|
+
rebuild();
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
try {
|
|
2670
|
+
rebuild();
|
|
2671
|
+
this.#failures.delete(entity);
|
|
2672
|
+
} catch (error) {
|
|
2673
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2674
|
+
if (this.#failures.get(entity) !== message) {
|
|
2675
|
+
this.#failures.set(entity, message);
|
|
2676
|
+
this.#ctx.app.onError.emit({
|
|
2677
|
+
error,
|
|
2678
|
+
source: "extension",
|
|
2679
|
+
phase: null,
|
|
2680
|
+
entity,
|
|
2681
|
+
component: null
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2493
2687
|
* Tears an entity's body down and builds a new one from its current components.
|
|
2494
2688
|
*
|
|
2495
2689
|
* @param entity - The entity to rebuild.
|
|
@@ -2510,7 +2704,10 @@ var Physics2DRuntime = class {
|
|
|
2510
2704
|
const controller = entity.getComponent(CharacterController2D);
|
|
2511
2705
|
const colliders = collidersOf(entity);
|
|
2512
2706
|
if (colliders.length === 0 && controller === null) return;
|
|
2513
|
-
if (entity.parent !== null
|
|
2707
|
+
if (entity.parent !== null && !this.#warnedChildren.has(entity)) {
|
|
2708
|
+
this.#warnedChildren.add(entity);
|
|
2709
|
+
this.#ctx.log.warn(`${Physics2DErrorCode.bodyOnChildEntity}: {entity} carries a 2D physics body but is not a root entity; Rapier writes its world pose.`, entity.name);
|
|
2710
|
+
}
|
|
2514
2711
|
const record = this.#build(entity, entity.getComponent(Rigidbody2D), controller, colliders);
|
|
2515
2712
|
if (record === null) return;
|
|
2516
2713
|
this.#records.push(record);
|
|
@@ -2568,11 +2765,19 @@ var Physics2DRuntime = class {
|
|
|
2568
2765
|
preStepVelocity: new Vec2(),
|
|
2569
2766
|
staticVersion: transform.worldMatrixVersion,
|
|
2570
2767
|
reportedMove: false,
|
|
2768
|
+
placedVersion: transform.worldMatrixVersion,
|
|
2769
|
+
placedScale: transform.lossyScale,
|
|
2571
2770
|
disconnects: []
|
|
2572
2771
|
};
|
|
2573
2772
|
this.#byEntity.set(entity, record);
|
|
2574
|
-
|
|
2575
|
-
|
|
2773
|
+
try {
|
|
2774
|
+
this.#attachColliders(record, colliders, layer);
|
|
2775
|
+
if (controller !== null) this.#attachController(record, controller, layer);
|
|
2776
|
+
} catch (error) {
|
|
2777
|
+
this.#teardown(record);
|
|
2778
|
+
this.#byEntity.delete(entity);
|
|
2779
|
+
throw error;
|
|
2780
|
+
}
|
|
2576
2781
|
if (record.colliders.length === 0) {
|
|
2577
2782
|
this.#byEntity.delete(entity);
|
|
2578
2783
|
destroyBody(this.#world, body);
|
|
@@ -3075,11 +3280,25 @@ var Physics2DRuntime = class {
|
|
|
3075
3280
|
* @returns `true` when interpolation applies.
|
|
3076
3281
|
*/
|
|
3077
3282
|
#shouldInterpolate(rigidbody, controller, motion) {
|
|
3078
|
-
if (!this.#settings.interpolation || motion === BodyMotion2D.static) return false;
|
|
3283
|
+
if (this.#isEditMode || !this.#settings.interpolation || motion === BodyMotion2D.static) return false;
|
|
3079
3284
|
if (controller !== null) return controller.interpolation === "interpolate";
|
|
3080
3285
|
return rigidbody !== null && rigidbody.interpolation === "interpolate";
|
|
3081
3286
|
}
|
|
3082
3287
|
};
|
|
3288
|
+
/** How far apart two scales may be and still count as the same, for edit mode's follow pass. */
|
|
3289
|
+
const SCALE_EPSILON = 1e-6;
|
|
3290
|
+
/**
|
|
3291
|
+
* Whether two vectors match to within {@link SCALE_EPSILON}.
|
|
3292
|
+
*
|
|
3293
|
+
* @param first - One vector.
|
|
3294
|
+
* @param second - The other.
|
|
3295
|
+
* @returns `true` when every component matches.
|
|
3296
|
+
*/
|
|
3297
|
+
function sameVector(first, second) {
|
|
3298
|
+
return Math.abs(first.x - second.x) < SCALE_EPSILON && Math.abs(first.y - second.y) < SCALE_EPSILON && Math.abs(first.z - second.z) < SCALE_EPSILON;
|
|
3299
|
+
}
|
|
3300
|
+
/** Drops an event an edit-mode priming step reported; nothing in edit mode delivers one. */
|
|
3301
|
+
function ignoreEvent() {}
|
|
3083
3302
|
/** How far below a one-way platform's top the character's feet may be and still land on it. */
|
|
3084
3303
|
const ONE_WAY_TOLERANCE = .02;
|
|
3085
3304
|
/**
|
|
@@ -3276,6 +3495,35 @@ var Physics2DInterpolationSystem = class {
|
|
|
3276
3495
|
this.#host.runtime?.interpolate(ctx.time.fixedStepAlpha);
|
|
3277
3496
|
}
|
|
3278
3497
|
};
|
|
3498
|
+
/**
|
|
3499
|
+
* Where the edit-mode sync system runs: in `PreRender`, after every script, tween and animation of
|
|
3500
|
+
* the frame has moved what it moves, and before the 2D render sync.
|
|
3501
|
+
*/
|
|
3502
|
+
const PHYSICS_2D_EDIT_SYNC_ORDER = -800;
|
|
3503
|
+
/**
|
|
3504
|
+
* Keeps the bodies of an edit-mode app in line with the world, without stepping
|
|
3505
|
+
* (ADR-0028, amendment of 2026-10-20). It is registered only in an edit-mode app, so a game never
|
|
3506
|
+
* pays for it.
|
|
3507
|
+
*
|
|
3508
|
+
* @internal
|
|
3509
|
+
*/
|
|
3510
|
+
var Physics2DEditSyncSystem = class {
|
|
3511
|
+
/** The name diagnostics and error reports use. */
|
|
3512
|
+
name = "physics2d-edit-sync";
|
|
3513
|
+
#host;
|
|
3514
|
+
/**
|
|
3515
|
+
* Binds the system to the runtime holder.
|
|
3516
|
+
*
|
|
3517
|
+
* @param host - The holder the runtime lands in.
|
|
3518
|
+
*/
|
|
3519
|
+
constructor(host) {
|
|
3520
|
+
this.#host = host;
|
|
3521
|
+
}
|
|
3522
|
+
/** Builds, re-places, and removes bodies for this frame's changes. */
|
|
3523
|
+
update() {
|
|
3524
|
+
this.#host.runtime?.syncEditMode();
|
|
3525
|
+
}
|
|
3526
|
+
};
|
|
3279
3527
|
//#endregion
|
|
3280
3528
|
//#region src/service.ts
|
|
3281
3529
|
/**
|
|
@@ -3365,7 +3613,7 @@ var Physics2DService = class {
|
|
|
3365
3613
|
* @param maxDistance - How far to travel; defaults to 10 km.
|
|
3366
3614
|
* @param options - Layer mask and trigger behaviour.
|
|
3367
3615
|
* @returns The hit, or `null` when the ray clears everything.
|
|
3368
|
-
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet.
|
|
3616
|
+
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet, outside edit mode.
|
|
3369
3617
|
*/
|
|
3370
3618
|
raycast(origin, direction, maxDistance = DEFAULT_RAY_DISTANCE, options) {
|
|
3371
3619
|
this.#requireStep("raycast");
|
|
@@ -3382,7 +3630,7 @@ var Physics2DService = class {
|
|
|
3382
3630
|
* @param maxDistance - How far to travel; defaults to 10 km.
|
|
3383
3631
|
* @param options - Layer mask and trigger behaviour.
|
|
3384
3632
|
* @returns The hits, in increasing distance. The array is reused between calls.
|
|
3385
|
-
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet.
|
|
3633
|
+
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet, outside edit mode.
|
|
3386
3634
|
*/
|
|
3387
3635
|
raycastAll(origin, direction, maxDistance = DEFAULT_RAY_DISTANCE, options) {
|
|
3388
3636
|
this.#requireStep("raycastAll");
|
|
@@ -3411,7 +3659,7 @@ var Physics2DService = class {
|
|
|
3411
3659
|
* @param radius - Its radius in metres.
|
|
3412
3660
|
* @param options - Layer mask and trigger behaviour.
|
|
3413
3661
|
* @returns The overlapping entities, in Rapier's order. The array is reused between calls.
|
|
3414
|
-
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet.
|
|
3662
|
+
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet, outside edit mode.
|
|
3415
3663
|
*/
|
|
3416
3664
|
overlapCircle(centre, radius, options) {
|
|
3417
3665
|
return this.#overlap({
|
|
@@ -3427,7 +3675,7 @@ var Physics2DService = class {
|
|
|
3427
3675
|
* @param rotation - Its rotation in degrees counter-clockwise; defaults to `0`.
|
|
3428
3676
|
* @param options - Layer mask and trigger behaviour.
|
|
3429
3677
|
* @returns The overlapping entities. The array is reused between calls.
|
|
3430
|
-
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet.
|
|
3678
|
+
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet, outside edit mode.
|
|
3431
3679
|
*/
|
|
3432
3680
|
overlapBox(centre, size, rotation = 0, options) {
|
|
3433
3681
|
return this.#overlap({
|
|
@@ -3445,7 +3693,7 @@ var Physics2DService = class {
|
|
|
3445
3693
|
* @param maxDistance - How far to sweep.
|
|
3446
3694
|
* @param options - Layer mask and trigger behaviour.
|
|
3447
3695
|
* @returns The hit, or `null`.
|
|
3448
|
-
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet.
|
|
3696
|
+
* @throws IgnifxError with code `IGX-1153` when no fixed step has run yet, outside edit mode.
|
|
3449
3697
|
*/
|
|
3450
3698
|
shapeCast(centre, radius, direction, maxDistance, options) {
|
|
3451
3699
|
this.#requireStep("shapeCast");
|
|
@@ -3518,13 +3766,68 @@ var Physics2DService = class {
|
|
|
3518
3766
|
return runtime;
|
|
3519
3767
|
}
|
|
3520
3768
|
/**
|
|
3521
|
-
*
|
|
3769
|
+
* The outlines of an entity's 2D colliders, as world-space line segments: what an editor draws as
|
|
3770
|
+
* collider gizmos (ADR-0028, amendment of 2026-10-20).
|
|
3771
|
+
*
|
|
3772
|
+
* @remarks
|
|
3773
|
+
* The segments are Rapier's own debug drawing of the colliders the entity's body is built from,
|
|
3774
|
+
* so a polygon, an edge chain, a capsule, a one-way platform, and every piece of a
|
|
3775
|
+
* `TilemapCollider2D` are outlined exactly. A body exists from the first fixed step in a game,
|
|
3776
|
+
* and from the first frame or query in an edit-mode app, which keeps bodies on the transforms the
|
|
3777
|
+
* editor moves; an entity without one (no enabled collider yet, or before that point) has no
|
|
3778
|
+
* outline. A `CharacterController2D`'s own shape is not a collider and is not outlined.
|
|
3779
|
+
*
|
|
3780
|
+
* @param entity - The entity whose colliders to outline.
|
|
3781
|
+
* @returns One outline per collider that produced a shape, in component order; empty when none.
|
|
3782
|
+
*
|
|
3783
|
+
* @example
|
|
3784
|
+
* ```ts
|
|
3785
|
+
* for (const outline of app.physics2d.colliderOutlines(selected)) {
|
|
3786
|
+
* drawLines2D(outline.lines, outline.isTrigger ? "orange" : "green");
|
|
3787
|
+
* }
|
|
3788
|
+
* ```
|
|
3789
|
+
*/
|
|
3790
|
+
colliderOutlines(entity) {
|
|
3791
|
+
const runtime = this.#host.runtime;
|
|
3792
|
+
const outlines = [];
|
|
3793
|
+
if (runtime === null || entity.isDestroyed) return outlines;
|
|
3794
|
+
if (runtime.isEditMode) runtime.syncEditMode();
|
|
3795
|
+
const record = runtime.recordOf(entity);
|
|
3796
|
+
if (record === null) return outlines;
|
|
3797
|
+
const byOwner = /* @__PURE__ */ new Map();
|
|
3798
|
+
for (let index = 0; index < record.colliders.length; index += 1) {
|
|
3799
|
+
const owner = record.owners[index] ?? null;
|
|
3800
|
+
const collider = record.colliders[index];
|
|
3801
|
+
if (owner === null || collider === void 0) continue;
|
|
3802
|
+
let handles = byOwner.get(owner);
|
|
3803
|
+
if (handles === void 0) {
|
|
3804
|
+
handles = /* @__PURE__ */ new Set();
|
|
3805
|
+
byOwner.set(owner, handles);
|
|
3806
|
+
}
|
|
3807
|
+
handles.add(colliderHandle(collider));
|
|
3808
|
+
}
|
|
3809
|
+
for (const [owner, handles] of byOwner) outlines.push({
|
|
3810
|
+
collider: owner,
|
|
3811
|
+
isTrigger: owner.isTrigger,
|
|
3812
|
+
lines: colliderLines(runtime.world, handles)
|
|
3813
|
+
});
|
|
3814
|
+
return outlines;
|
|
3815
|
+
}
|
|
3816
|
+
/**
|
|
3817
|
+
* Refuses a query that ran before Rapier built its broadphase (spike S6.2). In an edit-mode app,
|
|
3818
|
+
* which never steps, it brings the bodies in line with the world instead, so the query sees what
|
|
3819
|
+
* the editor shows (ADR-0028, amendment of 2026-10-20).
|
|
3522
3820
|
*
|
|
3523
3821
|
* @param query - The query name, for the diagnostic.
|
|
3524
|
-
* @throws IgnifxError with code `IGX-1153` when no step has completed.
|
|
3822
|
+
* @throws IgnifxError with code `IGX-1153` when no step has completed, outside edit mode.
|
|
3525
3823
|
*/
|
|
3526
3824
|
#requireStep(query) {
|
|
3527
|
-
|
|
3825
|
+
const runtime = this.#rt();
|
|
3826
|
+
if (runtime.isEditMode) {
|
|
3827
|
+
runtime.syncEditMode();
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3830
|
+
if (runtime.hasStepped) return;
|
|
3528
3831
|
throw physics2DError(Physics2DErrorCode.queryBeforeStep, `${query}() ran before the first fixed step.`, {
|
|
3529
3832
|
context: { query },
|
|
3530
3833
|
hint: "Step the app at least once (app.step(1 / 60)) before querying; Rapier builds its broadphase in the first step."
|
|
@@ -3672,15 +3975,22 @@ const physics2d = defineExtension((options = {}) => {
|
|
|
3672
3975
|
ctx.defineAppProperty("physics2d", () => service);
|
|
3673
3976
|
ctx.registerSystem(new Physics2DRestoreSystem(host), {
|
|
3674
3977
|
phase: Phase.FixedUpdate,
|
|
3675
|
-
order: PHYSICS_2D_RESTORE_ORDER
|
|
3978
|
+
order: PHYSICS_2D_RESTORE_ORDER,
|
|
3979
|
+
editMode: "skip"
|
|
3676
3980
|
});
|
|
3677
3981
|
ctx.registerSystem(new Physics2DStepSystem(host), {
|
|
3678
3982
|
phase: Phase.FixedUpdate,
|
|
3679
|
-
order: 100
|
|
3983
|
+
order: 100,
|
|
3984
|
+
editMode: "skip"
|
|
3680
3985
|
});
|
|
3681
3986
|
ctx.registerSystem(new Physics2DInterpolationSystem(host), {
|
|
3682
3987
|
phase: Phase.Update,
|
|
3683
|
-
order: PHYSICS_2D_INTERPOLATE_ORDER
|
|
3988
|
+
order: PHYSICS_2D_INTERPOLATE_ORDER,
|
|
3989
|
+
editMode: "skip"
|
|
3990
|
+
});
|
|
3991
|
+
if (ctx.app.isEditMode) ctx.registerSystem(new Physics2DEditSyncSystem(host), {
|
|
3992
|
+
phase: Phase.PreRender,
|
|
3993
|
+
order: PHYSICS_2D_EDIT_SYNC_ORDER
|
|
3684
3994
|
});
|
|
3685
3995
|
},
|
|
3686
3996
|
async onStart(app) {
|