@scenoco-three/box2d 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SceNoCo contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @scenoco-three/box2d
2
+
3
+ **2D physics** for [SceNoCo](https://github.com/) on [planck](https://piqnt.com/planck.js)
4
+ (a pure-JS [Box2D](https://box2d.org) port): a `<Physics2D>` scene setting plus
5
+ `<RigidBody2D>` / `<Collider2D>` components, built entirely on the public `System` seam in
6
+ `@scenoco-three/core`. Import it for 2D physics; don't, and none ships. The 3D counterpart is
7
+ [`@scenoco-three/rapier`](../rapier).
8
+
9
+ Unlike the Rapier add-on, **no async init** — planck is pure JS, so import and load directly.
10
+
11
+ ```ts
12
+ import '@scenoco-three/box2d'; // registers the tags (side effect)
13
+ engine.loadScene(bundle); // a scene with <Physics2D>/<RigidBody2D>/<Collider2D>
14
+ ```
15
+
16
+ ```xml
17
+ <Scene>
18
+ <Physics2D gravity="0 -9.81" />
19
+ <Mesh id="ball" position="0 6 0">
20
+ <SphereGeometry radius="0.3" />
21
+ <Components>
22
+ <RigidBody2D bullet="true" /> <!-- continuous collision for a fast ball -->
23
+ <Collider2D restitution="0.9" /> <!-- bouncy circle, auto-sized from the geometry -->
24
+ </Components>
25
+ </Mesh>
26
+ <Mesh id="paddle" position="0 0.5 0">
27
+ <BoxGeometry width="3" height="0.4" depth="0.5" />
28
+ <Components>
29
+ <RigidBody2D kind="kinematic" /> <!-- you move the node; the body follows -->
30
+ <Collider2D shape="box" halfExtents="1.5 0.2" />
31
+ </Components>
32
+ </Mesh>
33
+ </Scene>
34
+ ```
35
+
36
+ The simulation runs in the **XY plane**: a body's position drives the node's `x`/`y`, its
37
+ angle drives `rotation.z`. Contacts are delivered to components via duck-typed, **Unity-style
38
+ hooks** — implement any of:
39
+
40
+ ```ts
41
+ onCollisionEnter2D(other: Object3D): void // solid touch began
42
+ onCollisionExit2D(other: Object3D): void
43
+ onTriggerEnter2D(other: Object3D): void // sensor (isSensor) overlap began
44
+ onTriggerExit2D(other: Object3D): void
45
+ ```
46
+
47
+ Contacts are queued during the step and dispatched after it, so a hook may safely destroy a
48
+ brick (or any scene object) without corrupting the in-progress simulation.
49
+
50
+ `three` is a peer dependency. See the [repository](../../README.md) and
51
+ [ARCHITECTURE.md](../../ARCHITECTURE.md) (`System` seam).
52
+
53
+ ## License
54
+
55
+ [MIT](./LICENSE)
@@ -0,0 +1,26 @@
1
+ import { type FixtureOpt, type Shape } from 'planck';
2
+ import { Component } from '@scenoco-three/core';
3
+ /**
4
+ * A 2D collision shape on the node. The `Physics2DSystem` attaches it to the
5
+ * node's `<RigidBody2D>` when it builds that body (no manual registration).
6
+ *
7
+ * `shape="auto"` reads the host mesh's geometry: a `<BoxGeometry>` becomes a box
8
+ * (XY half-extents from width/height), a `<SphereGeometry>` a circle. Set
9
+ * `isSensor="true"` for a trigger — it reports overlaps (`onTriggerEnter2D`) but
10
+ * generates no collision response.
11
+ */
12
+ export declare class Collider2D extends Component {
13
+ shape: string;
14
+ halfExtents: number[];
15
+ radius: number;
16
+ density: number;
17
+ friction: number;
18
+ restitution: number;
19
+ isSensor: boolean;
20
+ /** The planck shape for this collider, resolving `auto` from the host geometry. */
21
+ toShape(): Shape;
22
+ /** Fixture material/flags for `body.createFixture(shape, opt)`. */
23
+ fixtureOpt(): FixtureOpt;
24
+ /** Derive a shape from the host mesh's geometry parameters. */
25
+ private autoShape;
26
+ }
@@ -0,0 +1,89 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BoxShape, CircleShape } from 'planck';
8
+ import { Component, component, property } from '@scenoco-three/core';
9
+ /**
10
+ * A 2D collision shape on the node. The `Physics2DSystem` attaches it to the
11
+ * node's `<RigidBody2D>` when it builds that body (no manual registration).
12
+ *
13
+ * `shape="auto"` reads the host mesh's geometry: a `<BoxGeometry>` becomes a box
14
+ * (XY half-extents from width/height), a `<SphereGeometry>` a circle. Set
15
+ * `isSensor="true"` for a trigger — it reports overlaps (`onTriggerEnter2D`) but
16
+ * generates no collision response.
17
+ */
18
+ let Collider2D = class Collider2D extends Component {
19
+ shape = 'auto';
20
+ halfExtents = [0.5, 0.5];
21
+ radius = 0.5;
22
+ density = 1;
23
+ friction = 0.3;
24
+ restitution = 0;
25
+ isSensor = false;
26
+ /** The planck shape for this collider, resolving `auto` from the host geometry. */
27
+ toShape() {
28
+ switch (this.shape) {
29
+ case 'box': {
30
+ const [hx, hy] = this.halfExtents;
31
+ return new BoxShape(hx, hy);
32
+ }
33
+ case 'circle':
34
+ return new CircleShape(this.radius);
35
+ default:
36
+ return this.autoShape();
37
+ }
38
+ }
39
+ /** Fixture material/flags for `body.createFixture(shape, opt)`. */
40
+ fixtureOpt() {
41
+ return {
42
+ density: this.density,
43
+ friction: this.friction,
44
+ restitution: this.restitution,
45
+ isSensor: this.isSensor,
46
+ };
47
+ }
48
+ /** Derive a shape from the host mesh's geometry parameters. */
49
+ autoShape() {
50
+ const geom = this.node.geometry;
51
+ const p = geom?.parameters;
52
+ if (geom && p) {
53
+ switch (geom.type) {
54
+ case 'BoxGeometry':
55
+ case 'PlaneGeometry':
56
+ return new BoxShape((p.width ?? 1) / 2, (p.height ?? 1) / 2);
57
+ case 'SphereGeometry':
58
+ case 'CircleGeometry':
59
+ return new CircleShape(p.radius ?? 0.5);
60
+ }
61
+ }
62
+ return new BoxShape(0.5, 0.5); // fallback: unit box
63
+ }
64
+ };
65
+ __decorate([
66
+ property({ type: 'string', default: 'auto', description: 'auto | box | circle' })
67
+ ], Collider2D.prototype, "shape", void 0);
68
+ __decorate([
69
+ property({ type: 'vec2', default: [0.5, 0.5], description: 'Box half-extents (shape="box")' })
70
+ ], Collider2D.prototype, "halfExtents", void 0);
71
+ __decorate([
72
+ property({ type: 'float', default: 0.5, description: 'Radius (shape="circle")' })
73
+ ], Collider2D.prototype, "radius", void 0);
74
+ __decorate([
75
+ property({ type: 'float', default: 1, description: 'Density (kg/m²); 0 for static' })
76
+ ], Collider2D.prototype, "density", void 0);
77
+ __decorate([
78
+ property({ type: 'float', default: 0.3, description: 'Friction 0..1' })
79
+ ], Collider2D.prototype, "friction", void 0);
80
+ __decorate([
81
+ property({ type: 'float', default: 0, description: 'Restitution (bounciness) 0..1' })
82
+ ], Collider2D.prototype, "restitution", void 0);
83
+ __decorate([
84
+ property({ type: 'bool', default: false, description: 'A trigger: reports overlaps, no collision response' })
85
+ ], Collider2D.prototype, "isSensor", void 0);
86
+ Collider2D = __decorate([
87
+ component({ name: 'Collider2D', description: 'A 2D collision shape (planck); the Physics2DSystem attaches it to the node’s RigidBody2D' })
88
+ ], Collider2D);
89
+ export { Collider2D };
@@ -0,0 +1,17 @@
1
+ import { Attachment } from '@scenoco-three/core';
2
+ /**
3
+ * Scene-level 2D physics config: `<Physics2D gravity="0 -9.81" />` inside `<Scene>`.
4
+ *
5
+ * A pure config attachment — the `Physics2DSystem` (auto-run while the scene has
6
+ * 2D bodies) reads `gravity` and the solver iteration counts via
7
+ * `engine.findAttachment(Physics2D)`. The simulation runs in the XY plane: a
8
+ * body's position drives the node's `x`/`y` and its angle drives `rotation.z`.
9
+ *
10
+ * Unlike the Rapier add-on, no async init is needed — planck (Box2D) is pure JS.
11
+ */
12
+ export declare class Physics2D extends Attachment {
13
+ gravity: number[];
14
+ velocityIterations: number;
15
+ positionIterations: number;
16
+ attach(): void;
17
+ }
@@ -0,0 +1,37 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Attachment, attachment, property } from '@scenoco-three/core';
8
+ /**
9
+ * Scene-level 2D physics config: `<Physics2D gravity="0 -9.81" />` inside `<Scene>`.
10
+ *
11
+ * A pure config attachment — the `Physics2DSystem` (auto-run while the scene has
12
+ * 2D bodies) reads `gravity` and the solver iteration counts via
13
+ * `engine.findAttachment(Physics2D)`. The simulation runs in the XY plane: a
14
+ * body's position drives the node's `x`/`y` and its angle drives `rotation.z`.
15
+ *
16
+ * Unlike the Rapier add-on, no async init is needed — planck (Box2D) is pure JS.
17
+ */
18
+ let Physics2D = class Physics2D extends Attachment {
19
+ gravity = [0, -9.81];
20
+ velocityIterations = 8;
21
+ positionIterations = 3;
22
+ // Config only: the values are read by Physics2DSystem; nothing to apply here.
23
+ attach() { }
24
+ };
25
+ __decorate([
26
+ property({ type: 'vec2', default: [0, -9.81], description: 'Gravity vector (XY plane)' })
27
+ ], Physics2D.prototype, "gravity", void 0);
28
+ __decorate([
29
+ property({ type: 'int', default: 8, description: 'Velocity solver iterations per step' })
30
+ ], Physics2D.prototype, "velocityIterations", void 0);
31
+ __decorate([
32
+ property({ type: 'int', default: 3, description: 'Position solver iterations per step' })
33
+ ], Physics2D.prototype, "positionIterations", void 0);
34
+ Physics2D = __decorate([
35
+ attachment({ name: 'Physics2D', target: 'Scene', group: 'physics2d', description: 'Scene 2D physics config (gravity, solver iterations)' })
36
+ ], Physics2D);
37
+ export { Physics2D };
@@ -0,0 +1,30 @@
1
+ import { World } from 'planck';
2
+ import { System } from '@scenoco-three/core';
3
+ import { RigidBody2D } from './RigidBody2D.js';
4
+ /**
5
+ * ECS-hybrid 2D physics on planck (Box2D). Registered for `RigidBody2D`, the
6
+ * engine auto-runs it while the scene contains 2D bodies and keeps
7
+ * `this.components` in sync. Each fixed step it reconciles the planck world with
8
+ * the live body set, drives kinematic bodies from their nodes, steps the world,
9
+ * and writes dynamic poses back to the XY plane (position → node.x/y, angle →
10
+ * node.rotation.z). Gravity/iterations come from the `<Physics2D>` attachment.
11
+ *
12
+ * Contacts are dispatched to components via duck-typed Unity-style hooks
13
+ * (`onCollisionEnter2D` / `onTriggerEnter2D` / …). They are queued during the
14
+ * step and delivered after it returns, so a hook may safely mutate the scene
15
+ * (e.g. destroy a brick) without corrupting the in-progress simulation.
16
+ *
17
+ * No async init is required — planck is pure JS.
18
+ */
19
+ export declare class Physics2DSystem extends System<RigidBody2D> {
20
+ world?: World;
21
+ private readonly bodies;
22
+ private readonly contacts;
23
+ onAttach(): void;
24
+ fixedStep(dt: number): void;
25
+ onDetach(): void;
26
+ private createBody;
27
+ private queue;
28
+ private flushContacts;
29
+ private dispatch;
30
+ }
@@ -0,0 +1,135 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { World, Vec2 } from 'planck';
8
+ import { System, system } from '@scenoco-three/core';
9
+ import { RigidBody2D } from './RigidBody2D.js';
10
+ import { Collider2D } from './Collider2D.js';
11
+ import { Physics2D } from './Physics2D.js';
12
+ /**
13
+ * ECS-hybrid 2D physics on planck (Box2D). Registered for `RigidBody2D`, the
14
+ * engine auto-runs it while the scene contains 2D bodies and keeps
15
+ * `this.components` in sync. Each fixed step it reconciles the planck world with
16
+ * the live body set, drives kinematic bodies from their nodes, steps the world,
17
+ * and writes dynamic poses back to the XY plane (position → node.x/y, angle →
18
+ * node.rotation.z). Gravity/iterations come from the `<Physics2D>` attachment.
19
+ *
20
+ * Contacts are dispatched to components via duck-typed Unity-style hooks
21
+ * (`onCollisionEnter2D` / `onTriggerEnter2D` / …). They are queued during the
22
+ * step and delivered after it returns, so a hook may safely mutate the scene
23
+ * (e.g. destroy a brick) without corrupting the in-progress simulation.
24
+ *
25
+ * No async init is required — planck is pure JS.
26
+ */
27
+ let Physics2DSystem = class Physics2DSystem extends System {
28
+ world;
29
+ bodies = new Map();
30
+ contacts = [];
31
+ onAttach() {
32
+ const cfg = this.engine.findAttachment(Physics2D);
33
+ const g = cfg?.gravity ?? [0, -9.81];
34
+ this.world = new World({ gravity: new Vec2(g[0] ?? 0, g[1] ?? -9.81) });
35
+ // Resolve contacts to nodes at event time and queue them; the side of the
36
+ // pair that owns a body may be gone by dispatch, but the nodes stay valid.
37
+ this.world.on('begin-contact', (contact) => this.queue(contact, 'enter'));
38
+ this.world.on('end-contact', (contact) => this.queue(contact, 'exit'));
39
+ }
40
+ fixedStep(dt) {
41
+ const world = this.world;
42
+ if (!world)
43
+ return;
44
+ // Reconcile bodies ↔ live components: create for new, remove for gone.
45
+ for (const rb of this.components)
46
+ if (!this.bodies.has(rb))
47
+ this.createBody(world, rb);
48
+ for (const [rb, body] of this.bodies) {
49
+ if (!this.components.includes(rb)) {
50
+ world.destroyBody(body);
51
+ this.bodies.delete(rb);
52
+ rb.body = undefined;
53
+ }
54
+ }
55
+ // Kinematic bodies are driven by their node (game code moves the node).
56
+ for (const [rb, body] of this.bodies) {
57
+ if (rb.kind === 'kinematic')
58
+ body.setTransform(new Vec2(rb.node.position.x, rb.node.position.y), rb.node.rotation.z);
59
+ }
60
+ const cfg = this.engine.findAttachment(Physics2D);
61
+ world.step(dt, cfg?.velocityIterations ?? 8, cfg?.positionIterations ?? 3);
62
+ // Dynamic bodies write their pose back to the scene graph (XY plane).
63
+ for (const [rb, body] of this.bodies) {
64
+ if (rb.kind !== 'dynamic')
65
+ continue;
66
+ const p = body.getPosition();
67
+ rb.node.position.x = p.x;
68
+ rb.node.position.y = p.y;
69
+ rb.node.rotation.z = body.getAngle();
70
+ }
71
+ this.flushContacts();
72
+ }
73
+ onDetach() {
74
+ for (const rb of this.bodies.keys())
75
+ rb.body = undefined;
76
+ this.bodies.clear();
77
+ this.contacts.length = 0;
78
+ this.world = undefined;
79
+ }
80
+ createBody(world, rb) {
81
+ const body = world.createBody({
82
+ type: rb.kind,
83
+ position: new Vec2(rb.node.position.x, rb.node.position.y),
84
+ angle: rb.node.rotation.z,
85
+ linearDamping: rb.linearDamping,
86
+ angularDamping: rb.angularDamping,
87
+ fixedRotation: rb.fixedRotation,
88
+ gravityScale: rb.gravityScale,
89
+ bullet: rb.bullet,
90
+ });
91
+ body.setUserData(rb);
92
+ // Attach the colliders declared on the same node.
93
+ for (const c of this.engine.getComponents(rb.node)) {
94
+ if (c instanceof Collider2D)
95
+ body.createFixture(c.toShape(), c.fixtureOpt());
96
+ }
97
+ this.bodies.set(rb, body);
98
+ rb.body = body;
99
+ }
100
+ queue(contact, phase) {
101
+ const fa = contact.getFixtureA();
102
+ const fb = contact.getFixtureB();
103
+ const rbA = fa.getBody().getUserData();
104
+ const rbB = fb.getBody().getUserData();
105
+ if (!rbA || !rbB)
106
+ return;
107
+ this.contacts.push({ a: rbA.node, b: rbB.node, sensor: fa.isSensor() || fb.isSensor(), phase });
108
+ }
109
+ flushContacts() {
110
+ if (this.contacts.length === 0)
111
+ return;
112
+ const batch = this.contacts.splice(0, this.contacts.length);
113
+ for (const { a, b, sensor, phase } of batch) {
114
+ this.dispatch(a, b, sensor, phase);
115
+ this.dispatch(b, a, sensor, phase);
116
+ }
117
+ }
118
+ dispatch(node, other, sensor, phase) {
119
+ for (const c of this.engine.getComponents(node)) {
120
+ const h = c;
121
+ const fn = sensor
122
+ ? phase === 'enter'
123
+ ? h.onTriggerEnter2D
124
+ : h.onTriggerExit2D
125
+ : phase === 'enter'
126
+ ? h.onCollisionEnter2D
127
+ : h.onCollisionExit2D;
128
+ fn?.call(c, other);
129
+ }
130
+ }
131
+ };
132
+ Physics2DSystem = __decorate([
133
+ system(RigidBody2D)
134
+ ], Physics2DSystem);
135
+ export { Physics2DSystem };
@@ -0,0 +1,29 @@
1
+ import type { Body } from 'planck';
2
+ import { Component } from '@scenoco-three/core';
3
+ /**
4
+ * Marks the host node as a Box2D (planck) 2D rigid body — plain data the
5
+ * `Physics2DSystem` reconciles into the world. Pair it with one or more
6
+ * `<Collider2D>` on the same node to give it shape. The system stamps the live
7
+ * planck `body` onto this component once built.
8
+ *
9
+ * dynamic — simulated; physics drives the node's x/y and rotation.z
10
+ * static — never moves (walls, ground)
11
+ * kinematic — moved by your code (set node.position); the body follows it,
12
+ * pushing dynamic bodies (paddles, moving platforms)
13
+ */
14
+ export declare class RigidBody2D extends Component {
15
+ kind: string;
16
+ linearDamping: number;
17
+ angularDamping: number;
18
+ fixedRotation: boolean;
19
+ gravityScale: number;
20
+ bullet: boolean;
21
+ /** The live planck body, set by the Physics2DSystem after it builds it. */
22
+ body?: Body;
23
+ /** Apply an instantaneous impulse at the body's center (world space), waking it. */
24
+ applyImpulse(x: number, y: number): void;
25
+ /** Set the linear velocity (world space), waking the body. */
26
+ setVelocity(x: number, y: number): void;
27
+ /** Current linear velocity, or `[0, 0]` before the body exists. */
28
+ getVelocity(): [number, number];
29
+ }
@@ -0,0 +1,64 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Component, component, property } from '@scenoco-three/core';
8
+ /**
9
+ * Marks the host node as a Box2D (planck) 2D rigid body — plain data the
10
+ * `Physics2DSystem` reconciles into the world. Pair it with one or more
11
+ * `<Collider2D>` on the same node to give it shape. The system stamps the live
12
+ * planck `body` onto this component once built.
13
+ *
14
+ * dynamic — simulated; physics drives the node's x/y and rotation.z
15
+ * static — never moves (walls, ground)
16
+ * kinematic — moved by your code (set node.position); the body follows it,
17
+ * pushing dynamic bodies (paddles, moving platforms)
18
+ */
19
+ let RigidBody2D = class RigidBody2D extends Component {
20
+ kind = 'dynamic';
21
+ linearDamping = 0;
22
+ angularDamping = 0;
23
+ fixedRotation = false;
24
+ gravityScale = 1;
25
+ bullet = false;
26
+ /** The live planck body, set by the Physics2DSystem after it builds it. */
27
+ body;
28
+ /** Apply an instantaneous impulse at the body's center (world space), waking it. */
29
+ applyImpulse(x, y) {
30
+ if (this.body)
31
+ this.body.applyLinearImpulse({ x, y }, this.body.getWorldCenter(), true);
32
+ }
33
+ /** Set the linear velocity (world space), waking the body. */
34
+ setVelocity(x, y) {
35
+ this.body?.setLinearVelocity({ x, y });
36
+ }
37
+ /** Current linear velocity, or `[0, 0]` before the body exists. */
38
+ getVelocity() {
39
+ const v = this.body?.getLinearVelocity();
40
+ return v ? [v.x, v.y] : [0, 0];
41
+ }
42
+ };
43
+ __decorate([
44
+ property({ type: 'string', default: 'dynamic', description: 'dynamic | static | kinematic' })
45
+ ], RigidBody2D.prototype, "kind", void 0);
46
+ __decorate([
47
+ property({ type: 'float', default: 0 })
48
+ ], RigidBody2D.prototype, "linearDamping", void 0);
49
+ __decorate([
50
+ property({ type: 'float', default: 0 })
51
+ ], RigidBody2D.prototype, "angularDamping", void 0);
52
+ __decorate([
53
+ property({ type: 'bool', default: false, description: 'Lock rotation (common for characters)' })
54
+ ], RigidBody2D.prototype, "fixedRotation", void 0);
55
+ __decorate([
56
+ property({ type: 'float', default: 1, description: 'Multiplier on world gravity for this body' })
57
+ ], RigidBody2D.prototype, "gravityScale", void 0);
58
+ __decorate([
59
+ property({ type: 'bool', default: false, description: 'Continuous collision for fast bodies (e.g. a ball)' })
60
+ ], RigidBody2D.prototype, "bullet", void 0);
61
+ RigidBody2D = __decorate([
62
+ component({ name: 'RigidBody2D', description: 'A 2D physics body (planck/Box2D); pair with <Collider2D>. Needs a <Physics2D> scene.' })
63
+ ], RigidBody2D);
64
+ export { RigidBody2D };
@@ -0,0 +1,10 @@
1
+ export { Physics2DSystem } from './Physics2DSystem.js';
2
+ export { RigidBody2D } from './RigidBody2D.js';
3
+ export { Collider2D } from './Collider2D.js';
4
+ export { Physics2D } from './Physics2D.js';
5
+ /**
6
+ * The XML tags this package registers — read by the compiler/Vite plugin
7
+ * (`registerPackages`) so a scene that uses them validates and imports the
8
+ * package. The convention for any SceNoCo tag package.
9
+ */
10
+ export declare const sceneTags: readonly ["RigidBody2D", "Collider2D", "Physics2D"];
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ // `@scenoco-three/box2d` — 2D physics for SceNoCo on planck (a pure-JS Box2D
2
+ // port), built entirely on the public seams: a `Physics2DSystem extends System`,
3
+ // `@component` `RigidBody2D`/`Collider2D`, and a `<Physics2D>` `@attachment`.
4
+ // Core has no physics dependency; an app that doesn't import this package ships
5
+ // no 2D physics. The 3D counterpart is `@scenoco-three/rapier`.
6
+ //
7
+ // Importing this module registers the `RigidBody2D`, `Collider2D`, and
8
+ // `Physics2D` tags (decorator side effects) — hence the package is `sideEffects`.
9
+ //
10
+ // Unlike Rapier, planck is pure JS: there is NO async init — register the tags by
11
+ // importing the package and load the scene directly.
12
+ //
13
+ // The simulation runs in the XY plane: a body's position drives the node's x/y
14
+ // and its angle drives rotation.z. Contacts are delivered to components via
15
+ // duck-typed Unity-style hooks: onCollisionEnter2D / onCollisionExit2D /
16
+ // onTriggerEnter2D / onTriggerExit2D, each receiving the other node.
17
+ export { Physics2DSystem } from './Physics2DSystem.js';
18
+ export { RigidBody2D } from './RigidBody2D.js';
19
+ export { Collider2D } from './Collider2D.js';
20
+ export { Physics2D } from './Physics2D.js';
21
+ /**
22
+ * The XML tags this package registers — read by the compiler/Vite plugin
23
+ * (`registerPackages`) so a scene that uses them validates and imports the
24
+ * package. The convention for any SceNoCo tag package.
25
+ */
26
+ export const sceneTags = ['RigidBody2D', 'Collider2D', 'Physics2D'];
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@scenoco-three/box2d",
3
+ "version": "0.1.0",
4
+ "description": "Box2D (planck) 2D physics for SceNoCo: <Physics2D> setting + RigidBody2D/Collider2D components on the System seam",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "keywords": ["three", "threejs", "physics", "2d", "box2d", "planck", "scene", "components", "gamedev"],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "//sideEffects": "Importing the package registers its @component/@attachment tags (decorator side effects), so it must not be tree-shaken away when imported for registration.",
23
+ "sideEffects": true,
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.build.json",
26
+ "typecheck": "npm --prefix ../core run build && tsc --noEmit -p tsconfig.json"
27
+ },
28
+ "dependencies": {
29
+ "@scenoco-three/core": "^0.1.0",
30
+ "planck": "^1.5.0"
31
+ },
32
+ "peerDependencies": {
33
+ "three": ">=0.160.0"
34
+ }
35
+ }