castle-web-cli 0.4.94 → 0.4.95
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/init.js +1 -1
- package/kits/physics-2d/CLAUDE.md +116 -6
- package/kits/physics-2d/behaviors/Collider.jsx +76 -10
- package/kits/physics-2d/behaviors/Sprite.jsx +6 -1
- package/kits/physics-2d/drawings/joint-rope.pxart +26 -0
- package/kits/physics-2d/editors/SceneEditor.jsx +58 -0
- package/kits/physics-2d/editors/SelectionOverlay.jsx +48 -19
- package/kits/physics-2d/editors/behaviorRegistry.js +8 -2
- package/kits/physics-2d/engine/blueprint.js +39 -3
- package/kits/physics-2d/engine/collider.js +13 -3
- package/kits/physics-2d/engine/files.js +26 -5
- package/kits/physics-2d/engine/scene.js +9 -1
- package/kits/physics-2d/engine/systemRegistry.js +5 -1
- package/kits/physics-2d/package-lock.json +10 -3
- package/kits/physics-2d/physics/PhysicsSystem.js +112 -0
- package/kits/physics-2d/physics/behaviors/Joints.jsx +291 -0
- package/kits/physics-2d/physics/jointArt.js +57 -0
- package/kits/physics-2d/physics/joints.js +136 -0
- package/kits/physics-2d/physics/matterBridge.js +22 -2
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// panel propagates to every scene that places it. Shared by the runtime
|
|
7
7
|
// (`scene.js`, so play mode and `PlayOnly` resolve blueprints too) and the
|
|
8
8
|
// editors (which also mint/fork/migrate/cascade-delete blueprint files).
|
|
9
|
+
import { resolveDeckFile } from 'castle-web-sdk';
|
|
9
10
|
import { formatJson } from './files';
|
|
10
11
|
import { DEFAULT_RESOLUTION, TRANSPARENT, serializeCompact } from './pxart';
|
|
11
12
|
import { cardSize, dedupeActorId } from './scene';
|
|
@@ -13,8 +14,19 @@ import { cardSize, dedupeActorId } from './scene';
|
|
|
13
14
|
export const BLUEPRINTS_DIR = 'blueprints';
|
|
14
15
|
export const DRAWINGS_DIR = 'drawings';
|
|
15
16
|
|
|
17
|
+
// A blueprint of this deck (`blueprints/x.scene`) OR of one it imports
|
|
18
|
+
// (`imports/<alias>/blueprints/x.scene`) -- an imported deck's blueprints are
|
|
19
|
+
// placeable like any other, which is most of the point of importing one.
|
|
20
|
+
const BLUEPRINT_PATH_RE = new RegExp(`^(imports/[^/]+/)?${BLUEPRINTS_DIR}/`);
|
|
21
|
+
|
|
16
22
|
export function isBlueprintPath(path) {
|
|
17
|
-
return typeof path === 'string' && path.
|
|
23
|
+
return typeof path === 'string' && path.endsWith('.scene') && BLUEPRINT_PATH_RE.test(path);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Blueprints an import owns: placeable, but not editable or deletable from here
|
|
27
|
+
// (they belong to the deck they came from).
|
|
28
|
+
export function isImportedPath(path) {
|
|
29
|
+
return typeof path === 'string' && path.startsWith('imports/');
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
// Per-property inherit metadata for a behavior prop. Behaviors opt a prop out
|
|
@@ -49,12 +61,33 @@ function parseBlueprintText(text) {
|
|
|
49
61
|
return { name, components: actor.components };
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
// A reference names a file the way its own deck sees it -- `drawings/rock.pxart`
|
|
65
|
+
// for one of its own, `@imports/<alias>/...` for one of a deck it imports (the
|
|
66
|
+
// SDK's resolveDeckFile is the single definition of that rule). Templates are
|
|
67
|
+
// resolved against the file they were written in, so a kit's blueprint keeps
|
|
68
|
+
// meaning the kit's drawing after the kit is imported by somebody else.
|
|
69
|
+
function resolveTemplateRefs(template, files, fromPath) {
|
|
70
|
+
if (!template) return template;
|
|
71
|
+
for (const props of Object.values(template.components ?? {})) {
|
|
72
|
+
for (const [key, value] of Object.entries(props ?? {})) {
|
|
73
|
+
if (typeof value !== 'string' || !value) continue;
|
|
74
|
+
const resolved = resolveDeckFile(value, fromPath);
|
|
75
|
+
if (resolved !== value && files?.[resolved] !== undefined) props[key] = resolved;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return template;
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
// Resolve a blueprint's template from the LIVE files map -- the load-bearing
|
|
53
82
|
// call for live propagation. Returns null when the file is missing, deleted,
|
|
54
83
|
// or malformed (a dangling `blueprint` ref degrades to "no template" rather
|
|
55
84
|
// than throwing).
|
|
56
85
|
export function getBlueprintTemplate(files, blueprintPath) {
|
|
57
|
-
|
|
86
|
+
// The ref may name this deck's blueprint or an import's; the template's own
|
|
87
|
+
// refs then resolve against whichever deck the blueprint turned out to be in.
|
|
88
|
+
const key = resolveDeckFile(blueprintPath);
|
|
89
|
+
const template = parseBlueprintText(files?.[key]);
|
|
90
|
+
return resolveTemplateRefs(template, files, key);
|
|
58
91
|
}
|
|
59
92
|
|
|
60
93
|
// Every blueprint currently in the deck, for the library/hotbar. Skips
|
|
@@ -63,7 +96,10 @@ export function listBlueprints(files) {
|
|
|
63
96
|
const out = [];
|
|
64
97
|
for (const path of Object.keys(files ?? {})) {
|
|
65
98
|
if (!isBlueprintPath(path)) continue;
|
|
66
|
-
|
|
99
|
+
// Same origin resolution getBlueprintTemplate does: an imported blueprint's
|
|
100
|
+
// `drawings/x.pxart` means the import's drawing, and the library previews
|
|
101
|
+
// these templates -- unresolved, the slot renders with no art.
|
|
102
|
+
const template = resolveTemplateRefs(parseBlueprintText(files[path]), files, path);
|
|
67
103
|
if (!template) continue;
|
|
68
104
|
out.push({ path, name: template.name, components: template.components });
|
|
69
105
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// A Collider is a LIST of shapes (`Collider.shapes`), each stored as a FRACTION
|
|
6
6
|
// of the actor's Layout box -- so a collider keeps its relative size and position
|
|
7
7
|
// when the box is resized (blueprint default OR per-instance override). Shapes:
|
|
8
|
-
// box: { type:'box', x, y, w, h }
|
|
8
|
+
// box: { type:'box', x, y, w, h, angle? } center + size (frac); angle deg
|
|
9
9
|
// circle: { type:'circle', x, y, r } center frac; r = frac of min(box side)
|
|
10
10
|
// triangle: { type:'triangle', points:[{x,y}×3] } fractions of box
|
|
11
11
|
// polygon: { type:'polygon', points:[{x,y}×N] }
|
|
@@ -140,7 +140,10 @@ function resolveShape(shape, layout) {
|
|
|
140
140
|
const h = (shape.h ?? 1) * bh;
|
|
141
141
|
const cx = wx(shape.x ?? 0.5);
|
|
142
142
|
const cy = wy(shape.y ?? 0.5);
|
|
143
|
-
|
|
143
|
+
// `angle` (deg) rotates the box about its own center, within the unrotated box
|
|
144
|
+
// frame; Layout.rotation composes on top (matter body + draw ctx). `x`/`y` are
|
|
145
|
+
// the UNROTATED top-left -- shapeAabb widens them when angled.
|
|
146
|
+
return { type: 'box', cx, cy, width: w, height: h, x: cx - w / 2, y: cy - h / 2, angle: shape.angle ?? 0 };
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
// World-space shapes for an actor's collider (list), or null if no Layout/Collider.
|
|
@@ -172,7 +175,14 @@ function shapeAabb(s) {
|
|
|
172
175
|
}
|
|
173
176
|
return [x0, y0, x1, y1];
|
|
174
177
|
}
|
|
175
|
-
|
|
178
|
+
const ang = ((s.angle ?? 0) * Math.PI) / 180;
|
|
179
|
+
if (!ang) return [s.x, s.y, s.x + s.width, s.y + s.height];
|
|
180
|
+
// AABB of a box rotated about its center: project the half-extents onto the axes.
|
|
181
|
+
const c = Math.abs(Math.cos(ang));
|
|
182
|
+
const sn = Math.abs(Math.sin(ang));
|
|
183
|
+
const ex = (s.width / 2) * c + (s.height / 2) * sn;
|
|
184
|
+
const ey = (s.width / 2) * sn + (s.height / 2) * c;
|
|
185
|
+
return [s.cx - ex, s.cy - ey, s.cx + ex, s.cy + ey];
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
// AABB of the whole collider (union of all shapes), world units -- for
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
// Seed the file map by scanning the deck dir. Vite module-cache is invalidated
|
|
2
2
|
// on restart, so a newly-created file just shows up on the next reload.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
// Anchored at the DECK ROOT (`/scenes/...`) rather than relative to this module,
|
|
4
|
+
// because the kit itself may be an import: living at `imports/<alias>/`, a
|
|
5
|
+
// `../scenes/*.scene` would find the KIT's scenes instead of the deck's. Anchored
|
|
6
|
+
// at the root, one pattern works whether the kit is the deck or a dependency.
|
|
7
|
+
//
|
|
8
|
+
// The first glob adds every import's content, keyed by its full path
|
|
9
|
+
// (`imports/<alias>/drawings/rock.pxart`) -- which is exactly how a scene or
|
|
10
|
+
// blueprint refers to a dependency's file, so no lookup elsewhere changes.
|
|
11
|
+
// (Patterns and options must be literals: import.meta.glob is a compile-time
|
|
12
|
+
// transform, so these can't be hoisted into shared constants.)
|
|
13
|
+
const rawModules = {
|
|
14
|
+
...import.meta.glob(
|
|
15
|
+
[
|
|
16
|
+
'/imports/*/scenes/*.scene',
|
|
17
|
+
'/imports/*/blueprints/*.scene',
|
|
18
|
+
'/imports/*/drawings/*.pxart',
|
|
19
|
+
'/imports/*/behaviors/*.jsx',
|
|
20
|
+
],
|
|
21
|
+
{ query: '?raw', import: 'default', eager: true }
|
|
22
|
+
),
|
|
23
|
+
...import.meta.glob(
|
|
24
|
+
['/scenes/*.scene', '/blueprints/*.scene', '/drawings/*.pxart', '/behaviors/*.jsx'],
|
|
25
|
+
{ query: '?raw', import: 'default', eager: true }
|
|
26
|
+
),
|
|
27
|
+
};
|
|
7
28
|
export const initialFiles = Object.fromEntries(
|
|
8
29
|
Object.entries(rawModules)
|
|
9
|
-
.map(([globPath, text]) => [globPath.replace(
|
|
30
|
+
.map(([globPath, text]) => [globPath.replace(/^\//, ''), text])
|
|
10
31
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
11
32
|
);
|
|
12
33
|
export function getFileKind(path) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveDeckFile } from 'castle-web-sdk';
|
|
1
2
|
import { initialFiles, parseJsonFile } from './files';
|
|
2
3
|
import { getBlueprintTemplate, mergeComponents } from './blueprint';
|
|
3
4
|
import { getColliderRect, intersects, spriteIsEmpty } from './collider';
|
|
@@ -18,7 +19,9 @@ export const cardSize = { width: CARD_WIDTH, height: CARD_HEIGHT };
|
|
|
18
19
|
// 'main.scene', 'scenes/main.scene', './scenes/main.scene', or 'main' -- the
|
|
19
20
|
// `.scene` extension and a leading `scenes/` are both optional.
|
|
20
21
|
function resolveSceneFileKey(name) {
|
|
21
|
-
|
|
22
|
+
// `@imports/<alias>/scenes/x.scene` names an import's scene; anything else is
|
|
23
|
+
// this deck's, with the `scenes/` folder and `.scene` suffix both optional.
|
|
24
|
+
const key = resolveDeckFile(String(name));
|
|
22
25
|
const candidates = [key];
|
|
23
26
|
if (!key.endsWith('.scene')) candidates.push(`${key}.scene`);
|
|
24
27
|
for (const candidate of [...candidates]) {
|
|
@@ -59,6 +62,11 @@ export class SceneRuntime {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
load(sceneData) {
|
|
65
|
+
// Reset registered systems (e.g. physics) so a reload/restart/scene
|
|
66
|
+
// transition starts from the authored layout -- otherwise a system holding
|
|
67
|
+
// simulation state (body positions, joints) would carry it across the load.
|
|
68
|
+
// No-op on the constructor's first load, before any system is registered.
|
|
69
|
+
for (const system of this.systems) system.reset?.(this);
|
|
62
70
|
this.data = structuredClone(sceneData);
|
|
63
71
|
this.actors = new Map();
|
|
64
72
|
for (const actor of this.data.actors ?? []) {
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
// updates. Empty in a kit with no `systems/` dir (e.g. basic-2d); a kit adds a
|
|
7
7
|
// system by dropping a file here -- no edits to the engine required. Symmetric
|
|
8
8
|
// with editors/behaviorRegistry.js.
|
|
9
|
-
|
|
9
|
+
// Root-anchored, deck + imports (see engine/files.js).
|
|
10
|
+
const modules = {
|
|
11
|
+
...import.meta.glob('/imports/*/systems/*.js', { eager: true }),
|
|
12
|
+
...import.meta.glob('/systems/*.js', { eager: true }),
|
|
13
|
+
};
|
|
10
14
|
export const systemInstallers = Object.values(modules)
|
|
11
15
|
.map((mod) => mod.installSystem)
|
|
12
16
|
.filter((fn) => typeof fn === 'function');
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "physics-2d",
|
|
3
3
|
"lockfileVersion": 3,
|
|
4
4
|
"requires": true,
|
|
5
5
|
"packages": {
|
|
6
6
|
"": {
|
|
7
|
-
"name": "
|
|
7
|
+
"name": "physics-2d",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@codemirror/commands": "^6.10.3",
|
|
10
10
|
"@codemirror/lang-javascript": "^6.2.5",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"@lezer/highlight": "^1.2.3",
|
|
16
16
|
"castle-web-sdk": "file:../../sdk",
|
|
17
17
|
"codemirror": "^6.0.2",
|
|
18
|
+
"matter-js": "^0.20.0",
|
|
18
19
|
"react": "^19.2.4",
|
|
19
20
|
"react-dom": "^19.2.4"
|
|
20
21
|
},
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
},
|
|
28
29
|
"../../sdk": {
|
|
29
30
|
"name": "castle-web-sdk",
|
|
30
|
-
"version": "0.4.
|
|
31
|
+
"version": "0.4.11",
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"eslint": "^9.0.0",
|
|
33
34
|
"jscpd": "^4.0.5",
|
|
@@ -1899,6 +1900,12 @@
|
|
|
1899
1900
|
"node": ">= 0.4"
|
|
1900
1901
|
}
|
|
1901
1902
|
},
|
|
1903
|
+
"node_modules/matter-js": {
|
|
1904
|
+
"version": "0.20.0",
|
|
1905
|
+
"resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.20.0.tgz",
|
|
1906
|
+
"integrity": "sha512-iC9fYR7zVT3HppNnsFsp9XOoQdQN2tUyfaKg4CHLH8bN+j6GT4Gw7IH2rP0tflAebrHFw730RR3DkVSZRX8hwA==",
|
|
1907
|
+
"license": "MIT"
|
|
1908
|
+
},
|
|
1902
1909
|
"node_modules/merge-stream": {
|
|
1903
1910
|
"version": "2.0.0",
|
|
1904
1911
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import Matter from 'matter-js';
|
|
11
11
|
import { getColliderRect } from '../engine/collider';
|
|
12
|
+
import { buildJointConstraints, jointSignature, patchJoint } from './joints';
|
|
12
13
|
import {
|
|
13
14
|
DEFAULT_GRAVITY,
|
|
14
15
|
FIXED_STEP_MS,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
applyAngularDrag,
|
|
19
20
|
applyWorldGravity,
|
|
20
21
|
bodySignature,
|
|
22
|
+
clampSpeeds,
|
|
21
23
|
createBody,
|
|
22
24
|
isPhysicsActor,
|
|
23
25
|
patchBody,
|
|
@@ -29,6 +31,7 @@ export class PhysicsSystem {
|
|
|
29
31
|
constructor() {
|
|
30
32
|
this.engine = null;
|
|
31
33
|
this.bodies = new Map(); // actorId -> { body, sig, offset: {dx, dy} }
|
|
34
|
+
this.joints = new Map(); // `${ownerId}#${listIndex}` -> { constraints, bodyA, bodyB, sig }
|
|
32
35
|
this.intents = new Map(); // actorId -> { velocity?, impulse?, force? }
|
|
33
36
|
this.gravity = { ...DEFAULT_GRAVITY };
|
|
34
37
|
this.acc = 0;
|
|
@@ -44,6 +47,24 @@ export class PhysicsSystem {
|
|
|
44
47
|
runtime.physics = this.makeApi();
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
// Tear down all simulation state so the next step rebuilds from scratch. The
|
|
51
|
+
// runtime calls this from `scene.load()` -- a reload/restart/scene-transition
|
|
52
|
+
// must reset physics to the authored layout, otherwise bodies keep their last
|
|
53
|
+
// simulated positions and joints keep stale rest lengths (a reloaded scene
|
|
54
|
+
// would come back mid-fall / tangled instead of fresh). Dropping the engine
|
|
55
|
+
// lets `ensureEngine` build a clean one on the next step.
|
|
56
|
+
reset() {
|
|
57
|
+
this.engine = null;
|
|
58
|
+
this.bodies.clear();
|
|
59
|
+
this.joints.clear();
|
|
60
|
+
this.intents.clear();
|
|
61
|
+
this.active.clear();
|
|
62
|
+
this.ndActive.clear();
|
|
63
|
+
this.enterPairs.length = 0;
|
|
64
|
+
this.exitPairs.length = 0;
|
|
65
|
+
this.acc = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
ensureEngine() {
|
|
48
69
|
if (this.engine) return;
|
|
49
70
|
this.engine = Matter.Engine.create();
|
|
@@ -63,9 +84,11 @@ export class PhysicsSystem {
|
|
|
63
84
|
}
|
|
64
85
|
|
|
65
86
|
afterBehaviors(scene, dt) {
|
|
87
|
+
this.lastScene = scene; // for the author-facing getJoints() API
|
|
66
88
|
this.ensureEngine();
|
|
67
89
|
this.readGravity(scene);
|
|
68
90
|
this.reconcile(scene);
|
|
91
|
+
this.reconcileJoints(scene);
|
|
69
92
|
this.applyIntents();
|
|
70
93
|
this.syncStaticBodies(scene);
|
|
71
94
|
this.step(dt);
|
|
@@ -103,6 +126,57 @@ export class PhysicsSystem {
|
|
|
103
126
|
for (const id of [...this.bodies.keys()]) if (!seen.has(id)) this.removeBody(id);
|
|
104
127
|
}
|
|
105
128
|
|
|
129
|
+
// Create/rebuild/remove joint constraints to match actors carrying a Joints
|
|
130
|
+
// list. Runs after body reconcile so both endpoints' bodies exist. Each list
|
|
131
|
+
// entry is tracked under `ownerId#index`; a joint whose owner or target has no
|
|
132
|
+
// body (yet), or whose target is missing/self, is skipped -- it binds
|
|
133
|
+
// automatically once both bodies appear.
|
|
134
|
+
reconcileJoints(scene) {
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
for (const actor of scene.getActors()) {
|
|
137
|
+
const list = actor.components.Joints?.list;
|
|
138
|
+
if (!Array.isArray(list) || list.length === 0) continue;
|
|
139
|
+
const bodyA = this.bodies.get(actor.id)?.body;
|
|
140
|
+
if (!bodyA) continue;
|
|
141
|
+
list.forEach((joint, i) => {
|
|
142
|
+
if (!joint?.target || joint.target === actor.id) return;
|
|
143
|
+
const bodyB = this.bodies.get(joint.target)?.body;
|
|
144
|
+
if (!bodyB) return;
|
|
145
|
+
const key = `${actor.id}#${i}`;
|
|
146
|
+
seen.add(key);
|
|
147
|
+
const sig = jointSignature(joint);
|
|
148
|
+
const entry = this.joints.get(key);
|
|
149
|
+
// Rebuild on structural change or when either endpoint's body was rebuilt
|
|
150
|
+
// (a collider edit swaps the body object, orphaning the old constraint).
|
|
151
|
+
if (!entry || entry.sig !== sig || entry.bodyA !== bodyA || entry.bodyB !== bodyB) {
|
|
152
|
+
this.removeJoint(key);
|
|
153
|
+
this.addJoint(key, bodyA, bodyB, joint, sig);
|
|
154
|
+
} else patchJoint(entry.constraints, bodyA, bodyB, joint);
|
|
155
|
+
// A weld is a pivot link PLUS frozen rotation on both bodies, so they
|
|
156
|
+
// can't turn relative to each other -- a rigid, stable fuse. Re-applied
|
|
157
|
+
// each frame because patchBody's setDensity resets inertia to finite.
|
|
158
|
+
if ((joint.type ?? 'spring') === 'weld') {
|
|
159
|
+
Matter.Body.setInertia(bodyA, Infinity);
|
|
160
|
+
Matter.Body.setInertia(bodyB, Infinity);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
for (const key of [...this.joints.keys()]) if (!seen.has(key)) this.removeJoint(key);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
addJoint(id, bodyA, bodyB, joint, sig) {
|
|
168
|
+
const constraints = buildJointConstraints(bodyA, bodyB, joint);
|
|
169
|
+
for (const c of constraints) Matter.Composite.add(this.engine.world, c);
|
|
170
|
+
this.joints.set(id, { constraints, bodyA, bodyB, sig });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
removeJoint(id) {
|
|
174
|
+
const entry = this.joints.get(id);
|
|
175
|
+
if (!entry) return;
|
|
176
|
+
for (const c of entry.constraints) Matter.Composite.remove(this.engine.world, c);
|
|
177
|
+
this.joints.delete(id);
|
|
178
|
+
}
|
|
179
|
+
|
|
106
180
|
addBody(actor, rect) {
|
|
107
181
|
const body = createBody(actor, rect);
|
|
108
182
|
const center = rectCenter(rect);
|
|
@@ -163,6 +237,7 @@ export class PhysicsSystem {
|
|
|
163
237
|
applyWorldGravity(dynamic, this.gravity);
|
|
164
238
|
applyAngularDrag(dynamic);
|
|
165
239
|
Matter.Engine.update(this.engine, FIXED_STEP_MS);
|
|
240
|
+
clampSpeeds(dynamic); // backstop: a joint blow-up can't fling a body off screen
|
|
166
241
|
this.acc -= FIXED_STEP_S;
|
|
167
242
|
steps += 1;
|
|
168
243
|
}
|
|
@@ -275,8 +350,45 @@ export class PhysicsSystem {
|
|
|
275
350
|
return this.active.has(pairKey(aId, bId));
|
|
276
351
|
},
|
|
277
352
|
bodyFor: (actor) => this.bodyOf(actor),
|
|
353
|
+
// Resolved joint geometry for custom rendering: each link's world-space
|
|
354
|
+
// endpoints (Layout centers, so art lines up with the sprites), type,
|
|
355
|
+
// length and angle. Draw your own art between `a` and `b` from a behavior's
|
|
356
|
+
// draw(); set the joint's `render: 'hidden'` so the built-in doesn't
|
|
357
|
+
// double-draw.
|
|
358
|
+
getJoints: () => this.collectJoints(),
|
|
278
359
|
};
|
|
279
360
|
}
|
|
361
|
+
|
|
362
|
+
collectJoints() {
|
|
363
|
+
const scene = this.lastScene;
|
|
364
|
+
if (!scene) return [];
|
|
365
|
+
const out = [];
|
|
366
|
+
for (const actor of scene.getActors()) {
|
|
367
|
+
const list = actor.components.Joints?.list;
|
|
368
|
+
if (!Array.isArray(list)) continue;
|
|
369
|
+
const a = centerOfLayout(actor.components.Layout);
|
|
370
|
+
for (const joint of list) {
|
|
371
|
+
const target = joint?.target && scene.getActor(joint.target);
|
|
372
|
+
if (!target || !a) continue;
|
|
373
|
+
const b = centerOfLayout(target.components.Layout);
|
|
374
|
+
if (!b) continue;
|
|
375
|
+
out.push({
|
|
376
|
+
ownerId: actor.id,
|
|
377
|
+
targetId: joint.target,
|
|
378
|
+
type: joint.type ?? 'spring',
|
|
379
|
+
a,
|
|
380
|
+
b,
|
|
381
|
+
length: Math.hypot(b.x - a.x, b.y - a.y),
|
|
382
|
+
angle: Math.atan2(b.y - a.y, b.x - a.x),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function centerOfLayout(L) {
|
|
391
|
+
return L ? { x: L.x + (L.width ?? 0) / 2, y: L.y + (L.height ?? 0) / 2 } : null;
|
|
280
392
|
}
|
|
281
393
|
|
|
282
394
|
function pairKey(a, b) {
|