@vworlds/vecs-physics 1.0.16
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/adapter/disposal.d.ts +8 -0
- package/adapter/disposal.js +14 -0
- package/adapter/disposal.js.map +1 -0
- package/adapter/runtime.d.ts +4 -0
- package/adapter/runtime.js +27 -0
- package/adapter/runtime.js.map +1 -0
- package/adapter/state.d.ts +79 -0
- package/adapter/state.js +89 -0
- package/adapter/state.js.map +1 -0
- package/components/body.d.ts +34 -0
- package/components/body.js +53 -0
- package/components/body.js.map +1 -0
- package/components/body_tuning.d.ts +21 -0
- package/components/body_tuning.js +33 -0
- package/components/body_tuning.js.map +1 -0
- package/components/events.d.ts +32 -0
- package/components/events.js +25 -0
- package/components/events.js.map +1 -0
- package/components/force.d.ts +22 -0
- package/components/force.js +35 -0
- package/components/force.js.map +1 -0
- package/components/geometry.d.ts +55 -0
- package/components/geometry.js +74 -0
- package/components/geometry.js.map +1 -0
- package/components/material.d.ts +18 -0
- package/components/material.js +32 -0
- package/components/material.js.map +1 -0
- package/components/shape_pose.d.ts +9 -0
- package/components/shape_pose.js +16 -0
- package/components/shape_pose.js.map +1 -0
- package/components/target.d.ts +13 -0
- package/components/target.js +21 -0
- package/components/target.js.map +1 -0
- package/index.d.ts +10 -0
- package/index.js +23 -0
- package/index.js.map +1 -0
- package/install.d.ts +30 -0
- package/install.js +139 -0
- package/install.js.map +1 -0
- package/package.json +22 -0
- package/systems/debug.d.ts +5 -0
- package/systems/debug.js +266 -0
- package/systems/debug.js.map +1 -0
- package/systems/event_opt_in_sync.d.ts +3 -0
- package/systems/event_opt_in_sync.js +41 -0
- package/systems/event_opt_in_sync.js.map +1 -0
- package/systems/events.d.ts +3 -0
- package/systems/events.js +217 -0
- package/systems/events.js.map +1 -0
- package/systems/forces.d.ts +3 -0
- package/systems/forces.js +26 -0
- package/systems/forces.js.map +1 -0
- package/systems/impulse_accumulate.d.ts +4 -0
- package/systems/impulse_accumulate.js +70 -0
- package/systems/impulse_accumulate.js.map +1 -0
- package/systems/impulse_zero.d.ts +3 -0
- package/systems/impulse_zero.js +25 -0
- package/systems/impulse_zero.js.map +1 -0
- package/systems/index.d.ts +16 -0
- package/systems/index.js +81 -0
- package/systems/index.js.map +1 -0
- package/systems/kinematic.d.ts +3 -0
- package/systems/kinematic.js +88 -0
- package/systems/kinematic.js.map +1 -0
- package/systems/lifecycle.d.ts +3 -0
- package/systems/lifecycle.js +99 -0
- package/systems/lifecycle.js.map +1 -0
- package/systems/mass_recompute.d.ts +3 -0
- package/systems/mass_recompute.js +36 -0
- package/systems/mass_recompute.js.map +1 -0
- package/systems/material_filter_sync.d.ts +3 -0
- package/systems/material_filter_sync.js +64 -0
- package/systems/material_filter_sync.js.map +1 -0
- package/systems/pose_sync_in.d.ts +3 -0
- package/systems/pose_sync_in.js +52 -0
- package/systems/pose_sync_in.js.map +1 -0
- package/systems/pose_sync_out.d.ts +3 -0
- package/systems/pose_sync_out.js +50 -0
- package/systems/pose_sync_out.js.map +1 -0
- package/systems/shape_lifecycle.d.ts +4 -0
- package/systems/shape_lifecycle.js +281 -0
- package/systems/shape_lifecycle.js.map +1 -0
- package/systems/shape_pose_sync.d.ts +3 -0
- package/systems/shape_pose_sync.js +18 -0
- package/systems/shape_pose_sync.js.map +1 -0
- package/systems/step.d.ts +3 -0
- package/systems/step.js +13 -0
- package/systems/step.js.map +1 -0
- package/util/resolve_body.d.ts +6 -0
- package/util/resolve_body.js +16 -0
- package/util/resolve_body.js.map +1 -0
- package/util/warn.d.ts +21 -0
- package/util/warn.js +42 -0
- package/util/warn.js.map +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Physical surface material for a shape (bundles density + surface props). */
|
|
2
|
+
export declare class Material {
|
|
3
|
+
density: number;
|
|
4
|
+
friction: number;
|
|
5
|
+
restitution: number;
|
|
6
|
+
rollingResistance: number;
|
|
7
|
+
tangentSpeed: number;
|
|
8
|
+
userMaterialId: number;
|
|
9
|
+
}
|
|
10
|
+
/** Collision category/mask/group. Missing means category 1, collides with all. */
|
|
11
|
+
export declare class CollisionFilter {
|
|
12
|
+
categoryBits: number;
|
|
13
|
+
maskBits: number;
|
|
14
|
+
groupIndex: number;
|
|
15
|
+
}
|
|
16
|
+
/** Marks a shape as an overlap-only sensor (no collision response). */
|
|
17
|
+
export declare class Sensor {
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Material, collision filtering, and the sensor marker. Defaults match
|
|
2
|
+
// `b2DefaultShapeDef()` / `b2DefaultSurfaceMaterial()` / `b2DefaultFilter()`
|
|
3
|
+
// (see `box2d/src/types.c`). Public API — see `docs/components.md`.
|
|
4
|
+
//
|
|
5
|
+
// Divergence from Box2D: `categoryBits`/`maskBits` are 32-bit and stored as
|
|
6
|
+
// plain `number`, not Box2D's `uint64_t`. 32 categories is plenty, and JS
|
|
7
|
+
// bitwise ops (`&`, `|`) are native 32-bit, so filtering — `(catA & maskB) !==
|
|
8
|
+
// 0` — stays fast and bigint-free. `userMaterialId` is an opaque id (not a
|
|
9
|
+
// bitfield), also a `number`. See PLAN → Settled Decisions.
|
|
10
|
+
/** Physical surface material for a shape (bundles density + surface props). */
|
|
11
|
+
export class Material {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.density = 1;
|
|
14
|
+
this.friction = 0.6;
|
|
15
|
+
this.restitution = 0;
|
|
16
|
+
this.rollingResistance = 0;
|
|
17
|
+
this.tangentSpeed = 0;
|
|
18
|
+
this.userMaterialId = 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Collision category/mask/group. Missing means category 1, collides with all. */
|
|
22
|
+
export class CollisionFilter {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.categoryBits = 1;
|
|
25
|
+
this.maskBits = 0xffffffff;
|
|
26
|
+
this.groupIndex = 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Marks a shape as an overlap-only sensor (no collision response). */
|
|
30
|
+
export class Sensor {
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=material.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"material.js","sourceRoot":"","sources":["../../../../src/components/material.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,6EAA6E;AAC7E,oEAAoE;AACpE,EAAE;AACF,4EAA4E;AAC5E,0EAA0E;AAC1E,+EAA+E;AAC/E,2EAA2E;AAC3E,4DAA4D;AAE5D,+EAA+E;AAC/E,MAAM,OAAO,QAAQ;IAArB;QACE,YAAO,GAAG,CAAC,CAAC;QACZ,aAAQ,GAAG,GAAG,CAAC;QACf,gBAAW,GAAG,CAAC,CAAC;QAChB,sBAAiB,GAAG,CAAC,CAAC;QACtB,iBAAY,GAAG,CAAC,CAAC;QACjB,mBAAc,GAAG,CAAC,CAAC;IACrB,CAAC;CAAA;AAED,kFAAkF;AAClF,MAAM,OAAO,eAAe;IAA5B;QACE,iBAAY,GAAG,CAAC,CAAC;QACjB,aAAQ,GAAG,UAAU,CAAC;QACtB,eAAU,GAAG,CAAC,CAAC;IACjB,CAAC;CAAA;AAED,uEAAuE;AACvE,MAAM,OAAO,MAAM;CAAG"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Shape-local pose components, relative to the immediate parent body. Optional:
|
|
2
|
+
// missing means the identity offset. Public API — see `docs/components.md`.
|
|
3
|
+
/** Local shape offset from the parent body origin. */
|
|
4
|
+
export class ShapePosition {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.x = 0;
|
|
7
|
+
this.y = 0;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Local shape rotation in radians. */
|
|
11
|
+
export class ShapeRotation {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.angle = 0;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=shape_pose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape_pose.js","sourceRoot":"","sources":["../../../../src/components/shape_pose.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,4EAA4E;AAE5E,sDAAsD;AACtD,MAAM,OAAO,aAAa;IAA1B;QACE,MAAC,GAAG,CAAC,CAAC;QACN,MAAC,GAAG,CAAC,CAAC;IACR,CAAC;CAAA;AAED,uCAAuC;AACvC,MAAM,OAAO,aAAa;IAA1B;QACE,UAAK,GAAG,CAAC,CAAC;IACZ,CAAC;CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Target world-space position for a kinematic body. */
|
|
2
|
+
export declare class KinematicTargetPosition {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
tolerance: number;
|
|
6
|
+
wake: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Target world-space rotation (radians) for a kinematic body. */
|
|
9
|
+
export declare class KinematicTargetRotation {
|
|
10
|
+
angle: number;
|
|
11
|
+
tolerance: number;
|
|
12
|
+
wake: boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Kinematic target components. Drive a kinematic body toward a target pose; once
|
|
2
|
+
// within `tolerance` physics leaves the component in place and stops driving.
|
|
3
|
+
// Public API — see `docs/components.md`.
|
|
4
|
+
/** Target world-space position for a kinematic body. */
|
|
5
|
+
export class KinematicTargetPosition {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.x = 0;
|
|
8
|
+
this.y = 0;
|
|
9
|
+
this.tolerance = 0.001;
|
|
10
|
+
this.wake = true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Target world-space rotation (radians) for a kinematic body. */
|
|
14
|
+
export class KinematicTargetRotation {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.angle = 0;
|
|
17
|
+
this.tolerance = 0.001;
|
|
18
|
+
this.wake = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target.js","sourceRoot":"","sources":["../../../../src/components/target.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,8EAA8E;AAC9E,yCAAyC;AAEzC,wDAAwD;AACxD,MAAM,OAAO,uBAAuB;IAApC;QACE,MAAC,GAAG,CAAC,CAAC;QACN,MAAC,GAAG,CAAC,CAAC;QACN,cAAS,GAAG,KAAK,CAAC;QAClB,SAAI,GAAG,IAAI,CAAC;IACd,CAAC;CAAA;AAED,kEAAkE;AAClE,MAAM,OAAO,uBAAuB;IAApC;QACE,UAAK,GAAG,CAAC,CAAC;QACV,cAAS,GAAG,KAAK,CAAC;QAClB,SAAI,GAAG,IAAI,CAAC;IACd,CAAC;CAAA"}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { installPhysics, type PhysicsOptions } from "./install.js";
|
|
2
|
+
export { preloadPhysics } from "./adapter/runtime.js";
|
|
3
|
+
export { Body, BodyType, Position, Rotation, LinearVelocity, AngularVelocity, } from "./components/body.js";
|
|
4
|
+
export { Damping, GravityScale, MotionLocks, Sleep } from "./components/body_tuning.js";
|
|
5
|
+
export { ShapePosition, ShapeRotation } from "./components/shape_pose.js";
|
|
6
|
+
export { Box, Circle, Capsule, Segment, Polygon, Chain, type Vec2 } from "./components/geometry.js";
|
|
7
|
+
export { Material, CollisionFilter, Sensor } from "./components/material.js";
|
|
8
|
+
export { Force, Torque, Impulse, AngularImpulse } from "./components/force.js";
|
|
9
|
+
export { KinematicTargetPosition, KinematicTargetRotation } from "./components/target.js";
|
|
10
|
+
export { ContactEvents, type ContactEvent, SensorEvents, type SensorEvent, HitEvents, type HitEvent, } from "./components/events.js";
|
package/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Public API surface for @vworlds/vecs-physics. Only re-exports — no logic.
|
|
2
|
+
// The stable contract is documented in docs/ and architecture/PLAN.md →
|
|
3
|
+
// Public API Contract.
|
|
4
|
+
// Entry point
|
|
5
|
+
export { installPhysics } from "./install.js";
|
|
6
|
+
export { preloadPhysics } from "./adapter/runtime.js";
|
|
7
|
+
// Body
|
|
8
|
+
export { Body, BodyType, Position, Rotation, LinearVelocity, AngularVelocity, } from "./components/body.js";
|
|
9
|
+
// Body tunables
|
|
10
|
+
export { Damping, GravityScale, MotionLocks, Sleep } from "./components/body_tuning.js";
|
|
11
|
+
// Shape pose
|
|
12
|
+
export { ShapePosition, ShapeRotation } from "./components/shape_pose.js";
|
|
13
|
+
// Geometry
|
|
14
|
+
export { Box, Circle, Capsule, Segment, Polygon, Chain } from "./components/geometry.js";
|
|
15
|
+
// Material / filter / sensor
|
|
16
|
+
export { Material, CollisionFilter, Sensor } from "./components/material.js";
|
|
17
|
+
// Forces & impulses
|
|
18
|
+
export { Force, Torque, Impulse, AngularImpulse } from "./components/force.js";
|
|
19
|
+
// Kinematic targets
|
|
20
|
+
export { KinematicTargetPosition, KinematicTargetRotation } from "./components/target.js";
|
|
21
|
+
// Events
|
|
22
|
+
export { ContactEvents, SensorEvents, HitEvents, } from "./components/events.js";
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
package/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wEAAwE;AACxE,uBAAuB;AAEvB,cAAc;AACd,OAAO,EAAE,cAAc,EAAuB,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO;AACP,OAAO,EACL,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAE9B,gBAAgB;AAChB,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAExF,aAAa;AACb,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE1E,WAAW;AACX,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAa,MAAM,0BAA0B,CAAC;AAEpG,6BAA6B;AAC7B,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAE7E,oBAAoB;AACpB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE/E,oBAAoB;AACpB,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAE1F,SAAS;AACT,OAAO,EACL,aAAa,EAEb,YAAY,EAEZ,SAAS,GAEV,MAAM,wBAAwB,CAAC"}
|
package/install.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type IPhase, type World } from "@vworlds/vecs";
|
|
2
|
+
/** Options for {@link installPhysics}. Every field is optional; omitted fields
|
|
3
|
+
* fall back to Box2D-aligned defaults. */
|
|
4
|
+
export type PhysicsOptions = {
|
|
5
|
+
gravity?: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
};
|
|
9
|
+
fixedTimeStep?: number;
|
|
10
|
+
subSteps?: number;
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
/** Override the warning sink used by `PhysicsDebugValidation` when
|
|
13
|
+
* `debug: true`. Accepts any `(message: string) => void` callback.
|
|
14
|
+
* Primarily for tests; defaults to `console.warn`. */
|
|
15
|
+
debugSink?: (message: string) => void;
|
|
16
|
+
preStepPhase?: IPhase | string;
|
|
17
|
+
stepPhase?: IPhase | string;
|
|
18
|
+
postStepPhase?: IPhase | string;
|
|
19
|
+
restitutionThreshold?: number;
|
|
20
|
+
hitEventThreshold?: number;
|
|
21
|
+
contactHertz?: number;
|
|
22
|
+
contactDampingRatio?: number;
|
|
23
|
+
contactSpeed?: number;
|
|
24
|
+
maximumLinearSpeed?: number;
|
|
25
|
+
enableSleep?: boolean;
|
|
26
|
+
enableContinuous?: boolean;
|
|
27
|
+
};
|
|
28
|
+
/** Wire the physics engine into `world`. Must be called before `world.start()`
|
|
29
|
+
* and at most once per world. */
|
|
30
|
+
export declare function installPhysics(world: World, opts?: PhysicsOptions): void;
|
package/install.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// `installPhysics` — the single entry point that wires the physics engine into a
|
|
2
|
+
// Vecs world. It registers every public component, declares geometry
|
|
3
|
+
// exclusivity, ensures the three physics phases exist, and attaches the private
|
|
4
|
+
// `PhysicsState`. See PLAN.md → _installPhysics Signature_ and
|
|
5
|
+
// _Component registration order_.
|
|
6
|
+
//
|
|
7
|
+
// VP-4 is wiring only: no systems are installed and the engine is a no-op. The
|
|
8
|
+
// physics systems (and the `PhysicsSystemTag` marker they carry) land in VP-6.
|
|
9
|
+
// Registration order still matters — Vecs assigns component type ids in
|
|
10
|
+
// registration order, so keeping the order stable keeps ids deterministic.
|
|
11
|
+
import { AngularVelocity, Body, LinearVelocity, Position, Rotation } from "./components/body.js";
|
|
12
|
+
import { Damping, GravityScale, MotionLocks, Sleep } from "./components/body_tuning.js";
|
|
13
|
+
import { Box, Capsule, Chain, Circle, Polygon, Segment } from "./components/geometry.js";
|
|
14
|
+
import { AngularImpulse, Force, Impulse, Torque } from "./components/force.js";
|
|
15
|
+
import { CollisionFilter, Material, Sensor } from "./components/material.js";
|
|
16
|
+
import { ContactEvents, HitEvents, SensorEvents } from "./components/events.js";
|
|
17
|
+
import { KinematicTargetPosition, KinematicTargetRotation } from "./components/target.js";
|
|
18
|
+
import { ShapePosition, ShapeRotation } from "./components/shape_pose.js";
|
|
19
|
+
import { getBox2D } from "./adapter/runtime.js";
|
|
20
|
+
import { withAllocated } from "./adapter/disposal.js";
|
|
21
|
+
import { attachPhysics, hasPhysics, PhysicsState, registerInternalComponents, } from "./adapter/state.js";
|
|
22
|
+
import { registerImpulseHooks } from "./systems/impulse_accumulate.js";
|
|
23
|
+
import { installPhysicsSystems } from "./systems/index.js";
|
|
24
|
+
const DEFAULT_PRE_STEP_PHASE = "physics-pre";
|
|
25
|
+
const DEFAULT_STEP_PHASE = "physics-step";
|
|
26
|
+
const DEFAULT_POST_STEP_PHASE = "physics-post";
|
|
27
|
+
/** Wire the physics engine into `world`. Must be called before `world.start()`
|
|
28
|
+
* and at most once per world. */
|
|
29
|
+
export function installPhysics(world, opts = {}) {
|
|
30
|
+
if (isWorldStarted(world)) {
|
|
31
|
+
throw new Error("installPhysics must be called before world.start()");
|
|
32
|
+
}
|
|
33
|
+
if (hasPhysics(world)) {
|
|
34
|
+
throw new Error("installPhysics has already been called on this world");
|
|
35
|
+
}
|
|
36
|
+
// 1. Register every public component, in a stable order (see file header).
|
|
37
|
+
// `ChildOf` is built into Vecs — referenced, never registered.
|
|
38
|
+
world.component(Position);
|
|
39
|
+
world.component(Rotation);
|
|
40
|
+
world.component(LinearVelocity);
|
|
41
|
+
world.component(AngularVelocity);
|
|
42
|
+
world.component(Body);
|
|
43
|
+
world.component(Damping);
|
|
44
|
+
world.component(GravityScale);
|
|
45
|
+
world.component(MotionLocks);
|
|
46
|
+
world.component(Sleep);
|
|
47
|
+
world.component(ShapePosition);
|
|
48
|
+
world.component(ShapeRotation);
|
|
49
|
+
world.component(Box);
|
|
50
|
+
world.component(Circle);
|
|
51
|
+
world.component(Capsule);
|
|
52
|
+
world.component(Segment);
|
|
53
|
+
world.component(Polygon);
|
|
54
|
+
world.component(Chain);
|
|
55
|
+
world.component(Material);
|
|
56
|
+
world.component(CollisionFilter);
|
|
57
|
+
world.component(Sensor);
|
|
58
|
+
world.component(Force);
|
|
59
|
+
world.component(Torque);
|
|
60
|
+
world.component(Impulse);
|
|
61
|
+
world.component(AngularImpulse);
|
|
62
|
+
world.component(KinematicTargetPosition);
|
|
63
|
+
world.component(KinematicTargetRotation);
|
|
64
|
+
world.component(ContactEvents);
|
|
65
|
+
world.component(SensorEvents);
|
|
66
|
+
world.component(HitEvents);
|
|
67
|
+
// 2. Internal, non-exported components (`PhysicsRoot`, `PhysicsSystemTag`),
|
|
68
|
+
// registered in their documented order for deterministic type ids.
|
|
69
|
+
registerInternalComponents(world);
|
|
70
|
+
// 3. Geometry components are mutually exclusive: an entity holds at most one.
|
|
71
|
+
world.setExclusiveComponents(Box, Circle, Capsule, Segment, Polygon, Chain);
|
|
72
|
+
// 4. Ensure the three physics phases exist (created if missing).
|
|
73
|
+
const preStep = resolvePhase(world, opts.preStepPhase, DEFAULT_PRE_STEP_PHASE);
|
|
74
|
+
const step = resolvePhase(world, opts.stepPhase, DEFAULT_STEP_PHASE);
|
|
75
|
+
const postStep = resolvePhase(world, opts.postStepPhase, DEFAULT_POST_STEP_PHASE);
|
|
76
|
+
// 5. Allocate WASM state, create the Box2D world, install the systems (bound
|
|
77
|
+
// to it), then attach it.
|
|
78
|
+
const box2d = getBox2D();
|
|
79
|
+
const options = resolveOptions(opts);
|
|
80
|
+
const state = new PhysicsState(box2d, options, opts.debugSink);
|
|
81
|
+
state.worldId = createWorldId(state, options);
|
|
82
|
+
registerImpulseHooks(world, state);
|
|
83
|
+
installPhysicsSystems(world, state, { preStep, step, postStep }, {
|
|
84
|
+
debug: opts.debug ?? false,
|
|
85
|
+
});
|
|
86
|
+
attachPhysics(world, state);
|
|
87
|
+
}
|
|
88
|
+
function createWorldId(state, options) {
|
|
89
|
+
const box2d = state.box2d;
|
|
90
|
+
return withAllocated(box2d.b2DefaultWorldDef, (def) => {
|
|
91
|
+
def.gravity.x = options.gravityX;
|
|
92
|
+
def.gravity.y = options.gravityY;
|
|
93
|
+
def.restitutionThreshold = options.restitutionThreshold;
|
|
94
|
+
def.hitEventThreshold = options.hitEventThreshold;
|
|
95
|
+
def.contactHertz = options.contactHertz;
|
|
96
|
+
def.contactDampingRatio = options.contactDampingRatio;
|
|
97
|
+
def.maximumLinearSpeed = options.maximumLinearSpeed;
|
|
98
|
+
def.enableSleep = options.enableSleep;
|
|
99
|
+
def.enableContinuous = options.enableContinuous;
|
|
100
|
+
const worldId = box2d.b2CreateWorld(def);
|
|
101
|
+
box2d.b2World_SetContactTuning(worldId, options.contactHertz, options.contactDampingRatio, options.contactSpeed);
|
|
102
|
+
return worldId;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Resolve a phase placement option to a concrete phase. An `IPhase` is used
|
|
106
|
+
* as-is; a name (or the default name) is looked up and created if missing. */
|
|
107
|
+
function resolvePhase(world, given, defaultName) {
|
|
108
|
+
if (given !== undefined && typeof given !== "string") {
|
|
109
|
+
return given;
|
|
110
|
+
}
|
|
111
|
+
const name = given ?? defaultName;
|
|
112
|
+
return world._pipeline.get(name) ?? world.addPhase(name);
|
|
113
|
+
}
|
|
114
|
+
/** Apply Box2D-aligned defaults to `opts`. */
|
|
115
|
+
function resolveOptions(opts) {
|
|
116
|
+
return {
|
|
117
|
+
gravityX: opts.gravity?.x ?? 0,
|
|
118
|
+
gravityY: opts.gravity?.y ?? -10,
|
|
119
|
+
fixedTimeStep: opts.fixedTimeStep ?? 1 / 60,
|
|
120
|
+
subSteps: opts.subSteps ?? 4,
|
|
121
|
+
debug: opts.debug ?? false,
|
|
122
|
+
restitutionThreshold: opts.restitutionThreshold ?? 1.0,
|
|
123
|
+
// box2d b2DefaultWorldDef: def.hitEventThreshold = 1.0f * lengthUnits (types.c:16).
|
|
124
|
+
hitEventThreshold: opts.hitEventThreshold ?? 1.0,
|
|
125
|
+
contactHertz: opts.contactHertz ?? 30,
|
|
126
|
+
contactDampingRatio: opts.contactDampingRatio ?? 10,
|
|
127
|
+
// box2d b2DefaultWorldDef: def.contactSpeed = 3.0f * lengthUnits (types.c:18).
|
|
128
|
+
contactSpeed: opts.contactSpeed ?? 3.0,
|
|
129
|
+
maximumLinearSpeed: opts.maximumLinearSpeed ?? 400,
|
|
130
|
+
enableSleep: opts.enableSleep ?? true,
|
|
131
|
+
enableContinuous: opts.enableContinuous ?? true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Whether `world.start()` has already run. Reads the world's internal started
|
|
135
|
+
* flag; there is no public accessor. */
|
|
136
|
+
function isWorldStarted(world) {
|
|
137
|
+
return world._started === true;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=install.js.map
|
package/install.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../../src/install.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vworlds/vecs-physics",
|
|
3
|
+
"version": "1.0.16",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"types": "./index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"import": "./index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/vworlds/vecs.git"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@vworlds/vecs": "1.0.16",
|
|
19
|
+
"box2d3-wasm": "^5.2.0"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type IPhase, type System, type World } from "@vworlds/vecs";
|
|
2
|
+
import { type PhysicsState } from "../adapter/state.js";
|
|
3
|
+
/** Create `PhysicsDebugValidation` on `world` in `phase`.
|
|
4
|
+
* Returns the system so the caller can tag its entity. */
|
|
5
|
+
export declare function installDebugValidation(world: World, state: PhysicsState, phase: IPhase): System;
|
package/systems/debug.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// `PhysicsDebugValidation` — per-step structural validation with warn-once
|
|
2
|
+
// semantics. Registered ONLY when `installPhysics` is called with
|
|
3
|
+
// `{ debug: true }`. In normal mode this system is never installed, and no
|
|
4
|
+
// validation overhead exists.
|
|
5
|
+
//
|
|
6
|
+
// Phase: `physics-pre`, AFTER all other physics-pre systems (last position).
|
|
7
|
+
// This ensures the validator sees the world after lifecycle/sync have run, so
|
|
8
|
+
// ephemeral intermediate states during entity construction are not flagged.
|
|
9
|
+
//
|
|
10
|
+
// Seven issue codes (docs/debug-validation.md):
|
|
11
|
+
// 1. shape-without-parent-body — geometry entity whose immediate parent
|
|
12
|
+
// lacks Body (or has no parent at all).
|
|
13
|
+
// 2. dynamic-body-without-density — dynamic Body with no valid shape child:
|
|
14
|
+
// either no children, or every child shape
|
|
15
|
+
// has Material.density === 0. Sensor shapes
|
|
16
|
+
// with positive density DO contribute mass
|
|
17
|
+
// (Box2D parity: skip only density === 0).
|
|
18
|
+
// 3. chain-too-few-points — Chain.points.length < 4.
|
|
19
|
+
// 4. polygon-invalid — Polygon.vertices fail validateHull.
|
|
20
|
+
// 5. shape-position-on-body — entity has Body AND ShapePosition or
|
|
21
|
+
// ShapeRotation.
|
|
22
|
+
// 6. position-on-shape-entity — entity has a geometry component AND
|
|
23
|
+
// Position.
|
|
24
|
+
// 7. force-without-target — force/impulse entity has no Body and no
|
|
25
|
+
// immediate Body parent.
|
|
26
|
+
//
|
|
27
|
+
// Re-arm semantics: when a (eid, code) pair that was previously warned becomes
|
|
28
|
+
// valid for that code (issue cleared), we call rearm() so a future relapse can
|
|
29
|
+
// warn again. The WarnDedupe key is `(eid, code)`, so each code is independent.
|
|
30
|
+
//
|
|
31
|
+
// Design points:
|
|
32
|
+
// - Uses world.filter() scans (non-reactive) — same pattern as ForceGather.
|
|
33
|
+
// An entity enters the relevant filter when it carries the matching
|
|
34
|
+
// component(s), and the validator re-checks it every step.
|
|
35
|
+
// - WarnDedupe instance is owned by PhysicsState (survives across systems).
|
|
36
|
+
// - The injectable WarnSink in WarnDedupe is the test hook; pass it via
|
|
37
|
+
// PhysicsOptions.debugSink.
|
|
38
|
+
// - Does NOT modify any engine state — read-only pass.
|
|
39
|
+
//
|
|
40
|
+
// Upstream docs: docs/debug-validation.md.
|
|
41
|
+
// Re-uses: validateHull / computeHull from engine/geometry.ts.
|
|
42
|
+
import { ChildOf, } from "@vworlds/vecs";
|
|
43
|
+
import { Body, BodyType, Position } from "../components/body.js";
|
|
44
|
+
import { AngularImpulse, Force, Impulse, Torque } from "../components/force.js";
|
|
45
|
+
import { Box, Capsule, Chain, Circle, Polygon, Segment } from "../components/geometry.js";
|
|
46
|
+
import { Material } from "../components/material.js";
|
|
47
|
+
import { ShapePosition, ShapeRotation } from "../components/shape_pose.js";
|
|
48
|
+
// ─── Issue codes ──────────────────────────────────────────────────────────────
|
|
49
|
+
const CODE_SHAPE_WITHOUT_PARENT_BODY = "shape-without-parent-body";
|
|
50
|
+
const CODE_DYNAMIC_BODY_WITHOUT_DENSITY = "dynamic-body-without-density";
|
|
51
|
+
const CODE_CHAIN_TOO_FEW_POINTS = "chain-too-few-points";
|
|
52
|
+
const CODE_POLYGON_INVALID = "polygon-invalid";
|
|
53
|
+
const CODE_SHAPE_POSITION_ON_BODY = "shape-position-on-body";
|
|
54
|
+
const CODE_POSITION_ON_SHAPE_ENTITY = "position-on-shape-entity";
|
|
55
|
+
const CODE_FORCE_WITHOUT_TARGET = "force-without-target";
|
|
56
|
+
// ─── System factory ───────────────────────────────────────────────────────────
|
|
57
|
+
/** Create `PhysicsDebugValidation` on `world` in `phase`.
|
|
58
|
+
* Returns the system so the caller can tag its entity. */
|
|
59
|
+
export function installDebugValidation(world, state, phase) {
|
|
60
|
+
// ── Filters (non-reactive; walked every step) ─────────────────────────────
|
|
61
|
+
// Geometry entities: any entity with at least one geometry component.
|
|
62
|
+
const geomFilter = world.filter({ any: [Box, Circle, Capsule, Segment, Polygon, Chain] });
|
|
63
|
+
// Body entities: any entity with Body.
|
|
64
|
+
const bodyFilter = world.filter(Body);
|
|
65
|
+
// Force-class entities: any entity with a force, torque, impulse, or angular impulse.
|
|
66
|
+
const forceFilter = world.filter({ any: [Force, Torque, Impulse, AngularImpulse] });
|
|
67
|
+
// Body entities that also carry ShapePosition or ShapeRotation.
|
|
68
|
+
const bodyPoseFilter = world.filter({
|
|
69
|
+
all: [Body, { any: [ShapePosition, ShapeRotation] }],
|
|
70
|
+
});
|
|
71
|
+
// Track body-pose entities seen in the previous step so we can rearm those
|
|
72
|
+
// that leave the filter (ShapePosition/ShapeRotation removed). Re-entry into
|
|
73
|
+
// the filter will then produce a fresh warning, completing the re-arm cycle.
|
|
74
|
+
const prevBodyPoseEids = new Set();
|
|
75
|
+
return world
|
|
76
|
+
.system("PhysicsDebugValidation")
|
|
77
|
+
.phase(phase)
|
|
78
|
+
.run(() => {
|
|
79
|
+
_validateGeomEntities(state, geomFilter, state.warn);
|
|
80
|
+
_validateBodyEntities(geomFilter, bodyFilter, state.warn);
|
|
81
|
+
_validateBodyShapePositionEntities(bodyPoseFilter, prevBodyPoseEids, state.warn);
|
|
82
|
+
_validateForceEntities(forceFilter, state.warn);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// ─── Geometry-entity validation ───────────────────────────────────────────────
|
|
86
|
+
/** Walk every entity with a geometry component and check:
|
|
87
|
+
* - shape-without-parent-body (code 1)
|
|
88
|
+
* - chain-too-few-points (code 3)
|
|
89
|
+
* - polygon-invalid (code 4)
|
|
90
|
+
* - position-on-shape-entity (code 6) */
|
|
91
|
+
function _validateGeomEntities(state, geomFilter, dedupe) {
|
|
92
|
+
geomFilter.forEach((e) => {
|
|
93
|
+
const eid = e.eid;
|
|
94
|
+
// ── shape-without-parent-body (code 1) ───────────────────────────────────
|
|
95
|
+
const parent = e.parent(ChildOf);
|
|
96
|
+
const parentHasBody = parent !== undefined && parent.get(Body) !== undefined;
|
|
97
|
+
if (!parentHasBody) {
|
|
98
|
+
dedupe.warnOnce(eid, CODE_SHAPE_WITHOUT_PARENT_BODY, `Entity${eid} has geometry but its immediate parent does not have Body.`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
dedupe.rearm(eid, CODE_SHAPE_WITHOUT_PARENT_BODY);
|
|
102
|
+
}
|
|
103
|
+
// ── chain-too-few-points (code 3) ────────────────────────────────────────
|
|
104
|
+
const chain = e.get(Chain);
|
|
105
|
+
if (chain !== undefined) {
|
|
106
|
+
if (chain.points.length < 4) {
|
|
107
|
+
dedupe.warnOnce(eid, CODE_CHAIN_TOO_FEW_POINTS, `Entity${eid} has Chain with ${chain.points.length} point(s); at least 4 are required.`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
dedupe.rearm(eid, CODE_CHAIN_TOO_FEW_POINTS);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ── polygon-invalid (code 4) ─────────────────────────────────────────────
|
|
114
|
+
const polygon = e.get(Polygon);
|
|
115
|
+
if (polygon !== undefined) {
|
|
116
|
+
const valid = isValidPolygon(state, polygon.vertices);
|
|
117
|
+
if (!valid) {
|
|
118
|
+
dedupe.warnOnce(eid, CODE_POLYGON_INVALID, `Entity${eid} has Polygon with invalid or non-convex vertices.`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
dedupe.rearm(eid, CODE_POLYGON_INVALID);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ── position-on-shape-entity (code 6) ────────────────────────────────────
|
|
125
|
+
const hasPosition = e.get(Position) !== undefined;
|
|
126
|
+
if (hasPosition) {
|
|
127
|
+
dedupe.warnOnce(eid, CODE_POSITION_ON_SHAPE_ENTITY, `Entity${eid} has a geometry component and Position; use ShapePosition for shape-local offsets.`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
dedupe.rearm(eid, CODE_POSITION_ON_SHAPE_ENTITY);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// ─── Body-entity validation ───────────────────────────────────────────────────
|
|
135
|
+
/** Walk every entity with a Body component and check:
|
|
136
|
+
* - dynamic-body-without-density (code 2)
|
|
137
|
+
*
|
|
138
|
+
* A dynamic body must have at least one child shape with density > 0. Sensor
|
|
139
|
+
* shapes count — Box2D parity skips only density === 0, not isSensor shapes.
|
|
140
|
+
*
|
|
141
|
+
* To find child shapes without engine-state access, we build a map of
|
|
142
|
+
* body-eid → geometry child entities by walking the geometry filter and checking
|
|
143
|
+
* which have a direct Body parent. This is a read-only ECS walk with no
|
|
144
|
+
* dependency on PhysicsState internals. */
|
|
145
|
+
function _validateBodyEntities(geomFilter, bodyFilter, dedupe) {
|
|
146
|
+
// Build a map: body-eid → list of geometry child entities (per this step).
|
|
147
|
+
const geomChildrenByBody = new Map();
|
|
148
|
+
geomFilter.forEach((e) => {
|
|
149
|
+
const parent = e.parent(ChildOf);
|
|
150
|
+
if (parent === undefined || parent.get(Body) === undefined) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const bodyEid = parent.eid;
|
|
154
|
+
let list = geomChildrenByBody.get(bodyEid);
|
|
155
|
+
if (list === undefined) {
|
|
156
|
+
list = [];
|
|
157
|
+
geomChildrenByBody.set(bodyEid, list);
|
|
158
|
+
}
|
|
159
|
+
list.push(e);
|
|
160
|
+
});
|
|
161
|
+
// Walk every body entity and check dynamic-body-without-density.
|
|
162
|
+
bodyFilter.forEach((e) => {
|
|
163
|
+
const eid = e.eid;
|
|
164
|
+
const body = e.get(Body);
|
|
165
|
+
if (body.type !== BodyType.Dynamic) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const children = geomChildrenByBody.get(eid);
|
|
169
|
+
// A dynamic body with no geometry children always warns.
|
|
170
|
+
if (children === undefined || children.length === 0) {
|
|
171
|
+
dedupe.warnOnce(eid, CODE_DYNAMIC_BODY_WITHOUT_DENSITY, `Entity${eid} is a dynamic body with no shape children; it will have zero mass.`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Check if ANY child shape has density > 0.
|
|
175
|
+
// Sensor shapes with positive density DO contribute mass (Box2D parity:
|
|
176
|
+
// b2UpdateBodyMassData skips only density === 0, not isSensor shapes).
|
|
177
|
+
// Missing Material means default density = 1 (fine).
|
|
178
|
+
let hasValidDensity = false;
|
|
179
|
+
for (const child of children) {
|
|
180
|
+
const mat = child.get(Material);
|
|
181
|
+
const density = mat !== undefined ? mat.density : 1; // default density is 1
|
|
182
|
+
if (density > 0) {
|
|
183
|
+
hasValidDensity = true;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!hasValidDensity) {
|
|
188
|
+
dedupe.warnOnce(eid, CODE_DYNAMIC_BODY_WITHOUT_DENSITY, `Entity${eid} is a dynamic body whose shapes all have density 0; it will have zero mass.`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// ─── Body-with-shape-pose validation ─────────────────────────────────────────
|
|
193
|
+
/** Walk entities that have Body AND ShapePosition or ShapeRotation (code 5).
|
|
194
|
+
*
|
|
195
|
+
* Re-arm mechanism: entities that leave this filter (ShapePosition/ShapeRotation
|
|
196
|
+
* removed) are detected via `prevBodyPoseEids`. They are rearmed so re-entry
|
|
197
|
+
* into the filter produces a fresh warning if the issue recurs. */
|
|
198
|
+
function _validateBodyShapePositionEntities(bodyPoseFilter, prevBodyPoseEids, dedupe) {
|
|
199
|
+
const currentEids = new Set();
|
|
200
|
+
bodyPoseFilter.forEach((e) => {
|
|
201
|
+
const eid = e.eid;
|
|
202
|
+
currentEids.add(eid);
|
|
203
|
+
const hasShapePos = e.get(ShapePosition) !== undefined;
|
|
204
|
+
const hasShapeRot = e.get(ShapeRotation) !== undefined;
|
|
205
|
+
const component = hasShapePos && hasShapeRot
|
|
206
|
+
? "ShapePosition and ShapeRotation"
|
|
207
|
+
: hasShapePos
|
|
208
|
+
? "ShapePosition"
|
|
209
|
+
: "ShapeRotation";
|
|
210
|
+
dedupe.warnOnce(eid, CODE_SHAPE_POSITION_ON_BODY, `Entity${eid} has Body and ${component}; move ${component} to a child shape entity.`);
|
|
211
|
+
});
|
|
212
|
+
// Rearm any entities that left the filter this step (ShapePosition/ShapeRotation
|
|
213
|
+
// was removed). If they re-enter in a future step, a fresh warning can fire.
|
|
214
|
+
for (const eid of prevBodyPoseEids) {
|
|
215
|
+
if (!currentEids.has(eid)) {
|
|
216
|
+
dedupe.rearm(eid, CODE_SHAPE_POSITION_ON_BODY);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Update the previous-seen set for next step.
|
|
220
|
+
prevBodyPoseEids.clear();
|
|
221
|
+
for (const eid of currentEids) {
|
|
222
|
+
prevBodyPoseEids.add(eid);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ─── Polygon hull validation ──────────────────────────────────────────────────
|
|
226
|
+
/**
|
|
227
|
+
* Returns true if `vertices` forms a valid convex polygon according to Box2D.
|
|
228
|
+
* Delegates to `b2ComputeHull` (WASM); a hull with count >= 3 is valid. Allocates
|
|
229
|
+
* and frees temporary b2Vec2 objects via the WASM heap.
|
|
230
|
+
*/
|
|
231
|
+
function isValidPolygon(state, vertices) {
|
|
232
|
+
if (vertices.length < 3) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
const box2d = state.box2d;
|
|
236
|
+
const points = vertices.map((v) => new box2d.b2Vec2(v.x, v.y));
|
|
237
|
+
try {
|
|
238
|
+
const hull = box2d.b2ComputeHull(points);
|
|
239
|
+
try {
|
|
240
|
+
return hull.count >= 3;
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
hull.delete();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
for (const p of points) {
|
|
248
|
+
p.delete();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ─── Force-target validation ─────────────────────────────────────────────────
|
|
253
|
+
function _validateForceEntities(forceFilter, dedupe) {
|
|
254
|
+
forceFilter.forEach((e) => {
|
|
255
|
+
const eid = e.eid;
|
|
256
|
+
const parent = e.parent(ChildOf);
|
|
257
|
+
const hasTarget = e.get(Body) !== undefined || (parent !== undefined && parent.get(Body) !== undefined);
|
|
258
|
+
if (!hasTarget) {
|
|
259
|
+
dedupe.warnOnce(eid, CODE_FORCE_WITHOUT_TARGET, `Entity${eid} has Force/Torque/Impulse/AngularImpulse but no Body or immediate Body parent.`);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
dedupe.rearm(eid, CODE_FORCE_WITHOUT_TARGET);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=debug.js.map
|