@ignifx/physics-2d 0.10.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 +98 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +273 -15
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/skills/physics-2d/SKILL.md +9 -7
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
|
*
|
|
@@ -1102,8 +1129,9 @@ var TilemapCollider2D = class TilemapCollider2D extends Collider2D {
|
|
|
1102
1129
|
* `Tilemap` on the same entity.
|
|
1103
1130
|
*
|
|
1104
1131
|
* @remarks
|
|
1105
|
-
* Reading it works in
|
|
1106
|
-
*
|
|
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.
|
|
1107
1135
|
*
|
|
1108
1136
|
* @returns The data, or `null` when nothing was assigned and the entity has no `Tilemap`.
|
|
1109
1137
|
*/
|
|
@@ -1888,6 +1916,21 @@ function toRadians(degrees) {
|
|
|
1888
1916
|
return degrees * TAU / 360;
|
|
1889
1917
|
}
|
|
1890
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
|
+
/**
|
|
1891
1934
|
* Turns Rapier's radians into the degrees `Transform.rotation2D` uses.
|
|
1892
1935
|
*
|
|
1893
1936
|
* @param radians - The angle in radians.
|
|
@@ -2163,6 +2206,12 @@ var Physics2DRuntime = class {
|
|
|
2163
2206
|
#filterFeet = 0;
|
|
2164
2207
|
#colliderSetChanged = true;
|
|
2165
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();
|
|
2166
2215
|
#disposed = false;
|
|
2167
2216
|
#stepsThisFrame = 0;
|
|
2168
2217
|
#stepMs = 0;
|
|
@@ -2180,6 +2229,7 @@ var Physics2DRuntime = class {
|
|
|
2180
2229
|
this.#settings = options.settings;
|
|
2181
2230
|
this.#world = options.world;
|
|
2182
2231
|
this.#counters = options.counters;
|
|
2232
|
+
this.#isEditMode = options.ctx.app.isEditMode;
|
|
2183
2233
|
this.#queue = createEventQueue();
|
|
2184
2234
|
this.#contacts.push(this.#contactPoint);
|
|
2185
2235
|
setGravity(this.#world, this.#settings.gravity);
|
|
@@ -2204,6 +2254,15 @@ var Physics2DRuntime = class {
|
|
|
2204
2254
|
return this.#stepped;
|
|
2205
2255
|
}
|
|
2206
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
|
+
/**
|
|
2207
2266
|
* Every body, in creation order.
|
|
2208
2267
|
*
|
|
2209
2268
|
* @returns The records.
|
|
@@ -2427,6 +2486,31 @@ var Physics2DRuntime = class {
|
|
|
2427
2486
|
this.#dispatchEvents();
|
|
2428
2487
|
}
|
|
2429
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
|
+
/**
|
|
2430
2514
|
* Writes the interpolated display pose of every interpolated body.
|
|
2431
2515
|
*
|
|
2432
2516
|
* @param alpha - `time.fixedStepAlpha`, in `[0, 1)`.
|
|
@@ -2522,6 +2606,34 @@ var Physics2DRuntime = class {
|
|
|
2522
2606
|
if (record !== void 0 && record.entity.layer !== record.layer) this.markDirty(record.entity);
|
|
2523
2607
|
}
|
|
2524
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
|
+
}
|
|
2525
2637
|
/** Marks every entity whose tilemap collision data changed since its shapes were built. */
|
|
2526
2638
|
#pollTilemaps() {
|
|
2527
2639
|
const colliders = this.#ctx.app.world.components(TilemapCollider2D);
|
|
@@ -2535,10 +2647,43 @@ var Physics2DRuntime = class {
|
|
|
2535
2647
|
if (this.#dirty.size === 0) return;
|
|
2536
2648
|
const pending = [...this.#dirty];
|
|
2537
2649
|
this.#dirty.clear();
|
|
2538
|
-
for (const entity of pending) this.#
|
|
2650
|
+
for (const entity of pending) this.#guarded(entity, () => {
|
|
2651
|
+
this.#rebuildEntity(entity);
|
|
2652
|
+
});
|
|
2539
2653
|
this.#colliderSetChanged = true;
|
|
2540
2654
|
}
|
|
2541
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
|
+
/**
|
|
2542
2687
|
* Tears an entity's body down and builds a new one from its current components.
|
|
2543
2688
|
*
|
|
2544
2689
|
* @param entity - The entity to rebuild.
|
|
@@ -2559,7 +2704,10 @@ var Physics2DRuntime = class {
|
|
|
2559
2704
|
const controller = entity.getComponent(CharacterController2D);
|
|
2560
2705
|
const colliders = collidersOf(entity);
|
|
2561
2706
|
if (colliders.length === 0 && controller === null) return;
|
|
2562
|
-
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
|
+
}
|
|
2563
2711
|
const record = this.#build(entity, entity.getComponent(Rigidbody2D), controller, colliders);
|
|
2564
2712
|
if (record === null) return;
|
|
2565
2713
|
this.#records.push(record);
|
|
@@ -2617,11 +2765,19 @@ var Physics2DRuntime = class {
|
|
|
2617
2765
|
preStepVelocity: new Vec2(),
|
|
2618
2766
|
staticVersion: transform.worldMatrixVersion,
|
|
2619
2767
|
reportedMove: false,
|
|
2768
|
+
placedVersion: transform.worldMatrixVersion,
|
|
2769
|
+
placedScale: transform.lossyScale,
|
|
2620
2770
|
disconnects: []
|
|
2621
2771
|
};
|
|
2622
2772
|
this.#byEntity.set(entity, record);
|
|
2623
|
-
|
|
2624
|
-
|
|
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
|
+
}
|
|
2625
2781
|
if (record.colliders.length === 0) {
|
|
2626
2782
|
this.#byEntity.delete(entity);
|
|
2627
2783
|
destroyBody(this.#world, body);
|
|
@@ -3124,11 +3280,25 @@ var Physics2DRuntime = class {
|
|
|
3124
3280
|
* @returns `true` when interpolation applies.
|
|
3125
3281
|
*/
|
|
3126
3282
|
#shouldInterpolate(rigidbody, controller, motion) {
|
|
3127
|
-
if (!this.#settings.interpolation || motion === BodyMotion2D.static) return false;
|
|
3283
|
+
if (this.#isEditMode || !this.#settings.interpolation || motion === BodyMotion2D.static) return false;
|
|
3128
3284
|
if (controller !== null) return controller.interpolation === "interpolate";
|
|
3129
3285
|
return rigidbody !== null && rigidbody.interpolation === "interpolate";
|
|
3130
3286
|
}
|
|
3131
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() {}
|
|
3132
3302
|
/** How far below a one-way platform's top the character's feet may be and still land on it. */
|
|
3133
3303
|
const ONE_WAY_TOLERANCE = .02;
|
|
3134
3304
|
/**
|
|
@@ -3325,6 +3495,35 @@ var Physics2DInterpolationSystem = class {
|
|
|
3325
3495
|
this.#host.runtime?.interpolate(ctx.time.fixedStepAlpha);
|
|
3326
3496
|
}
|
|
3327
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
|
+
};
|
|
3328
3527
|
//#endregion
|
|
3329
3528
|
//#region src/service.ts
|
|
3330
3529
|
/**
|
|
@@ -3414,7 +3613,7 @@ var Physics2DService = class {
|
|
|
3414
3613
|
* @param maxDistance - How far to travel; defaults to 10 km.
|
|
3415
3614
|
* @param options - Layer mask and trigger behaviour.
|
|
3416
3615
|
* @returns The hit, or `null` when the ray clears everything.
|
|
3417
|
-
* @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.
|
|
3418
3617
|
*/
|
|
3419
3618
|
raycast(origin, direction, maxDistance = DEFAULT_RAY_DISTANCE, options) {
|
|
3420
3619
|
this.#requireStep("raycast");
|
|
@@ -3431,7 +3630,7 @@ var Physics2DService = class {
|
|
|
3431
3630
|
* @param maxDistance - How far to travel; defaults to 10 km.
|
|
3432
3631
|
* @param options - Layer mask and trigger behaviour.
|
|
3433
3632
|
* @returns The hits, in increasing distance. The array is reused between calls.
|
|
3434
|
-
* @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.
|
|
3435
3634
|
*/
|
|
3436
3635
|
raycastAll(origin, direction, maxDistance = DEFAULT_RAY_DISTANCE, options) {
|
|
3437
3636
|
this.#requireStep("raycastAll");
|
|
@@ -3460,7 +3659,7 @@ var Physics2DService = class {
|
|
|
3460
3659
|
* @param radius - Its radius in metres.
|
|
3461
3660
|
* @param options - Layer mask and trigger behaviour.
|
|
3462
3661
|
* @returns The overlapping entities, in Rapier's order. The array is reused between calls.
|
|
3463
|
-
* @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.
|
|
3464
3663
|
*/
|
|
3465
3664
|
overlapCircle(centre, radius, options) {
|
|
3466
3665
|
return this.#overlap({
|
|
@@ -3476,7 +3675,7 @@ var Physics2DService = class {
|
|
|
3476
3675
|
* @param rotation - Its rotation in degrees counter-clockwise; defaults to `0`.
|
|
3477
3676
|
* @param options - Layer mask and trigger behaviour.
|
|
3478
3677
|
* @returns The overlapping entities. The array is reused between calls.
|
|
3479
|
-
* @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.
|
|
3480
3679
|
*/
|
|
3481
3680
|
overlapBox(centre, size, rotation = 0, options) {
|
|
3482
3681
|
return this.#overlap({
|
|
@@ -3494,7 +3693,7 @@ var Physics2DService = class {
|
|
|
3494
3693
|
* @param maxDistance - How far to sweep.
|
|
3495
3694
|
* @param options - Layer mask and trigger behaviour.
|
|
3496
3695
|
* @returns The hit, or `null`.
|
|
3497
|
-
* @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.
|
|
3498
3697
|
*/
|
|
3499
3698
|
shapeCast(centre, radius, direction, maxDistance, options) {
|
|
3500
3699
|
this.#requireStep("shapeCast");
|
|
@@ -3567,13 +3766,68 @@ var Physics2DService = class {
|
|
|
3567
3766
|
return runtime;
|
|
3568
3767
|
}
|
|
3569
3768
|
/**
|
|
3570
|
-
*
|
|
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).
|
|
3571
3820
|
*
|
|
3572
3821
|
* @param query - The query name, for the diagnostic.
|
|
3573
|
-
* @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.
|
|
3574
3823
|
*/
|
|
3575
3824
|
#requireStep(query) {
|
|
3576
|
-
|
|
3825
|
+
const runtime = this.#rt();
|
|
3826
|
+
if (runtime.isEditMode) {
|
|
3827
|
+
runtime.syncEditMode();
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3830
|
+
if (runtime.hasStepped) return;
|
|
3577
3831
|
throw physics2DError(Physics2DErrorCode.queryBeforeStep, `${query}() ran before the first fixed step.`, {
|
|
3578
3832
|
context: { query },
|
|
3579
3833
|
hint: "Step the app at least once (app.step(1 / 60)) before querying; Rapier builds its broadphase in the first step."
|
|
@@ -3734,6 +3988,10 @@ const physics2d = defineExtension((options = {}) => {
|
|
|
3734
3988
|
order: PHYSICS_2D_INTERPOLATE_ORDER,
|
|
3735
3989
|
editMode: "skip"
|
|
3736
3990
|
});
|
|
3991
|
+
if (ctx.app.isEditMode) ctx.registerSystem(new Physics2DEditSyncSystem(host), {
|
|
3992
|
+
phase: Phase.PreRender,
|
|
3993
|
+
order: PHYSICS_2D_EDIT_SYNC_ORDER
|
|
3994
|
+
});
|
|
3737
3995
|
},
|
|
3738
3996
|
async onStart(app) {
|
|
3739
3997
|
const ctx = context;
|