castle-web-cli 0.4.93 → 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/dist/shell/assets/{index-CWNH9QiB.css → index-0Nmmod9f.css} +1 -1
- package/dist/shell/index.html +2 -2
- 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
- /package/dist/shell/assets/{index-_C2BvstY.js → index-DG5ZD4p8.js} +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Joints connect two physics actors with matter-js constraints. matter has one
|
|
2
|
+
// primitive -- Matter.Constraint (a point-to-point distance link with stiffness
|
|
3
|
+
// / damping / length) -- so every joint TYPE here is composed from one or two of
|
|
4
|
+
// them:
|
|
5
|
+
//
|
|
6
|
+
// spring soft elastic tether 1 constraint, low stiffness, at rest length
|
|
7
|
+
// rod rigid fixed-distance link 1 constraint, high stiffness, free rotation
|
|
8
|
+
// weld fused, no relative rotation 1 zero-length pivot + frozen rotation
|
|
9
|
+
// rope slack, taut only at max length 1 constraint, stiffness toggled per step
|
|
10
|
+
//
|
|
11
|
+
// The joint lives on the OWNER actor (bodyA) and points at a `target` actor
|
|
12
|
+
// (bodyB). Anchors are px offsets in each body's local (unrotated) frame; default
|
|
13
|
+
// (0,0) attaches at the body center. Geometry is resolved against the live matter
|
|
14
|
+
// bodies, so a joint tracks whatever the collider/rigidbody reconcile produced.
|
|
15
|
+
|
|
16
|
+
import Matter from 'matter-js';
|
|
17
|
+
|
|
18
|
+
// No `pin` (free-rotating coincident-pivot hinge): a length-0 revolute between two
|
|
19
|
+
// dynamic bodies is matter's most unstable case and no tuning made it robust. Use
|
|
20
|
+
// `rod` (to a static anchor) for a hinge / pendulum instead.
|
|
21
|
+
export const JOINT_TYPES = ['spring', 'rod', 'weld', 'rope'];
|
|
22
|
+
|
|
23
|
+
const sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
24
|
+
|
|
25
|
+
// A body-local point (unrotated frame) -> world, and the inverse. matter rotates
|
|
26
|
+
// a constraint's local point by the body angle when solving, so we store points
|
|
27
|
+
// in that same local frame.
|
|
28
|
+
function toWorld(body, local) {
|
|
29
|
+
return Matter.Vector.add(body.position, Matter.Vector.rotate(local, body.angle));
|
|
30
|
+
}
|
|
31
|
+
function toLocal(body, world) {
|
|
32
|
+
return Matter.Vector.rotate(sub(world, body.position), -body.angle);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// springiness (0..1) -> matter stiffness. Kept low so springs read as springs;
|
|
36
|
+
// rigid types use a fixed high stiffness instead.
|
|
37
|
+
function springStiffness(springiness) {
|
|
38
|
+
const s = Math.min(1, Math.max(0, springiness ?? 0.4));
|
|
39
|
+
return 0.002 + s * 0.05;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Rope stiffness WHEN TAUT, from `springiness`: 0 = a dead rope (stiff 0.9, holds
|
|
43
|
+
// firm at max length), 1 = a bungee (very soft ~0.002, stretches well past and
|
|
44
|
+
// springs back). Geometric interpolation -- a light body barely stretches a stiff
|
|
45
|
+
// constraint, so the useful range is tiny stiffnesses and a linear map would waste
|
|
46
|
+
// most of the slider in the "firm" zone.
|
|
47
|
+
function ropeStiffness(springiness) {
|
|
48
|
+
const s = Math.min(1, Math.max(0, springiness ?? 0));
|
|
49
|
+
return 0.9 * Math.pow(0.0025, s);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ownerAnchorLocal(joint) {
|
|
53
|
+
return { x: joint.anchorX ?? 0, y: joint.anchorY ?? 0 };
|
|
54
|
+
}
|
|
55
|
+
function targetAnchorLocal(joint) {
|
|
56
|
+
return { x: joint.targetAnchorX ?? 0, y: joint.targetAnchorY ?? 0 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Structural signature: a change here rebuilds the constraint set; everything
|
|
60
|
+
// else (length, springiness, damping) is live-patched in place.
|
|
61
|
+
export function jointSignature(joint) {
|
|
62
|
+
const a = ownerAnchorLocal(joint);
|
|
63
|
+
const b = targetAnchorLocal(joint);
|
|
64
|
+
return `${joint.type ?? 'spring'}|${joint.target ?? ''}|${a.x},${a.y}|${b.x},${b.y}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Distance between the two resolved anchor world points right now.
|
|
68
|
+
function anchorGap(bodyA, bodyB, joint) {
|
|
69
|
+
const wa = toWorld(bodyA, ownerAnchorLocal(joint));
|
|
70
|
+
const wb = toWorld(bodyB, targetAnchorLocal(joint));
|
|
71
|
+
return Matter.Vector.magnitude(sub(wa, wb));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The rest/target length for distance-style joints: authored `length`, or the
|
|
75
|
+
// current gap when length is auto (-1).
|
|
76
|
+
function restLength(bodyA, bodyB, joint) {
|
|
77
|
+
const len = joint.length ?? -1;
|
|
78
|
+
return len >= 0 ? len : anchorGap(bodyA, bodyB, joint);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Build the matter constraint(s) for a joint. `weld` is a single zero-length
|
|
82
|
+
// pivot link; PhysicsSystem additionally freezes both bodies' rotation so they
|
|
83
|
+
// can't turn relative to each other (a rigid fuse). A single stable pivot avoids
|
|
84
|
+
// the numerical blow-up that two fighting stiff length-0 links cause under
|
|
85
|
+
// collision, especially with a diagonal offset.
|
|
86
|
+
export function buildJointConstraints(bodyA, bodyB, joint) {
|
|
87
|
+
const type = joint.type ?? 'spring';
|
|
88
|
+
if (type === 'weld') return [pivotConstraint(bodyA, bodyB, joint)];
|
|
89
|
+
const pointA = ownerAnchorLocal(joint);
|
|
90
|
+
const pointB = targetAnchorLocal(joint);
|
|
91
|
+
const length = restLength(bodyA, bodyB, joint);
|
|
92
|
+
const stiffness =
|
|
93
|
+
type === 'spring' ? springStiffness(joint.springiness) : type === 'rope' ? ropeStiffness(joint.springiness) : 0.9;
|
|
94
|
+
// Rope carries no damping so a bungee actually bounces; spring/rod use theirs.
|
|
95
|
+
const damping = type === 'rope' ? 0 : joint.damping ?? 0.1;
|
|
96
|
+
return [Matter.Constraint.create({ bodyA, bodyB, pointA, pointB, length, stiffness, damping })];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Zero-length link at the midpoint of the two centers, shifted by the owner
|
|
100
|
+
// anchor -- the position half of a weld (rotation is frozen separately). 0.7 is
|
|
101
|
+
// firm but below matter's length-0 instability threshold; the velocity clamp in
|
|
102
|
+
// PhysicsSystem backstops any residual runaway.
|
|
103
|
+
function pivotConstraint(bodyA, bodyB, joint) {
|
|
104
|
+
const a = ownerAnchorLocal(joint);
|
|
105
|
+
const pivotWorld = {
|
|
106
|
+
x: (bodyA.position.x + bodyB.position.x) / 2 + a.x,
|
|
107
|
+
y: (bodyA.position.y + bodyB.position.y) / 2 + a.y,
|
|
108
|
+
};
|
|
109
|
+
return Matter.Constraint.create({
|
|
110
|
+
bodyA,
|
|
111
|
+
bodyB,
|
|
112
|
+
pointA: toLocal(bodyA, pivotWorld),
|
|
113
|
+
pointB: toLocal(bodyB, pivotWorld),
|
|
114
|
+
length: 0,
|
|
115
|
+
stiffness: 0.7,
|
|
116
|
+
damping: 0.1,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Live-patch the mutable feel of an existing constraint set (no rebuild). Rope is
|
|
121
|
+
// one-sided: it pulls only when stretched past its length (stiffness from
|
|
122
|
+
// `springiness` -- dead rope to bungee), and goes fully slack (zero stiffness)
|
|
123
|
+
// when closer -- matter has no native max-only constraint.
|
|
124
|
+
export function patchJoint(constraints, bodyA, bodyB, joint) {
|
|
125
|
+
const type = joint.type ?? 'spring';
|
|
126
|
+
if (type === 'weld') return;
|
|
127
|
+
const c = constraints[0];
|
|
128
|
+
if (!c) return;
|
|
129
|
+
if ((joint.length ?? -1) >= 0) c.length = joint.length;
|
|
130
|
+
if (type === 'spring') {
|
|
131
|
+
c.stiffness = springStiffness(joint.springiness);
|
|
132
|
+
c.damping = joint.damping ?? 0.1;
|
|
133
|
+
} else if (type === 'rope') {
|
|
134
|
+
c.stiffness = anchorGap(bodyA, bodyB, joint) > c.length ? ropeStiffness(joint.springiness) : 0;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -27,6 +27,13 @@ export const FIXED_STEP_MS = 1000 / 60;
|
|
|
27
27
|
export const FIXED_STEP_S = FIXED_STEP_MS / 1000;
|
|
28
28
|
export const MAX_STEPS_PER_FRAME = 5;
|
|
29
29
|
|
|
30
|
+
// Hard cap on a body's speed (px per fixed step) -- a safety net so a constraint
|
|
31
|
+
// blow-up (a stiff joint at a bad angle can inject huge velocity in one step)
|
|
32
|
+
// can't fling a body off screen. Well above any intended motion (a slingshot
|
|
33
|
+
// launch is ~20), so normal gameplay never touches it; it only ever engages to
|
|
34
|
+
// bound a runaway.
|
|
35
|
+
export const MAX_SPEED = 60;
|
|
36
|
+
|
|
30
37
|
// World gravity in matter's own units. `scale` matches matter's internal
|
|
31
38
|
// default; `y` is what a scene tunes (1 ~= a gentle arcade fall).
|
|
32
39
|
export const DEFAULT_GRAVITY = { x: 0, y: 1, scale: 0.001 };
|
|
@@ -73,7 +80,10 @@ function shapeToPart(s) {
|
|
|
73
80
|
if (part) return part;
|
|
74
81
|
return Matter.Bodies.rectangle((x0 + x1) / 2, (y0 + y1) / 2, Math.max(1, x1 - x0), Math.max(1, y1 - y0));
|
|
75
82
|
}
|
|
76
|
-
|
|
83
|
+
// `s.angle` (deg) rotates the box part about its center at creation; the
|
|
84
|
+
// compound body's Layout.rotation composes on top.
|
|
85
|
+
const opts = s.angle ? { angle: s.angle * DEG_TO_RAD } : undefined;
|
|
86
|
+
return Matter.Bodies.rectangle(s.cx, s.cy, Math.max(1, s.width), Math.max(1, s.height), opts);
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
// Structural signature: when this changes we rebuild the body (shape set / sizes
|
|
@@ -87,7 +97,7 @@ export function bodySignature(actor) {
|
|
|
87
97
|
s.type === 'circle'
|
|
88
98
|
? `c${Math.round(s.radius)}`
|
|
89
99
|
: s.type === 'box'
|
|
90
|
-
? `b${Math.round(s.width)}x${Math.round(s.height)}`
|
|
100
|
+
? `b${Math.round(s.width)}x${Math.round(s.height)}@${Math.round(s.angle ?? 0)}`
|
|
91
101
|
: `${s.type[0]}${(s.points ?? []).length}`
|
|
92
102
|
)
|
|
93
103
|
.join(',');
|
|
@@ -173,3 +183,13 @@ export function applyAngularDrag(bodies) {
|
|
|
173
183
|
if (drag > 0 && !body.isStatic) body.angularVelocity *= Math.max(0, 1 - drag * FIXED_STEP_S);
|
|
174
184
|
}
|
|
175
185
|
}
|
|
186
|
+
|
|
187
|
+
// Safety clamp: bound each body's speed to MAX_SPEED so a constraint blow-up
|
|
188
|
+
// can't fling it off screen. Run right after each Engine.update.
|
|
189
|
+
export function clampSpeeds(bodies) {
|
|
190
|
+
for (const body of bodies) {
|
|
191
|
+
if (body.isStatic) continue;
|
|
192
|
+
const s = Math.hypot(body.velocity.x, body.velocity.y);
|
|
193
|
+
if (s > MAX_SPEED) Matter.Body.setVelocity(body, { x: (body.velocity.x / s) * MAX_SPEED, y: (body.velocity.y / s) * MAX_SPEED });
|
|
194
|
+
}
|
|
195
|
+
}
|
package/package.json
CHANGED
|
File without changes
|